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:
Lam Wei Lun
2026-09-10 18:34:05 +08:00
parent f520af9901
commit 8bbce371b2
12 changed files with 585 additions and 120 deletions
+26 -10
View File
@@ -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) ----------
+12
View File
@@ -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);
+116 -98
View File
@@ -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
+18
View File
@@ -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;
+130 -3
View File
@@ -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();
+15 -6
View File
@@ -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);
+8
View File
@@ -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; }
};
//------------------------------------------
+41 -1
View File
@@ -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