mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +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 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 ----------------------------------------------------
|
||||
// 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,
|
||||
@@ -105,11 +110,15 @@ function searchActions(actions, query) {
|
||||
matchIndex = {};
|
||||
if (!q) { searchNeedle = ""; return list.slice(0); }
|
||||
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
|
||||
// between fields, and EscapeRegExp keeps regex metachars in the query literal.
|
||||
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
|
||||
|
||||
var scored = [];
|
||||
var seen = {};
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var t = fieldMatchScore(titleNorm(a), wwRe);
|
||||
@@ -126,13 +135,28 @@ function searchActions(actions, query) {
|
||||
useEyebrowGroup: !!(a.group)
|
||||
};
|
||||
scored.push({ a: a, s: score });
|
||||
seen[a.id] = true;
|
||||
}
|
||||
scored.sort(function (x, y) {
|
||||
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;
|
||||
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.
|
||||
@@ -264,6 +288,40 @@ function prettySource(source) {
|
||||
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
|
||||
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
||||
function actionLabel(action, actions) {
|
||||
@@ -289,6 +347,7 @@ function stateFromPayload(payload) {
|
||||
actions: payload.actions || [],
|
||||
favourites: payload.favourites || [],
|
||||
recent: payload.recent || [],
|
||||
userMode: payload.user_mode || "simple",
|
||||
query: "",
|
||||
sel: { zone: "list", i: 0 },
|
||||
lastResizeHeight: 0,
|
||||
@@ -348,6 +407,7 @@ window.HandleStudio = function (payload) {
|
||||
ACTIONS = next.actions;
|
||||
FAVS = next.favourites;
|
||||
RECENTS = next.recent;
|
||||
USER_MODE = next.userMode;
|
||||
query = next.query;
|
||||
sel = next.sel;
|
||||
lastResizeHeight = next.lastResizeHeight;
|
||||
@@ -470,7 +530,8 @@ function renderFav() {
|
||||
tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
|
||||
tile.style.setProperty("--h", hue(id));
|
||||
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.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); };
|
||||
// Numbered quick-launch badge (Alt/Option+digit), drawn on the corner.
|
||||
@@ -584,6 +645,13 @@ function renderActionRow(a, i) {
|
||||
line.className = "row-line";
|
||||
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
||||
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) {
|
||||
var sc = document.createElement("div");
|
||||
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" }]),
|
||||
"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");
|
||||
|
||||
@@ -364,6 +364,18 @@ body {
|
||||
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 {
|
||||
display: inline-flex;
|
||||
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.
|
||||
// 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
|
||||
@@ -180,7 +191,8 @@ struct SettingAction : AppAction
|
||||
std::string title,
|
||||
std::string group,
|
||||
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))
|
||||
, opt_key(std::move(opt_key_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
|
||||
// (like the sidebar's own settings search), then the dial closes. run() performs the jump.
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = std::move(group);
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = std::move(group);
|
||||
this->required_mode = mode_in;
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
// fresh OptionsSearcher has an empty groups_and_categories, so append_options() would drop every
|
||||
// option and nothing would materialise. Turn each visible option into a SettingAction.
|
||||
const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_options();
|
||||
// configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting,
|
||||
// including those above the user's current mode, and can prompt to switch before jumping.
|
||||
const std::vector<Search::Option>& options = wxGetApp().sidebar().get_searcher().all_modes_options();
|
||||
|
||||
// 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.
|
||||
@@ -540,7 +552,7 @@ void ActionRegistry::materialize_setting_actions()
|
||||
// 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.
|
||||
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();
|
||||
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
|
||||
@@ -726,7 +738,8 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
{"source", a->source_name()},
|
||||
{"group", a->group},
|
||||
{"input", a->input},
|
||||
{"shortcut", ""}});
|
||||
{"shortcut", ""},
|
||||
{"mode", mode_key(a->required_mode)}});
|
||||
};
|
||||
|
||||
nlohmann::json actions = nlohmann::json::array();
|
||||
@@ -765,7 +778,10 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
for (const AppAction* a : recent)
|
||||
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) ----------
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <libslic3r/Config.hpp>
|
||||
|
||||
#include <wx/string.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
|
||||
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
|
||||
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;
|
||||
// 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
|
||||
};
|
||||
|
||||
// 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
|
||||
// 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);
|
||||
|
||||
@@ -578,113 +578,131 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
|
||||
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
|
||||
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
|
||||
auto sub_menu = new wxMenu;
|
||||
|
||||
// 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 and
|
||||
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come
|
||||
// with that). Labels are wrapped in L() so they are picked up for translation.
|
||||
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);
|
||||
const std::vector<HandyModel>& models = handy_models();
|
||||
for (std::size_t i = 0; i < models.size(); ++i) {
|
||||
append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "",
|
||||
[i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", 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) {
|
||||
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); };
|
||||
|
||||
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
|
||||
type == ModelVolumeType::INVALID // cannot use gizmo without selected object
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
|
||||
@@ -51,6 +52,23 @@ public:
|
||||
static std::vector<wxBitmap> get_text_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() = default;
|
||||
|
||||
|
||||
@@ -2,18 +2,23 @@
|
||||
|
||||
#include "calib_dlg.hpp"
|
||||
#include "Camera.hpp"
|
||||
#include "DailyTips.hpp"
|
||||
#include "GCodeViewer.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "IMSlider.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "NetworkTestDialog.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "PluginsDialog.hpp"
|
||||
#include "PlateSettingsDialog.hpp"
|
||||
#include "DeviceCore/DevManager.h"
|
||||
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -24,6 +29,8 @@
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <wx/utils.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
@@ -112,6 +119,19 @@ AppActionRunResult calib_command(CalibKind kind)
|
||||
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> out;
|
||||
@@ -174,17 +194,26 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
|
||||
// ---- Mode ----
|
||||
add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) {
|
||||
wxGetApp().save_mode(comSimple);
|
||||
select_mode(comSimple);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) {
|
||||
wxGetApp().save_mode(comAdvanced);
|
||||
select_mode(comAdvanced);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) {
|
||||
wxGetApp().save_mode(comExpert);
|
||||
select_mode(comExpert);
|
||||
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 ----
|
||||
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(); });
|
||||
});
|
||||
|
||||
// ---- 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 ----
|
||||
add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
@@ -458,6 +538,53 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
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 ----
|
||||
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||
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)
|
||||
{
|
||||
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];
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
|
||||
|
||||
@@ -99,13 +99,14 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty
|
||||
}
|
||||
|
||||
if (!label.IsEmpty())
|
||||
options.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()});
|
||||
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(),
|
||||
false, opt_mode});
|
||||
};
|
||||
|
||||
for (std::string opt_key : config->keys()) {
|
||||
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
||||
if (opt.mode > mode) continue;
|
||||
const bool in_filtered = opt.mode <= mode;
|
||||
|
||||
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;
|
||||
|
||||
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)
|
||||
emplace(key, label);
|
||||
add(key);
|
||||
else
|
||||
for (int i = 0; i < cnt; ++i)
|
||||
// ! 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)
|
||||
{
|
||||
options.clear();
|
||||
options_all_modes.clear();
|
||||
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
||||
sort_options();
|
||||
|
||||
@@ -318,6 +325,8 @@ void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, Confi
|
||||
if (options.empty()) return;
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ struct Option
|
||||
std::wstring category;
|
||||
std::wstring category_local;
|
||||
bool multi_category { false };
|
||||
ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt
|
||||
|
||||
std::string opt_key() const;
|
||||
};
|
||||
@@ -97,6 +98,9 @@ class OptionsSearcher
|
||||
PrinterTechnology printer_technology;
|
||||
|
||||
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{};
|
||||
|
||||
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).
|
||||
// Used by the Speed Dial to materialise config settings as first-class actions.
|
||||
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 "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <libslic3r/AppConfig.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
// 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(); }
|
||||
|
||||
// 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.
|
||||
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
|
||||
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())
|
||||
EndModal(wxID_CANCEL);
|
||||
else
|
||||
@@ -231,7 +270,8 @@ void SpeedDialWebDialog::send_actions()
|
||||
call_web_handler({{"command", "list_actions"},
|
||||
{"actions", std::move(snap["actions"])},
|
||||
{"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
|
||||
|
||||
@@ -112,3 +112,91 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[speeddi
|
||||
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