mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
Add Help actions. Add Toggle Developer action. Add warning popup when switching between process mode. Add primitives/handy models actions
This commit is contained in:
@@ -10,6 +10,11 @@ var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
|
|||||||
var lastResizeHeight = 0;
|
var lastResizeHeight = 0;
|
||||||
var matchIndex = {};
|
var matchIndex = {};
|
||||||
|
|
||||||
|
// The user's current settings mode (from the C++ payload) plus the rank order of the modes. Each
|
||||||
|
// action carries the mode it requires, so "would this need a switch?" is a rank comparison.
|
||||||
|
var USER_MODE = "simple";
|
||||||
|
var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 };
|
||||||
|
|
||||||
// ---- windowed list render ----------------------------------------------------
|
// ---- windowed list render ----------------------------------------------------
|
||||||
// The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build
|
// The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build
|
||||||
// the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h,
|
// the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h,
|
||||||
@@ -105,11 +110,15 @@ function searchActions(actions, query) {
|
|||||||
matchIndex = {};
|
matchIndex = {};
|
||||||
if (!q) { searchNeedle = ""; return list.slice(0); }
|
if (!q) { searchNeedle = ""; return list.slice(0); }
|
||||||
searchNeedle = NormText(q, false);
|
searchNeedle = NormText(q, false);
|
||||||
|
// Mode keywords ("advanced"/"expert"/"developer") are a union, not a filter: the normal text
|
||||||
|
// search still runs on the full query, and every setting requiring a named mode is appended.
|
||||||
|
var modes = modeFilterFromQuery(q);
|
||||||
// Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks
|
// 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.
|
// between fields, and EscapeRegExp keeps regex metachars in the query literal.
|
||||||
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
|
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
|
||||||
|
|
||||||
var scored = [];
|
var scored = [];
|
||||||
|
var seen = {};
|
||||||
for (var i = 0; i < list.length; i++) {
|
for (var i = 0; i < list.length; i++) {
|
||||||
var a = list[i];
|
var a = list[i];
|
||||||
var t = fieldMatchScore(titleNorm(a), wwRe);
|
var t = fieldMatchScore(titleNorm(a), wwRe);
|
||||||
@@ -126,13 +135,28 @@ function searchActions(actions, query) {
|
|||||||
useEyebrowGroup: !!(a.group)
|
useEyebrowGroup: !!(a.group)
|
||||||
};
|
};
|
||||||
scored.push({ a: a, s: score });
|
scored.push({ a: a, s: score });
|
||||||
|
seen[a.id] = true;
|
||||||
}
|
}
|
||||||
scored.sort(function (x, y) {
|
scored.sort(function (x, y) {
|
||||||
if (x.s !== y.s) return y.s - x.s;
|
if (x.s !== y.s) return y.s - x.s;
|
||||||
if (x.a.title !== y.a.title) return x.a.title < y.a.title ? -1 : 1;
|
if (x.a.title !== y.a.title) return x.a.title < y.a.title ? -1 : 1;
|
||||||
return x.a.id < y.a.id ? -1 : x.a.id > y.a.id ? 1 : 0;
|
return x.a.id < y.a.id ? -1 : x.a.id > y.a.id ? 1 : 0;
|
||||||
});
|
});
|
||||||
return scored.map(function (e) { return e.a; });
|
var result = scored.map(function (e) { return e.a; });
|
||||||
|
if (modes.length) {
|
||||||
|
// Settings requiring a named mode, after the ranked text matches and without duplicates.
|
||||||
|
var extras = [];
|
||||||
|
for (var j = 0; j < list.length; j++) {
|
||||||
|
if (!seen[list[j].id] && modes.indexOf(list[j].mode) !== -1)
|
||||||
|
extras.push(list[j]);
|
||||||
|
}
|
||||||
|
extras.sort(function (x, y) {
|
||||||
|
if (x.title !== y.title) return x.title < y.title ? -1 : 1;
|
||||||
|
return x.id < y.id ? -1 : x.id > y.id ? 1 : 0;
|
||||||
|
});
|
||||||
|
result = result.concat(extras);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pure: how many rows must be materialized to cover the given starting index plus `size` more.
|
// Pure: how many rows must be materialized to cover the given starting index plus `size` more.
|
||||||
@@ -264,6 +288,40 @@ function prettySource(source) {
|
|||||||
return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True when the action's required mode is above the user's current mode, i.e. selecting it will
|
||||||
|
// prompt a mode switch. Unknown/empty modes are treated as "simple" so commands never flag.
|
||||||
|
function needsModeSwitch(item, userMode) {
|
||||||
|
var need = MODE_RANK[(item && item.mode) || "simple"] || 0;
|
||||||
|
var have = MODE_RANK[userMode || "simple"] || 0;
|
||||||
|
return need > have;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short mode tag for an action that needs a switch, or "" when it is already available.
|
||||||
|
function modeBadge(item, userMode) {
|
||||||
|
if (!needsModeSwitch(item, userMode)) return "";
|
||||||
|
if (item.mode === "develop") return "Developer";
|
||||||
|
if (item.mode === "expert") return "Expert";
|
||||||
|
if (item.mode === "advanced") return "Advanced";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search keywords that name a settings mode. "developer" (and the internal "develop") both select
|
||||||
|
// Developer; Simple is deliberately absent so it never floods the list with every command.
|
||||||
|
var MODE_WORDS = { advanced: "advanced", expert: "expert", developer: "develop", develop: "develop" };
|
||||||
|
|
||||||
|
// The mode values named as whole words in `query`, deduped. Case/diacritic-insensitive via Norm. A
|
||||||
|
// query with no mode keyword returns [] so the normal text search is completely unaffected.
|
||||||
|
function modeFilterFromQuery(query) {
|
||||||
|
var norm = NormText(String(query || "").trim(), false);
|
||||||
|
var found = [];
|
||||||
|
norm.split(/[^a-z0-9]+/).forEach(function (word) {
|
||||||
|
var mode = MODE_WORDS[word];
|
||||||
|
if (mode && found.indexOf(mode) === -1)
|
||||||
|
found.push(mode);
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another
|
// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another
|
||||||
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
||||||
function actionLabel(action, actions) {
|
function actionLabel(action, actions) {
|
||||||
@@ -289,6 +347,7 @@ function stateFromPayload(payload) {
|
|||||||
actions: payload.actions || [],
|
actions: payload.actions || [],
|
||||||
favourites: payload.favourites || [],
|
favourites: payload.favourites || [],
|
||||||
recent: payload.recent || [],
|
recent: payload.recent || [],
|
||||||
|
userMode: payload.user_mode || "simple",
|
||||||
query: "",
|
query: "",
|
||||||
sel: { zone: "list", i: 0 },
|
sel: { zone: "list", i: 0 },
|
||||||
lastResizeHeight: 0,
|
lastResizeHeight: 0,
|
||||||
@@ -348,6 +407,7 @@ window.HandleStudio = function (payload) {
|
|||||||
ACTIONS = next.actions;
|
ACTIONS = next.actions;
|
||||||
FAVS = next.favourites;
|
FAVS = next.favourites;
|
||||||
RECENTS = next.recent;
|
RECENTS = next.recent;
|
||||||
|
USER_MODE = next.userMode;
|
||||||
query = next.query;
|
query = next.query;
|
||||||
sel = next.sel;
|
sel = next.sel;
|
||||||
lastResizeHeight = next.lastResizeHeight;
|
lastResizeHeight = next.lastResizeHeight;
|
||||||
@@ -470,7 +530,8 @@ function renderFav() {
|
|||||||
tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
|
tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
|
||||||
tile.style.setProperty("--h", hue(id));
|
tile.style.setProperty("--h", hue(id));
|
||||||
fillTile(tile, a);
|
fillTile(tile, a);
|
||||||
tile.title = a.title;
|
var tileBadge = modeBadge(a, USER_MODE);
|
||||||
|
tile.title = a.title + (tileBadge ? " (" + tileBadge + ")" : "");
|
||||||
tile.setAttribute("aria-label", actionLabel(a, ACTIONS));
|
tile.setAttribute("aria-label", actionLabel(a, ACTIONS));
|
||||||
tile.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); };
|
tile.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); };
|
||||||
// Numbered quick-launch badge (Alt/Option+digit), drawn on the corner.
|
// Numbered quick-launch badge (Alt/Option+digit), drawn on the corner.
|
||||||
@@ -584,6 +645,13 @@ function renderActionRow(a, i) {
|
|||||||
line.className = "row-line";
|
line.className = "row-line";
|
||||||
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
||||||
line.appendChild(name);
|
line.appendChild(name);
|
||||||
|
var badge = modeBadge(a, USER_MODE);
|
||||||
|
if (badge) {
|
||||||
|
var tag = document.createElement("span");
|
||||||
|
tag.className = "row-mode";
|
||||||
|
tag.textContent = badge;
|
||||||
|
line.appendChild(tag);
|
||||||
|
}
|
||||||
if (a.shortcut) {
|
if (a.shortcut) {
|
||||||
var sc = document.createElement("div");
|
var sc = document.createElement("div");
|
||||||
sc.className = "row-sc";
|
sc.className = "row-sc";
|
||||||
|
|||||||
@@ -211,4 +211,53 @@ assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR",
|
|||||||
assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]),
|
assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]),
|
||||||
"S", "a unique title resolves to the bare title initial");
|
"S", "a unique title resolves to the bare title initial");
|
||||||
|
|
||||||
|
// needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode.
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode");
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "simple"), true, "Expert is gated in Simple mode");
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "advanced"), true, "Expert is gated in Advanced mode");
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "expert"), true, "Developer is gated in Expert mode");
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "simple" }, "simple"), false, "a Simple setting is not gated");
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "advanced"), false, "an Advanced setting is not gated in Advanced mode");
|
||||||
|
assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "develop"), false, "a Developer setting is not gated in Developer mode");
|
||||||
|
assert.equal(ctx.needsModeSwitch({}, "simple"), false, "a command with no mode is never gated");
|
||||||
|
|
||||||
|
// modeBadge: the tag text for gated settings, empty once the setting is available.
|
||||||
|
assert.equal(ctx.modeBadge({ mode: "advanced" }, "simple"), "Advanced", "Advanced badge text");
|
||||||
|
assert.equal(ctx.modeBadge({ mode: "expert" }, "simple"), "Expert", "Expert badge text");
|
||||||
|
assert.equal(ctx.modeBadge({ mode: "develop" }, "simple"), "Developer", "Developer badge text");
|
||||||
|
assert.equal(ctx.modeBadge({ mode: "advanced" }, "advanced"), "", "no badge when the mode already matches");
|
||||||
|
|
||||||
|
// modeFilterFromQuery: whole-word, case-insensitive mode keywords -> internal mode values. "developer"
|
||||||
|
// maps to the internal "develop"; "simple" is deliberately not a keyword; a prefix is not a match.
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("advanced"), ["advanced"], "Advanced is a mode keyword");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("Expert"), ["expert"], "the keyword is case-insensitive");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("developer"), ["develop"], "Developer maps to the develop value");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("develop"), ["develop"], "the internal develop spelling also works");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("expert retraction"), ["expert"], "a keyword is found among other text");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("advanced expert"), ["advanced", "expert"], "multiple keywords are deduped in order");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("advanced advanced"), ["advanced"], "a repeated keyword is deduped");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("simple"), [], "Simple is not a mode keyword");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery("advance"), [], "a mode-word prefix is not a whole-word match");
|
||||||
|
assert.deepEqual(ctx.modeFilterFromQuery(""), [], "an empty query names no mode");
|
||||||
|
|
||||||
|
// searchActions mode union: a mode keyword keeps the normal text matches AND appends every setting
|
||||||
|
// requiring that mode. Not a filter - a Simple setting literally named "Advanced..." still shows, and
|
||||||
|
// commands (mode "simple") are never pulled in by a keyword.
|
||||||
|
const modePool = [
|
||||||
|
{ id: "a1", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", mode: "advanced" },
|
||||||
|
{ id: "a2", title: "Advanced Detection", source: "Quality", group: "Quality", mode: "simple" },
|
||||||
|
{ id: "a3", title: "Retraction Length", source: "Process", group: "Process : Quality", mode: "expert" },
|
||||||
|
{ id: "a4", title: "Slice", source: "OrcaSlicer", group: "Commands", mode: "simple" }
|
||||||
|
];
|
||||||
|
var adv = ctx.searchActions(modePool, "advanced");
|
||||||
|
assert.deepEqual(adv.map(function (a) { return a.id; }), ["a2", "a1"],
|
||||||
|
"a text match (a2) ranks above the mode-only setting (a1), and no expert/command leaks in");
|
||||||
|
var expert = ctx.searchActions(modePool, "expert");
|
||||||
|
assert.deepEqual(expert.map(function (a) { return a.id; }), ["a3"], "the expert keyword pulls in the expert setting");
|
||||||
|
assert.deepEqual(ctx.searchActions(modePool, "developer"), [], "no developer settings means no mode extras");
|
||||||
|
assert.equal(ctx.searchActions(modePool, "retraction")[0].id, "a3",
|
||||||
|
"a query with no mode keyword is unaffected by the mode union");
|
||||||
|
assert.equal(ctx.searchActions([{ id: "both", title: "Advanced", source: "Quality", group: "", mode: "advanced" }], "advanced").length,
|
||||||
|
1, "a setting that both matches text and requires the mode appears exactly once");
|
||||||
|
|
||||||
console.log("ok");
|
console.log("ok");
|
||||||
|
|||||||
@@ -364,6 +364,18 @@ body {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mode tag on settings above the user's current mode (Advanced/Expert/Developer). */
|
||||||
|
.row-mode {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--plugin-status-warn);
|
||||||
|
background: var(--plugin-status-warn-bg);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -162,6 +162,17 @@ std::string setting_type_context(Preset::Type type)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.).
|
||||||
|
const char* mode_key(ConfigOptionMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case comAdvanced: return "advanced";
|
||||||
|
case comExpert: return "expert";
|
||||||
|
case comDevelop: return "develop";
|
||||||
|
default: return "simple";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A config setting exposed as a first-class action: selecting it jumps the sidebar to the option.
|
// A config setting exposed as a first-class action: selecting it jumps the sidebar to the option.
|
||||||
// The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys
|
// The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys
|
||||||
// the action; title/group/source are purely for display + search. run() performs the jump, and
|
// the action; title/group/source are purely for display + search. run() performs the jump, and
|
||||||
@@ -180,7 +191,8 @@ struct SettingAction : AppAction
|
|||||||
std::string title,
|
std::string title,
|
||||||
std::string group,
|
std::string group,
|
||||||
std::wstring category_in,
|
std::wstring category_in,
|
||||||
std::string source_name)
|
std::string source_name,
|
||||||
|
ConfigOptionMode mode_in)
|
||||||
: AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name))
|
: AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name))
|
||||||
, opt_key(std::move(opt_key_in))
|
, opt_key(std::move(opt_key_in))
|
||||||
, type(type_in)
|
, type(type_in)
|
||||||
@@ -188,8 +200,9 @@ struct SettingAction : AppAction
|
|||||||
{
|
{
|
||||||
// A setting is a single-phase command: activating it jumps the sidebar to the option
|
// A setting is a single-phase command: activating it jumps the sidebar to the option
|
||||||
// (like the sidebar's own settings search), then the dial closes. run() performs the jump.
|
// (like the sidebar's own settings search), then the dial closes. run() performs the jump.
|
||||||
this->kind = AppActionKind::Command;
|
this->kind = AppActionKind::Command;
|
||||||
this->group = std::move(group);
|
this->group = std::move(group);
|
||||||
|
this->required_mode = mode_in;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppActionRunResult run(const std::string& /*param*/) const override
|
AppActionRunResult run(const std::string& /*param*/) const override
|
||||||
@@ -509,10 +522,9 @@ void ActionRegistry::materialize_setting_actions()
|
|||||||
|
|
||||||
// Reuse the Sidebar's live searcher: it's the only OptionsSearcher whose groups_and_categories
|
// Reuse the Sidebar's live searcher: it's the only OptionsSearcher whose groups_and_categories
|
||||||
// map is populated (Tab::add_key feeds it at build time), and it already mirrors the current
|
// map is populated (Tab::add_key feeds it at build time), and it already mirrors the current
|
||||||
// configs/mode/printer-technology - i.e. exactly what the sidebar's own search would show. A
|
// configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting,
|
||||||
// fresh OptionsSearcher has an empty groups_and_categories, so append_options() would drop every
|
// including those above the user's current mode, and can prompt to switch before jumping.
|
||||||
// option and nothing would materialise. Turn each visible option into a SettingAction.
|
const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_modes_options();
|
||||||
const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_options();
|
|
||||||
|
|
||||||
// Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps
|
// Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps
|
||||||
// its recency/favourite; mirroring seed_state but amortised over the whole option set.
|
// its recency/favourite; mirroring seed_state but amortised over the whole option set.
|
||||||
@@ -540,7 +552,7 @@ void ActionRegistry::materialize_setting_actions()
|
|||||||
// title = the option leaf name (last label segment); group stays empty so the source path
|
// title = the option leaf name (last label segment); group stays empty so the source path
|
||||||
// (above) is the single display/search breadcrumb rather than being duplicated.
|
// (above) is the single display/search breadcrumb rather than being duplicated.
|
||||||
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(),
|
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(),
|
||||||
opt.category_local, boost::nowide::narrow(path));
|
opt.category_local, boost::nowide::narrow(path), opt.mode);
|
||||||
|
|
||||||
action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
|
action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
|
||||||
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
|
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
|
||||||
@@ -726,7 +738,8 @@ nlohmann::json ActionRegistry::snapshot()
|
|||||||
{"source", a->source_name()},
|
{"source", a->source_name()},
|
||||||
{"group", a->group},
|
{"group", a->group},
|
||||||
{"input", a->input},
|
{"input", a->input},
|
||||||
{"shortcut", ""}});
|
{"shortcut", ""},
|
||||||
|
{"mode", mode_key(a->required_mode)}});
|
||||||
};
|
};
|
||||||
|
|
||||||
nlohmann::json actions = nlohmann::json::array();
|
nlohmann::json actions = nlohmann::json::array();
|
||||||
@@ -765,7 +778,10 @@ nlohmann::json ActionRegistry::snapshot()
|
|||||||
for (const AppAction* a : recent)
|
for (const AppAction* a : recent)
|
||||||
recent_json.push_back(action_to_json(a));
|
recent_json.push_back(action_to_json(a));
|
||||||
|
|
||||||
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}, {"recent", std::move(recent_json)}};
|
return {{"actions", std::move(actions)},
|
||||||
|
{"favourites", std::move(favourites)},
|
||||||
|
{"recent", std::move(recent_json)},
|
||||||
|
{"user_mode", mode_key(wxGetApp().get_mode())}};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
|
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <libslic3r/Config.hpp>
|
||||||
|
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/thread.h>
|
#include <wx/thread.h>
|
||||||
|
|
||||||
@@ -75,6 +77,9 @@ struct AppAction
|
|||||||
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
|
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
|
||||||
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
|
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
|
||||||
std::string input;
|
std::string input;
|
||||||
|
// Settings mode required to edit this action (SettingActions only). The palette prompts before
|
||||||
|
// running an action whose mode is above the user's current mode. comSimple for everything else.
|
||||||
|
ConfigOptionMode required_mode = comSimple;
|
||||||
|
|
||||||
virtual ~AppAction() = default;
|
virtual ~AppAction() = default;
|
||||||
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
|
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
|
||||||
@@ -106,6 +111,13 @@ private:
|
|||||||
std::string m_source_name; // display name of the action's source
|
std::string m_source_name; // display name of the action's source
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch
|
||||||
|
// first. Developer settings are handled as a separate prompt by the Speed Dial.
|
||||||
|
inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode)
|
||||||
|
{
|
||||||
|
return setting_mode > current_mode;
|
||||||
|
}
|
||||||
|
|
||||||
// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or
|
// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or
|
||||||
// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin.
|
// 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);
|
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit);
|
||||||
|
|||||||
@@ -578,113 +578,131 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
|
|||||||
return sub_menu;
|
return sub_menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table keeps
|
||||||
|
// the menu label, the files to load and the per-model behavior in a single place. Labels are wrapped
|
||||||
|
// in L() so they are picked up for translation. Shared with the command palette.
|
||||||
|
const std::vector<MenuFactory::HandyModel>& MenuFactory::handy_models()
|
||||||
|
{
|
||||||
|
static const std::vector<HandyModel> models = {
|
||||||
|
{"orca_cube", L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||||
|
{"orcasliced_combo", L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||||
|
{"orca_badge", L("Orca Badge"), {"OrcaBadge.3mf"}},
|
||||||
|
{"orca_tolerance_test", L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
|
||||||
|
{"3dbenchy", L("3DBenchy"), {"3DBenchy.drc"}},
|
||||||
|
{"cali_cat", L("Cali Cat"), {"calicat.drc"}},
|
||||||
|
{"autodesk_fdm_test", L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
|
||||||
|
{"voron_cube", L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
|
||||||
|
{"stanford_bunny", L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
|
||||||
|
{"orca_string_hell", L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
|
||||||
|
};
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuFactory::load_handy_model(std::size_t index)
|
||||||
|
{
|
||||||
|
const std::vector<HandyModel>& models = handy_models();
|
||||||
|
if (index >= models.size())
|
||||||
|
return;
|
||||||
|
const HandyModel& model = models[index];
|
||||||
|
|
||||||
|
std::vector<boost::filesystem::path> input_files;
|
||||||
|
input_files.reserve(model.file_names.size());
|
||||||
|
for (const auto& file_name : model.file_names)
|
||||||
|
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
|
||||||
|
|
||||||
|
Plater* pl = plater();
|
||||||
|
if (!pl)
|
||||||
|
return;
|
||||||
|
pl->load_files(input_files, LoadStrategy::LoadModel);
|
||||||
|
if (model.arrange_after_import) {
|
||||||
|
pl->set_prepare_state(Job::PREPARE_STATE_MENU);
|
||||||
|
pl->arrange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggest to change settings for stringhell
|
||||||
|
// This serves as mini tutorial for new users
|
||||||
|
if (model.is_stringhell) {
|
||||||
|
wxGetApp().CallAfter([=] {
|
||||||
|
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||||
|
|
||||||
|
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
|
||||||
|
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
|
||||||
|
if (is_only_one_wall_top && min_width_top_surface > 0) {
|
||||||
|
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
|
||||||
|
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
|
||||||
|
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
|
||||||
|
"Yes - Change these settings automatically\n"
|
||||||
|
"No - Do not change these settings for me");
|
||||||
|
|
||||||
|
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
|
||||||
|
if (dialog.ShowModal() == wxID_YES) {
|
||||||
|
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
|
||||||
|
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
|
||||||
|
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
|
||||||
|
}
|
||||||
|
wxGetApp().plater()->update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Orca: add submenu for adding handy models
|
// Orca: add submenu for adding handy models
|
||||||
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
|
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
|
||||||
auto sub_menu = new wxMenu;
|
auto sub_menu = new wxMenu;
|
||||||
|
|
||||||
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table
|
const std::vector<HandyModel>& models = handy_models();
|
||||||
// keeps the menu label, the files to load and the per-model behavior in a single place and
|
for (std::size_t i = 0; i < models.size(); ++i) {
|
||||||
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come
|
append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "",
|
||||||
// with that). Labels are wrapped in L() so they are picked up for translation.
|
[i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", menu);
|
||||||
struct HandyModel
|
|
||||||
{
|
|
||||||
const char* label;
|
|
||||||
std::vector<std::string> file_names;
|
|
||||||
bool arrange_after_import = false;
|
|
||||||
bool is_stringhell = false;
|
|
||||||
};
|
|
||||||
static const std::vector<HandyModel> handy_models = {
|
|
||||||
{L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
|
||||||
{L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
|
||||||
{L("Orca Badge"), {"OrcaBadge.3mf"}},
|
|
||||||
{L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
|
|
||||||
{L("3DBenchy"), {"3DBenchy.drc"}},
|
|
||||||
{L("Cali Cat"), {"calicat.drc"}},
|
|
||||||
{L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
|
|
||||||
{L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
|
|
||||||
{L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
|
|
||||||
{L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& model : handy_models) {
|
|
||||||
append_menu_item(
|
|
||||||
sub_menu, wxID_ANY, _(model.label), "",
|
|
||||||
[&model](wxCommandEvent&) {
|
|
||||||
std::vector<boost::filesystem::path> input_files;
|
|
||||||
input_files.reserve(model.file_names.size());
|
|
||||||
for (const auto& file_name : model.file_names)
|
|
||||||
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
|
|
||||||
|
|
||||||
plater()->load_files(input_files, LoadStrategy::LoadModel);
|
|
||||||
if (model.arrange_after_import) {
|
|
||||||
plater()->set_prepare_state(Job::PREPARE_STATE_MENU);
|
|
||||||
plater()->arrange();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggest to change settings for stringhell
|
|
||||||
// This serves as mini tutorial for new users
|
|
||||||
if (model.is_stringhell) {
|
|
||||||
wxGetApp().CallAfter([=] {
|
|
||||||
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
|
||||||
|
|
||||||
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
|
|
||||||
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
|
|
||||||
if (is_only_one_wall_top && min_width_top_surface > 0) {
|
|
||||||
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
|
|
||||||
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
|
|
||||||
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
|
|
||||||
"Yes - Change these settings automatically\n"
|
|
||||||
"No - Do not change these settings for me");
|
|
||||||
|
|
||||||
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
|
|
||||||
if (dialog.ShowModal() == wxID_YES) {
|
|
||||||
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
|
|
||||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
|
|
||||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
|
|
||||||
}
|
|
||||||
wxGetApp().plater()->update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"", menu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return sub_menu;
|
return sub_menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a Text/SVG volume through the matching gizmo. `type == INVALID` means "create a new object".
|
||||||
|
// Shared by the add menu and the command palette.
|
||||||
|
static void add_volume_with_gizmo(GLGizmosManager::EType gizmo_type, ModelVolumeType type)
|
||||||
|
{
|
||||||
|
Plater* pl = plater();
|
||||||
|
if (!pl)
|
||||||
|
return;
|
||||||
|
const GLCanvas3D* canvas = pl->canvas3D();
|
||||||
|
if (!canvas)
|
||||||
|
return;
|
||||||
|
GLGizmoBase* gizmo_base = canvas->get_gizmos_manager().get_gizmo(gizmo_type);
|
||||||
|
if (!gizmo_base)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ModelVolumeType volume_type = type;
|
||||||
|
// no selected object means create new object
|
||||||
|
if (volume_type == ModelVolumeType::INVALID)
|
||||||
|
volume_type = ModelVolumeType::MODEL_PART;
|
||||||
|
|
||||||
|
auto screen_position = canvas->get_popup_menu_position();
|
||||||
|
if (gizmo_type == GLGizmosManager::Emboss) {
|
||||||
|
auto* emboss = dynamic_cast<GLGizmoEmboss*>(gizmo_base);
|
||||||
|
if (emboss == nullptr)
|
||||||
|
return;
|
||||||
|
if (screen_position.has_value())
|
||||||
|
emboss->create_volume(volume_type, *screen_position);
|
||||||
|
else
|
||||||
|
emboss->create_volume(volume_type);
|
||||||
|
} else if (gizmo_type == GLGizmosManager::Svg) {
|
||||||
|
auto* svg = dynamic_cast<GLGizmoSVG*>(gizmo_base);
|
||||||
|
if (svg == nullptr)
|
||||||
|
return;
|
||||||
|
if (screen_position.has_value())
|
||||||
|
svg->create_volume(volume_type, *screen_position);
|
||||||
|
else
|
||||||
|
svg->create_volume(volume_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuFactory::add_text_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Emboss, type); }
|
||||||
|
void MenuFactory::add_svg_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Svg, type); }
|
||||||
|
|
||||||
static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) {
|
static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) {
|
||||||
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) {
|
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); };
|
||||||
const GLCanvas3D *canvas = plater()->canvas3D();
|
|
||||||
const GLGizmosManager &mng = canvas->get_gizmos_manager();
|
|
||||||
GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type);
|
|
||||||
|
|
||||||
ModelVolumeType volume_type = type;
|
|
||||||
// no selected object means create new object
|
|
||||||
if (volume_type == ModelVolumeType::INVALID)
|
|
||||||
volume_type = ModelVolumeType::MODEL_PART;
|
|
||||||
|
|
||||||
auto screen_position = canvas->get_popup_menu_position();
|
|
||||||
if (gizmo_type == GLGizmosManager::Emboss) {
|
|
||||||
auto emboss = dynamic_cast<GLGizmoEmboss *>(gizmo_base);
|
|
||||||
assert(emboss != nullptr);
|
|
||||||
if (emboss == nullptr) return;
|
|
||||||
if (screen_position.has_value()) {
|
|
||||||
emboss->create_volume(volume_type, *screen_position);
|
|
||||||
} else {
|
|
||||||
emboss->create_volume(volume_type);
|
|
||||||
}
|
|
||||||
} else if (gizmo_type == GLGizmosManager::Svg) {
|
|
||||||
auto svg = dynamic_cast<GLGizmoSVG *>(gizmo_base);
|
|
||||||
assert(svg != nullptr);
|
|
||||||
if (svg == nullptr) return;
|
|
||||||
if (screen_position.has_value()) {
|
|
||||||
svg->create_volume(volume_type, *screen_position);
|
|
||||||
} else {
|
|
||||||
svg->create_volume(volume_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
|
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
|
||||||
type == ModelVolumeType::INVALID // cannot use gizmo without selected object
|
type == ModelVolumeType::INVALID // cannot use gizmo without selected object
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
#include <wx/bitmap.h>
|
#include <wx/bitmap.h>
|
||||||
|
|
||||||
@@ -51,6 +52,23 @@ public:
|
|||||||
static std::vector<wxBitmap> get_text_volume_bitmaps();
|
static std::vector<wxBitmap> get_text_volume_bitmaps();
|
||||||
static std::vector<wxBitmap> get_svg_volume_bitmaps();
|
static std::vector<wxBitmap> get_svg_volume_bitmaps();
|
||||||
|
|
||||||
|
// Orca: handy models shipped under <resources>/handy_models. The menu and the command palette
|
||||||
|
// share this table so the model list and its per-model behavior live in one place.
|
||||||
|
struct HandyModel
|
||||||
|
{
|
||||||
|
const char* key;
|
||||||
|
const char* label;
|
||||||
|
std::vector<std::string> file_names;
|
||||||
|
bool arrange_after_import = false;
|
||||||
|
bool is_stringhell = false;
|
||||||
|
};
|
||||||
|
static const std::vector<HandyModel>& handy_models();
|
||||||
|
static void load_handy_model(std::size_t index);
|
||||||
|
|
||||||
|
// Add a Text/SVG volume through the Emboss/SVG gizmo. Shared by the add menu and the palette.
|
||||||
|
static void add_text_volume(ModelVolumeType type);
|
||||||
|
static void add_svg_volume(ModelVolumeType type);
|
||||||
|
|
||||||
MenuFactory();
|
MenuFactory();
|
||||||
~MenuFactory() = default;
|
~MenuFactory() = default;
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,23 @@
|
|||||||
|
|
||||||
#include "calib_dlg.hpp"
|
#include "calib_dlg.hpp"
|
||||||
#include "Camera.hpp"
|
#include "Camera.hpp"
|
||||||
|
#include "DailyTips.hpp"
|
||||||
#include "GCodeViewer.hpp"
|
#include "GCodeViewer.hpp"
|
||||||
#include "GLCanvas3D.hpp"
|
#include "GLCanvas3D.hpp"
|
||||||
#include "GUI.hpp"
|
#include "GUI.hpp"
|
||||||
#include "GUI_App.hpp"
|
#include "GUI_App.hpp"
|
||||||
|
#include "GUI_Factories.hpp"
|
||||||
|
#include "GUI_ObjectList.hpp"
|
||||||
#include "I18N.hpp"
|
#include "I18N.hpp"
|
||||||
#include "IMSlider.hpp"
|
#include "IMSlider.hpp"
|
||||||
#include "MainFrame.hpp"
|
#include "MainFrame.hpp"
|
||||||
|
#include "NetworkTestDialog.hpp"
|
||||||
#include "Plater.hpp"
|
#include "Plater.hpp"
|
||||||
#include "PluginsDialog.hpp"
|
#include "PluginsDialog.hpp"
|
||||||
#include "PlateSettingsDialog.hpp"
|
#include "PlateSettingsDialog.hpp"
|
||||||
#include "DeviceCore/DevManager.h"
|
#include "DeviceCore/DevManager.h"
|
||||||
|
|
||||||
|
#include <libslic3r/Model.hpp>
|
||||||
#include <libslic3r/Utils.hpp>
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -24,6 +29,8 @@
|
|||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include <wx/utils.h>
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -112,6 +119,19 @@ AppActionRunResult calib_command(CalibKind kind)
|
|||||||
return {AppActionRunResult::Level::Success};
|
return {AppActionRunResult::Level::Success};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Palette-only: developer mode overrides the saved mode (get_mode returns comDevelop), so choosing
|
||||||
|
// Simple/Advanced/Expert must clear it first. Mirrors Preferences: persist the flag, then update.
|
||||||
|
void select_mode(ConfigOptionMode mode)
|
||||||
|
{
|
||||||
|
GUI_App& app = wxGetApp();
|
||||||
|
const bool was_developer = app.app_config->get_bool("developer_mode");
|
||||||
|
if (was_developer)
|
||||||
|
app.app_config->set_bool("developer_mode", false);
|
||||||
|
app.save_mode(mode);
|
||||||
|
if (was_developer)
|
||||||
|
app.app_config->save();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<NativeCommand> build_command_catalog()
|
std::vector<NativeCommand> build_command_catalog()
|
||||||
{
|
{
|
||||||
std::vector<NativeCommand> out;
|
std::vector<NativeCommand> out;
|
||||||
@@ -174,17 +194,26 @@ std::vector<NativeCommand> build_command_catalog()
|
|||||||
|
|
||||||
// ---- Mode ----
|
// ---- Mode ----
|
||||||
add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) {
|
add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) {
|
||||||
wxGetApp().save_mode(comSimple);
|
select_mode(comSimple);
|
||||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
});
|
});
|
||||||
add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) {
|
add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) {
|
||||||
wxGetApp().save_mode(comAdvanced);
|
select_mode(comAdvanced);
|
||||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
});
|
});
|
||||||
add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) {
|
add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) {
|
||||||
wxGetApp().save_mode(comExpert);
|
select_mode(comExpert);
|
||||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
});
|
});
|
||||||
|
// Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI.
|
||||||
|
add("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), [](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);
|
||||||
|
app.app_config->save();
|
||||||
|
app.update_mode();
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success, on ? _L("Developer mode enabled.") : _L("Developer mode disabled.")};
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Export pipeline ----
|
// ---- Export pipeline ----
|
||||||
add("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), [](const std::string&) {
|
add("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), [](const std::string&) {
|
||||||
@@ -320,6 +349,57 @@ std::vector<NativeCommand> build_command_catalog()
|
|||||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
|
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, const char* type_name) {
|
||||||
|
add(std::move(key), std::move(title), _u8L("Add Primitive"), [type_name](const std::string&) {
|
||||||
|
Plater* plater = wxGetApp().plater();
|
||||||
|
if (plater) {
|
||||||
|
ensure_3d_view(plater);
|
||||||
|
if (ObjectList* list = wxGetApp().obj_list())
|
||||||
|
list->load_generic_subobject(type_name, ModelVolumeType::INVALID);
|
||||||
|
}
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
add_primitive("add_primitive_cube", _u8L("Cube"), "Cube");
|
||||||
|
add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "Cylinder");
|
||||||
|
add_primitive("add_primitive_sphere", _u8L("Sphere"), "Sphere");
|
||||||
|
add_primitive("add_primitive_cone", _u8L("Cone"), "Cone");
|
||||||
|
add_primitive("add_primitive_disc", _u8L("Disc"), "Disc");
|
||||||
|
add_primitive("add_primitive_torus", _u8L("Torus"), "Torus");
|
||||||
|
add("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), [](const std::string&) {
|
||||||
|
Plater* plater = wxGetApp().plater();
|
||||||
|
if (plater) {
|
||||||
|
ensure_3d_view(plater);
|
||||||
|
if (GLCanvas3D* canvas = plater->canvas3D())
|
||||||
|
canvas->clear_popup_menu_position();
|
||||||
|
MenuFactory::add_text_volume(ModelVolumeType::INVALID);
|
||||||
|
}
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), [](const std::string&) {
|
||||||
|
Plater* plater = wxGetApp().plater();
|
||||||
|
if (plater) {
|
||||||
|
ensure_3d_view(plater);
|
||||||
|
if (GLCanvas3D* canvas = plater->canvas3D())
|
||||||
|
canvas->clear_popup_menu_position();
|
||||||
|
MenuFactory::add_svg_volume(ModelVolumeType::INVALID);
|
||||||
|
}
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- 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"),
|
||||||
|
[i](const std::string&) {
|
||||||
|
if (Plater* plater = wxGetApp().plater())
|
||||||
|
ensure_3d_view(plater);
|
||||||
|
MenuFactory::load_handy_model(i);
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Plate ----
|
// ---- Plate ----
|
||||||
add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) {
|
add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) {
|
||||||
Plater* plater = wxGetApp().plater();
|
Plater* plater = wxGetApp().plater();
|
||||||
@@ -458,6 +538,53 @@ std::vector<NativeCommand> build_command_catalog()
|
|||||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
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&) {
|
||||||
|
wxGetApp().keyboard_shortcuts();
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
|
||||||
|
wxGetApp().ShowUserGuide();
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), [](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&) {
|
||||||
|
wxGetApp().troubleshoot();
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) {
|
||||||
|
NetworkTestDialog dlg(wxGetApp().mainframe);
|
||||||
|
dlg.ShowModal();
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), [](const std::string&) {
|
||||||
|
if (Plater* plater = wxGetApp().plater()) {
|
||||||
|
plater->get_dailytips()->open();
|
||||||
|
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||||
|
canvas->set_as_dirty();
|
||||||
|
}
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), [](const std::string&) {
|
||||||
|
wxGetApp().check_new_version_sf(true, 1);
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), [](const std::string&) {
|
||||||
|
Slic3r::GUI::about();
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
add("open_wiki", _u8L("Open Wiki"), _u8L("Help"), [](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&) {
|
||||||
|
wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW);
|
||||||
|
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Plugins ----
|
// ---- Plugins ----
|
||||||
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
|
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||||
wxGetApp().open_plugins_dialog();
|
wxGetApp().open_plugins_dialog();
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ static std::string get_key(const std::string &opt_key, Preset::Type type) { retu
|
|||||||
|
|
||||||
void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||||
{
|
{
|
||||||
auto emplace = [this, type](const std::string key, const wxString &label) {
|
auto emplace = [this, type](std::vector<Option> &dst, const std::string &key, const wxString &label, ConfigOptionMode opt_mode) {
|
||||||
const GroupAndCategory &gc = groups_and_categories[key];
|
const GroupAndCategory &gc = groups_and_categories[key];
|
||||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
|
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
|
||||||
|
|
||||||
@@ -99,13 +99,14 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!label.IsEmpty())
|
if (!label.IsEmpty())
|
||||||
options.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(),
|
dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(),
|
||||||
_(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring()});
|
_(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(),
|
||||||
|
false, opt_mode});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (std::string opt_key : config->keys()) {
|
for (std::string opt_key : config->keys()) {
|
||||||
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
||||||
if (opt.mode > mode) continue;
|
const bool in_filtered = opt.mode <= mode;
|
||||||
|
|
||||||
int cnt = 0;
|
int cnt = 0;
|
||||||
|
|
||||||
@@ -128,12 +129,17 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty
|
|||||||
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
|
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
|
||||||
|
|
||||||
std::string key = get_key(opt_key, type);
|
std::string key = get_key(opt_key, type);
|
||||||
|
auto add = [&](const std::string &k) {
|
||||||
|
if (in_filtered)
|
||||||
|
emplace(options, k, label, opt.mode);
|
||||||
|
emplace(options_all_modes, k, label, opt.mode);
|
||||||
|
};
|
||||||
if (cnt == 0)
|
if (cnt == 0)
|
||||||
emplace(key, label);
|
add(key);
|
||||||
else
|
else
|
||||||
for (int i = 0; i < cnt; ++i)
|
for (int i = 0; i < cnt; ++i)
|
||||||
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
|
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
|
||||||
emplace(key + "#" + std::to_string(i), label);
|
add(key + "#" + std::to_string(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +313,7 @@ OptionsSearcher::~OptionsSearcher() {}
|
|||||||
void OptionsSearcher::init(std::vector<InputInfo> input_values)
|
void OptionsSearcher::init(std::vector<InputInfo> input_values)
|
||||||
{
|
{
|
||||||
options.clear();
|
options.clear();
|
||||||
|
options_all_modes.clear();
|
||||||
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
||||||
sort_options();
|
sort_options();
|
||||||
|
|
||||||
@@ -318,6 +325,8 @@ void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, Confi
|
|||||||
if (options.empty()) return;
|
if (options.empty()) return;
|
||||||
|
|
||||||
options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end());
|
options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end());
|
||||||
|
options_all_modes.erase(std::remove_if(options_all_modes.begin(), options_all_modes.end(), [type](Option opt) { return opt.type == type; }),
|
||||||
|
options_all_modes.end());
|
||||||
|
|
||||||
append_options(config, type, mode);
|
append_options(config, type, mode);
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ struct Option
|
|||||||
std::wstring category;
|
std::wstring category;
|
||||||
std::wstring category_local;
|
std::wstring category_local;
|
||||||
bool multi_category { false };
|
bool multi_category { false };
|
||||||
|
ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt
|
||||||
|
|
||||||
std::string opt_key() const;
|
std::string opt_key() const;
|
||||||
};
|
};
|
||||||
@@ -97,6 +98,9 @@ class OptionsSearcher
|
|||||||
PrinterTechnology printer_technology;
|
PrinterTechnology printer_technology;
|
||||||
|
|
||||||
std::vector<Option> options{};
|
std::vector<Option> options{};
|
||||||
|
// Every option regardless of the current UI mode (Simple/Advanced/Expert/Developer), for the
|
||||||
|
// Speed Dial. The sidebar search keeps using the mode-filtered `options`.
|
||||||
|
std::vector<Option> options_all_modes{};
|
||||||
std::vector<FoundOption> found{};
|
std::vector<FoundOption> found{};
|
||||||
|
|
||||||
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||||
@@ -152,6 +156,10 @@ public:
|
|||||||
// The full gated option set built by init() (after visibility/mode/printer-tech filtering).
|
// The full gated option set built by init() (after visibility/mode/printer-tech filtering).
|
||||||
// Used by the Speed Dial to materialise config settings as first-class actions.
|
// Used by the Speed Dial to materialise config settings as first-class actions.
|
||||||
const std::vector<Option>& all_options() const { return options; }
|
const std::vector<Option>& all_options() const { return options; }
|
||||||
|
|
||||||
|
// Every option across all UI modes (Developer included), regardless of the current mode.
|
||||||
|
// Used by the Speed Dial so it can list settings the user would have to switch mode to edit.
|
||||||
|
const std::vector<Option>& all_modes_options() const { return options_all_modes; }
|
||||||
};
|
};
|
||||||
|
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "Plater.hpp"
|
#include "Plater.hpp"
|
||||||
#include "Widgets/WebViewHostDialog.hpp"
|
#include "Widgets/WebViewHostDialog.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/AppConfig.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <wx/display.h>
|
#include <wx/display.h>
|
||||||
@@ -35,6 +37,17 @@ int json_int_or(const nlohmann::json& j, const char* key, int fallback)
|
|||||||
return it != j.end() && it->is_number() ? it->get<int>() : fallback;
|
return it != j.end() && it->is_number() ? it->get<int>() : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display name of a settings mode, for the mode-switch confirmation.
|
||||||
|
wxString mode_label(ConfigOptionMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case comAdvanced: return _L("Advanced");
|
||||||
|
case comExpert: return _L("Expert");
|
||||||
|
case comDevelop: return _L("Developer");
|
||||||
|
default: return _L("Simple");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
|
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
|
||||||
|
|
||||||
// Give the WebKitGTK widget itself input focus, not its GtkScrolledWindow container.
|
// Give the WebKitGTK widget itself input focus, not its GtkScrolledWindow container.
|
||||||
@@ -192,6 +205,32 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
|||||||
// Only plugin actions get the "Run plugin?" confirm. Built-in commands act immediately.
|
// Only plugin actions get the "Run plugin?" confirm. Built-in commands act immediately.
|
||||||
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
|
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
|
||||||
const std::string atitle = a->title();
|
const std::string atitle = a->title();
|
||||||
|
const ConfigOptionMode required = a->required_mode;
|
||||||
|
|
||||||
|
// Settings the current mode hides require a switch first. Ask while the dial is still up; a
|
||||||
|
// cancel dismisses both (the dial also auto-hides when the modal takes activation).
|
||||||
|
if (requires_mode_switch(required, wxGetApp().get_mode())) {
|
||||||
|
const wxString setting = title.empty() ? from_u8(atitle) : from_u8(title);
|
||||||
|
if (required == comDevelop) {
|
||||||
|
RichMessageDialog dlg(wxGetApp().mainframe,
|
||||||
|
wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"),
|
||||||
|
setting),
|
||||||
|
_L("Developer setting"), wxOK | wxCANCEL);
|
||||||
|
if (dlg.ShowModal() != wxID_OK)
|
||||||
|
return;
|
||||||
|
wxGetApp().app_config->set_bool("developer_mode", true);
|
||||||
|
wxGetApp().update_mode();
|
||||||
|
} else {
|
||||||
|
RichMessageDialog dlg(wxGetApp().mainframe,
|
||||||
|
wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"),
|
||||||
|
setting, mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)),
|
||||||
|
_L("Switch settings mode"), wxOK | wxCANCEL);
|
||||||
|
if (dlg.ShowModal() != wxID_OK)
|
||||||
|
return;
|
||||||
|
wxGetApp().save_mode(required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (IsModal())
|
if (IsModal())
|
||||||
EndModal(wxID_CANCEL);
|
EndModal(wxID_CANCEL);
|
||||||
else
|
else
|
||||||
@@ -231,7 +270,8 @@ void SpeedDialWebDialog::send_actions()
|
|||||||
call_web_handler({{"command", "list_actions"},
|
call_web_handler({{"command", "list_actions"},
|
||||||
{"actions", std::move(snap["actions"])},
|
{"actions", std::move(snap["actions"])},
|
||||||
{"favourites", std::move(snap["favourites"])},
|
{"favourites", std::move(snap["favourites"])},
|
||||||
{"recent", std::move(snap["recent"])}});
|
{"recent", std::move(snap["recent"])},
|
||||||
|
{"user_mode", std::move(snap["user_mode"])}});
|
||||||
}
|
}
|
||||||
|
|
||||||
}} // namespace Slic3r::GUI
|
}} // namespace Slic3r::GUI
|
||||||
|
|||||||
@@ -112,3 +112,91 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[speeddi
|
|||||||
CHECK(seen.insert(c.key).second);
|
CHECK(seen.insert(c.key).second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Help-menu commands, wiki/YouTube links and the developer-mode toggle are part of the palette.
|
||||||
|
// Guard their presence and that they stay grouped with their peers, so a catalog edit cannot drop
|
||||||
|
// or scatter them. Groups are compared to the peer's own group to stay independent of translation.
|
||||||
|
TEST_CASE("Native command catalog includes the Help and developer-mode commands", "[speeddial][actions]")
|
||||||
|
{
|
||||||
|
const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog();
|
||||||
|
auto find = [&commands](const std::string& key) -> const Slic3r::GUI::NativeCommand* {
|
||||||
|
for (const auto& c : commands)
|
||||||
|
if (c.key == key)
|
||||||
|
return &c;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts");
|
||||||
|
REQUIRE(first != nullptr);
|
||||||
|
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test",
|
||||||
|
"help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki", "open_youtube"}) {
|
||||||
|
const Slic3r::GUI::NativeCommand* c = find(key);
|
||||||
|
REQUIRE(c != nullptr);
|
||||||
|
CHECK(c->group == first->group);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Slic3r::GUI::NativeCommand* mode_simple = find("mode_simple");
|
||||||
|
const Slic3r::GUI::NativeCommand* dev_mode = find("toggle_developer_mode");
|
||||||
|
REQUIRE(mode_simple != nullptr);
|
||||||
|
REQUIRE(dev_mode != nullptr);
|
||||||
|
CHECK(dev_mode->group == mode_simple->group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every "Add Primitive" item and shipped handy model has a palette command, grouped as in the Add
|
||||||
|
// menu. Groups are compared to a peer's own group to stay independent of translation.
|
||||||
|
TEST_CASE("Native command catalog covers the Add menus", "[speeddial][actions]")
|
||||||
|
{
|
||||||
|
const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog();
|
||||||
|
auto group_of = [&commands](const std::string& key) -> const std::string* {
|
||||||
|
for (const auto& c : commands)
|
||||||
|
if (c.key == key)
|
||||||
|
return &c.group;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string* primitive_group = group_of("add_primitive_cube");
|
||||||
|
REQUIRE(primitive_group != nullptr);
|
||||||
|
for (const char* key : {"add_primitive_cylinder", "add_primitive_sphere", "add_primitive_cone", "add_primitive_disc",
|
||||||
|
"add_primitive_torus", "add_primitive_text", "add_primitive_svg"}) {
|
||||||
|
const std::string* group = group_of(key);
|
||||||
|
INFO(key);
|
||||||
|
REQUIRE(group != nullptr);
|
||||||
|
CHECK(*group == *primitive_group);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string* handy_group = group_of("add_handy_orca_cube");
|
||||||
|
REQUIRE(handy_group != nullptr);
|
||||||
|
for (const char* key : {"add_handy_orcasliced_combo", "add_handy_orca_badge", "add_handy_orca_tolerance_test",
|
||||||
|
"add_handy_3dbenchy", "add_handy_cali_cat", "add_handy_autodesk_fdm_test", "add_handy_voron_cube",
|
||||||
|
"add_handy_stanford_bunny", "add_handy_orca_string_hell"}) {
|
||||||
|
const std::string* group = group_of(key);
|
||||||
|
INFO(key);
|
||||||
|
REQUIRE(group != nullptr);
|
||||||
|
CHECK(*group == *handy_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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", "[speeddial][actions]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::requires_mode_switch;
|
||||||
|
using Slic3r::comAdvanced;
|
||||||
|
using Slic3r::comDevelop;
|
||||||
|
using Slic3r::comExpert;
|
||||||
|
using Slic3r::comSimple;
|
||||||
|
|
||||||
|
CHECK(requires_mode_switch(comAdvanced, comSimple));
|
||||||
|
CHECK(requires_mode_switch(comExpert, comSimple));
|
||||||
|
CHECK(requires_mode_switch(comExpert, comAdvanced));
|
||||||
|
CHECK(requires_mode_switch(comDevelop, comSimple));
|
||||||
|
CHECK(requires_mode_switch(comDevelop, comAdvanced));
|
||||||
|
CHECK(requires_mode_switch(comDevelop, comExpert));
|
||||||
|
|
||||||
|
CHECK_FALSE(requires_mode_switch(comSimple, comSimple));
|
||||||
|
CHECK_FALSE(requires_mode_switch(comSimple, comAdvanced));
|
||||||
|
CHECK_FALSE(requires_mode_switch(comAdvanced, comAdvanced));
|
||||||
|
CHECK_FALSE(requires_mode_switch(comAdvanced, comExpert));
|
||||||
|
CHECK_FALSE(requires_mode_switch(comExpert, comExpert));
|
||||||
|
CHECK_FALSE(requires_mode_switch(comDevelop, comDevelop));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user