mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 18:01:14 +00:00
Support searching the Speed Dial by English names in any UI language (#16369)
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
// - action.input "percent"/"tab" -> NativeCommands catalog (phases handled in activateEntry)
|
||||
// - action.icon SVG base name -> AppAction::icon / resources/images/<name>.svg
|
||||
// - action.desc/wiki -> AppAction::tooltip / help_url (footer detail strip)
|
||||
// - action.*_en / tab.title_en -> ActionRegistry action_json / tab_options (English search aliases,
|
||||
// sent only when they differ from the displayed text)
|
||||
// - payload.tooltip_expanded -> ActionRegistry::tooltip_expanded (persisted footer state)
|
||||
// - action list is frecency-sorted -> ActionRegistry::snapshot()
|
||||
|
||||
@@ -97,45 +99,49 @@ var ghostEl = null, ghostTypedEl = null, ghostSuffixEl = null;
|
||||
var activeCompletion = null;
|
||||
|
||||
// ---- pure helpers (no DOM; unit-tested) -------------------------------------
|
||||
// Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per
|
||||
// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/group/source text correctly.
|
||||
// The action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once.
|
||||
function titleNorm(a) {
|
||||
if (a._tn === undefined)
|
||||
a._tn = NormText(a.title, false);
|
||||
return a._tn;
|
||||
}
|
||||
// The eyebrow (header) line shows group when present, else source. Settings keep an empty group so
|
||||
// their source path is the eyebrow; commands / plates / recents carry a non-empty group. Splitting the
|
||||
// two lets a match range stay aligned to whichever string the eyebrow actually renders.
|
||||
function groupNorm(a) {
|
||||
if (a._gn === undefined)
|
||||
a._gn = NormText(a.group || "", false);
|
||||
return a._gn;
|
||||
}
|
||||
function sourceNorm(a) {
|
||||
if (a._sn === undefined)
|
||||
a._sn = NormText(a.source || "", false);
|
||||
return a._sn;
|
||||
}
|
||||
function pluginCategoryNorm(a) {
|
||||
if (a._pcn === undefined)
|
||||
a._pcn = a.kind === "plugin" ? NormText(T("sd_plugins", "Plugins"), false) : "";
|
||||
return a._pcn;
|
||||
}
|
||||
// Plugin type is searchable metadata, so typing "plugin" can find runnable plugin actions even
|
||||
// when neither their capability nor plugin name contains that word. Keep both category forms.
|
||||
function pluginTypeNorm(a) {
|
||||
if (a._pn === undefined)
|
||||
a._pn = a.kind === "plugin" ? NormText("plugin plugins", false) : "";
|
||||
return a._pn;
|
||||
}
|
||||
// Search-only alias for the descriptive name when the title differs (e.g. "Reverse on even" vs
|
||||
// "Overhang reversal"). Never rendered, so no highlight ranges.
|
||||
function fullNorm(a) {
|
||||
if (a._fn === undefined)
|
||||
a._fn = NormText(a.full_label || "", false);
|
||||
return a._fn;
|
||||
// Every searchable field of an action: its text, its ranking weight (title > group/category >
|
||||
// source/aliases) and the matchIndex key its highlight ranges go to (null for a search-only field).
|
||||
// - group vs source: the eyebrow (header) line shows group when present, else source. Settings keep
|
||||
// an empty group so their source path is the eyebrow; commands / plates / recents carry a non-empty
|
||||
// group. Splitting the two lets a match range stay aligned to whichever string the eyebrow renders.
|
||||
// - full_label: search-only alias for the descriptive name when the title differs (e.g. "Reverse on
|
||||
// even" vs "Overhang reversal"). Never rendered, so no highlight ranges.
|
||||
// - plugin category / type: the visible "Plugins" category, plus the plugin kind as searchable
|
||||
// metadata, so typing "plugin" finds runnable plugin actions even when neither their capability
|
||||
// nor plugin name contains that word. Keep both category forms.
|
||||
// - *_en: English counterparts of the translated fields, sent by C++ only when they differ from the
|
||||
// displayed text (a non-English UI), so the page can still be searched in English. Each carries the
|
||||
// weight of the localized field it mirrors, so a match ranks the same in either language. Search-only,
|
||||
// except title_en, whose ranges drive the row's English hint.
|
||||
var SEARCH_FIELDS = [
|
||||
{ text: function (a) { return a.title; }, weight: SCORE_TITLE, range: "title" },
|
||||
{ text: function (a) { return a.group; }, weight: SCORE_GROUP, range: "group" },
|
||||
{ text: function (a) { return a.source; }, weight: 0, range: "source" },
|
||||
{ text: function (a) { return a.full_label; }, weight: 0, range: null },
|
||||
{ text: function (a) { return a.kind === "plugin" ? T("sd_plugins", "Plugins") : ""; }, weight: SCORE_GROUP, range: "plugin" },
|
||||
{ text: function (a) { return a.kind === "plugin" ? "plugin plugins" : ""; }, weight: SCORE_GROUP, range: null },
|
||||
{ text: function (a) { return a.title_en; }, weight: SCORE_TITLE, range: "title_en" },
|
||||
{ text: function (a) { return a.group_en; }, weight: SCORE_GROUP, range: null },
|
||||
{ text: function (a) { return a.source_en; }, weight: 0, range: null },
|
||||
{ text: function (a) { return a.full_label_en; }, weight: 0, range: null }
|
||||
];
|
||||
var MATCH_RANGE_KEYS = SEARCH_FIELDS.map(function (f) { return f.range; }).filter(Boolean);
|
||||
|
||||
// The action's non-empty SEARCH_FIELDS as pre-normalized haystacks ({norm, weight, range}), cached on
|
||||
// the action object. The action objects arrive from C++ and are stable for the dialog's lifetime, so
|
||||
// this runs once per action; skipping empty fields keeps the per-keystroke scan to the text an action
|
||||
// actually has (an English UI pays nothing for the aliases it never receives). The fold is
|
||||
// length-preserving (1:1 per char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL text.
|
||||
function searchFields(a) {
|
||||
if (a._sf === undefined) {
|
||||
a._sf = [];
|
||||
for (var i = 0; i < SEARCH_FIELDS.length; i++) {
|
||||
var norm = NormText(SEARCH_FIELDS[i].text(a) || "", false);
|
||||
if (norm)
|
||||
a._sf.push({ norm: norm, weight: SEARCH_FIELDS[i].weight, range: SEARCH_FIELDS[i].range });
|
||||
}
|
||||
}
|
||||
return a._sf;
|
||||
}
|
||||
|
||||
// Match one pre-normalized field vs one pre-normalized needle. Returns {score, ranges, contiguous}
|
||||
@@ -175,26 +181,22 @@ function tokenWordRe(token) {
|
||||
return new RegExp("\\b" + EscapeRegExp(token) + "\\b");
|
||||
}
|
||||
|
||||
// One token's best match across an action's searchable fields, keeping per-field ranges for
|
||||
// highlighting. Returns {score, title, group, source}, or null if no field matches.
|
||||
function tokenMatch(a, token, wwRe) {
|
||||
var t = fieldMatchScore(titleNorm(a), token, wwRe);
|
||||
var g = fieldMatchScore(groupNorm(a), token, wwRe);
|
||||
var s = fieldMatchScore(sourceNorm(a), token, wwRe);
|
||||
var f = fieldMatchScore(fullNorm(a), token, wwRe);
|
||||
var p = fieldMatchScore(pluginCategoryNorm(a), token, wwRe);
|
||||
var typeMatch = fieldMatchScore(pluginTypeNorm(a), token, wwRe);
|
||||
if (!t && !g && !s && !f && !p && !typeMatch) return null;
|
||||
var score = Math.max(
|
||||
t ? (t.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_TITLE + t.score : -Infinity,
|
||||
g ? (g.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + g.score : -Infinity,
|
||||
s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity,
|
||||
f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity,
|
||||
p ? (p.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + p.score : -Infinity,
|
||||
typeMatch ? (typeMatch.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + typeMatch.score : -Infinity
|
||||
);
|
||||
return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null,
|
||||
plugin: p ? p.ranges : null };
|
||||
// One needle (the whole query, or one token of it) against every SEARCH_FIELDS field of an action.
|
||||
// Returns {score, ranges}: the best weighted field score (contiguous > fuzzy, then field weight, then
|
||||
// start/gaps) and the ranges of each matching highlighted field keyed by its range name. Null when no
|
||||
// field matches.
|
||||
function matchFields(a, needle, wwRe) {
|
||||
var fields = searchFields(a);
|
||||
var best = null, ranges = null; // allocated on the first match: most actions match nothing
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var field = fields[i];
|
||||
var m = fieldMatchScore(field.norm, needle, wwRe);
|
||||
if (!m) continue;
|
||||
var v = (m.contiguous ? SCORE_CONTIGUOUS : 0) + field.weight + m.score;
|
||||
if (best === null || v > best) best = v;
|
||||
if (field.range) (ranges || (ranges = {}))[field.range] = m.ranges;
|
||||
}
|
||||
return best === null ? null : { score: best, ranges: ranges || {} };
|
||||
}
|
||||
|
||||
// Merge per-field token ranges into sorted, coalesced ranges for highlighting. Overlapping or adjacent
|
||||
@@ -217,24 +219,6 @@ function mergeRanges(ranges) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Combine per-field scores into one value, or null when nothing matched.
|
||||
// Ranking: contiguous > fuzzy, then title > group/category > source/full alias, then start/gaps.
|
||||
function scoreFields(t, g, s, f, p, typeMatch) {
|
||||
var best = null;
|
||||
function consider(m, weight) {
|
||||
if (!m) return;
|
||||
var v = (m.contiguous ? SCORE_CONTIGUOUS : 0) + weight + m.score;
|
||||
best = best === null ? v : Math.max(best, v);
|
||||
}
|
||||
consider(t, SCORE_TITLE);
|
||||
consider(g, SCORE_GROUP);
|
||||
consider(s, 0);
|
||||
consider(f, 0);
|
||||
consider(p, SCORE_GROUP);
|
||||
consider(typeMatch, SCORE_GROUP);
|
||||
return best;
|
||||
}
|
||||
|
||||
// The unified main-phase search: every action (command/plugin/setting) matching the query, ranked
|
||||
// by relevance (not by action type). Sets matchIndex so rows highlight their match ranges. The query
|
||||
// is normalized ONCE per pass - FuzzyRangesNorm then runs against each action's pre-normalized
|
||||
@@ -245,8 +229,8 @@ function scoreFields(t, g, s, f, p, typeMatch) {
|
||||
// - tokens: every whitespace-separated word must match SOME field, but different words may match
|
||||
// different fields. This is what lets "speed acceleration inner" find "Inner wall" whose path is
|
||||
// "Process : Speed : Acceleration" (title + source breadcrumb together).
|
||||
// full_label and canonical plugin kind are searchable aliases. The visible category is also searchable
|
||||
// so its own match can be highlighted in plugin rows.
|
||||
// full_label, canonical plugin kind and the English counterparts (*_en) are searchable aliases. The
|
||||
// visible category is also searchable so its own match can be highlighted in plugin rows.
|
||||
// A phrase match always outranks a distributed token match.
|
||||
function searchActions(actions, query) {
|
||||
var q = (query || "").trim();
|
||||
@@ -262,50 +246,43 @@ function searchActions(actions, query) {
|
||||
// Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks
|
||||
// between fields, and EscapeRegExp keeps regex metachars in the query literal.
|
||||
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
|
||||
// A one-word query's only token is the needle itself, so its token pass would repeat the failed
|
||||
// phrase match exactly; such an action is dropped without it.
|
||||
var tokenPass = searchTokens.length !== 1 || searchTokens[0] !== searchNeedle;
|
||||
|
||||
var scored = [];
|
||||
var seen = {};
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var t = fieldMatchScore(titleNorm(a), searchNeedle, wwRe);
|
||||
var g = fieldMatchScore(groupNorm(a), searchNeedle, wwRe);
|
||||
var s = fieldMatchScore(sourceNorm(a), searchNeedle, wwRe);
|
||||
var f = fieldMatchScore(fullNorm(a), searchNeedle, wwRe);
|
||||
var p = fieldMatchScore(pluginCategoryNorm(a), searchNeedle, wwRe);
|
||||
var typeMatch = fieldMatchScore(pluginTypeNorm(a), searchNeedle, wwRe);
|
||||
var phrase = scoreFields(t, g, s, f, p, typeMatch);
|
||||
var score, ranges;
|
||||
if (phrase !== null) {
|
||||
score = phrase + SCORE_PHRASE;
|
||||
ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null,
|
||||
plugin: p ? p.ranges : null };
|
||||
var phrase = matchFields(a, searchNeedle, wwRe);
|
||||
var score, perField;
|
||||
if (phrase) {
|
||||
score = phrase.score + SCORE_PHRASE;
|
||||
} else {
|
||||
if (!tokenPass) continue;
|
||||
// Require every token; a token that matches nothing drops the action immediately. Ranges
|
||||
// from all matching tokens are merged per field so each matched word highlights.
|
||||
var sum = 0, titleR = null, groupR = null, sourceR = null, pluginR = null, all = true;
|
||||
var sum = 0, all = true;
|
||||
perField = {};
|
||||
for (var tokenIndex = 0; tokenIndex < searchTokens.length; tokenIndex++) {
|
||||
var m = tokenMatch(a, searchTokens[tokenIndex], searchTokenRes[tokenIndex]);
|
||||
var m = matchFields(a, searchTokens[tokenIndex], searchTokenRes[tokenIndex]);
|
||||
if (!m) { all = false; break; }
|
||||
sum += m.score;
|
||||
if (m.title) (titleR || (titleR = [])).push(m.title);
|
||||
if (m.group) (groupR || (groupR = [])).push(m.group);
|
||||
if (m.source) (sourceR || (sourceR = [])).push(m.source);
|
||||
if (m.plugin) (pluginR || (pluginR = [])).push(m.plugin);
|
||||
for (var key in m.ranges)
|
||||
(perField[key] || (perField[key] = [])).push(m.ranges[key]);
|
||||
}
|
||||
if (!all) continue;
|
||||
score = sum;
|
||||
ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR),
|
||||
plugin: mergeRanges(pluginR) };
|
||||
}
|
||||
// Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or
|
||||
// source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label.
|
||||
matchIndex[a.id] = {
|
||||
title: ranges.title,
|
||||
group: ranges.group,
|
||||
source: ranges.source,
|
||||
plugin: ranges.plugin,
|
||||
useEyebrowGroup: !!(a.group)
|
||||
};
|
||||
// Ranges are per-field against the ACTUAL text drawn: title for the row-name, group (or source
|
||||
// when group is empty) for the eyebrow, and title_en for the English hint - so highlight offsets
|
||||
// stay aligned to the label.
|
||||
var entry = { useEyebrowGroup: !!(a.group) };
|
||||
for (var k = 0; k < MATCH_RANGE_KEYS.length; k++) {
|
||||
var rangeKey = MATCH_RANGE_KEYS[k];
|
||||
entry[rangeKey] = phrase ? phrase.ranges[rangeKey] || null : mergeRanges(perField[rangeKey]);
|
||||
}
|
||||
matchIndex[a.id] = entry;
|
||||
scored.push({ a: a, s: score });
|
||||
seen[a.id] = true;
|
||||
}
|
||||
@@ -350,7 +327,8 @@ function completionFor(query, list) {
|
||||
var top = (list || []).slice(0, 10);
|
||||
for (var i = 0; i < top.length; i++) {
|
||||
var a = top[i];
|
||||
var fields = [a.title, a.group, a.source, a.full_label];
|
||||
// English aliases after the displayed text, so English typing completes in a localized UI.
|
||||
var fields = [a.title, a.group, a.source, a.full_label, a.title_en, a.group_en, a.source_en, a.full_label_en];
|
||||
for (var f = 0; f < fields.length; f++) {
|
||||
var words = completionWords(fields[f]);
|
||||
for (var w = 0; w < words.length; w++) {
|
||||
@@ -427,13 +405,15 @@ function tabTitle(t) {
|
||||
return title || prettySource((t && t.id) || "");
|
||||
}
|
||||
|
||||
// Filter the tab list by a fuzzy title/id match. Pure so the node-vm test can exercise it.
|
||||
// Filter the tab list by a fuzzy title/id/English-title match (title_en is sent only when the page
|
||||
// label is translated). Pure so the node-vm test can exercise it.
|
||||
function filterTabs(tabs, query) {
|
||||
var q = (query || "").trim();
|
||||
if (!q)
|
||||
return (tabs || []).slice(0);
|
||||
return (tabs || []).filter(function (t) {
|
||||
return FuzzyRanges(tabTitle(t), q, false) || FuzzyRanges(t.id, q, false);
|
||||
return FuzzyRanges(tabTitle(t), q, false) || FuzzyRanges(t.id, q, false) ||
|
||||
(t.title_en && FuzzyRanges(t.title_en, q, false));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -973,6 +953,10 @@ function renderActionRow(a, i) {
|
||||
var eyebrowMatch = mi ? (showPluginCategory ? mi.plugin : (mi.useEyebrowGroup ? mi.group : mi.source)) : null;
|
||||
shell.left.insertBefore(markedText("row-eyebrow", eyebrow, eyebrowMatch), shell.line);
|
||||
shell.line.appendChild(markedText("row-name", a.title, mi ? mi.title : null));
|
||||
// A row found only through its English title shows that title, highlighted, so the match is
|
||||
// explained even when nothing in the translated name resembles the query.
|
||||
if (mi && !mi.title && mi.title_en)
|
||||
shell.line.appendChild(markedText("row-alias", a.title_en, mi.title_en));
|
||||
var badge = modeBadge(a, USER_MODE);
|
||||
if (badge) {
|
||||
var tag = document.createElement("span");
|
||||
|
||||
@@ -50,6 +50,15 @@ assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1,
|
||||
"a typed query matches a tab title");
|
||||
assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [],
|
||||
"a typed query with no match returns an empty list");
|
||||
// A translated tab whose id is not its English name carries title_en, so English still finds it.
|
||||
const localizedTabs = [
|
||||
{ id: "monitor", title: "设备", title_en: "Device" },
|
||||
{ id: "prepare", title: "准备" }
|
||||
];
|
||||
assert.deepEqual(ctx.filterTabs(localizedTabs, "device").map(function (t) { return t.id; }), ["monitor"],
|
||||
"a localized tab matches its English title");
|
||||
assert.deepEqual(ctx.filterTabs(localizedTabs, "设备").map(function (t) { return t.id; }), ["monitor"],
|
||||
"a localized tab still matches its translated title");
|
||||
|
||||
// tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id.
|
||||
assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home",
|
||||
@@ -192,6 +201,48 @@ assert.equal(ctx.completionFor("", compPool), null, "an empty query has no compl
|
||||
assert.equal(ctx.completionFor("zzz", ctx.searchActions(compPool, "zzz")), null,
|
||||
"a query with no match has no completion");
|
||||
|
||||
// English aliases: in a non-English UI each translated field may carry its English counterpart
|
||||
// (*_en), searched with the weight of the field it mirrors. An English UI sends none.
|
||||
const zhPool = [
|
||||
{ id: "rev", title: "反转偶数层悬垂方向", full_label: "悬垂反转", source: "工艺 : 质量 : 悬垂",
|
||||
title_en: "Reverse on even", full_label_en: "Overhang reversal", source_en: "Process : Quality : Overhangs",
|
||||
group: "", input: "" },
|
||||
{ id: "slice", title: "切片并预览", source: "OrcaSlicer", group: "切片和导出",
|
||||
title_en: "Slice and Preview", group_en: "Slice & Export", input: "" },
|
||||
{ id: "brim", title: "Brim宽度", source: "工艺 : 其他 : Brim", source_en: "Process : Others : Brim",
|
||||
group: "", input: "" }
|
||||
];
|
||||
assert.deepEqual(ctx.searchActions(zhPool, "reverse on even").map(function (a) { return a.id; }), ["rev"],
|
||||
"an English query finds a translated setting through its English title");
|
||||
assert.equal(ctx.matchIndex.rev.title, null, "an English-only match does not highlight the translated title");
|
||||
assert.deepEqual(ctx.matchIndex.rev.title_en, [[0, 15]], "the English title keeps its ranges for the hint");
|
||||
assert.deepEqual(ctx.searchActions(zhPool, "偶数层").map(function (a) { return a.id; }), ["rev"],
|
||||
"the translated title is still searchable");
|
||||
assert.equal(ctx.matchIndex.rev.title_en, null, "a translated-title match leaves no English hint ranges");
|
||||
assert.deepEqual(ctx.searchActions(zhPool, "reverse 悬垂").map(function (a) { return a.id; }), ["rev"],
|
||||
"a mixed query may match one token in English and another in the translation");
|
||||
assert.deepEqual(ctx.searchActions(zhPool, "overhang reversal").map(function (a) { return a.id; }), ["rev"],
|
||||
"the English full_label alias is searchable");
|
||||
assert.deepEqual(ctx.searchActions(zhPool, "export").map(function (a) { return a.id; }), ["slice"],
|
||||
"the English group alias is searchable");
|
||||
assert.deepEqual(ctx.searchActions(zhPool, "brim").map(function (a) { return a.id; }), ["brim"],
|
||||
"a translation that keeps the English word matches the displayed title");
|
||||
assert.deepEqual(ctx.matchIndex.brim.title, [[0, 4]], "that match highlights the displayed title, so no hint is drawn");
|
||||
// Weights mirror the localized fields: an English title match outranks an English breadcrumb match.
|
||||
const zhRankPool = [
|
||||
{ id: "src", title: "内壁", source: "工艺 : 速度", source_en: "Process : Speed", group: "", input: "" },
|
||||
{ id: "ttl", title: "速度", source: "工艺 : 其他", title_en: "Speed", group: "", input: "" }
|
||||
];
|
||||
assert.deepEqual(ctx.searchActions(zhRankPool, "speed").map(function (a) { return a.id; }), ["ttl", "src"],
|
||||
"an English title match outranks an English breadcrumb match, as their localized fields do");
|
||||
// Absent aliases (an English UI) leave no English hint; every pool above has none and ranks as before.
|
||||
ctx.searchActions(pool, "layer");
|
||||
assert.equal(ctx.matchIndex.c1.title_en, null, "an action without English aliases has no English hint ranges");
|
||||
assert.equal(ctx.completionFor("reverse o", ctx.searchActions(zhPool, "reverse o")).suffix, "n",
|
||||
"completion extends an English word from title_en");
|
||||
assert.equal(ctx.completionFor("quality overh", ctx.searchActions(zhPool, "quality overh")).suffix, "angs",
|
||||
"completion also reaches the English breadcrumb");
|
||||
|
||||
// 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",
|
||||
@@ -456,9 +507,9 @@ assert.equal(ctx.stateFromPayload({}).tooltipExpanded, true, "expansion defaults
|
||||
assert.equal(ctx.stateFromPayload({ tooltip_expanded: false }).tooltipExpanded, false, "a collapsed payload is honored");
|
||||
assert.equal(ctx.stateFromPayload({ tooltip_expanded: true }).tooltipExpanded, true, "an expanded payload is honored");
|
||||
|
||||
// resultCountText: a search counts the shown matches only ("Showing N actions"); the total is used
|
||||
// resultCountText: a search counts the shown matches only ("Matching actions: N"); the total is used
|
||||
// solely for the empty-query count.
|
||||
assert.equal(ctx.resultCountText(100, 3, "lay"), "Showing 3 actions", "a search reports the shown match count only");
|
||||
assert.equal(ctx.resultCountText(100, 100, ""), "100 actions", "an empty query reports the total");
|
||||
assert.equal(ctx.resultCountText(100, 3, "lay"), "Matching actions: 3", "a search reports the shown match count only");
|
||||
assert.equal(ctx.resultCountText(100, 100, ""), "Actions: 100", "an empty query reports the total");
|
||||
|
||||
console.log("ok");
|
||||
|
||||
@@ -414,8 +414,20 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* English title of a row found only through it (non-English UI); gives way to the title first. */
|
||||
.row-alias {
|
||||
flex: 0 100 auto;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row-name mark,
|
||||
.row-eyebrow mark {
|
||||
.row-eyebrow mark,
|
||||
.row-alias mark {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
border-radius: 2px;
|
||||
|
||||
@@ -206,19 +206,52 @@ constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
|
||||
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
|
||||
|
||||
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
|
||||
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
|
||||
std::string setting_type_context(Preset::Type type)
|
||||
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs. Untranslated:
|
||||
// the caller translates it for the displayed breadcrumb and keeps it for the English one.
|
||||
const char* setting_type_context(Preset::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Preset::TYPE_FILAMENT:
|
||||
case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament");
|
||||
case Preset::TYPE_PRINTER: return _u8L("Printer");
|
||||
case Preset::TYPE_SLA_MATERIAL: return L("Filament");
|
||||
case Preset::TYPE_PRINTER: return L("Printer");
|
||||
case Preset::TYPE_PRINT:
|
||||
case Preset::TYPE_SLA_PRINT:
|
||||
default: return _u8L("Process");
|
||||
default: return L("Process");
|
||||
}
|
||||
}
|
||||
|
||||
// A setting's text in one language. The displayed text and its English search aliases both come from
|
||||
// here, so in an English UI the two are equal and no alias is sent.
|
||||
struct SettingText
|
||||
{
|
||||
std::string title; // the label the settings row draws
|
||||
std::string full_label; // the descriptive ConfigOptionDef name when it differs from title: a
|
||||
// search-only alias ("overhang reversal" still finds "Reverse on even")
|
||||
std::string source; // the full settings path "Process : Quality : Layers". The JS renders
|
||||
// group || source and searches every field, so putting the whole path in
|
||||
// source both displays it and makes it matchable by any segment.
|
||||
};
|
||||
|
||||
SettingText setting_text(const std::wstring& def_label, const std::string& display_label, const wxString& row_label, bool row_multi,
|
||||
const std::string& type_name, const std::wstring& category, const std::wstring& group)
|
||||
{
|
||||
SettingText text;
|
||||
const std::string def = boost::nowide::narrow(def_label);
|
||||
text.title = into_u8(Search::resolve_setting_title(from_u8(display_label), row_label, row_multi));
|
||||
if (text.title.empty())
|
||||
text.title = def;
|
||||
if (text.title != def)
|
||||
text.full_label = def;
|
||||
|
||||
std::wstring path = boost::nowide::widen(type_name);
|
||||
if (!category.empty())
|
||||
path += L" : " + category;
|
||||
if (!group.empty())
|
||||
path += L" : " + group;
|
||||
text.source = boost::nowide::narrow(path);
|
||||
return text;
|
||||
}
|
||||
|
||||
// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.).
|
||||
const char* mode_key(ConfigOptionMode mode)
|
||||
{
|
||||
@@ -305,8 +338,9 @@ struct PlateAction : AppAction
|
||||
PlateAction(int index, std::string title, std::string source_name)
|
||||
: AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index)
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = _u8L("Plate");
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group_en = L("Plate");
|
||||
this->group = I18N::translate_utf8(this->group_en);
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
@@ -328,8 +362,9 @@ struct RecentProjectAction : AppAction
|
||||
RecentProjectAction(std::string path, std::string title, std::string source)
|
||||
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path))
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = _u8L("Recent Projects");
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group_en = L("Recent Projects");
|
||||
this->group = I18N::translate_utf8(this->group_en);
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
@@ -342,6 +377,30 @@ struct RecentProjectAction : AppAction
|
||||
|
||||
} // namespace
|
||||
|
||||
nlohmann::json action_json(const AppAction& a)
|
||||
{
|
||||
nlohmann::json j({{"id", a.id()},
|
||||
{"title", a.title()},
|
||||
{"full_label", a.full_label},
|
||||
{"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)},
|
||||
{"desc", a.tooltip},
|
||||
{"wiki", !a.help_url.empty()}});
|
||||
auto add_alias = [&j](const char* key, const std::string& english, const std::string& local) {
|
||||
if (!english.empty() && english != local)
|
||||
j[key] = english;
|
||||
};
|
||||
add_alias("title_en", a.title_en, a.title());
|
||||
add_alias("group_en", a.group_en, a.group);
|
||||
add_alias("source_en", a.source_en, a.source_name());
|
||||
add_alias("full_label_en", a.full_label_en, a.full_label);
|
||||
return j;
|
||||
}
|
||||
|
||||
ActionRegistry::~ActionRegistry() = default;
|
||||
|
||||
void ActionRegistry::init()
|
||||
@@ -430,7 +489,8 @@ void ActionRegistry::relocalize_builtins()
|
||||
|
||||
// Drop only the built-in commands; plugins and the dynamically materialised families are
|
||||
// either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert()
|
||||
// re-seeds favourites/stats from config, so key-based ids keep their pinned state.
|
||||
// re-seeds favourites/stats from config, so key-based ids keep their pinned state. make_action()
|
||||
// translates in the new locale.
|
||||
std::vector<std::string> stale;
|
||||
for (const auto& [id, action] : m_actions)
|
||||
if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command)
|
||||
@@ -438,7 +498,6 @@ void ActionRegistry::relocalize_builtins()
|
||||
for (const std::string& id : stale)
|
||||
remove(id);
|
||||
|
||||
NativeCommands::rebuild_catalog();
|
||||
for (const NativeCommand& c : NativeCommands::catalog())
|
||||
upsert(NativeCommands::make_action(c));
|
||||
}
|
||||
@@ -672,28 +731,22 @@ void ActionRegistry::materialize_setting_actions()
|
||||
const std::string id = SettingAction::id_for(opt.opt_key(), opt.type);
|
||||
seen.insert(id);
|
||||
|
||||
// The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias
|
||||
// ("overhang reversal" still finds "Reverse on even").
|
||||
const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local);
|
||||
std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi));
|
||||
if (title.empty())
|
||||
title = search_label;
|
||||
// The localized text the page shows, and its English search aliases from the untranslated row
|
||||
// label, def name and breadcrumb.
|
||||
const char* type_context = setting_type_context(opt.type);
|
||||
SettingText local = setting_text(opt.label_local.empty() ? opt.label : opt.label_local, opt.display_label, row.label, row.multi,
|
||||
I18N::translate_utf8(type_context), opt.category_local, opt.group_local);
|
||||
SettingText english = setting_text(opt.label, opt.display_label_en, row.label_en, row.multi, type_context,
|
||||
Tab::translate_category(opt.category, opt.type, false).ToStdWstring(), opt.group);
|
||||
|
||||
// Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS
|
||||
// renders group || source and searches source + " " + group, so putting the whole path in
|
||||
// source both displays it and makes it matchable by any segment (e.g. a "quality" query).
|
||||
std::wstring path = boost::nowide::widen(setting_type_context(opt.type));
|
||||
if (!opt.category_local.empty())
|
||||
path += L" : " + opt.category_local;
|
||||
if (!opt.group_local.empty())
|
||||
path += L" : " + opt.group_local;
|
||||
|
||||
// title = the label the settings row draws; group stays empty so the source path (above) is
|
||||
// the single display/search breadcrumb rather than being duplicated.
|
||||
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category,
|
||||
boost::nowide::narrow(path), opt.mode);
|
||||
if (title != search_label)
|
||||
action->full_label = search_label;
|
||||
// group stays empty so the source path is the single display/search breadcrumb rather than
|
||||
// being duplicated.
|
||||
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, local.title, std::string(), opt.category, local.source,
|
||||
opt.mode);
|
||||
action->full_label = std::move(local.full_label);
|
||||
action->title_en = std::move(english.title);
|
||||
action->full_label_en = std::move(english.full_label);
|
||||
action->source_en = std::move(english.source);
|
||||
|
||||
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
|
||||
// the one shown next to it in the page. Fall back to the page/category icon for groups
|
||||
@@ -751,13 +804,14 @@ void ActionRegistry::materialize_plate_actions()
|
||||
seen.insert(id);
|
||||
|
||||
// "Go to Plate N" + " (name)" when the plate is named, matching the object-list label.
|
||||
std::string title(_u8L("Go to Plate"));
|
||||
title += " " + std::to_string(i + 1);
|
||||
const std::string name = plate->get_plate_name();
|
||||
const char* go_to_plate = L("Go to Plate");
|
||||
std::string suffix = " " + std::to_string(i + 1);
|
||||
const std::string name = plate->get_plate_name();
|
||||
if (!name.empty())
|
||||
title += " (" + name + ")";
|
||||
suffix += " (" + name + ")";
|
||||
|
||||
auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName);
|
||||
auto action = std::make_unique<PlateAction>(int(i), I18N::translate_utf8(go_to_plate) + suffix, kOrcaSourceName);
|
||||
action->title_en = go_to_plate + suffix;
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
@@ -868,23 +922,9 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
return a->id() < b->id();
|
||||
});
|
||||
|
||||
auto action_to_json = [](const AppAction* a) {
|
||||
return nlohmann::json({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"full_label", a->full_label},
|
||||
{"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)},
|
||||
{"desc", a->tooltip},
|
||||
{"wiki", !a->help_url.empty()}});
|
||||
};
|
||||
|
||||
nlohmann::json actions = nlohmann::json::array();
|
||||
for (const AppAction* a : sorted)
|
||||
actions.push_back(action_to_json(a));
|
||||
actions.push_back(action_json(*a));
|
||||
|
||||
// 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
|
||||
@@ -917,7 +957,7 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
recent.resize(recent_limit);
|
||||
nlohmann::json recent_json = nlohmann::json::array();
|
||||
for (const AppAction* a : recent)
|
||||
recent_json.push_back(action_to_json(a));
|
||||
recent_json.push_back(action_json(*a));
|
||||
|
||||
return {{"actions", std::move(actions)},
|
||||
{"favourites", std::move(favourites)},
|
||||
@@ -937,14 +977,21 @@ nlohmann::json ActionRegistry::tab_options() const
|
||||
MainFrame* mf = wxGetApp().mainframe;
|
||||
if (!mf || !mf->m_tabpanel)
|
||||
return out;
|
||||
// English labels of the built-in pages whose id is not already their English name, so the tab
|
||||
// picker can be searched in English in any UI language.
|
||||
static const std::unordered_map<std::string, const char*> english_labels{
|
||||
{TAB_ID_MONITOR, L("Device")}, {TAB_ID_MONITOR_WEB, L("Device (Web)")}, {TAB_ID_MULTI_DEVICE, L("Multi-device")}};
|
||||
|
||||
Notebook* notebook = mf->m_tabpanel;
|
||||
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
|
||||
const wxString id = notebook->GetPageName(i);
|
||||
if (id.empty())
|
||||
continue;
|
||||
out.push_back({{"id", id.ToStdString()},
|
||||
{"title", notebook->GetPageLabel(i).ToStdString()},
|
||||
{"icon", notebook->GetPageIcon(i)}});
|
||||
const std::string title = notebook->GetPageLabel(i).ToStdString();
|
||||
nlohmann::json tab({{"id", id.ToStdString()}, {"title", title}, {"icon", notebook->GetPageIcon(i)}});
|
||||
if (auto it = english_labels.find(id.ToStdString()); it != english_labels.end() && title != it->second)
|
||||
tab["title_en"] = it->second;
|
||||
out.push_back(std::move(tab));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ struct AppAction
|
||||
// Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title
|
||||
// "Reverse on even", full_label "Overhang reversal"). Empty when the two agree.
|
||||
std::string full_label;
|
||||
// Search-only English counterparts of title(), group, source_name() and full_label, so a
|
||||
// localized UI can still be searched in English. Each is derived exactly like the localized
|
||||
// text it mirrors, minus the translation, so in an English UI it equals that text. Empty when
|
||||
// there is no translated counterpart.
|
||||
std::string title_en;
|
||||
std::string group_en;
|
||||
std::string source_en;
|
||||
std::string full_label_en;
|
||||
// Full wiki URL, when the action has one (SettingActions whose row declared a label_path).
|
||||
std::string help_url;
|
||||
|
||||
@@ -138,6 +146,11 @@ inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode
|
||||
// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin.
|
||||
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit);
|
||||
|
||||
// One action as the Speed Dial page receives it. Each English alias (title_en, group_en, source_en,
|
||||
// full_label_en) is sent only when it is non-empty and differs from its localized counterpart, so an
|
||||
// English UI receives none and searches exactly the displayed text.
|
||||
nlohmann::json action_json(const AppAction& a);
|
||||
|
||||
// Self-contained sink and single owner of runnable actions for the app session.
|
||||
//
|
||||
// Workflow:
|
||||
|
||||
@@ -1020,7 +1020,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
const BrimType brim_type = config->opt_enum<BrimType>("brim_type");
|
||||
const bool have_auto_brim_ear = brim_type == btEar && !is_belt_tilted;
|
||||
const bool have_painted_brim_ear = brim_type == btPainted && !is_belt_tilted;
|
||||
set_option_label("brim_width", have_auto_brim_ear ? _L("Brim ear radius") : _L("Brim width"));
|
||||
set_option_label("brim_width", have_auto_brim_ear ? L("Brim ear radius") : L("Brim width"));
|
||||
const auto brim_width = config->opt_float("brim_width");
|
||||
// Automatic brim ear settings require a non-zero brim width.
|
||||
toggle_field("brim_ears_max_angle", brim_width > 0.0f);
|
||||
|
||||
@@ -80,6 +80,8 @@ public:
|
||||
t_config_option_keys const &applying_keys() const;
|
||||
void toggle_field(const std::string& field_key, const bool toggle, int opt_index = -1);
|
||||
void toggle_line(const std::string& field_key, const bool toggle, int opt_index = -1);
|
||||
// Renames an option's row. `label` is the untranslated English msgid (an L("...") string); the
|
||||
// row translates it for display.
|
||||
void set_option_label(const std::string& field_key, const wxString& label, int opt_index = -1);
|
||||
|
||||
// FFF print
|
||||
|
||||
+106
-113
@@ -142,19 +142,24 @@ struct CommandAction : AppAction
|
||||
AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); }
|
||||
|
||||
explicit CommandAction(const NativeCommand& c)
|
||||
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName)
|
||||
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, I18N::translate_utf8(c.title), kOrcaSourceKey,
|
||||
kOrcaSourceName)
|
||||
, command_key(c.key)
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = c.group;
|
||||
this->input = c.input;
|
||||
this->icon = c.icon;
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = I18N::translate_utf8(c.group);
|
||||
this->title_en = c.title;
|
||||
this->group_en = c.group;
|
||||
this->input = c.input;
|
||||
this->icon = c.icon;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<NativeCommand> build_command_catalog()
|
||||
{
|
||||
std::vector<NativeCommand> out;
|
||||
// `title` and `group` are the untranslated English msgids, L("...")-marked at each call so xgettext
|
||||
// extracts them.
|
||||
auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
|
||||
std::string input = {}, std::string icon = {}) {
|
||||
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)});
|
||||
@@ -166,7 +171,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
};
|
||||
|
||||
// ---- Slice & Export ----
|
||||
add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) {
|
||||
add_with_icon("slice_and_preview", L("Slice and Preview"), L("Slice & Export"), "media_play", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->reslice();
|
||||
@@ -178,7 +183,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
});
|
||||
|
||||
add_with_icon(
|
||||
"go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer",
|
||||
"go_to_layer", L("Go to layer (percent)"), L("Commands"), "height_range_layer",
|
||||
[](const std::string& param) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
@@ -194,7 +199,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
// "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab
|
||||
// id back as `param` (same contract as go_to_layer's percent).
|
||||
add(
|
||||
"go_to_tab", _u8L("Go to tab..."), _u8L("Commands"),
|
||||
"go_to_tab", L("Go to tab..."), L("Commands"),
|
||||
[](const std::string& param) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty())
|
||||
mf->select_tab(from_u8(param));
|
||||
@@ -202,41 +207,41 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
},
|
||||
"tab");
|
||||
|
||||
add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) {
|
||||
add_with_icon("load_project", L("Load Project"), L("Commands"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->load_project();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) {
|
||||
add_with_icon("save_project", L("Save Project"), L("Commands"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->save_project(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) {
|
||||
add_with_icon("save_project_as", L("Save Project As"), L("Commands"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->save_project(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) {
|
||||
add_with_icon("open_preferences", L("Preferences"), L("Commands"), "cog", [](const std::string&) {
|
||||
wxGetApp().open_preferences();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Mode ----
|
||||
add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
add_with_icon("mode_simple", L("Mode: Simple"), L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comSimple);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
add_with_icon("mode_advanced", L("Mode: Advanced"), L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comAdvanced);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
add_with_icon("mode_expert", L("Mode: Expert"), L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comExpert);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
// Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI.
|
||||
add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
add_with_icon("toggle_developer_mode", L("Toggle Developer Mode"), L("Mode"), "advanced", [](const std::string&) {
|
||||
GUI_App& app = wxGetApp();
|
||||
const bool on = !app.app_config->get_bool("developer_mode");
|
||||
app.app_config->set_bool("developer_mode", on);
|
||||
@@ -246,27 +251,27 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
});
|
||||
|
||||
// ---- Export pipeline ----
|
||||
add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
add_with_icon("export_gcode", L("Export G-code"), L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_stl", L("Export STL"), L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) {
|
||||
add_with_icon("export_3mf", L("Export 3MF"), L("Slice & Export"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_core_3mf();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_sliced_file", L("Export Sliced File"), L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode_3mf(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_all_sliced_file", L("Export All Sliced Files"), L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode_3mf(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
@@ -275,39 +280,39 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
// ---- Calibration ----
|
||||
// The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's
|
||||
// light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green).
|
||||
add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature",
|
||||
add_with_icon("calib_temperature", L("Temperature Calibration"), L("Calibration"), "param_temperature",
|
||||
[](const std::string&) { return calib_command(CalibKind::Temperature); });
|
||||
add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed",
|
||||
add_with_icon("calib_max_volumetric", L("Max Volumetric Speed Calibration"), L("Calibration"), "param_volumetric_speed",
|
||||
[](const std::string&) { return calib_command(CalibKind::MaxVolumetric); });
|
||||
add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
add_with_icon("calib_pressure_advance", L("Pressure Advance Calibration"), L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
[](const std::string&) { return calib_command(CalibKind::PressureAdvance); });
|
||||
add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
add_with_icon("calib_flow_ratio", L("Flow Ratio Calibration"), L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
[](const std::string&) { return calib_command(CalibKind::FlowRatio); });
|
||||
add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction",
|
||||
add_with_icon("calib_retraction", L("Retraction Calibration"), L("Calibration"), "param_retraction",
|
||||
[](const std::string&) { return calib_command(CalibKind::Retraction); });
|
||||
add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision",
|
||||
add_with_icon("calib_cornering", L("Cornering Calibration"), L("Calibration"), "param_precision",
|
||||
[](const std::string&) { return calib_command(CalibKind::Cornering); });
|
||||
add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
|
||||
add_with_icon("calib_input_shaping_freq", L("Input Shaping Frequency Calibration"), L("Calibration"), "param_resonance_avoidance",
|
||||
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
|
||||
add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
|
||||
add_with_icon("calib_input_shaping_damp", L("Input Shaping Damping Calibration"), L("Calibration"), "param_resonance_avoidance",
|
||||
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
|
||||
add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); });
|
||||
add_with_icon("calib_vfa", L("VFA Calibration"), L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); });
|
||||
|
||||
// ---- View ----
|
||||
// Titles are built with _u8L here (not via a variable) so xgettext can extract them.
|
||||
// Titles are L("...")-marked here (not via a variable) so xgettext can extract them.
|
||||
for (auto [key, dir, title] :
|
||||
std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", _u8L("View: Top")},
|
||||
{"view_bottom", "bottom", _u8L("View: Bottom")},
|
||||
{"view_front", "front", _u8L("View: Front")},
|
||||
{"view_rear", "rear", _u8L("View: Rear")},
|
||||
{"view_left", "left", _u8L("View: Left")},
|
||||
{"view_right", "right", _u8L("View: Right")},
|
||||
{"view_iso", "iso", _u8L("View: Isometric")}}) {
|
||||
std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", L("View: Top")},
|
||||
{"view_bottom", "bottom", L("View: Bottom")},
|
||||
{"view_front", "front", L("View: Front")},
|
||||
{"view_rear", "rear", L("View: Rear")},
|
||||
{"view_left", "left", L("View: Left")},
|
||||
{"view_right", "right", L("View: Right")},
|
||||
{"view_iso", "iso", L("View: Isometric")}}) {
|
||||
std::string k = key, d = dir;
|
||||
add(k, title, _u8L("View"),
|
||||
add(k, title, L("View"),
|
||||
[d](const std::string&) { return view_command(wxGetApp().plater(), d); });
|
||||
}
|
||||
add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) {
|
||||
add("view_default", L("View: Default"), L("View"), [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->select_view("plate");
|
||||
@@ -316,75 +321,75 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) {
|
||||
add("view_fit_bed", L("Fit Bed to View"), L("View"), [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
canvas->zoom_to_bed();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) {
|
||||
add("view_toggle_perspective", L("Toggle Perspective"), L("View"), [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->get_camera().select_next_type();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) {
|
||||
add_with_icon("reset_window_layout", L("Reset Window Layout"), L("View"), "toolbar_reset", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->reset_window_layout();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Object ----
|
||||
add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) {
|
||||
add_with_icon("obj_delete", L("Delete Selected"), L("Object"), "delete", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
|
||||
});
|
||||
add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) {
|
||||
add_with_icon("obj_delete_all", L("Delete All Objects"), L("Object"), "delete", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
|
||||
});
|
||||
add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) {
|
||||
add_with_icon("obj_mirror_x", L("Mirror X"), L("Object"), "menu_mirror_x", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
|
||||
});
|
||||
add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) {
|
||||
add_with_icon("obj_mirror_y", L("Mirror Y"), L("Object"), "menu_mirror_y", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
|
||||
});
|
||||
add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) {
|
||||
add_with_icon("obj_mirror_z", L("Mirror Z"), L("Object"), "menu_mirror_z", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
|
||||
});
|
||||
add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) {
|
||||
add_with_icon("obj_split_objects", L("Split to Objects"), L("Object"), "menu_split_objects", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
|
||||
});
|
||||
add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) {
|
||||
add_with_icon("obj_split_parts", L("Split to Parts"), L("Object"), "menu_split_parts", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
|
||||
});
|
||||
add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) {
|
||||
add("obj_center", L("Center Selected on Plate"), L("Object"), [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
add_with_icon("obj_drop", L("Drop to Bed"), L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) {
|
||||
add("obj_fit_volume", L("Scale to Fit Print Volume"), L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); },
|
||||
[](Plater* p) { p->scale_selection_to_fit_print_volume(); });
|
||||
});
|
||||
add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) {
|
||||
add_with_icon("obj_instances_up", L("Increase Instances"), L("Object"), "instance_add", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
|
||||
});
|
||||
add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) {
|
||||
add_with_icon("obj_instances_down", L("Decrease Instances"), L("Object"), "instance_remove", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
|
||||
});
|
||||
add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) {
|
||||
add_with_icon("obj_arrange", L("Auto-Arrange"), L("Object"), "toolbar_arrange", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
|
||||
});
|
||||
add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) {
|
||||
add_with_icon("obj_orient", L("Auto-Orient"), L("Object"), "toolbar_orient", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
|
||||
});
|
||||
|
||||
// ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object)
|
||||
auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) {
|
||||
add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) {
|
||||
add_with_icon(std::move(key), std::move(title), L("Add Primitive"), std::move(icon), [type_name](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
ensure_3d_view(plater);
|
||||
@@ -394,13 +399,13 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
};
|
||||
add_primitive("add_primitive_cube", _u8L("Cube"), "menu_obj_cube", "Cube");
|
||||
add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder");
|
||||
add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere");
|
||||
add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone");
|
||||
add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc");
|
||||
add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus");
|
||||
add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) {
|
||||
add_primitive("add_primitive_cube", L("Cube"), "menu_obj_cube", "Cube");
|
||||
add_primitive("add_primitive_cylinder", L("Cylinder"), "menu_obj_cylinder", "Cylinder");
|
||||
add_primitive("add_primitive_sphere", L("Sphere"), "menu_obj_sphere", "Sphere");
|
||||
add_primitive("add_primitive_cone", L("Cone"), "menu_obj_cone", "Cone");
|
||||
add_primitive("add_primitive_disc", L("Disc"), "menu_obj_disc", "Disc");
|
||||
add_primitive("add_primitive_torus", L("Torus"), "menu_obj_torus", "Torus");
|
||||
add_with_icon("add_primitive_text", L("Text"), L("Add Primitive"), "menu_obj_text", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
ensure_3d_view(plater);
|
||||
@@ -410,7 +415,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](const std::string&) {
|
||||
add_with_icon("add_primitive_svg", L("SVG"), L("Add Primitive"), "menu_obj_svg", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
ensure_3d_view(plater);
|
||||
@@ -424,7 +429,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
// ---- Add Handy models ---- (the Add > Add Handy models submenu)
|
||||
const std::vector<MenuFactory::HandyModel>& handy = MenuFactory::handy_models();
|
||||
for (std::size_t i = 0; i < handy.size(); ++i) {
|
||||
add("add_handy_" + std::string(handy[i].key), Slic3r::GUI::I18N::translate_utf8(handy[i].label), _u8L("Add Handy models"),
|
||||
add("add_handy_" + std::string(handy[i].key), handy[i].label, L("Add Handy models"),
|
||||
[i](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
ensure_3d_view(plater);
|
||||
@@ -434,7 +439,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
}
|
||||
|
||||
// ---- Plate ----
|
||||
add_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) {
|
||||
add_with_icon("plate_add", L("Add Plate"), L("Plate"), "toolbar_add_plate", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
@@ -443,7 +448,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
plater->add_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) {
|
||||
add_with_icon("plate_duplicate", L("Duplicate Plate"), L("Plate"), "menu_copy", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
@@ -452,7 +457,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
plater->duplicate_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) {
|
||||
add_with_icon("plate_delete", L("Delete Plate"), L("Plate"), "delete", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
@@ -461,7 +466,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
plater->delete_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) {
|
||||
add_with_icon("plate_rename", L("Rename Plate"), L("Plate"), "plate_name_edit", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
@@ -472,7 +477,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) {
|
||||
add_with_icon("plate_toggle_lock", L("Toggle Plate Lock"), L("Plate"), "lock_normal", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
@@ -482,7 +487,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
plates.lock_plate(index, !plates.is_locked(index));
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) {
|
||||
add_with_icon("plate_goto", L("Go to Plate"), L("Plate"), "go_next_plate", [](const std::string& param) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
@@ -500,7 +505,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
});
|
||||
|
||||
// ---- Printer / device connection ----
|
||||
add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) {
|
||||
add_with_icon("sync_ams", L("Synchronize Filament List from AMS"), L("Printer"), "ams_fila_sync", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
if (dev && dev->get_selected_machine() && plater) {
|
||||
@@ -511,11 +516,11 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
});
|
||||
|
||||
// ---- Presets / cloud ----
|
||||
add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) {
|
||||
add_with_icon("preset_bundle", L("Open Preset Bundle"), L("Presets"), "menu_edit_preset", [](const std::string&) {
|
||||
wxGetApp().open_presetbundledialog();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) {
|
||||
add_with_icon("sync_presets", L("Sync Presets"), L("Presets"), "printer_sync_ok", [](const std::string&) {
|
||||
if (!wxGetApp().is_user_login())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")};
|
||||
wxGetApp().restart_sync_user_preset();
|
||||
@@ -523,7 +528,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
});
|
||||
|
||||
// ---- Import ----
|
||||
add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
add_with_icon("import_file", L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), L("Import"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater()) {
|
||||
#ifdef __APPLE__
|
||||
plater->add_model();
|
||||
@@ -533,71 +538,71 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
add_with_icon("import_zip_archive", L("Import ZIP Archive"), L("Import"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->import_zip_archive();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
add_with_icon("import_configs", L("Import Configs"), L("Import"), "menu_open", [](const std::string&) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe)
|
||||
mf->load_config_file();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Export extras ----
|
||||
add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_stl_multi", L("Export All Objects as STLs"), L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_drc_single", L("Export All Objects as DRC (one file)"), L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, false, FT_DRC);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_drc_multi", L("Export All Objects as DRCs"), L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, true, FT_DRC);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
add_with_icon("export_toolpaths_obj", L("Export Toolpaths as OBJ"), L("Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_toolpaths_to_obj();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) {
|
||||
add_with_icon("export_config", L("Export Preset Bundle"), L("Export"), "save", [](const std::string&) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe)
|
||||
mf->export_config();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links)
|
||||
add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) {
|
||||
add("help_keyboard_shortcuts", L("Keyboard Shortcuts"), L("Help"), [](const std::string&) {
|
||||
wxGetApp().keyboard_shortcuts(ShortcutContext::Global);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
|
||||
add("help_setup_wizard", L("Setup Wizard"), L("Help"), [](const std::string&) {
|
||||
wxGetApp().ShowUserGuide();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) {
|
||||
add_with_icon("help_open_config_folder", L("Show Configuration Folder"), L("Help"), "open_project", [](const std::string&) {
|
||||
Slic3r::GUI::desktop_open_datadir_folder();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) {
|
||||
add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) {
|
||||
wxGetApp().troubleshoot();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) {
|
||||
add("help_network_test", L("Open Network Test"), L("Help"), [](const std::string&) {
|
||||
NetworkTestDialog dlg(wxGetApp().mainframe);
|
||||
dlg.ShowModal();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_benchmark_3d_scene", _u8L("Benchmark 3D Scene"), _u8L("Help"), [](const std::string&) {
|
||||
add("help_benchmark_3d_scene", L("Benchmark 3D Scene"), L("Help"), [](const std::string&) {
|
||||
run_scene_benchmark();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) {
|
||||
add_with_icon("help_tip_of_the_day", L("Show Tip of the Day"), L("Help"), "info", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater()) {
|
||||
plater->get_dailytips()->open();
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
@@ -605,37 +610,37 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) {
|
||||
add_with_icon("help_check_updates", L("Check for Updates"), L("Help"), "refresh", [](const std::string&) {
|
||||
wxGetApp().check_new_version_sf(true, 1);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) {
|
||||
add_with_icon("help_about", L("About OrcaSlicer"), L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) {
|
||||
Slic3r::GUI::about();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) {
|
||||
add_with_icon("open_wiki", L("Open Wiki"), L("Help"), "link_wiki_img", [](const std::string&) {
|
||||
wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) {
|
||||
add("open_youtube", L("Open YouTube Channel"), L("Help"), [](const std::string&) {
|
||||
wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Plugins ----
|
||||
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||
add("open_plugins", L("Open Plugins"), L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().open_plugins_dialog();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||
add("refresh_plugins", L("Refresh Plugins"), L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().refresh_plugins();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) {
|
||||
add("install_plugin", L("Install Plugin"), L("Plugins"), [](const std::string&) {
|
||||
open_plugin_hub();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) {
|
||||
add("install_local_plugin", L("Install Local Plugin"), L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().install_local_plugin();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
@@ -643,24 +648,12 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<NativeCommand>& catalog_storage()
|
||||
{
|
||||
static std::vector<NativeCommand> commands = build_command_catalog();
|
||||
return commands;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<NativeCommand>& NativeCommands::catalog()
|
||||
{
|
||||
return catalog_storage();
|
||||
}
|
||||
|
||||
void NativeCommands::rebuild_catalog()
|
||||
{
|
||||
// build_command_catalog() re-runs _u8L under the current locale, so replacing the storage
|
||||
// refreshes every translated title/group after a language switch.
|
||||
catalog_storage() = build_command_catalog();
|
||||
static const std::vector<NativeCommand> commands = build_command_catalog();
|
||||
return commands;
|
||||
}
|
||||
|
||||
std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command)
|
||||
|
||||
@@ -16,27 +16,25 @@ namespace Slic3r { namespace GUI {
|
||||
struct NativeCommand
|
||||
{
|
||||
std::string key;
|
||||
std::string title;
|
||||
std::string group;
|
||||
std::string title; // untranslated English msgid; make_action() translates it for display
|
||||
std::string group; // likewise
|
||||
std::string input; // "percent"/"tab" or "" for immediate run
|
||||
std::string icon; // SVG base name, or "" to render a blank tile
|
||||
std::function<AppActionRunResult(const std::string& param)> runner;
|
||||
};
|
||||
|
||||
namespace NativeCommands {
|
||||
// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a
|
||||
// live UI language switch so the translated titles/groups match the new locale. UI thread only.
|
||||
// The full built-in command catalog. Built on first use and reused; it holds no translated text, so
|
||||
// it stays valid across a UI language switch. UI thread only.
|
||||
const std::vector<NativeCommand>& catalog();
|
||||
|
||||
// Rebuilds the catalog in the current locale. UI thread only.
|
||||
void rebuild_catalog();
|
||||
|
||||
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.
|
||||
AppActionRunResult run(const std::string& key, const std::string& param = {});
|
||||
|
||||
// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity,
|
||||
// presentation and behaviour as the single source of truth; ActionRegistry only stores and
|
||||
// dispatches the result. UI thread only.
|
||||
// Materialises one catalog entry as a runnable AppAction, titled in the current UI language with the
|
||||
// English title/group kept as search aliases. Keeps the catalog's identity, presentation and
|
||||
// behaviour as the single source of truth; ActionRegistry only stores and dispatches the result.
|
||||
// UI thread only.
|
||||
std::unique_ptr<AppAction> make_action(const NativeCommand& command);
|
||||
} // namespace NativeCommands
|
||||
|
||||
|
||||
@@ -275,7 +275,8 @@ void OptionsGroup::append_line(const Line& line)
|
||||
const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ?
|
||||
_L_CONTEXT(leaf_src, "Layers") :
|
||||
_(leaf_src);
|
||||
index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi));
|
||||
index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi),
|
||||
Search::compose_display_label(line.label_en, from_u8(leaf_src), multi));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +651,7 @@ Line OptionsGroup::create_single_option_line(const Option& option, const std::st
|
||||
{
|
||||
wxString tooltip = _(get_formatted_tooltip_text(option.opt, option.opt_id));
|
||||
|
||||
Line retval{_(option.opt.label), tooltip};
|
||||
Line retval{from_u8(option.opt.label), tooltip};
|
||||
retval.label_path = path;
|
||||
retval.append_option(option);
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class Line : public UndoValueUIManager
|
||||
bool m_is_separator{ false };
|
||||
public:
|
||||
wxString label;
|
||||
wxString label_en; // untranslated English of `label`, searchable by the Speed Dial in any UI language
|
||||
wxString label_tooltip;
|
||||
std::string label_path;
|
||||
bool undo_to_sys{false}; // BBS: object config
|
||||
@@ -86,12 +87,16 @@ public:
|
||||
void append_widget(const widget_t widget) {
|
||||
m_extra_widgets.push_back(widget);
|
||||
}
|
||||
// `label` and `tooltip` are the untranslated English msgids; they are translated for display.
|
||||
Line(wxString label, wxString tooltip) :
|
||||
label(_(label)), label_tooltip(_(tooltip)) {}
|
||||
label(_(label)), label_en(label), label_tooltip(_(tooltip)) {}
|
||||
Line() : m_is_separator(true) {}
|
||||
|
||||
// `new_label` is the untranslated English msgid (an L("...") string), like the ctor's label:
|
||||
// it is translated for display and kept as label_en.
|
||||
void set_label(const wxString& new_label) {
|
||||
label = new_label;
|
||||
label = _(new_label);
|
||||
label_en = new_label;
|
||||
if (label_widget != nullptr) {
|
||||
label_widget->SetLabel(label + (label.IsEmpty() ? "" : ": "));
|
||||
label_widget->Refresh();
|
||||
|
||||
@@ -63,7 +63,7 @@ static Option make_option(const std::string &key, Preset::Type type, const wxStr
|
||||
GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path,
|
||||
// The settings page draws Line::label; carrying it lets the Speed Dial name a setting
|
||||
// the way the page does. `label`/`label_local` stay the search-oriented name.
|
||||
into_u8(gc.line_label)};
|
||||
into_u8(gc.line_label), into_u8(gc.line_label_en)};
|
||||
return option;
|
||||
}
|
||||
|
||||
@@ -250,11 +250,13 @@ void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, cons
|
||||
m_groups_and_categories[get_key(opt_key, type)].path = path;
|
||||
}
|
||||
|
||||
void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label)
|
||||
void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label, const wxString &label_en)
|
||||
{
|
||||
if (label.IsEmpty())
|
||||
return;
|
||||
m_groups_and_categories[get_key(opt_key, type)].line_label = label;
|
||||
GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)];
|
||||
gc.line_label = label;
|
||||
gc.line_label_en = label_en;
|
||||
}
|
||||
|
||||
} // namespace Search
|
||||
|
||||
@@ -28,12 +28,14 @@ struct GroupAndCategory
|
||||
wxString group;
|
||||
wxString category;
|
||||
wxString icon; // icon of the group's own header, or empty
|
||||
wxString line_label; // label the settings row actually draws (Line::label), or empty
|
||||
std::string path; // wiki path (Line::label_path) of the option's line, or empty
|
||||
wxString line_label; // label the settings row actually draws (Line::label), or empty
|
||||
wxString line_label_en; // untranslated English of line_label, or empty
|
||||
std::string path; // wiki path (Line::label_path) of the option's line, or empty
|
||||
};
|
||||
|
||||
// Title for a setting: the row label, qualified with the field leaf when the row packs several
|
||||
// options (e.g. "Cool Plate \u2013 First layer"). Pure; inputs are already localized.
|
||||
// options (e.g. "Cool Plate \u2013 First layer"). Pure; both inputs are in the same language (the
|
||||
// localized title, or its English counterpart).
|
||||
inline wxString compose_display_label(const wxString& line_label, const wxString& leaf_label, bool multi)
|
||||
{
|
||||
if (line_label.empty())
|
||||
@@ -73,6 +75,7 @@ struct Option
|
||||
std::string tooltip; // localized ConfigOptionDef::tooltip, or empty
|
||||
std::string wiki_path; // Line::label_path for the option's row, or empty
|
||||
std::string display_label; // label the settings row draws (localized); empty falls back to label
|
||||
std::string display_label_en; // untranslated English of display_label, or empty
|
||||
|
||||
std::string opt_key() const;
|
||||
};
|
||||
@@ -98,9 +101,9 @@ public:
|
||||
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category,
|
||||
const wxString &icon = wxEmptyString);
|
||||
void set_path(const std::string &opt_key, Preset::Type type, const std::string &path);
|
||||
// Record the label the option's row draws, so the Speed Dial names a setting like the page
|
||||
// (ConfigOptionDef::label/full_label is a search name, not the row text).
|
||||
void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label);
|
||||
// Record the label the option's row draws and its untranslated English, so the Speed Dial names a
|
||||
// setting like the page (ConfigOptionDef::label/full_label is a search name, not the row text).
|
||||
void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label, const wxString &label_en);
|
||||
|
||||
const std::vector<Option> &options() const { return m_options; }
|
||||
const std::vector<Option> &all_options() const { return m_all_modes; }
|
||||
|
||||
@@ -817,23 +817,24 @@ Slic3r::GUI::PageShp Tab::add_options_page(const wxString& title, const std::str
|
||||
// Names of categories is save in English always. We translate them only for UI.
|
||||
// But category "Extruder n" can't be translated regularly (using _()), so
|
||||
// just for this category we should splite the title and translate "Extruder" word separately
|
||||
wxString Tab::translate_category(const wxString& title, Preset::Type preset_type)
|
||||
wxString Tab::translate_category(const wxString& title, Preset::Type preset_type, bool translate)
|
||||
{
|
||||
auto tr = [translate](const wxString& s) { return translate ? _(s) : s; };
|
||||
if (preset_type == Preset::TYPE_PRINTER && title.Contains("Extruder ")) {
|
||||
auto preset = wxGetApp().preset_bundle;
|
||||
if (preset && preset->is_bbl_vendor()) {
|
||||
if (title == "Extruder 1") return _("Left Extruder");
|
||||
if (title == "Extruder 2") return _("Right Extruder");
|
||||
if (title == "Extruder 1") return tr(L("Left Extruder"));
|
||||
if (title == "Extruder 2") return tr(L("Right Extruder"));
|
||||
}
|
||||
return _("Extruder") + title.SubString(8, title.Last());
|
||||
return tr(L("Extruder")) + title.SubString(8, title.Last());
|
||||
}
|
||||
// Orca: one "Extruder" page serves all extruders; name it "Extruders" when there are several.
|
||||
if (preset_type == Preset::TYPE_PRINTER && title == "Extruder") {
|
||||
auto preset = wxGetApp().preset_bundle;
|
||||
if (preset && preset->get_printer_extruder_count() > 1)
|
||||
return _("Extruders");
|
||||
return tr(L("Extruders"));
|
||||
}
|
||||
return _(title);
|
||||
return tr(title);
|
||||
}
|
||||
|
||||
void Tab::OnActivate()
|
||||
@@ -1865,14 +1866,14 @@ Tab::SettingRowState Tab::setting_row_state(const std::string &opt_id) const
|
||||
if (opt.opt_id != opt_id)
|
||||
continue;
|
||||
if (line.toggle_visible) // shown on any owning page is enough
|
||||
return {true, line.label, line.get_options().size() > 1};
|
||||
return {true, line.label, line.label_en, line.get_options().size() > 1};
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Never registered on a page -> visible, but with no row label to contribute.
|
||||
return {!found, wxString(), false};
|
||||
return {!found, wxString(), wxString(), false};
|
||||
}
|
||||
|
||||
// To be called by custom widgets, load a value into a config,
|
||||
|
||||
@@ -405,7 +405,11 @@ public:
|
||||
void on_roll_back_value(const bool to_sys = false);
|
||||
|
||||
PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false);
|
||||
static wxString translate_category(const wxString& title, Preset::Type preset_type);
|
||||
// The name a category is displayed under. A few printer pages are renamed for display (BBL
|
||||
// "Extruder 1/2" -> "Left/Right Extruder", the shared "Extruder" page -> "Extruders" when there are
|
||||
// several). With `translate` false the same name is returned untranslated, i.e. as an English UI
|
||||
// displays it.
|
||||
static wxString translate_category(const wxString& title, Preset::Type preset_type, bool translate = true);
|
||||
|
||||
virtual void OnActivate();
|
||||
virtual void on_preset_loaded() {}
|
||||
@@ -432,6 +436,8 @@ public:
|
||||
Field* get_field(const t_config_option_key &opt_key, Page** selected_page, int opt_index = -1);
|
||||
void toggle_option(const std::string &opt_key, bool toggle, int opt_index = -1);
|
||||
void toggle_line(const std::string &opt_key, bool toggle, int opt_index = -1); // BBS: hide some line
|
||||
// Renames the row that owns the option. `label` is the untranslated English msgid (an L("...")
|
||||
// string); it is translated for display and kept as the row's English search alias.
|
||||
void set_option_label(const std::string &opt_key, const wxString &label, int opt_index = -1);
|
||||
|
||||
// Live state of the settings row that owns an option, read from the built pages.
|
||||
@@ -439,6 +445,7 @@ public:
|
||||
{
|
||||
bool visible{true}; // false when ConfigManipulation hides the row
|
||||
wxString label; // Line::label the row draws (may change at runtime)
|
||||
wxString label_en; // Line::label_en, the untranslated English of label
|
||||
bool multi{false}; // row packs several options, so label is precomposed
|
||||
};
|
||||
SettingRowState setting_row_state(const std::string &opt_id) const;
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include "slic3r/GUI/ActionRegistry.hpp"
|
||||
#include "slic3r/GUI/GUI_Factories.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/NativeCommands.hpp"
|
||||
#include "slic3r/GUI/OptionsGroup.hpp"
|
||||
#include "slic3r/GUI/SettingsIndex.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using Slic3r::GUI::AppAction;
|
||||
@@ -27,7 +34,9 @@ namespace {
|
||||
class TestAppAction final : public AppAction
|
||||
{
|
||||
public:
|
||||
TestAppAction() : AppAction("test", "Action title", "src-key", "Action source") {}
|
||||
explicit TestAppAction(std::string title = "Action title", std::string source_name = "Action source")
|
||||
: AppAction("test", std::move(title), "src-key", std::move(source_name))
|
||||
{}
|
||||
|
||||
AppActionRunResult run(const std::string& param = {}) const override { return {}; }
|
||||
};
|
||||
@@ -93,7 +102,9 @@ TEST_CASE("Command actions are keyed by catalog key, not display title", "[Actio
|
||||
}
|
||||
|
||||
// The real catalog -> action mapping keys by the stable catalog key and copies presentation from the
|
||||
// catalog, so a rename or a UI-language switch never re-keys the action.
|
||||
// catalog, so a rename or a UI-language switch never re-keys the action. The catalog's English title
|
||||
// and group become the action's English search aliases; no translation catalog is loaded in the tests,
|
||||
// so the displayed title and group equal them.
|
||||
TEST_CASE("Command action construction keys by catalog key", "[ActionSource][SpeedDial]")
|
||||
{
|
||||
const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog();
|
||||
@@ -106,6 +117,8 @@ TEST_CASE("Command action construction keys by catalog key", "[ActionSource][Spe
|
||||
CHECK(action->id() != AppAction::compose_id("orca_command", c.title, "orca"));
|
||||
CHECK(action->title() == c.title);
|
||||
CHECK(action->group == c.group);
|
||||
CHECK(action->title_en == c.title);
|
||||
CHECK(action->group_en == c.group);
|
||||
CHECK(action->input == c.input);
|
||||
CHECK(action->icon == c.icon);
|
||||
}
|
||||
@@ -171,6 +184,7 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[ActionS
|
||||
for (const auto& c : commands) {
|
||||
CHECK_FALSE(c.key.empty());
|
||||
CHECK_FALSE(c.title.empty());
|
||||
CHECK_FALSE(c.group.empty());
|
||||
// A duplicated key would silently shadow the earlier command in the palette.
|
||||
CHECK(seen.insert(c.key).second);
|
||||
}
|
||||
@@ -288,6 +302,16 @@ TEST_CASE("Native command catalog covers the Add menus", "[ActionSource][SpeedDi
|
||||
REQUIRE(group != nullptr);
|
||||
CHECK(*group == *handy_group);
|
||||
}
|
||||
|
||||
// A handy model's command is titled by the English label its Add-menu entry is built from, so it is
|
||||
// searchable in English in any UI language.
|
||||
for (const auto& model : Slic3r::GUI::MenuFactory::handy_models()) {
|
||||
const std::string key = "add_handy_" + std::string(model.key);
|
||||
INFO(key);
|
||||
const auto it = std::find_if(commands.begin(), commands.end(), [&key](const auto& c) { return c.key == key; });
|
||||
REQUIRE(it != commands.end());
|
||||
CHECK(it->title == model.label);
|
||||
}
|
||||
}
|
||||
|
||||
// A setting action is named like its settings row, not the ConfigOptionDef label: the row's
|
||||
@@ -319,6 +343,61 @@ TEST_CASE("Setting display labels mirror the settings row", "[ActionSource][Spee
|
||||
CHECK(resolve_setting_title(L"", L"", false).IsEmpty());
|
||||
}
|
||||
|
||||
// The page receives an English alias only where it differs from the displayed text, so an English UI
|
||||
// (where every alias equals its text) receives none and searches exactly the displayed text.
|
||||
TEST_CASE("Action JSON sends English aliases only when they differ from the displayed text", "[ActionSource][SpeedDial]")
|
||||
{
|
||||
const char* aliases[] = {"title_en", "group_en", "source_en", "full_label_en"};
|
||||
|
||||
TestAppAction english("Reverse on even", "Process : Quality : Overhangs");
|
||||
english.full_label = "Overhang reversal";
|
||||
english.title_en = english.title();
|
||||
english.source_en = english.source_name();
|
||||
english.full_label_en = english.full_label;
|
||||
const nlohmann::json same = Slic3r::GUI::action_json(english);
|
||||
for (const char* key : aliases) {
|
||||
INFO(key);
|
||||
CHECK_FALSE(same.contains(key));
|
||||
}
|
||||
CHECK(same.at("title") == "Reverse on even");
|
||||
|
||||
// A translated action (German stands in for any UI language) carries each differing English name.
|
||||
TestAppAction localized("Bei geraden Schichten umkehren", "Prozess : Qualitaet : Ueberhaenge");
|
||||
localized.group = "Befehle";
|
||||
localized.full_label = "Ueberhangumkehr";
|
||||
localized.title_en = "Reverse on even";
|
||||
localized.group_en = "Commands";
|
||||
localized.source_en = "Process : Quality : Overhangs";
|
||||
localized.full_label_en = "Overhang reversal";
|
||||
const nlohmann::json differs = Slic3r::GUI::action_json(localized);
|
||||
CHECK(differs.at("title_en") == "Reverse on even");
|
||||
CHECK(differs.at("group_en") == "Commands");
|
||||
CHECK(differs.at("source_en") == "Process : Quality : Overhangs");
|
||||
CHECK(differs.at("full_label_en") == "Overhang reversal");
|
||||
CHECK(differs.at("title") == "Bei geraden Schichten umkehren");
|
||||
|
||||
// No English counterpart (a plugin, a file name) sends nothing.
|
||||
const nlohmann::json none = Slic3r::GUI::action_json(TestAppAction("Optimize", "Gcode Optimizer"));
|
||||
for (const char* key : aliases) {
|
||||
INFO(key);
|
||||
CHECK_FALSE(none.contains(key));
|
||||
}
|
||||
}
|
||||
|
||||
// A settings row keeps the untranslated English of its label, from the ctor and from a runtime
|
||||
// rename, so the Speed Dial can offer the row's English name in any UI language. No catalog is loaded
|
||||
// in the tests, so the displayed label equals the English.
|
||||
TEST_CASE("Settings rows keep the English of their label", "[ActionSource][SpeedDial]")
|
||||
{
|
||||
Slic3r::GUI::Line line{L("Cool Plate"), ""};
|
||||
CHECK(line.label_en == "Cool Plate");
|
||||
CHECK(line.label == "Cool Plate");
|
||||
|
||||
line.set_label(L("Brim ear radius"));
|
||||
CHECK(line.label_en == "Brim ear radius");
|
||||
CHECK(line.label == "Brim ear radius");
|
||||
}
|
||||
|
||||
// A setting whose mode is above the user's current mode must be prompted before it can be edited.
|
||||
// Developer settings (comDevelop) are above every non-developer mode, so they always prompt then.
|
||||
TEST_CASE("Settings above the current mode require a switch", "[ActionSource][SpeedDial]")
|
||||
|
||||
Reference in New Issue
Block a user