mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-22 08:22:58 +00:00
Speed Dial Enhancements (#15562)
This commit is contained in:
@@ -286,6 +286,8 @@ void AppConfig::set_defaults()
|
||||
// The getter already defaults, parses and clamps; write back what it resolves to.
|
||||
set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
|
||||
|
||||
set(SETTING_SPEED_DIAL_RECENT_COUNT, std::to_string(get_speed_dial_recent_count()));
|
||||
|
||||
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
|
||||
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
|
||||
|
||||
@@ -1684,6 +1686,22 @@ int AppConfig::get_plugin_pages_visible_count() const
|
||||
return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
|
||||
}
|
||||
|
||||
int AppConfig::get_speed_dial_recent_count() const
|
||||
{
|
||||
std::string value = get(SETTING_SPEED_DIAL_RECENT_COUNT);
|
||||
if (value.empty())
|
||||
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
|
||||
int recent_count = SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
try {
|
||||
recent_count = std::stoi(value);
|
||||
}
|
||||
catch (...) {
|
||||
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
}
|
||||
return std::clamp(recent_count, SPEED_DIAL_RECENT_COUNT_MIN, SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
}
|
||||
|
||||
std::vector<std::string> AppConfig::get_skipped_network_versions() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
|
||||
@@ -46,6 +46,11 @@ using namespace nlohmann;
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
|
||||
|
||||
#define SETTING_SPEED_DIAL_RECENT_COUNT "speed_dial_recent_count"
|
||||
#define SPEED_DIAL_RECENT_COUNT_MIN 0
|
||||
#define SPEED_DIAL_RECENT_COUNT_DEFAULT 5
|
||||
#define SPEED_DIAL_RECENT_COUNT_MAX 10
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
|
||||
#else
|
||||
@@ -394,6 +399,9 @@ public:
|
||||
// dropdown on the last tab.
|
||||
int get_plugin_pages_visible_count() const;
|
||||
|
||||
// Number of recently launched actions shown at the top of the Speed Dial; 0 hides them.
|
||||
int get_speed_dial_recent_count() const;
|
||||
|
||||
std::vector<std::string> get_skipped_network_versions() const;
|
||||
void add_skipped_network_version(const std::string& version);
|
||||
bool is_network_version_skipped(const std::string& version) const;
|
||||
|
||||
@@ -129,6 +129,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/SpeedDialDialog.hpp
|
||||
GUI/ActionRegistry.cpp
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/NativeCommands.cpp
|
||||
GUI/NativeCommands.hpp
|
||||
GUI/PluginsConfigDialog.cpp
|
||||
GUI/PluginsConfigDialog.hpp
|
||||
GUI/ProcessRunner.cpp
|
||||
@@ -476,6 +478,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/SkipPartCanvas.hpp
|
||||
GUI/Search.cpp
|
||||
GUI/Search.hpp
|
||||
GUI/SettingsIndex.cpp
|
||||
GUI/SettingsIndex.hpp
|
||||
GUI/Selection.cpp
|
||||
GUI/Selection.hpp
|
||||
GUI/SelectMachine.cpp
|
||||
|
||||
@@ -3,20 +3,48 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "NativeCommands.hpp"
|
||||
#include "Notebook.hpp"
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/AppConfig.hpp>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(std::min(ids.size(), limit));
|
||||
for (const auto& id : ids) {
|
||||
if (out.size() >= limit)
|
||||
break;
|
||||
if (std::find(out.begin(), out.end(), id) == out.end())
|
||||
out.push_back(id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kConfigSection = "speed_dial";
|
||||
@@ -28,14 +56,9 @@ nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallba
|
||||
}
|
||||
|
||||
nlohmann::json read_section(const char* key, nlohmann::json fallback)
|
||||
{
|
||||
return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback));
|
||||
}
|
||||
{ return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback)); }
|
||||
|
||||
void write_section(const char* key, const nlohmann::json& j)
|
||||
{
|
||||
wxGetApp().app_config->set(kConfigSection, key, j.dump());
|
||||
}
|
||||
void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
|
||||
|
||||
std::vector<std::string> read_string_array(const char* key)
|
||||
{
|
||||
@@ -53,7 +76,7 @@ double frecency_score(int count, long long last, long long now)
|
||||
if (count <= 0)
|
||||
return 0.0;
|
||||
constexpr double HALF_LIFE_DAYS = 30.0;
|
||||
double age = std::max(0.0, double(now - last) / 86400.0);
|
||||
double age = std::max(0.0, double(now - last) / 86400.0);
|
||||
return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
|
||||
}
|
||||
|
||||
@@ -79,26 +102,25 @@ struct PluginScriptAction : AppAction
|
||||
// The id an action for (plugin_key, capability) would have - lets refresh_capability
|
||||
// remove a gone capability without materialising the action.
|
||||
static std::string id_for(const std::string& plugin_key, const std::string& capability)
|
||||
{
|
||||
return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key);
|
||||
}
|
||||
{ return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); }
|
||||
|
||||
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
|
||||
: AppAction(kIdPrefix,
|
||||
capability_in.empty() ? plugin_key_in : capability_in, // title
|
||||
plugin_key_in, // source_key
|
||||
std::move(source_name)),
|
||||
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in))
|
||||
capability_in.empty() ? plugin_key_in : capability_in, // title
|
||||
plugin_key_in, // source_key
|
||||
std::move(source_name))
|
||||
, plugin_key(std::move(plugin_key_in))
|
||||
, capability(std::move(capability_in))
|
||||
{}
|
||||
|
||||
AppActionRunResult run() const override
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
std::string error;
|
||||
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
|
||||
if (!error.empty())
|
||||
return {AppActionRunResult::Level::Error, from_u8(error)};
|
||||
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
|
||||
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
|
||||
result.message.empty() ? fallback : from_u8(result.message)};
|
||||
@@ -107,8 +129,7 @@ struct PluginScriptAction : AppAction
|
||||
|
||||
// Builds an action for a capability, or nullptr if it is not a currently-loaded,
|
||||
// enabled script capability.
|
||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability,
|
||||
const std::string& source_name)
|
||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (!manager.is_plugin_loaded(plugin_key))
|
||||
@@ -119,8 +140,151 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
|
||||
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
||||
}
|
||||
|
||||
// ---- built-in command actions (the speed dial "commands" section) ------
|
||||
|
||||
constexpr const char* kSettingPrefix = "orca_setting";
|
||||
constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
|
||||
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
|
||||
|
||||
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
|
||||
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
|
||||
std::string setting_type_context(Preset::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Preset::TYPE_FILAMENT:
|
||||
case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament");
|
||||
case Preset::TYPE_PRINTER: return _u8L("Printer");
|
||||
case Preset::TYPE_PRINT:
|
||||
case Preset::TYPE_SLA_PRINT:
|
||||
default: return _u8L("Process");
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// the generic registry run() bumps stats so a jump shows up in "recents" like any other action.
|
||||
struct SettingAction : AppAction
|
||||
{
|
||||
std::string opt_key;
|
||||
Preset::Type type;
|
||||
std::wstring category; // English category, forwarded to jump_to_option (it localizes)
|
||||
|
||||
static std::string id_for(const std::string& opt_key, Preset::Type type)
|
||||
{ return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); }
|
||||
|
||||
SettingAction(std::string opt_key_in,
|
||||
Preset::Type type_in,
|
||||
std::string title,
|
||||
std::string group,
|
||||
std::wstring category_in,
|
||||
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)
|
||||
, category(std::move(category_in))
|
||||
{
|
||||
// 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->required_mode = mode_in;
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
wxGetApp().sidebar().jump_to_option(opt_key, type, category);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
};
|
||||
|
||||
// Seed one action's persisted state (favourite flag + frecency counters) from an already-parsed
|
||||
// stats blob and capped favourite list. Shared by the dynamic materialisers.
|
||||
void seed_from(const nlohmann::json& stats, const std::vector<std::string>& favs, const std::string& id, AppAction& a)
|
||||
{
|
||||
a.favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
|
||||
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
|
||||
a.count = it->value("count", 0);
|
||||
a.last = it->value("last", 0LL);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop actions whose id starts with `prefix` but that were not seen in this pass (a stale materialisation).
|
||||
void drop_stale(std::unordered_map<std::string, std::shared_ptr<AppAction>>& actions, const char* prefix,
|
||||
const std::unordered_set<std::string>& seen)
|
||||
{
|
||||
for (auto it = actions.begin(); it != actions.end();) {
|
||||
if (it->first.rfind(prefix, 0) == 0 && !seen.count(it->first))
|
||||
it = actions.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a
|
||||
// rename/move immediately shows up). id is keyed by plate index, NOT the display title, so
|
||||
// renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to
|
||||
// Plate N" whose plate is deleted simply stops resolving (visibleFavourites drops dead pins).
|
||||
struct PlateAction : AppAction
|
||||
{
|
||||
int plate_index;
|
||||
|
||||
static std::string id_for(int index) { return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); }
|
||||
|
||||
PlateAction(int index, std::string title, std::string source_name)
|
||||
: AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index)
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = _u8L("Plate");
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{ return NativeCommands::run("plate_goto", std::to_string(plate_index)); }
|
||||
};
|
||||
|
||||
// A dynamic "Open recent project <name>" action, one per recent project file, rebuilt on every
|
||||
// snapshot() (like PlateAction) so the list always reflects the current recents. The id is keyed
|
||||
// by the file PATH, NOT the display title - the same contract as SettingAction/PlateAction, so a
|
||||
// rename of a project (or a reordered recents list) never re-keys the action. A pinned recent whose
|
||||
// file is deleted simply stops resolving (visibleFavourites drops dead pins). run() loads the
|
||||
// project through MainFrame::open_recent_project so the existing missing-file handling is reused.
|
||||
struct RecentProjectAction : AppAction
|
||||
{
|
||||
std::string file_path;
|
||||
|
||||
static std::string id_for(const std::string& path) { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); }
|
||||
|
||||
RecentProjectAction(std::string path, std::string title, std::string source)
|
||||
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path))
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = _u8L("Recent Projects");
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->open_recent_project(size_t(-1), wxString::FromUTF8(file_path));
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ActionRegistry::~ActionRegistry() = default;
|
||||
|
||||
void ActionRegistry::init()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
@@ -151,18 +315,12 @@ void ActionRegistry::init()
|
||||
// Subscribe before enumerating so a concurrent load cannot land between the initial
|
||||
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
|
||||
// id and the m_actions scan in refresh_source is idempotent.
|
||||
manager.subscribe_on_load_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
manager.subscribe_on_unload_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
manager.subscribe_on_load_callback([on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
manager.subscribe_on_unload_callback([on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
manager.subscribe_on_capability_load_callback(
|
||||
[on_capability](const PluginCapabilityId& capability) {
|
||||
on_capability(capability, ActionChange::Added);
|
||||
});
|
||||
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
|
||||
manager.subscribe_on_capability_unload_callback(
|
||||
[on_capability](const PluginCapabilityId& capability) {
|
||||
on_capability(capability, ActionChange::Removed);
|
||||
});
|
||||
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
|
||||
|
||||
// enumerate current script capabilities
|
||||
std::unordered_map<std::string, std::string> source_names;
|
||||
@@ -173,12 +331,40 @@ void ActionRegistry::init()
|
||||
for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) {
|
||||
if (!capability)
|
||||
continue;
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& source_name = it == source_names.end() ? key : it->second;
|
||||
if (auto action = make_action(key, capability->name(), source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
|
||||
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
|
||||
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
|
||||
// by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in
|
||||
// NativeCommands; the registry only stores and dispatches the result.
|
||||
for (const NativeCommand& c : NativeCommands::catalog())
|
||||
upsert(NativeCommands::make_action(c));
|
||||
}
|
||||
|
||||
void ActionRegistry::relocalize_builtins()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
if (!m_started)
|
||||
return;
|
||||
|
||||
// Drop only the built-in commands; plugins and the dynamically materialised families are
|
||||
// either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert()
|
||||
// re-seeds favourites/stats from config, so key-based ids keep their pinned state.
|
||||
std::vector<std::string> stale;
|
||||
for (const auto& [id, action] : m_actions)
|
||||
if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command)
|
||||
stale.push_back(id);
|
||||
for (const std::string& id : stale)
|
||||
remove(id);
|
||||
|
||||
NativeCommands::rebuild_catalog();
|
||||
for (const NativeCommand& c : NativeCommands::catalog())
|
||||
upsert(NativeCommands::make_action(c));
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
|
||||
@@ -198,7 +384,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
if (change == ActionChange::Removed)
|
||||
return;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
const std::string source_name = find_loaded_source_name(manager, plugin_key);
|
||||
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) {
|
||||
if (!capability)
|
||||
@@ -208,8 +394,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
|
||||
ActionChange change)
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
@@ -233,7 +418,7 @@ void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
|
||||
return;
|
||||
|
||||
seed_state(*action);
|
||||
std::string id = action->id();
|
||||
std::string id = action->id();
|
||||
std::shared_ptr<AppAction> stored = std::move(action);
|
||||
m_actions.insert_or_assign(std::move(id), std::move(stored));
|
||||
}
|
||||
@@ -246,11 +431,13 @@ void ActionRegistry::remove(const std::string& id)
|
||||
|
||||
void ActionRegistry::seed_state(AppAction& a) const
|
||||
{
|
||||
auto favs = read_string_array("favourite_actions");
|
||||
// Favourites carry the quick-launch order, so the persisted list is the source of truth
|
||||
// (not re-derived from the frecency sort). Cap it so stale configs can't exceed kFavLimit.
|
||||
auto favs = favourite_ids();
|
||||
a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end();
|
||||
|
||||
nlohmann::json stats = read_section("stats", nlohmann::json::object());
|
||||
auto it = stats.find(a.id());
|
||||
auto it = stats.find(a.id());
|
||||
if (it != stats.end() && it->is_object()) {
|
||||
a.count = it->value("count", 0);
|
||||
a.last = it->value("last", 0LL);
|
||||
@@ -260,6 +447,14 @@ void ActionRegistry::seed_state(AppAction& a) const
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const
|
||||
{
|
||||
stats = read_section("stats", nlohmann::json::object());
|
||||
if (!stats.is_object())
|
||||
stats = nlohmann::json::object();
|
||||
favs = favourite_ids();
|
||||
}
|
||||
|
||||
// ---- read surface -----------------------------------------------------------
|
||||
|
||||
const AppAction* ActionRegistry::by_id(const std::string& id) const
|
||||
@@ -269,14 +464,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const
|
||||
return it == m_actions.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
AppAction* ActionRegistry::find(const std::string& id)
|
||||
{
|
||||
return const_cast<AppAction*>(by_id(id));
|
||||
}
|
||||
AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
|
||||
|
||||
// ---- dispatch + write-through ----------------------------------------------
|
||||
|
||||
AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
auto it = m_actions.find(id);
|
||||
@@ -286,13 +478,13 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
// nested event loop; a queued source refresh can erase the entry while the
|
||||
// keep-alive preserves the action until run returns.
|
||||
std::shared_ptr<AppAction> keep = it->second;
|
||||
AppActionRunResult o = keep->run();
|
||||
AppActionRunResult o = keep->run(param);
|
||||
if (o.level == AppActionRunResult::Level::Busy)
|
||||
return o;
|
||||
|
||||
// Bump stats (write-through). Re-read to avoid clobbering a concurrent field.
|
||||
nlohmann::json stats = read_section("stats", nlohmann::json::object());
|
||||
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
|
||||
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
|
||||
stats = nlohmann::json::object();
|
||||
nlohmann::json& e = stats[id];
|
||||
if (!e.is_object())
|
||||
@@ -300,22 +492,37 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
e["count"] = e.value("count", 0) + 1;
|
||||
e["last"] = (long long) std::time(nullptr);
|
||||
write_section("stats", stats);
|
||||
if (AppAction* live = find(id)) { live->count = e["count"]; live->last = e["last"]; }
|
||||
if (AppAction* live = find(id)) {
|
||||
live->count = e["count"];
|
||||
live->last = e["last"];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
void ActionRegistry::set_favourite(const std::string& id, bool on)
|
||||
bool ActionRegistry::set_favourite(const std::string& id, bool on)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
auto favs = read_string_array("favourite_actions");
|
||||
// Start from the capped, deduped list so a persisted config can never be written back larger.
|
||||
auto favs = favourite_ids();
|
||||
auto it = std::find(favs.begin(), favs.end(), id);
|
||||
if (on && it == favs.end())
|
||||
if (on && it == favs.end()) {
|
||||
if (favs.size() >= kFavLimit)
|
||||
return false; // bar is full - the caller surfaces a hint
|
||||
favs.push_back(id);
|
||||
}
|
||||
if (!on && it != favs.end())
|
||||
favs.erase(it);
|
||||
write_section("favourite_actions", nlohmann::json(favs));
|
||||
if (AppAction* live = find(id))
|
||||
live->favourite = on;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> ActionRegistry::favourite_ids() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
// Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, even from an older config.
|
||||
return cap_favourites(read_string_array("favourite_actions"), kFavLimit);
|
||||
}
|
||||
|
||||
void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
|
||||
@@ -325,16 +532,184 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
|
||||
std::vector<std::string> next;
|
||||
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
|
||||
for (const auto& id : ids)
|
||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() &&
|
||||
std::find(next.begin(), next.end(), id) == next.end())
|
||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
|
||||
next.push_back(id);
|
||||
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
|
||||
for (const auto& id : cur)
|
||||
if (std::find(next.begin(), next.end(), id) == next.end())
|
||||
next.push_back(id);
|
||||
// never write the bar back larger than the quick-launch slots
|
||||
next = cap_favourites(next, kFavLimit);
|
||||
write_section("favourite_actions", nlohmann::json(next));
|
||||
}
|
||||
|
||||
void ActionRegistry::materialize_setting_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Reuse the Sidebar's live settings index: it's the only catalog whose group/category map is
|
||||
// populated (Tab::add_key feeds it at build time), and it already mirrors the current
|
||||
// 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().settings_index().all_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.
|
||||
nlohmann::json stats;
|
||||
std::vector<std::string> favs;
|
||||
load_persisted(stats, favs);
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const Search::Option& opt : options) {
|
||||
// The row's live state drives both the hidden filter and the title (labels can change at
|
||||
// runtime, e.g. brim_width -> "Brim ear radius"). Hidden rows are skipped, not marked seen.
|
||||
Tab* tab = wxGetApp().get_tab(opt.type);
|
||||
Tab::SettingRowState row;
|
||||
if (tab)
|
||||
row = tab->setting_row_state(opt.opt_key());
|
||||
if (!row.visible)
|
||||
continue;
|
||||
|
||||
const std::string id = SettingAction::id_for(opt.opt_key(), opt.type);
|
||||
seen.insert(id);
|
||||
|
||||
// The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias
|
||||
// ("overhang reversal" still finds "Reverse on even").
|
||||
const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local);
|
||||
std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi));
|
||||
if (title.empty())
|
||||
title = search_label;
|
||||
|
||||
// Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS
|
||||
// renders group || source and searches source + " " + group, so putting the whole path in
|
||||
// source both displays it and makes it matchable by any segment (e.g. a "quality" query).
|
||||
std::wstring path = boost::nowide::widen(setting_type_context(opt.type));
|
||||
if (!opt.category_local.empty())
|
||||
path += L" : " + opt.category_local;
|
||||
if (!opt.group_local.empty())
|
||||
path += L" : " + opt.group_local;
|
||||
|
||||
// title = the label the settings row draws; group stays empty so the source path (above) is
|
||||
// the single display/search breadcrumb rather than being duplicated.
|
||||
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category,
|
||||
boost::nowide::narrow(path), opt.mode);
|
||||
if (title != search_label)
|
||||
action->full_label = search_label;
|
||||
|
||||
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
|
||||
// the one shown next to it in the page. Fall back to the page/category icon for groups
|
||||
// without one. Keys are the English titles the GUI registers.
|
||||
action->icon = opt.group_icon;
|
||||
if (action->icon.empty() && !opt.category.empty() && tab) {
|
||||
const auto& icons = tab->get_category_icon_map();
|
||||
auto it = icons.find(wxString(opt.category));
|
||||
if (it != icons.end())
|
||||
action->icon = it->second;
|
||||
}
|
||||
|
||||
// Footer description + wiki affordance; only settings whose row declared a wiki path have one.
|
||||
action->tooltip = opt.tooltip;
|
||||
if (!opt.wiki_path.empty())
|
||||
action->help_url = into_u8(OptionsGroup::get_url(opt.wiki_path));
|
||||
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
m_actions.insert_or_assign(action_id, app_action);
|
||||
}
|
||||
|
||||
// Drop SettingActions whose option no longer exists in the current configs (e.g. the printer
|
||||
// technology / UI mode changed). Non-setting actions are untouched.
|
||||
drop_stale(m_actions, kSettingPrefix, seen);
|
||||
}
|
||||
|
||||
void ActionRegistry::materialize_plate_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Plates are a filament (FFF) feature: SLA has a single plate and no plate UI, and gcode-only
|
||||
// mode has no editable project - so no "Go to Plate N" actions are offered there.
|
||||
Plater* plater = wxTheApp ? wxGetApp().plater() : nullptr;
|
||||
if (!plater || plater->printer_technology() != ptFFF || plater->only_gcode_mode()) {
|
||||
// Drop any stale plate actions (e.g. the printer technology switched to SLA).
|
||||
drop_stale(m_actions, kPlateGotoPrefix, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Persisted per-action state, read ONCE (mirrors materialize_setting_actions) so a relisted
|
||||
// "Go to Plate N" keeps its recency/favourite when the plate is renamed - the id is index-keyed.
|
||||
nlohmann::json stats;
|
||||
std::vector<std::string> favs;
|
||||
load_persisted(stats, favs);
|
||||
|
||||
const std::vector<PartPlate*>& list = plater->get_partplate_list().get_plate_list();
|
||||
std::unordered_set<std::string> seen;
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
PartPlate* plate = list[i];
|
||||
if (!plate)
|
||||
continue;
|
||||
const std::string id = PlateAction::id_for(int(i));
|
||||
seen.insert(id);
|
||||
|
||||
// "Go to Plate N" + " (name)" when the plate is named, matching the object-list label.
|
||||
std::string title(_u8L("Go to Plate"));
|
||||
title += " " + std::to_string(i + 1);
|
||||
const std::string name = plate->get_plate_name();
|
||||
if (!name.empty())
|
||||
title += " (" + name + ")";
|
||||
|
||||
auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName);
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
m_actions.insert_or_assign(action_id, app_action);
|
||||
}
|
||||
|
||||
// Drop plate actions whose index no longer exists (a plate was deleted / moved to the front).
|
||||
drop_stale(m_actions, kPlateGotoPrefix, seen);
|
||||
}
|
||||
|
||||
void ActionRegistry::materialize_recent_project_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent
|
||||
// project keeps its recency/favourite when the recents list reorders - the id is path-keyed.
|
||||
nlohmann::json stats;
|
||||
std::vector<std::string> favs;
|
||||
load_persisted(stats, favs);
|
||||
|
||||
// app_config stores recents oldest-first; the palette shows newest-first.
|
||||
std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects();
|
||||
std::reverse(recents.begin(), recents.end());
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const std::string& path : recents) {
|
||||
// Skip projects whose file is gone; the stale id is dropped below.
|
||||
boost::system::error_code ec;
|
||||
if (path.empty() || !boost::filesystem::exists(boost::filesystem::path(path), ec))
|
||||
continue;
|
||||
|
||||
const std::string id = RecentProjectAction::id_for(path);
|
||||
seen.insert(id);
|
||||
|
||||
// Title = file basename; source/eyebrow = the full path so search can match either.
|
||||
boost::filesystem::path p(path);
|
||||
std::string title = p.filename().string();
|
||||
if (title.empty())
|
||||
title = path;
|
||||
|
||||
auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path);
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
m_actions.insert_or_assign(action_id, app_action);
|
||||
}
|
||||
|
||||
// Drop recent-project actions whose file no longer exists / was removed from the recents list.
|
||||
drop_stale(m_actions, kRecentProjectPrefix, seen);
|
||||
}
|
||||
|
||||
bool ActionRegistry::should_ask(const std::string& id) const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
@@ -351,11 +726,31 @@ void ActionRegistry::suppress_ask(const std::string& id)
|
||||
write_section("ask_suppressed", nlohmann::json(arr));
|
||||
}
|
||||
|
||||
// ---- snapshot ---------------------------------------------------------------
|
||||
|
||||
nlohmann::json ActionRegistry::snapshot() const
|
||||
bool ActionRegistry::tooltip_expanded() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
const nlohmann::json j = read_section("tooltip_expanded", nlohmann::json(true));
|
||||
return j.is_boolean() ? j.get<bool>() : true;
|
||||
}
|
||||
|
||||
void ActionRegistry::set_tooltip_expanded(bool expanded)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
write_section("tooltip_expanded", nlohmann::json(expanded));
|
||||
}
|
||||
|
||||
// ---- snapshot ---------------------------------------------------------------
|
||||
|
||||
nlohmann::json ActionRegistry::snapshot()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
// Settings and plates are first-class actions; make sure the current visible option set and the
|
||||
// live plate list are materialised before we serialise the pool (tabs_list is built by the time
|
||||
// the palette opens).
|
||||
materialize_setting_actions();
|
||||
materialize_plate_actions();
|
||||
materialize_recent_project_actions();
|
||||
|
||||
std::vector<const AppAction*> sorted;
|
||||
sorted.reserve(m_actions.size());
|
||||
for (const auto& entry : m_actions)
|
||||
@@ -374,17 +769,85 @@ nlohmann::json ActionRegistry::snapshot() const
|
||||
return a->id() < b->id();
|
||||
});
|
||||
|
||||
auto action_to_json = [](const AppAction* a) {
|
||||
return nlohmann::json({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"full_label", a->full_label},
|
||||
{"source", a->source_name()},
|
||||
{"group", a->group},
|
||||
{"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"},
|
||||
{"input", a->input},
|
||||
{"icon", a->icon},
|
||||
{"mode", mode_key(a->required_mode)},
|
||||
{"desc", a->tooltip},
|
||||
{"wiki", !a->help_url.empty()}});
|
||||
};
|
||||
|
||||
nlohmann::json actions = nlohmann::json::array();
|
||||
for (const AppAction* a : sorted)
|
||||
actions.push_back({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"source", a->source_name()},
|
||||
{"shortcut", ""}});
|
||||
actions.push_back(action_to_json(a));
|
||||
|
||||
// why: favourites is the ORDERED pin list - it must come from favourite_actions
|
||||
// as stored, not be re-derived from the frecency-sorted actions (that would
|
||||
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
|
||||
nlohmann::json favourites(read_string_array("favourite_actions"));
|
||||
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}};
|
||||
// reorder the favourites bar). Drop pins with no live action (an option hidden by the
|
||||
// current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so
|
||||
// invisible pins can't silently fill the quick-launch cap. Order is preserved.
|
||||
std::vector<std::string> favs = favourite_ids();
|
||||
std::vector<std::string> live_favs;
|
||||
live_favs.reserve(favs.size());
|
||||
for (const auto& id : favs)
|
||||
if (m_actions.count(id))
|
||||
live_favs.push_back(id);
|
||||
if (live_favs.size() != favs.size())
|
||||
write_section("favourite_actions", nlohmann::json(live_favs));
|
||||
nlohmann::json favourites(live_favs);
|
||||
|
||||
// Recent = the last-N launched actions by recency (only actions with a run history). N is a
|
||||
// user preference; 0 hides recents without affecting the frecency order below.
|
||||
const size_t recent_limit = size_t(wxGetApp().app_config->get_speed_dial_recent_count());
|
||||
std::vector<const AppAction*> recent;
|
||||
for (const auto& entry : m_actions)
|
||||
if (entry.second->last > 0)
|
||||
recent.push_back(entry.second.get());
|
||||
std::sort(recent.begin(), recent.end(), [](const AppAction* a, const AppAction* b) {
|
||||
if (a->last != b->last)
|
||||
return a->last > b->last;
|
||||
return a->id() < b->id();
|
||||
});
|
||||
if (recent.size() > recent_limit)
|
||||
recent.resize(recent_limit);
|
||||
nlohmann::json recent_json = nlohmann::json::array();
|
||||
for (const AppAction* a : recent)
|
||||
recent_json.push_back(action_to_json(a));
|
||||
|
||||
return {{"actions", std::move(actions)},
|
||||
{"favourites", std::move(favourites)},
|
||||
{"recent", std::move(recent_json)},
|
||||
{"user_mode", mode_key(wxGetApp().get_mode())},
|
||||
{"tooltip_expanded", tooltip_expanded()}};
|
||||
}
|
||||
|
||||
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
|
||||
|
||||
nlohmann::json ActionRegistry::tab_options() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
if (!wxTheApp || wxGetApp().is_closing())
|
||||
return out;
|
||||
MainFrame* mf = wxGetApp().mainframe;
|
||||
if (!mf || !mf->m_tabpanel)
|
||||
return out;
|
||||
Notebook* notebook = mf->m_tabpanel;
|
||||
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
|
||||
const wxString id = notebook->GetPageName(i);
|
||||
if (id.empty())
|
||||
continue;
|
||||
out.push_back({{"id", id.ToStdString()},
|
||||
{"title", notebook->GetPageLabel(i).ToStdString()},
|
||||
{"icon", notebook->GetPageIcon(i)}});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <libslic3r/Config.hpp>
|
||||
|
||||
#include <wx/string.h>
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -18,14 +21,25 @@ namespace Slic3r { namespace GUI {
|
||||
// How a source's action set changed. Drives the registry's refresh handlers.
|
||||
enum class ActionChange { Added, Removed };
|
||||
|
||||
// What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't).
|
||||
enum class AppActionKind { Plugin, Command };
|
||||
|
||||
// Result of running an AppAction, in the action layer's own vocabulary. Concrete
|
||||
// actions translate their runner-specific result into this generic shape.
|
||||
struct AppActionRunResult
|
||||
{
|
||||
enum class Level { Success, Info, Error, Busy };
|
||||
|
||||
Level level = Level::Info;
|
||||
wxString message; // empty = "nothing worth showing"
|
||||
Level level = Level::Info;
|
||||
wxString message; // empty = "nothing worth showing"
|
||||
};
|
||||
|
||||
// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the
|
||||
// id ctor and the compose-from-prefix ctor are NOT both reachable from a `const char*` first
|
||||
// argument (which would make calls like AppAction("orca_command", ...) ambiguous).
|
||||
struct AppActionId
|
||||
{
|
||||
std::string id;
|
||||
};
|
||||
|
||||
// A speed-dial action: identity + user-state seeded from config + how to run itself.
|
||||
@@ -52,12 +66,35 @@ struct AppAction
|
||||
}
|
||||
|
||||
// seeded from AppConfig for the snapshot / sort:
|
||||
bool favourite = false;
|
||||
int count = 0;
|
||||
long long last = 0; // epoch seconds
|
||||
bool favourite = false;
|
||||
int count = 0;
|
||||
long long last = 0; // epoch seconds
|
||||
|
||||
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
|
||||
// source name); Command sets a section label (e.g. "Commands", "Mode").
|
||||
AppActionKind kind = AppActionKind::Plugin;
|
||||
std::string group;
|
||||
// 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;
|
||||
// Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands
|
||||
// without a GUI icon, plugins). Set from NativeCommands / the setting's category icon.
|
||||
std::string icon;
|
||||
// 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;
|
||||
// Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip).
|
||||
std::string tooltip;
|
||||
// Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title
|
||||
// "Reverse on even", full_label "Overhang reversal"). Empty when the two agree.
|
||||
std::string full_label;
|
||||
// Full wiki URL, when the action has one (SettingActions whose row declared a label_path).
|
||||
std::string help_url;
|
||||
|
||||
virtual ~AppAction() = default;
|
||||
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread)
|
||||
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
|
||||
// commands (e.g. a layer percentage); plugins ignore it.
|
||||
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
|
||||
|
||||
protected:
|
||||
// The definition is constructor-set and immutable. Refreshes replace an action
|
||||
@@ -65,10 +102,17 @@ protected:
|
||||
// why: source_key (not the display name) carries identity, so renaming the source's
|
||||
// display name leaves the id - and its persisted stats/favourite - intact.
|
||||
AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name)
|
||||
: m_id(compose_id(prefix, title, source_key)),
|
||||
m_title(std::move(title)),
|
||||
m_source_key(std::move(source_key)),
|
||||
m_source_name(std::move(source_name)) {}
|
||||
: m_id(compose_id(prefix, title, source_key))
|
||||
, m_title(std::move(title))
|
||||
, m_source_key(std::move(source_key))
|
||||
, m_source_name(std::move(source_name))
|
||||
{}
|
||||
|
||||
// Explicit-id ctor: for actions whose id must NOT be derived from the display title
|
||||
// (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it).
|
||||
AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name)
|
||||
: m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name))
|
||||
{}
|
||||
|
||||
private:
|
||||
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
|
||||
@@ -77,26 +121,53 @@ private:
|
||||
std::string m_source_name; // display name of the action's source
|
||||
};
|
||||
|
||||
// Stable identity/display name of the built-in ("OrcaSlicer") action source. Shared by the native
|
||||
// command catalog and the dynamically materialised setting/plate/recent actions so every built-in
|
||||
// action re-keys together.
|
||||
inline constexpr const char* kOrcaSourceKey = "orca";
|
||||
inline constexpr const char* kOrcaSourceName = "OrcaSlicer";
|
||||
|
||||
// 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);
|
||||
|
||||
// Self-contained sink and single owner of runnable actions for the app session.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the
|
||||
// current script capabilities into actions.
|
||||
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script
|
||||
// capabilities into actions, then materialises the static built-ins from the NativeCommands catalog.
|
||||
// 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(),
|
||||
// which upsert()/remove() actions. The registry keeps the only action list and
|
||||
// restores persisted user state as actions arrive.
|
||||
// 3. Consumers use by_id(), snapshot(), and run() without knowing the source.
|
||||
// 3. Dynamic built-in families (settings, plates, recent projects) are re-materialised at the top of
|
||||
// snapshot(), because their membership follows live state (the current configs, plate list, recents).
|
||||
// 4. Consumers use by_id(), snapshot(), and run() without knowing the source.
|
||||
//
|
||||
// note: there is exactly one source (script plugins), so it lives inline here rather
|
||||
// than behind a polymorphic source interface.
|
||||
// note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and
|
||||
// dispatch, and materialises the dynamic families inline rather than behind a source interface.
|
||||
class ActionRegistry
|
||||
{
|
||||
public:
|
||||
~ActionRegistry();
|
||||
|
||||
// Subscribes to the plugin loader and enumerates its current actions. Call once
|
||||
// on the UI thread after the plugin system is up; wires the initial list and live
|
||||
// updates together.
|
||||
void init();
|
||||
|
||||
// Rebuilds the built-in command actions in the current UI locale. The command catalog copies
|
||||
// translated titles/groups at construction, so after a live language switch the stored titles
|
||||
// are stale until this runs. Ids are key-based and upsert re-seeds persisted state, so
|
||||
// favourites/run history survive. UI thread only. No-op before init().
|
||||
void relocalize_builtins();
|
||||
|
||||
// Takes ownership, seeds persisted state, then inserts the action or replaces
|
||||
// the action with the same id. A null action is ignored.
|
||||
void upsert(std::unique_ptr<AppAction> action);
|
||||
@@ -105,31 +176,72 @@ public:
|
||||
void remove(const std::string& id);
|
||||
|
||||
// Always-clean read surface. UI thread only.
|
||||
const AppAction* by_id(const std::string& id) const;
|
||||
const AppAction* by_id(const std::string& id) const;
|
||||
|
||||
// Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0).
|
||||
static constexpr size_t kFavLimit = 10;
|
||||
|
||||
// Dispatch + write-through (registry is the only thing that touches AppConfig).
|
||||
AppActionRunResult run(const std::string& id); // runs + bumps stats
|
||||
void set_favourite(const std::string& id, bool on);
|
||||
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
||||
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
|
||||
// Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
|
||||
// caller can surface a "favourites are full" hint instead of silently dropping the pin.
|
||||
bool set_favourite(const std::string& id, bool on);
|
||||
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
||||
|
||||
// Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the
|
||||
// visible bar the palette renders.
|
||||
std::vector<std::string> favourite_ids() const;
|
||||
|
||||
// Run-confirm gate, keyed by action id (per-action "don't ask again").
|
||||
bool should_ask(const std::string& id) const;
|
||||
void suppress_ask(const std::string& id);
|
||||
|
||||
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}.
|
||||
nlohmann::json snapshot() const;
|
||||
// Footer expand/collapse preference. Global (applies to every action) and persisted; absent
|
||||
// means expanded, so a fresh config picks the richer default with no migration.
|
||||
bool tooltip_expanded() const;
|
||||
void set_tooltip_expanded(bool expanded);
|
||||
|
||||
// Flat, frecency-sorted snapshot for the webview:
|
||||
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
|
||||
nlohmann::json snapshot();
|
||||
|
||||
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
|
||||
// as [{id,title,icon},...], using the page's real label (not the compact-blanked button text).
|
||||
// Live by construction - built-in tabs (Home/Prepare/Preview/Device/Project/Calibration) and
|
||||
// plugin tabs (plugin.<key>.<name>) are all Notebook pages, so a page appears/disappears with
|
||||
// the notebook. Plugin tabs hidden in the overflow menu (many plugins) aren't separate pages and
|
||||
// are not listed. Call on the UI thread; null-safe.
|
||||
nlohmann::json tab_options() const;
|
||||
|
||||
private:
|
||||
void seed_state(AppAction& a) const; // favourite/stats from config
|
||||
AppAction* find(const std::string& id);
|
||||
void seed_state(AppAction& a) const; // favourite/stats from config
|
||||
AppAction* find(const std::string& id);
|
||||
|
||||
// Read the persisted stats blob + capped favourite list once for a materialisation pass.
|
||||
void load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const;
|
||||
|
||||
// (Re)materialise the current visible config settings as SettingActions from the live
|
||||
// searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones.
|
||||
// Called at the top of snapshot() so the palette always reflects the current configs.
|
||||
void materialize_setting_actions();
|
||||
|
||||
// (Re)materialise one "Go to Plate N" action per live plate, so the palette lists every plate
|
||||
// directly on each spawn (no second-phase picker). FFF-editor only; SLA/gcode modes have no
|
||||
// plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot().
|
||||
void materialize_plate_actions();
|
||||
|
||||
// (Re)materialise one "Open recent project <name>" action per recent project file, so the
|
||||
// palette lists every recent project and can load it by clicking. Keyed by file path (stable);
|
||||
// files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot().
|
||||
void materialize_recent_project_actions();
|
||||
|
||||
// Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds
|
||||
// one plugin's whole action set; refresh_capability touches a single capability.
|
||||
void refresh_source(const std::string& plugin_key, ActionChange change);
|
||||
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
|
||||
|
||||
bool m_started = false; // init() runs exactly once; guards double-subscription
|
||||
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
|
||||
bool m_started = false; // init() runs exactly once; guards double-subscription
|
||||
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -1042,7 +1042,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
|
||||
@@ -3582,11 +3581,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
|
||||
break;
|
||||
}
|
||||
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
|
||||
case ' ': {
|
||||
if (m_canvas_type == ECanvasType::CanvasView3D)
|
||||
post_event(SimpleEvent(EVT_GLCANVAS_OPEN_SPEED_DIAL));
|
||||
break;
|
||||
}
|
||||
case 'A':
|
||||
case 'a':
|
||||
{
|
||||
|
||||
@@ -169,7 +169,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
|
||||
|
||||
@@ -2624,6 +2624,10 @@ void GUI_App::init_app_config()
|
||||
}
|
||||
#endif // _WIN32
|
||||
}
|
||||
// Speed Dial opens on a bare Space from any page by default. Seed the flag so Preferences and the
|
||||
// MainFrame shortcut read the same value; an existing config (true or false) is left untouched.
|
||||
if (app_config->get("enable_speed_dial").empty())
|
||||
app_config->set_bool("enable_speed_dial", true);
|
||||
set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level")));
|
||||
|
||||
}
|
||||
@@ -4603,6 +4607,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter";
|
||||
m_is_recreating_gui = true;
|
||||
|
||||
// The palette injects its translated strings once, at creation; drop the cached dialog so the
|
||||
// next open rebuilds it in the current locale (and can't outlive the old mainframe).
|
||||
if (m_speed_dial_dialog) {
|
||||
m_speed_dial_dialog->Destroy();
|
||||
m_speed_dial_dialog = nullptr;
|
||||
}
|
||||
|
||||
mainframe->shutdown();
|
||||
ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE);
|
||||
@@ -8171,6 +8181,22 @@ void GUI_App::save_mode(const /*ConfigOptionMode*/int mode)
|
||||
update_mode();
|
||||
}
|
||||
|
||||
void GUI_App::set_mode(ConfigOptionMode mode)
|
||||
{
|
||||
const bool was_developer = app_config->get_bool("developer_mode");
|
||||
if (was_developer)
|
||||
app_config->set_bool("developer_mode", false);
|
||||
save_mode(mode);
|
||||
if (was_developer)
|
||||
app_config->save();
|
||||
}
|
||||
|
||||
void GUI_App::enable_developer_mode()
|
||||
{
|
||||
app_config->set_bool("developer_mode", true);
|
||||
update_mode();
|
||||
}
|
||||
|
||||
// Update view mode according to selected menu
|
||||
void GUI_App::update_mode()
|
||||
{
|
||||
@@ -8319,6 +8345,65 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::refresh_plugins()
|
||||
{
|
||||
// The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker
|
||||
// and report completion through the notification manager -- the speed dial needs no dialog.
|
||||
std::thread([]() {
|
||||
wxString error;
|
||||
try {
|
||||
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
|
||||
} catch (const std::exception& ex) {
|
||||
error = from_u8(ex.what());
|
||||
} catch (...) {
|
||||
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
|
||||
}
|
||||
if (!wxTheApp)
|
||||
return;
|
||||
wxTheApp->CallAfter([error]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
if (error.IsEmpty())
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
into_u8(_L("Plugins refreshed.")));
|
||||
else
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::install_local_plugin()
|
||||
{
|
||||
if (mainframe == nullptr)
|
||||
return;
|
||||
|
||||
wxFileDialog dialog(mainframe, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
|
||||
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
if (dialog.ShowModal() != wxID_OK)
|
||||
return;
|
||||
|
||||
wxString message;
|
||||
const bool ok = install_local_plugin_package(boost::filesystem::path(dialog.GetPath().ToUTF8().data()), mainframe, message);
|
||||
if (message.IsEmpty())
|
||||
return; // user cancelled the overwrite prompt
|
||||
|
||||
Plater* plater = this->plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
ok ? NotificationManager::NotificationLevel::RegularNotificationLevel : NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
into_u8(message));
|
||||
}
|
||||
|
||||
void GUI_App::open_terminal_dialog()
|
||||
{
|
||||
// Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
|
||||
@@ -8444,6 +8529,9 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
|
||||
this->plater_->get_current_canvas3D()->force_set_focus();
|
||||
return;
|
||||
}
|
||||
// Built-in Speed Dial command titles are copied from the catalog at init and don't follow a
|
||||
// live locale switch; rebuild them in the new language before the GUI (and palette) rebuilds.
|
||||
m_action_registry.relocalize_builtins();
|
||||
}
|
||||
|
||||
if (need_recreate_gui)
|
||||
|
||||
@@ -599,6 +599,11 @@ public:
|
||||
std::string get_saved_mode_str();
|
||||
std::string get_mode_str();
|
||||
void save_mode(const /*ConfigOptionMode*/int mode) ;
|
||||
// Switch to `mode` from the Speed Dial: a developer-mode override hides the saved mode
|
||||
// (get_mode returns comDevelop), so clear it first and persist the choice.
|
||||
void set_mode(ConfigOptionMode mode);
|
||||
// Turn the developer-mode override on and refresh the UI (used before jumping to a Developer setting).
|
||||
void enable_developer_mode();
|
||||
void update_mode();
|
||||
void update_internal_development();
|
||||
void show_ip_address_enter_dialog(wxString title = wxEmptyString);
|
||||
@@ -637,6 +642,9 @@ public:
|
||||
void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
|
||||
void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
|
||||
void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
|
||||
// Dialog-free plugin actions used by the speed dial: they never require the Plugins dialog to be open.
|
||||
void refresh_plugins();
|
||||
void install_local_plugin();
|
||||
void open_terminal_dialog();
|
||||
void open_speed_dial();
|
||||
ActionRegistry& action_registry() { return m_action_registry; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -478,6 +478,8 @@ int get_dpi_for_window(const wxWindow *window);
|
||||
#ifdef __WXOSX__
|
||||
void dataview_remove_insets(wxDataViewCtrl* dv);
|
||||
void staticbox_remove_margin(wxStaticBox* sb);
|
||||
// Clip a top-level window (and its webview) to a rounded rect with a native layer.
|
||||
void set_window_corner_radius(wxWindow* win, int radius);
|
||||
#endif
|
||||
|
||||
#ifdef __WXGTK__
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/sysctl.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <wx/osx/cocoa/dataview.h>
|
||||
#import "GUI_Utils.hpp"
|
||||
|
||||
@@ -21,6 +22,22 @@ void staticbox_remove_margin(wxStaticBox* sb) {
|
||||
[nativeBox setBorderWidth:0];
|
||||
}
|
||||
|
||||
// wxOSX SetShape only clears the window background; it cannot clip to a region. Clipping the
|
||||
// window's view layer to a rounded rect is what actually rounds the opaque webview inside.
|
||||
void set_window_corner_radius(wxWindow* win, int radius) {
|
||||
if (!win)
|
||||
return;
|
||||
NSView* view = (NSView*)win->GetHandle();
|
||||
if (!view)
|
||||
return;
|
||||
NSWindow* window = [view window];
|
||||
[window setOpaque:NO];
|
||||
[window setBackgroundColor:[NSColor clearColor]];
|
||||
[view setWantsLayer:YES];
|
||||
[[view layer] setCornerRadius:radius];
|
||||
[[view layer] setMasksToBounds:YES];
|
||||
}
|
||||
|
||||
bool is_debugger_present()
|
||||
// Returns true if the current process is being debugged (either
|
||||
// running under the debugger or has a debugger attached post facto).
|
||||
|
||||
@@ -198,6 +198,9 @@ void KBShortcutsDialog::fill_shortcuts()
|
||||
|
||||
// Switch table page
|
||||
{ ctrl + L("Tab"), L("Switch table page")},
|
||||
// Open speed dial
|
||||
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open speed dial") },
|
||||
{ alt + "1..9,0", L("Run a Speed Dial favourite (while the Speed Dial is open)") },
|
||||
//DEL
|
||||
#ifdef __APPLE__
|
||||
{"fn+⌫", L("Delete Selected")},
|
||||
@@ -268,8 +271,6 @@ void KBShortcutsDialog::fill_shortcuts()
|
||||
{ "O", L("Zoom out") },
|
||||
{ "V", L("Toggle printable for object/part") },
|
||||
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
|
||||
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open actions speed dial") },
|
||||
|
||||
};
|
||||
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
|
||||
|
||||
|
||||
+144
-93
@@ -1,6 +1,7 @@
|
||||
#include "MainFrame.hpp"
|
||||
|
||||
#include <wx/panel.h>
|
||||
#include <wx/textentry.h>
|
||||
#include <wx/notebook.h>
|
||||
#include <wx/listbook.h>
|
||||
#include <wx/simplebook.h>
|
||||
@@ -46,7 +47,9 @@
|
||||
// BBS
|
||||
#include "PartPlate.hpp"
|
||||
#include "Preferences.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
#include "Widgets/StaticBox.hpp"
|
||||
#include "BindDialog.hpp"
|
||||
#include "../Utils/MacDarkMode.hpp"
|
||||
#include "../Utils/NetworkAgentFactory.hpp"
|
||||
@@ -107,6 +110,31 @@ enum class ERescaleTarget
|
||||
SettingsDialog
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// Space opens the speed dial, but it is the activation key for buttons, checkboxes and other
|
||||
// controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has
|
||||
// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not controls and
|
||||
// fall through to "open"; the Notebook itself does too, so Space still opens the dial on any page.
|
||||
bool focus_keeps_space(wxWindow* focus)
|
||||
{
|
||||
if (!focus)
|
||||
return false;
|
||||
if (dynamic_cast<wxTextEntryBase*>(focus))
|
||||
return true; // typing a space into a text field
|
||||
if (dynamic_cast<wxWebView*>(focus))
|
||||
return true; // web content scrolls and hosts its own text fields
|
||||
if (dynamic_cast<::Button*>(focus))
|
||||
return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl)
|
||||
if (dynamic_cast<StaticBox*>(focus))
|
||||
return true; // custom composites (ComboBox, SpinInput, ...) activate with Space and are wxWindow
|
||||
if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus))
|
||||
return true; // stock button/checkbox/choice/list/etc. keep Space
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef __WXGTK__
|
||||
// A thin transparent panel placed at a window edge to handle resize.
|
||||
// Works regardless of underlying content (GLCanvas3D, wxWebView, etc.)
|
||||
@@ -702,6 +730,22 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
}
|
||||
return;}
|
||||
#endif
|
||||
// Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so
|
||||
// editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window
|
||||
// doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields),
|
||||
// so a bare Space there still clicks/toggles instead of being hijacked. Gated by a preference
|
||||
// (default on) so users can hand Space back to the focused control entirely.
|
||||
if (wxGetApp().app_config->get_bool("enable_speed_dial") && !evt.CmdDown() && !evt.ShiftDown() &&
|
||||
!evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) {
|
||||
if (focus_keeps_space(wxWindow::FindFocus())) {
|
||||
evt.Skip(); // let the focused control keep Space
|
||||
return;
|
||||
}
|
||||
// Defer out of the native key-event stack: open_speed_dial() may create a WebView and
|
||||
// run script, the same window work the codebase avoids doing on native callbacks.
|
||||
this->CallAfter([] { wxGetApp().open_speed_dial(); });
|
||||
return;
|
||||
}
|
||||
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
|
||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
|
||||
m_plater->apply_background_progress();
|
||||
@@ -3344,6 +3388,11 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().open_preferences();
|
||||
},
|
||||
"", nullptr, []() { return true; }, this, 1);
|
||||
parent_menu->AppendSeparator();
|
||||
append_menu_item(
|
||||
parent_menu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
//parent_menu->Insert(1, preference_item);
|
||||
#endif
|
||||
// Help menu
|
||||
@@ -3368,7 +3417,13 @@ void MainFrame::init_menubar_as_editor()
|
||||
auto top_menu = m_topbar->GetTopMenu();
|
||||
top_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
append_menu_item(
|
||||
top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + "Space", "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
top_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
|
||||
[this](wxCommandEvent &) {
|
||||
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
|
||||
@@ -3414,88 +3469,51 @@ void MainFrame::init_menubar_as_editor()
|
||||
|
||||
// Temperature
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_temp_calib_dlg)
|
||||
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_temp_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Max Volumetric Speed
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vol_test_dlg)
|
||||
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vol_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Pressure Advance
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_pa_calib_dlg)
|
||||
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_pa_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Flow rate (Wizard Dialog)
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_plater) return;
|
||||
if (!m_flow_rate_calib_dlg)
|
||||
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_flow_rate_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Retraction
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_retraction_calib_dlg)
|
||||
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_retraction_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Cornering
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Input Shaping (with submenu)
|
||||
auto input_shaping_menu = new wxMenu();
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
|
||||
|
||||
// VFA
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vfa_test_dlg)
|
||||
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vfa_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// help
|
||||
@@ -3506,6 +3524,10 @@ void MainFrame::init_menubar_as_editor()
|
||||
#else
|
||||
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
|
||||
fileMenu->AppendSeparator();
|
||||
append_menu_item(
|
||||
fileMenu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
|
||||
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
append_menu_item(
|
||||
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
|
||||
[this](wxCommandEvent&) {
|
||||
@@ -3552,89 +3574,52 @@ void MainFrame::init_menubar_as_editor()
|
||||
|
||||
// Temperature
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_temp_calib_dlg)
|
||||
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_temp_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Max Volumetric Speed
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vol_test_dlg)
|
||||
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vol_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Pressure Advance
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_pa_calib_dlg)
|
||||
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_pa_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Flowrate (with submenu)
|
||||
// ORCA: Flow rate (Wizard Dialog)
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_plater) return;
|
||||
if (!m_flow_rate_calib_dlg)
|
||||
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_flow_rate_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Retraction
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_retraction_calib_dlg)
|
||||
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_retraction_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Cornering
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Input Shaping (with submenu)
|
||||
auto input_shaping_menu = new wxMenu();
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
|
||||
|
||||
// VFA
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vfa_test_dlg)
|
||||
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vfa_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
// help
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"),
|
||||
@@ -4408,6 +4393,72 @@ void MainFrame::technology_changed()
|
||||
m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings"));
|
||||
}
|
||||
|
||||
// Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle:
|
||||
// the Calibration menu handlers and the Speed Dial native commands both call it. Most wizards are
|
||||
// cached members reused across launches; cornering/input-shaping build a fresh transient dialog.
|
||||
// Call while the Prepare (3D) panel is shown.
|
||||
void MainFrame::run_calibration(CalibKind calib_kind)
|
||||
{
|
||||
switch (calib_kind) {
|
||||
case CalibKind::Temperature: {
|
||||
if (!m_temp_calib_dlg)
|
||||
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_temp_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::MaxVolumetric: {
|
||||
if (!m_vol_test_dlg)
|
||||
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_vol_test_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::PressureAdvance: {
|
||||
if (!m_pa_calib_dlg)
|
||||
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_pa_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::FlowRatio: {
|
||||
if (!m_plater)
|
||||
break;
|
||||
if (!m_flow_rate_calib_dlg)
|
||||
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_flow_rate_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::Retraction: {
|
||||
if (!m_retraction_calib_dlg)
|
||||
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_retraction_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::Cornering: {
|
||||
auto dlg = new Cornering_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
break;
|
||||
}
|
||||
case CalibKind::InputShapingFreq: {
|
||||
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
break;
|
||||
}
|
||||
case CalibKind::InputShapingDamp: {
|
||||
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
break;
|
||||
}
|
||||
case CalibKind::VFA: {
|
||||
if (!m_vfa_test_dlg)
|
||||
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_vfa_test_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Called after the Preferences dialog is closed and the program settings are saved.
|
||||
|
||||
@@ -113,6 +113,20 @@ protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
};
|
||||
|
||||
// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners.
|
||||
enum class CalibKind : int
|
||||
{
|
||||
Temperature,
|
||||
MaxVolumetric,
|
||||
PressureAdvance,
|
||||
FlowRatio,
|
||||
Retraction,
|
||||
Cornering,
|
||||
InputShapingFreq,
|
||||
InputShapingDamp,
|
||||
VFA
|
||||
};
|
||||
|
||||
class MainFrame : public DPIFrame
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
@@ -360,6 +374,11 @@ public:
|
||||
|
||||
void technology_changed();
|
||||
|
||||
// Opens the calibration wizard for `kind`. Single source of truth for the wizard lifecycle:
|
||||
// the Calibration menu handlers and the Speed Dial native commands both call this. Most wizards
|
||||
// are cached members; cornering/input-shaping are transient. Call while the Prepare (3D) panel
|
||||
// is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first).
|
||||
void run_calibration(CalibKind calib_kind);
|
||||
|
||||
//BBS
|
||||
void load_url(wxString url);
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
#include "NativeCommands.hpp"
|
||||
|
||||
#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>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <wx/utils.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
// Plate ops are an FFF feature: SLA has a single plate and no plate UI, gcode-only mode has no
|
||||
// editable project - so gate every plate op on FFF + the normal editor.
|
||||
bool is_fff_plater(Plater* plater) { return plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode(); }
|
||||
|
||||
AppActionRunResult plate_unavailable() { return {AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; }
|
||||
|
||||
// Switch to the Prepare (3D) panel so object/calibration ops have a live canvas + selection, and
|
||||
// the notebook page label matches. A no-op when the 3D panel is already shown.
|
||||
void ensure_3d_view(Plater* plater)
|
||||
{
|
||||
if (plater && !plater->is_view3D_shown()) {
|
||||
plater->select_view_3D("3D");
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->select_tab(TAB_ID_PREPARE);
|
||||
}
|
||||
}
|
||||
|
||||
// Object op guard + run: object ops read the Prepare canvas selection, so ensure that view first so
|
||||
// a launch from another tab doesn't report a spuriously empty selection.
|
||||
AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Plater*))
|
||||
{
|
||||
if (!plater)
|
||||
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
|
||||
ensure_3d_view(plater);
|
||||
if (!ok(plater))
|
||||
return {AppActionRunResult::Level::Info, _L("Select an object first.")};
|
||||
op(plater);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
|
||||
// Jump the preview to a layer selected by a 0-100 percent of the layer range. The caller has already
|
||||
// switched to Preview (which may request a slice); if a slicer result is present the slider is
|
||||
// repositioned immediately, otherwise the jump is a no-op until the user re-slices.
|
||||
void go_to_layer(Plater* plater, const std::string& param)
|
||||
{
|
||||
if (!plater)
|
||||
return;
|
||||
double pct = 50.0;
|
||||
try {
|
||||
pct = std::stod(param);
|
||||
} catch (const std::exception&) {}
|
||||
pct = std::clamp(pct, 0.0, 100.0);
|
||||
|
||||
GLCanvas3D* canvas = plater->get_current_canvas3D();
|
||||
if (!canvas)
|
||||
return;
|
||||
GCodeViewer& viewer = canvas->get_gcode_viewer();
|
||||
IMSlider* layers = viewer.get_layers_slider();
|
||||
IMSlider* moves = viewer.get_moves_slider();
|
||||
if (!layers || layers->GetMaxValue() <= 0)
|
||||
return;
|
||||
|
||||
const double max = double(layers->GetMaxValue());
|
||||
const int target = int(std::lround(pct / 100.0 * max));
|
||||
layers->SetHigherValue(target);
|
||||
if (layers->is_one_layer())
|
||||
layers->SetLowerValue(target);
|
||||
layers->set_as_dirty();
|
||||
if (moves) {
|
||||
moves->SetHigherValue(moves->GetMaxValue());
|
||||
moves->set_as_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Select a named camera view. Plater::select_view dispatches to the current panel.
|
||||
AppActionRunResult view_command(Plater* plater, const std::string& dir)
|
||||
{
|
||||
if (plater)
|
||||
plater->select_view(dir);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
|
||||
// Calibration wizards. Routes through MainFrame::run_calibration, the same entry point as the
|
||||
// Calibration menu (which caches most of the wizard dialogs).
|
||||
AppActionRunResult calib_command(CalibKind kind)
|
||||
{
|
||||
MainFrame* mf = wxGetApp().mainframe;
|
||||
if (!mf)
|
||||
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
|
||||
ensure_3d_view(wxGetApp().plater());
|
||||
mf->run_calibration(kind);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
|
||||
constexpr const char* kCommandPrefix = "orca_command";
|
||||
|
||||
// Thin AppAction wrapper for one catalog entry: identity and presentation come from the catalog,
|
||||
// run() routes back to it. The id is keyed by the stable catalog key (not the display title), so a
|
||||
// rename or a UI-language switch never re-keys the action.
|
||||
struct CommandAction : AppAction
|
||||
{
|
||||
std::string command_key;
|
||||
|
||||
AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); }
|
||||
|
||||
explicit CommandAction(const NativeCommand& c)
|
||||
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName)
|
||||
, command_key(c.key)
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = c.group;
|
||||
this->input = c.input;
|
||||
this->icon = c.icon;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<NativeCommand> build_command_catalog()
|
||||
{
|
||||
std::vector<NativeCommand> out;
|
||||
auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
|
||||
std::string input = {}, std::string icon = {}) {
|
||||
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)});
|
||||
};
|
||||
// Presentation-first overload: keeps the tile icon next to the title/group it belongs to.
|
||||
auto add_with_icon = [&](std::string key, std::string title, std::string group, std::string icon,
|
||||
std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) {
|
||||
add(std::move(key), std::move(title), std::move(group), std::move(runner), std::move(input), std::move(icon));
|
||||
};
|
||||
|
||||
// ---- Slice & Export ----
|
||||
add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->reslice();
|
||||
plater->select_view_3D("Preview", false);
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->select_tab(TAB_ID_PREVIEW);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
add_with_icon(
|
||||
"go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer",
|
||||
[](const std::string& param) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->select_view_3D("Preview", false);
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->select_tab(TAB_ID_PREVIEW);
|
||||
go_to_layer(plater, param);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
},
|
||||
"percent");
|
||||
|
||||
// "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab
|
||||
// id back as `param` (same contract as go_to_layer's percent).
|
||||
add(
|
||||
"go_to_tab", _u8L("Go to tab..."), _u8L("Commands"),
|
||||
[](const std::string& param) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty())
|
||||
mf->select_tab(from_u8(param));
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
},
|
||||
"tab");
|
||||
|
||||
add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->load_project();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->save_project(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->save_project(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) {
|
||||
wxGetApp().open_preferences();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Mode ----
|
||||
add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comSimple);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comAdvanced);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comExpert);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
// Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI.
|
||||
add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
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_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_core_3mf();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode_3mf(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode_3mf(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Calibration ----
|
||||
// The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's
|
||||
// light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green).
|
||||
add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature",
|
||||
[](const std::string&) { return calib_command(CalibKind::Temperature); });
|
||||
add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed",
|
||||
[](const std::string&) { return calib_command(CalibKind::MaxVolumetric); });
|
||||
add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
[](const std::string&) { return calib_command(CalibKind::PressureAdvance); });
|
||||
add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
[](const std::string&) { return calib_command(CalibKind::FlowRatio); });
|
||||
add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction",
|
||||
[](const std::string&) { return calib_command(CalibKind::Retraction); });
|
||||
add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision",
|
||||
[](const std::string&) { return calib_command(CalibKind::Cornering); });
|
||||
add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
|
||||
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
|
||||
add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
|
||||
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
|
||||
add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); });
|
||||
|
||||
// ---- View ----
|
||||
// Titles are built with _u8L here (not via a variable) so xgettext can extract them.
|
||||
for (auto [key, dir, title] :
|
||||
std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", _u8L("View: Top")},
|
||||
{"view_bottom", "bottom", _u8L("View: Bottom")},
|
||||
{"view_front", "front", _u8L("View: Front")},
|
||||
{"view_rear", "rear", _u8L("View: Rear")},
|
||||
{"view_left", "left", _u8L("View: Left")},
|
||||
{"view_right", "right", _u8L("View: Right")},
|
||||
{"view_iso", "iso", _u8L("View: Isometric")}}) {
|
||||
std::string k = key, d = dir;
|
||||
add(k, title, _u8L("View"),
|
||||
[d](const std::string&) { return view_command(wxGetApp().plater(), d); });
|
||||
}
|
||||
add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->select_view("plate");
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
canvas->zoom_to_bed();
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
canvas->zoom_to_bed();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->get_camera().select_next_type();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->reset_window_layout();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Object ----
|
||||
add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
|
||||
});
|
||||
add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
|
||||
});
|
||||
add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
|
||||
});
|
||||
add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
|
||||
});
|
||||
add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
|
||||
});
|
||||
add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
|
||||
});
|
||||
add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
|
||||
});
|
||||
add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); },
|
||||
[](Plater* p) { p->scale_selection_to_fit_print_volume(); });
|
||||
});
|
||||
add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
|
||||
});
|
||||
add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
|
||||
});
|
||||
add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
|
||||
});
|
||||
add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
|
||||
});
|
||||
|
||||
// ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object)
|
||||
auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) {
|
||||
add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) {
|
||||
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"), "menu_obj_cube", "Cube");
|
||||
add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder");
|
||||
add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere");
|
||||
add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone");
|
||||
add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc");
|
||||
add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus");
|
||||
add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) {
|
||||
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_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](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_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
if (!plater->can_add_plate())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
|
||||
plater->add_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
if (!plater->can_add_plate())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
|
||||
plater->duplicate_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
if (!plater->can_delete_plate())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
|
||||
plater->delete_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
PartPlate* curr = plater->get_partplate_list().get_curr_plate();
|
||||
PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name"));
|
||||
dlg.set_plate_name(from_u8(curr->get_plate_name()));
|
||||
if (dlg.ShowModal() == wxID_YES)
|
||||
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
PartPlateList& plates = plater->get_partplate_list();
|
||||
const int index = plates.get_curr_plate_index();
|
||||
plater->take_snapshot("lock partplate");
|
||||
plates.lock_plate(index, !plates.is_locked(index));
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
PartPlateList& plates = plater->get_partplate_list();
|
||||
const int count = plates.get_plate_count();
|
||||
if (count <= 0)
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("No plates available.")};
|
||||
int index = 0;
|
||||
try {
|
||||
index = std::stoi(param);
|
||||
} catch (const std::exception&) {}
|
||||
index = std::clamp(index, 0, count - 1);
|
||||
plater->select_plate(index, false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Printer / device connection ----
|
||||
add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
if (dev && dev->get_selected_machine() && plater) {
|
||||
plater->sidebar().sync_ams_list();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Connect a printer to synchronize the AMS filament list.")};
|
||||
});
|
||||
|
||||
// ---- Presets / cloud ----
|
||||
add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) {
|
||||
wxGetApp().open_presetbundledialog();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) {
|
||||
if (!wxGetApp().is_user_login())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")};
|
||||
wxGetApp().restart_sync_user_preset();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Import ----
|
||||
add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater()) {
|
||||
#ifdef __APPLE__
|
||||
plater->add_model();
|
||||
#else
|
||||
plater->add_file();
|
||||
#endif
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->import_zip_archive();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe)
|
||||
mf->load_config_file();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Export extras ----
|
||||
add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, false, FT_DRC);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, true, FT_DRC);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_toolpaths_to_obj();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe)
|
||||
mf->export_config();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links)
|
||||
add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) {
|
||||
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_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) {
|
||||
Slic3r::GUI::desktop_open_datadir_folder();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) {
|
||||
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_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](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_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) {
|
||||
wxGetApp().check_new_version_sf(true, 1);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) {
|
||||
Slic3r::GUI::about();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) {
|
||||
wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) {
|
||||
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();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().refresh_plugins();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) {
|
||||
open_plugin_hub();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().install_local_plugin();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<NativeCommand>& catalog_storage()
|
||||
{
|
||||
static std::vector<NativeCommand> commands = build_command_catalog();
|
||||
return commands;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<NativeCommand>& NativeCommands::catalog()
|
||||
{
|
||||
return catalog_storage();
|
||||
}
|
||||
|
||||
void NativeCommands::rebuild_catalog()
|
||||
{
|
||||
// build_command_catalog() re-runs _u8L under the current locale, so replacing the storage
|
||||
// refreshes every translated title/group after a language switch.
|
||||
catalog_storage() = build_command_catalog();
|
||||
}
|
||||
|
||||
std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command)
|
||||
{
|
||||
return std::make_unique<CommandAction>(command);
|
||||
}
|
||||
|
||||
AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param)
|
||||
{
|
||||
GUI_App& app = wxGetApp();
|
||||
if (app.is_closing())
|
||||
return {};
|
||||
for (const NativeCommand& c : catalog())
|
||||
if (c.key == key)
|
||||
return c.runner(param);
|
||||
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ActionRegistry.hpp" // for AppAction / AppActionRunResult
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// A built-in speed-dial command: identity + how to run it. make_action() wraps a value as a thin
|
||||
// AppAction for the registry, so this catalog is the single source of truth for the behaviour
|
||||
// (runner => an owner method), the presentation (title/group/input), and the tile pictogram
|
||||
// (icon = an SVG base name under resources/images, "" for no icon).
|
||||
struct NativeCommand
|
||||
{
|
||||
std::string key;
|
||||
std::string title;
|
||||
std::string group;
|
||||
std::string input; // "percent"/"tab" or "" for immediate run
|
||||
std::string icon; // SVG base name, or "" to render a blank tile
|
||||
std::function<AppActionRunResult(const std::string& param)> runner;
|
||||
};
|
||||
|
||||
namespace NativeCommands {
|
||||
// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a
|
||||
// live UI language switch so the translated titles/groups match the new locale. UI thread only.
|
||||
const std::vector<NativeCommand>& catalog();
|
||||
|
||||
// Rebuilds the catalog in the current locale. UI thread only.
|
||||
void rebuild_catalog();
|
||||
|
||||
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.
|
||||
AppActionRunResult run(const std::string& key, const std::string& param = {});
|
||||
|
||||
// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity,
|
||||
// presentation and behaviour as the single source of truth; ActionRegistry only stores and
|
||||
// dispatches the result. UI thread only.
|
||||
std::unique_ptr<AppAction> make_action(const NativeCommand& command);
|
||||
} // namespace NativeCommands
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Widgets/Label.hpp"
|
||||
|
||||
#include <wx/button.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
|
||||
@@ -158,12 +159,22 @@ void ButtonsListCtrl::SetSelection(int sel)
|
||||
|
||||
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
|
||||
{
|
||||
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
|
||||
Button * btn = new Button(this, text, bmp_name, wxNO_BORDER);
|
||||
btn->SetCornerRadius(0);
|
||||
|
||||
if (bmp_name.empty() && bmp.IsOk())
|
||||
btn->SetIcon(bmp);
|
||||
|
||||
// The label no longer carries a leading space, so widen the icon<->text gap to keep the
|
||||
// original spacing between a tab's icon and its caption.
|
||||
{
|
||||
wxClientDC dc(btn);
|
||||
dc.SetFont(btn->GetFont());
|
||||
int space_w = 0;
|
||||
dc.GetTextExtent(" ", &space_w, nullptr);
|
||||
btn->SetIconSpacing(5 + space_w);
|
||||
}
|
||||
|
||||
int em = em_unit(this);
|
||||
//BBS set size for button
|
||||
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
@@ -190,6 +201,7 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
|
||||
Slic3r::GUI::wxGetApp().UpdateDarkUI(btn);
|
||||
m_pageButtons.insert(m_pageButtons.begin() + n, btn);
|
||||
m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA
|
||||
m_pageIcons.insert(m_pageIcons.begin() + n, bmp_name);
|
||||
m_buttons_sizer->Insert(n, new wxSizerItem(btn));
|
||||
m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1);
|
||||
m_sizer->Layout();
|
||||
@@ -209,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n)
|
||||
Button* btn = m_pageButtons[n];
|
||||
m_pageButtons.erase(m_pageButtons.begin() + n);
|
||||
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
|
||||
m_pageIcons.erase(m_pageIcons.begin() + n);
|
||||
m_buttons_sizer->Remove(n);
|
||||
#if __WXOSX__
|
||||
RemoveChild(btn);
|
||||
@@ -245,7 +258,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
|
||||
int em = em_unit(this);
|
||||
Button* btn = m_pageButtons[n];
|
||||
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
btn->SetLabel(compact ? "" : (" " + m_pageLabels[n]));
|
||||
btn->SetLabel(compact ? "" : m_pageLabels[n]);
|
||||
}
|
||||
|
||||
wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||
@@ -254,6 +267,12 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||
return btn->GetLabel();
|
||||
}
|
||||
|
||||
// ORCA
|
||||
wxString ButtonsListCtrl::GetPageLabel(size_t n) const
|
||||
{
|
||||
return n < m_pageLabels.size() ? m_pageLabels[n] : wxString();
|
||||
}
|
||||
|
||||
// ORCA
|
||||
void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,14 @@ public:
|
||||
void SetPageText(size_t n, const wxString& strText);
|
||||
void SetCompact(size_t n, bool compact); // ORCA
|
||||
wxString GetPageText(size_t n) const;
|
||||
// ORCA: the full page label, unaffected by SetCompact() blanking the button text.
|
||||
wxString GetPageLabel(size_t n) const;
|
||||
// Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap).
|
||||
const std::string& GetPageIcon(size_t n) const
|
||||
{
|
||||
static const std::string empty;
|
||||
return n < m_pageIcons.size() ? m_pageIcons[n] : empty;
|
||||
}
|
||||
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
|
||||
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
|
||||
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
|
||||
@@ -47,6 +55,7 @@ private:
|
||||
int m_btn_margin;
|
||||
int m_line_margin;
|
||||
std::vector<wxString> m_pageLabels; // ORCA
|
||||
std::vector<std::string> m_pageIcons; // ORCA: resource icon name per page, plugin pages empty
|
||||
wxWindow* m_overflow_button{nullptr}; // ORCA
|
||||
};
|
||||
|
||||
@@ -241,6 +250,20 @@ public:
|
||||
return GetBtnsListCtrl()->GetPageText(n);
|
||||
}
|
||||
|
||||
// ORCA: the real page label. GetPageText() returns the button label, which SetCompact() blanks.
|
||||
wxString GetPageLabel(size_t n) const
|
||||
{
|
||||
wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page"));
|
||||
return GetBtnsListCtrl()->GetPageLabel(n);
|
||||
}
|
||||
|
||||
// Resource icon name the page was inserted with; empty for pages added with a wxBitmap.
|
||||
std::string GetPageIcon(size_t n) const
|
||||
{
|
||||
wxCHECK_MSG(n < GetPageCount(), std::string(), wxS("Invalid page"));
|
||||
return GetBtnsListCtrl()->GetPageIcon(n);
|
||||
}
|
||||
|
||||
virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "ConfigExceptions.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "OG_CustomCtrl.hpp"
|
||||
@@ -244,6 +245,25 @@ void OptionsGroup::append_line(const Line& line)
|
||||
{
|
||||
m_lines.emplace_back(line);
|
||||
|
||||
// Feed the searcher the row's wiki path (Line::label_path, for the Speed Dial's "open wiki"
|
||||
// affordance) and the label the row actually draws, so a setting action is named like the page.
|
||||
if (m_use_custom_ctrl) {
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
const Preset::Type type = static_cast<Preset::Type>(config_type());
|
||||
const bool multi = line.get_options().size() > 1;
|
||||
for (const auto& opt : line.get_options()) {
|
||||
if (!line.label_path.empty())
|
||||
index.set_path(opt.opt_id, type, line.label_path);
|
||||
// Mirror the sub-label OG_CustomCtrl draws for a multi-option row, so the palette
|
||||
// names each field like the page does.
|
||||
const std::string& leaf_src = opt.opt.label;
|
||||
const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ?
|
||||
_L_CONTEXT(leaf_src, "Layers") :
|
||||
_(leaf_src);
|
||||
index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi));
|
||||
}
|
||||
}
|
||||
|
||||
if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty()))
|
||||
return;
|
||||
|
||||
@@ -650,7 +670,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
|
||||
m_opt_map.emplace(opt_id, pair);
|
||||
|
||||
if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab
|
||||
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category());
|
||||
wxGetApp().sidebar().settings_index().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
|
||||
|
||||
return Option(*m_config->def()->get(opt_key), opt_id);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,10 @@ protected:
|
||||
virtual void back_to_initial_value(const std::string& opt_key) {}
|
||||
virtual void back_to_sys_value(const std::string& opt_key) {}
|
||||
|
||||
// Preset::Type of a settings group; -1 for groups not tied to a preset. Used by append_line to
|
||||
// register each option's wiki path with the searcher. Overridden by ConfigOptionsGroup.
|
||||
virtual int config_type() const { return -1; }
|
||||
|
||||
public:
|
||||
static wxString get_url(const std::string& path_end);
|
||||
static bool launch_browser(const std::string& path_end);
|
||||
@@ -273,7 +277,7 @@ public:
|
||||
OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {}
|
||||
|
||||
const wxString& config_category() const throw() { return m_config_category; }
|
||||
int config_type() const throw() { return m_config_type; }
|
||||
int config_type() const throw() override { return m_config_type; }
|
||||
const t_opt_map& opt_map() const throw() { return m_opt_map; }
|
||||
|
||||
void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; }
|
||||
|
||||
@@ -324,7 +324,9 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
|
||||
wxID_ANY,
|
||||
wxDefaultPosition,
|
||||
wxDefaultSize,
|
||||
wxVSCROLL) // hide hori-bar will cause hidden field mis-position
|
||||
wxVSCROLL // hide hori-bar will cause hidden field mis-position
|
||||
| wxTAB_TRAVERSAL // Allows for traversal via tab key
|
||||
)
|
||||
{
|
||||
// ShowScrollBar(GetHandle(), SB_BOTH, FALSE);
|
||||
Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) {
|
||||
|
||||
+25
-15
@@ -6422,6 +6422,11 @@ Search::OptionsSearcher& Sidebar::get_searcher()
|
||||
return p->searcher;
|
||||
}
|
||||
|
||||
Search::SettingsIndex& Sidebar::settings_index()
|
||||
{
|
||||
return p->searcher.index();
|
||||
}
|
||||
|
||||
std::string& Sidebar::get_search_line()
|
||||
{
|
||||
return p->searcher.search_string();
|
||||
@@ -7571,10 +7576,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
|
||||
if (this->q->is_view3D_shown())
|
||||
wxGetApp().open_speed_dial();
|
||||
});
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
|
||||
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
|
||||
@@ -12831,17 +12832,8 @@ void Plater::priv::on_action_add(SimpleEvent&)
|
||||
//BBS: add plate from toolbar
|
||||
void Plater::priv::on_action_add_plate(SimpleEvent&)
|
||||
{
|
||||
if (q != nullptr) {
|
||||
take_snapshot("add partplate");
|
||||
this->partplate_list.create_plate();
|
||||
int new_plate = this->partplate_list.get_plate_count() - 1;
|
||||
this->partplate_list.select_plate(new_plate);
|
||||
update();
|
||||
|
||||
// BBS set default view
|
||||
//q->get_camera().select_view("topfront");
|
||||
q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
|
||||
}
|
||||
if (q != nullptr)
|
||||
q->add_plate();
|
||||
}
|
||||
|
||||
//BBS: remove plate from toolbar
|
||||
@@ -21913,6 +21905,24 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
|
||||
return ret;
|
||||
}
|
||||
|
||||
//BBS: add an empty plate and switch to it (mirrors the toolbar's Add Plate).
|
||||
int Plater::add_plate()
|
||||
{
|
||||
if (!p->can_add_plate())
|
||||
return -1;
|
||||
take_snapshot("add partplate");
|
||||
int new_plate = p->partplate_list.create_plate();
|
||||
if (new_plate < 0)
|
||||
return new_plate;
|
||||
p->partplate_list.select_plate(new_plate);
|
||||
update();
|
||||
|
||||
// BBS set default view
|
||||
//get_camera().select_view("topfront");
|
||||
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
|
||||
return new_plate;
|
||||
}
|
||||
|
||||
int Plater::duplicate_plate(int plate_index)
|
||||
{
|
||||
int index = plate_index, ret;
|
||||
|
||||
@@ -283,6 +283,7 @@ public:
|
||||
std::vector<std::string>& types,
|
||||
std::vector<size_t>* config_indices = nullptr);
|
||||
Search::OptionsSearcher& get_searcher();
|
||||
Search::SettingsIndex& settings_index();
|
||||
std::string& get_search_line();
|
||||
void update_printer_thumbnail();
|
||||
|
||||
@@ -781,6 +782,9 @@ public:
|
||||
void apply_background_progress();
|
||||
//BBS: select the plate by hover_id
|
||||
int select_plate_by_hover_id(int hover_id, bool right_click = false, bool isModidyPlateName = false);
|
||||
//BBS: add an empty plate and switch to it (the toolbar's Add Plate). Returns the new
|
||||
// plate index, or -1 when the plate cap is reached.
|
||||
int add_plate();
|
||||
//BBS: delete the plate, index= -1 means the current plate
|
||||
int delete_plate(int plate_index = -1);
|
||||
int duplicate_plate(int plate_index = -1);
|
||||
|
||||
+185
-122
@@ -126,15 +126,6 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st
|
||||
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
|
||||
{
|
||||
std::vector<PluginDescriptor> cloud_entries;
|
||||
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
|
||||
if (entry.is_cloud_plugin())
|
||||
cloud_entries.push_back(entry);
|
||||
return cloud_entries;
|
||||
}
|
||||
|
||||
PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
|
||||
{
|
||||
descriptor.plugin_root.clear();
|
||||
@@ -150,41 +141,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
void refresh_plugin_metadata_blocking(bool fetch_cloud)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
std::vector<std::string> not_found, unauthorized;
|
||||
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
|
||||
current_cloud_metadata_snapshot();
|
||||
|
||||
manager.rescan_plugins();
|
||||
|
||||
if (!fetch_cloud) {
|
||||
manager.update_cloud_metadata(current_cloud_metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||
|
||||
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
|
||||
for (const auto& uuid : not_found)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
for (const auto& uuid : unauthorized)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
});
|
||||
}
|
||||
|
||||
std::string to_string(PluginUpdateStatus status);
|
||||
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions);
|
||||
|
||||
@@ -448,6 +404,176 @@ bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& s
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ── Dialog-independent plugin actions (also used by the speed dial) ───────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a
|
||||
// network round-trip (kUseCurrentCloudMeta).
|
||||
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
|
||||
{
|
||||
std::vector<PluginDescriptor> cloud_entries;
|
||||
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
|
||||
if (entry.is_cloud_plugin())
|
||||
cloud_entries.push_back(entry);
|
||||
return cloud_entries;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void refresh_plugin_metadata_blocking(bool fetch_cloud)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
std::vector<std::string> not_found, unauthorized;
|
||||
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
|
||||
current_cloud_metadata_snapshot();
|
||||
|
||||
manager.rescan_plugins();
|
||||
|
||||
if (!fetch_cloud) {
|
||||
manager.update_cloud_metadata(current_cloud_metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||
|
||||
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
|
||||
for (const auto& uuid : not_found)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
for (const auto& uuid : unauthorized)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
});
|
||||
}
|
||||
|
||||
void open_plugin_hub()
|
||||
{
|
||||
std::string cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
if (wxGetApp().getAgent()) {
|
||||
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
|
||||
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
|
||||
cloud_base_url = orca_agent->get_cloud_base_url();
|
||||
}
|
||||
|
||||
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
|
||||
cloud_base_url.pop_back();
|
||||
if (cloud_base_url.empty())
|
||||
cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
|
||||
}
|
||||
|
||||
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message)
|
||||
{
|
||||
message.clear();
|
||||
if (package_file.empty())
|
||||
return false;
|
||||
|
||||
// ---- pre-flight (main thread): validate + inspect + overwrite prompt ----
|
||||
const wxString package_name = from_u8(package_file.filename().string());
|
||||
|
||||
std::string extension = package_file.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
if (extension != ".py" && extension != ".whl") {
|
||||
message = _L("Select a .py or .whl plugin package.");
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginDescriptor plugin_descriptor;
|
||||
bool existing_installation = false;
|
||||
std::string error;
|
||||
try {
|
||||
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error;
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what();
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file;
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existing_installation) {
|
||||
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
wxMessageDialog dialog(parent,
|
||||
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
|
||||
plugin_name),
|
||||
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
|
||||
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
|
||||
if (dialog.ShowModal() != wxID_OK) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file
|
||||
<< " plugin=" << plugin_descriptor.name;
|
||||
return false; // cancelled: message stays empty so callers stay silent
|
||||
}
|
||||
}
|
||||
|
||||
// ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ----
|
||||
bool installed = false;
|
||||
{
|
||||
struct Result
|
||||
{
|
||||
std::mutex mutex;
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
};
|
||||
auto state = std::make_shared<Result>();
|
||||
|
||||
detail::run_wait_with_progress(
|
||||
[state, package_file]() {
|
||||
std::string error;
|
||||
bool ok = false;
|
||||
try {
|
||||
ok = PluginManager::instance().install_plugin(package_file, error);
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
}
|
||||
if (ok) {
|
||||
// Reflect the new package in discovery/cloud metadata without blocking the caller.
|
||||
try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {}
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->ok = ok;
|
||||
state->error = std::move(error);
|
||||
},
|
||||
parent, _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
|
||||
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME, /*alive=*/nullptr, /*restore=*/{});
|
||||
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
installed = state->ok;
|
||||
error = std::move(state->error);
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error;
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file;
|
||||
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
message = wxString::Format(_L("Installed \"%s\"."), installed_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style)
|
||||
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
|
||||
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
|
||||
@@ -819,78 +945,31 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
|
||||
{
|
||||
if (package_path.empty())
|
||||
return false;
|
||||
BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path;
|
||||
std::string error;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path;
|
||||
|
||||
const boost::filesystem::path package_file(package_path);
|
||||
const wxString package_name = from_u8(package_file.filename().string());
|
||||
wxString message;
|
||||
const bool installed = install_local_plugin_package(package_file, this, message);
|
||||
// The helper's overwrite prompt and progress dialog can push this webview behind; re-raise it
|
||||
// once, after both have closed (the speed-dial path parents to the mainframe instead).
|
||||
restore_z_order();
|
||||
|
||||
std::string extension = package_file.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
if (extension != ".py" && extension != ".whl") {
|
||||
show_status(_L("Select a .py or .whl plugin package."), "info");
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginDescriptor plugin_descriptor;
|
||||
bool existing_installation = false;
|
||||
auto report_inspection_failure = [&]() {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error;
|
||||
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
|
||||
// The shared helper reports a user-cancelled overwrite with an empty message: stay silent.
|
||||
if (message.IsEmpty()) {
|
||||
send_plugins();
|
||||
return false;
|
||||
};
|
||||
|
||||
try {
|
||||
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error))
|
||||
return report_inspection_failure();
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
return report_inspection_failure();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
return report_inspection_failure();
|
||||
}
|
||||
|
||||
if (existing_installation) {
|
||||
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
wxMessageDialog dialog(
|
||||
this,
|
||||
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
|
||||
plugin_name),
|
||||
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
|
||||
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
|
||||
const int overwrite_rc = dialog.ShowModal();
|
||||
restore_z_order();
|
||||
if (overwrite_rc != wxID_OK) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
|
||||
<< " plugin=" << plugin_descriptor.name;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool installed = false;
|
||||
try {
|
||||
installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); },
|
||||
_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
|
||||
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME);
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error;
|
||||
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
|
||||
show_status(message, "warn");
|
||||
send_plugins();
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
|
||||
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
|
||||
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
|
||||
show_status(message, "success");
|
||||
prompt_for_missing_plugins();
|
||||
send_plugins();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1086,23 +1165,7 @@ void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token)
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token));
|
||||
}
|
||||
|
||||
void PluginsDialog::open_plugin_hub()
|
||||
{
|
||||
std::string cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
if (wxGetApp().getAgent()) {
|
||||
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
|
||||
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
|
||||
cloud_base_url = orca_agent->get_cloud_base_url();
|
||||
}
|
||||
|
||||
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
|
||||
cloud_base_url.pop_back();
|
||||
if (cloud_base_url.empty())
|
||||
cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
|
||||
}
|
||||
void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); }
|
||||
|
||||
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
|
||||
{
|
||||
|
||||
+183
-124
@@ -25,6 +25,8 @@
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
class wxTimer;
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -35,6 +37,184 @@ enum class PluginCapabilityType;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
// Dialog-independent plugin-management actions, shared by the Plugins dialog and the speed dial:
|
||||
// they never require the webview dialog to be open.
|
||||
|
||||
// Rescans local plugins and (optionally) re-fetches cloud metadata. Blocking: run off the UI
|
||||
// thread. Used by PluginsDialog (behind its progress dialog) and GUI_App::refresh_plugins().
|
||||
void refresh_plugin_metadata_blocking(bool fetch_cloud);
|
||||
|
||||
// Opens the Cloud plugin hub in the default browser. No dialog needed.
|
||||
void open_plugin_hub();
|
||||
|
||||
// Synchronously installs a local plugin package (.py/.whl). Runs on the UI thread but keeps it
|
||||
// responsive by performing the install on a worker behind a modal progress dialog. `parent` owns
|
||||
// the overwrite prompt and the progress dialog. On success `message` carries the localized
|
||||
// confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason.
|
||||
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message);
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Shared worker + modal-progress machinery: pulse a progress dialog while `run` executes on a
|
||||
// detached worker, then run `on_finish` back on the UI thread. `alive`, when non-null, gates both
|
||||
// the pulse and `on_finish` so a worker outliving its dialog can't touch freed windows; pass null
|
||||
// for a dialog-independent caller. `restore` runs after the progress dialog is destroyed and before
|
||||
// `on_finish`, so a webview host can re-raise itself. `finish_after_dialog_destroyed` still calls
|
||||
// `on_finish` (without touching the dialog) when the host died, so a waiting loop can exit.
|
||||
template<typename Run, typename OnFinish>
|
||||
void run_off_thread_with_progress(Run&& run,
|
||||
OnFinish&& on_finish,
|
||||
wxWindow* parent,
|
||||
const wxString& title,
|
||||
const wxString& message,
|
||||
int maximum,
|
||||
int style,
|
||||
std::shared_ptr<std::atomic<bool>> alive,
|
||||
bool finish_after_dialog_destroyed,
|
||||
std::function<void()> restore)
|
||||
{
|
||||
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, parent, style);
|
||||
wxTimer* timer = new wxTimer();
|
||||
|
||||
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
|
||||
if ((!alive || alive->load(std::memory_order_acquire)) && progress)
|
||||
progress->Pulse(message);
|
||||
});
|
||||
|
||||
timer->Start(100);
|
||||
|
||||
std::thread([alive,
|
||||
progress,
|
||||
timer,
|
||||
run = std::forward<Run>(run),
|
||||
on_finish = std::forward<OnFinish>(on_finish),
|
||||
finish_after_dialog_destroyed,
|
||||
restore = std::move(restore)]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
|
||||
}
|
||||
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
wxTheApp->CallAfter([alive,
|
||||
progress,
|
||||
timer,
|
||||
on_finish = std::move(on_finish),
|
||||
finish_after_dialog_destroyed,
|
||||
restore = std::move(restore)]() mutable {
|
||||
timer->Stop();
|
||||
delete timer;
|
||||
|
||||
if (!alive || alive->load(std::memory_order_acquire)) {
|
||||
progress->Destroy();
|
||||
if (restore)
|
||||
restore();
|
||||
on_finish();
|
||||
} else if (finish_after_dialog_destroyed) {
|
||||
on_finish();
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
// Wait for a worker behind a progress dialog, returning its result (or rethrowing). The waiting
|
||||
// loop stays responsive because it pumps the event loop the worker posts its completion into.
|
||||
template<typename Run>
|
||||
std::invoke_result_t<std::decay_t<Run>&> run_wait_with_progress(Run&& run,
|
||||
wxWindow* parent,
|
||||
const wxString& title,
|
||||
const wxString& message,
|
||||
int maximum,
|
||||
int style,
|
||||
std::shared_ptr<std::atomic<bool>> alive,
|
||||
std::function<void()> restore)
|
||||
{
|
||||
using Result = std::invoke_result_t<std::decay_t<Run>&>;
|
||||
|
||||
bool finished = false;
|
||||
wxEventLoop loop;
|
||||
auto on_finish = [&finished, &loop]() {
|
||||
finished = true;
|
||||
if (loop.IsRunning())
|
||||
loop.Exit();
|
||||
};
|
||||
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_off_thread_with_progress(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
} else {
|
||||
using StoredResult = std::decay_t<Result>;
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_off_thread_with_progress(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
StoredResult result = run();
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->result.emplace(std::move(result));
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (state->result)
|
||||
result.emplace(std::move(*state->result));
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
return std::move(*result);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class PluginsDialog : public Slic3r::GUI::WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
@@ -111,53 +291,8 @@ private:
|
||||
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button
|
||||
bool finish_after_dialog_destroyed = false)
|
||||
{
|
||||
const auto alive = m_alive;
|
||||
ProgressDialog* progress = new ProgressDialog(title, message, maximum, this, style);
|
||||
wxTimer* timer = new wxTimer();
|
||||
|
||||
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
|
||||
if (alive->load(std::memory_order_acquire) && progress)
|
||||
progress->Pulse(message);
|
||||
});
|
||||
|
||||
timer->Start(100);
|
||||
|
||||
std::thread([this,
|
||||
alive,
|
||||
progress,
|
||||
timer,
|
||||
run = std::forward<Run>(run),
|
||||
on_finish = std::forward<OnFinish>(on_finish),
|
||||
finish_after_dialog_destroyed]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
|
||||
}
|
||||
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
wxTheApp->CallAfter([this,
|
||||
alive,
|
||||
progress,
|
||||
timer,
|
||||
on_finish = std::move(on_finish),
|
||||
finish_after_dialog_destroyed]() mutable {
|
||||
timer->Stop();
|
||||
delete timer;
|
||||
|
||||
if (alive->load(std::memory_order_acquire)) {
|
||||
progress->Destroy();
|
||||
restore_z_order();
|
||||
on_finish();
|
||||
} else if (finish_after_dialog_destroyed) {
|
||||
on_finish();
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
detail::run_off_thread_with_progress(std::forward<Run>(run), std::forward<OnFinish>(on_finish), this, title, message, maximum, style,
|
||||
m_alive, finish_after_dialog_destroyed, [this] { restore_z_order(); });
|
||||
}
|
||||
|
||||
template<typename Run>
|
||||
@@ -167,83 +302,7 @@ private:
|
||||
int maximum = 100,
|
||||
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||
{
|
||||
using Result = std::invoke_result_t<std::decay_t<Run>&>;
|
||||
|
||||
bool finished = false;
|
||||
wxEventLoop loop;
|
||||
auto on_finish = [&finished, &loop]() {
|
||||
finished = true;
|
||||
if (loop.IsRunning())
|
||||
loop.Exit();
|
||||
};
|
||||
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_with_dialog(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, title, message, maximum, style, true);
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
} else {
|
||||
using StoredResult = std::decay_t<Result>;
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_with_dialog(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
StoredResult result = run();
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->result.emplace(std::move(result));
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, title, message, maximum, style, true);
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (state->result)
|
||||
result.emplace(std::move(*state->result));
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
return std::move(*result);
|
||||
}
|
||||
return detail::run_wait_with_progress(std::forward<Run>(run), this, title, message, maximum, style, m_alive, [this] { restore_z_order(); });
|
||||
}
|
||||
|
||||
std::function<void()> m_open_terminal_dlg_fn;
|
||||
|
||||
@@ -1726,6 +1726,21 @@ void PreferencesDialog::create_items()
|
||||
auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)"));
|
||||
g_sizer->Add(item_multi_machine);
|
||||
|
||||
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial with the Space key"),
|
||||
_L("When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page."),
|
||||
"enable_speed_dial");
|
||||
g_sizer->Add(item_speed_dial);
|
||||
|
||||
auto item_speed_dial_recents = create_item_spinctrl(
|
||||
_L("Recent actions"),
|
||||
"",
|
||||
_L("actions"),
|
||||
_L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."),
|
||||
SETTING_SPEED_DIAL_RECENT_COUNT,
|
||||
SPEED_DIAL_RECENT_COUNT_MIN,
|
||||
SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
g_sizer->Add(item_speed_dial_recents);
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"),
|
||||
_L("With this option enabled, the Design tab is shown, where models can be built and edited "
|
||||
|
||||
+6
-200
@@ -62,95 +62,12 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt)
|
||||
}
|
||||
}
|
||||
|
||||
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
|
||||
|
||||
void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const
|
||||
{
|
||||
*label_ = marked_label.c_str();
|
||||
*tooltip_ = tooltip.c_str();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
|
||||
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
|
||||
{
|
||||
T *opt_cur = static_cast<T *>(config->option(opt_key));
|
||||
cnt = opt_cur->values.size();
|
||||
return;
|
||||
|
||||
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
|
||||
}
|
||||
|
||||
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
|
||||
|
||||
void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
auto emplace = [this, type](const std::string key, const wxString &label) {
|
||||
const GroupAndCategory &gc = groups_and_categories[key];
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
|
||||
|
||||
wxString suffix;
|
||||
wxString suffix_local;
|
||||
if (gc.category == "Machine limits") {
|
||||
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
|
||||
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
|
||||
suffix_local = " " + _(suffix);
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
|
||||
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()});
|
||||
};
|
||||
|
||||
for (std::string opt_key : config->keys()) {
|
||||
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
||||
if (opt.mode > mode) continue;
|
||||
|
||||
int cnt = 0;
|
||||
|
||||
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
|
||||
switch (config->option(opt_key)->type()) {
|
||||
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
|
||||
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
|
||||
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
|
||||
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
|
||||
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
|
||||
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
|
||||
// BBS
|
||||
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
|
||||
opt_key += "#0";
|
||||
|
||||
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
|
||||
|
||||
std::string key = get_key(opt_key, type);
|
||||
if (cnt == 0)
|
||||
emplace(key, label);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
inline void OptionsSearcher::sort_options()
|
||||
{
|
||||
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
|
||||
Option * last = nullptr;
|
||||
for (auto& opt : options) {
|
||||
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
|
||||
last->multi_category = true;
|
||||
opt.multi_category = true;
|
||||
}
|
||||
last = &opt;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark a string using ColorMarkerStart and ColorMarkerEnd symbols
|
||||
static std::wstring mark_string(const std::wstring &str, const std::vector<uint16_t> &matches, Preset::Type type, PrinterTechnology pt)
|
||||
{
|
||||
@@ -235,7 +152,8 @@ bool OptionsSearcher::search(const std::string &search, bool force /* = false*/,
|
||||
return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.group_local + sep + opt.label_local;
|
||||
};
|
||||
|
||||
std::vector<uint16_t> matches, matches2;
|
||||
std::vector<uint16_t> matches, matches2;
|
||||
const std::vector<Option> &options = m_index.options();
|
||||
for (size_t i = 0; i < options.size(); i++) {
|
||||
const Option &opt = options[i];
|
||||
if (full_list) {
|
||||
@@ -307,128 +225,21 @@ OptionsSearcher::~OptionsSearcher() {}
|
||||
|
||||
void OptionsSearcher::init(std::vector<InputInfo> input_values)
|
||||
{
|
||||
options.clear();
|
||||
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
||||
sort_options();
|
||||
m_index.init(std::move(input_values));
|
||||
|
||||
search(search_line, true, search_type);
|
||||
}
|
||||
|
||||
void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
if (options.empty()) return;
|
||||
|
||||
options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end());
|
||||
|
||||
append_options(config, type, mode);
|
||||
|
||||
sort_options();
|
||||
|
||||
search(search_line, true, search_type);
|
||||
if (m_index.apply(config, type, mode))
|
||||
search(search_line, true, search_type);
|
||||
}
|
||||
|
||||
const Option &OptionsSearcher::get_option(size_t pos_in_filter) const
|
||||
{
|
||||
assert(pos_in_filter != size_t(-1) && found[pos_in_filter].option_idx != size_t(-1));
|
||||
return options[found[pos_in_filter].option_idx];
|
||||
}
|
||||
|
||||
const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
|
||||
{
|
||||
auto not_found = [&variant_index]() -> const Option& {
|
||||
static const Option empty_option;
|
||||
variant_index = -2;
|
||||
return empty_option;
|
||||
};
|
||||
|
||||
variant_index = -1;
|
||||
std::string opt_key2 = opt_key;
|
||||
if (auto n = opt_key.find('#'); n != std::string::npos) {
|
||||
variant_index = std::atoi(opt_key.c_str() + n + 1);
|
||||
opt_key2 = opt_key.substr(0, n);
|
||||
}
|
||||
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
|
||||
auto it = std::lower_bound(options.begin(), options.end(), Option({key}));
|
||||
if (it == options.end()) return not_found();
|
||||
if (it->key == key) {
|
||||
variant_index = -1;
|
||||
} else {
|
||||
const std::wstring prefix = key + L"#";
|
||||
it = std::lower_bound(it, options.end(), Option({prefix}));
|
||||
if (it == options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
|
||||
return not_found();
|
||||
// Orca: Copy-parameters dialogs request the base key, without a vector index.
|
||||
if (variant_index < 0) return *it;
|
||||
|
||||
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
|
||||
const bool has_variant =
|
||||
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
|
||||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
|
||||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
|
||||
if (!has_variant || has_mode) {
|
||||
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
|
||||
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
|
||||
boost::nowide::widen(get_key(opt_key, type));
|
||||
it = std::lower_bound(it, options.end(), Option({indexed_key}));
|
||||
if (it == options.end() || it->key != indexed_key)
|
||||
return not_found();
|
||||
if (!has_variant)
|
||||
variant_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return options[it - options.begin()];
|
||||
}
|
||||
|
||||
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
|
||||
{
|
||||
wxString suffix;
|
||||
wxString suffix_local;
|
||||
if (gc.category == "Machine limits") {
|
||||
//suffix = opt_key.back() == '1' ? L("Stealth") : L("Normal");
|
||||
suffix = opt_key.back() == '1' ? wxEmptyString : wxEmptyString;
|
||||
suffix_local = " " + _(suffix);
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
|
||||
wxString category = gc.category;
|
||||
if (type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
|
||||
std::string opt_idx = opt_key.substr(opt_key.find("#") + 1);
|
||||
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
|
||||
}
|
||||
|
||||
return Option{boost::nowide::widen(get_key(opt_key, type)),
|
||||
type,
|
||||
(label + suffix).ToStdWstring(),
|
||||
(_(label) + suffix_local).ToStdWstring(),
|
||||
gc.group.ToStdWstring(),
|
||||
_(gc.group).ToStdWstring(),
|
||||
gc.category.ToStdWstring(),
|
||||
GUI::Tab::translate_category(category, type).ToStdWstring()};
|
||||
}
|
||||
|
||||
Option OptionsSearcher::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
|
||||
{
|
||||
std::string key = get_key(opt_key, type);
|
||||
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(key)}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
if (it == options.end()) return options[0];
|
||||
if (it->key == boost::nowide::widen(key)) return options[it - options.begin()];
|
||||
if (groups_and_categories.find(key) == groups_and_categories.end()) {
|
||||
size_t pos = key.find('#');
|
||||
if (pos == std::string::npos) return options[it - options.begin()];
|
||||
|
||||
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
|
||||
|
||||
if (groups_and_categories.find(zero_opt_key) == groups_and_categories.end()) return options[it - options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, groups_and_categories.at(zero_opt_key));
|
||||
}
|
||||
|
||||
const GroupAndCategory &gc = groups_and_categories.at(key);
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return options[it - options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, gc);
|
||||
return m_index.option_at(found[pos_in_filter].option_idx);
|
||||
}
|
||||
|
||||
void OptionsSearcher::show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow* ssearch_btn)
|
||||
@@ -455,11 +266,6 @@ void OptionsSearcher::dlg_msw_rescale()
|
||||
{
|
||||
if (search_dialog) search_dialog->msw_rescale();
|
||||
}
|
||||
|
||||
void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category)
|
||||
{
|
||||
groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category};
|
||||
}
|
||||
//------------------------------------------
|
||||
// SearchItem
|
||||
//------------------------------------------
|
||||
|
||||
+10
-54
@@ -19,6 +19,7 @@
|
||||
#include "wxExtensions.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "Widgets/ScrolledWindow.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/PopupWindow.hpp"
|
||||
@@ -34,39 +35,6 @@ namespace Search {
|
||||
|
||||
class SearchDialog;
|
||||
|
||||
struct InputInfo
|
||||
{
|
||||
DynamicPrintConfig *config{nullptr};
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
ConfigOptionMode mode{comSimple};
|
||||
};
|
||||
|
||||
struct GroupAndCategory
|
||||
{
|
||||
wxString group;
|
||||
wxString category;
|
||||
};
|
||||
|
||||
struct Option
|
||||
{
|
||||
// bool operator<(const Option& other) const { return other.label > this->label; }
|
||||
bool operator<(const Option &other) const { return other.key > this->key; }
|
||||
|
||||
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
|
||||
// though for some languages (Chinese?) it may not work correctly.
|
||||
std::wstring key;
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
std::wstring label;
|
||||
std::wstring label_local;
|
||||
std::wstring group;
|
||||
std::wstring group_local;
|
||||
std::wstring category;
|
||||
std::wstring category_local;
|
||||
bool multi_category { false };
|
||||
|
||||
std::string opt_key() const;
|
||||
};
|
||||
|
||||
struct FoundOption
|
||||
{
|
||||
// UTF8 encoding, to be consumed by ImGUI by reference.
|
||||
@@ -90,25 +58,19 @@ struct OptionViewParameters
|
||||
|
||||
class OptionsSearcher
|
||||
{
|
||||
std::string search_line;
|
||||
Preset::Type search_type = Preset::TYPE_INVALID;
|
||||
SettingsIndex m_index;
|
||||
|
||||
std::map<std::string, GroupAndCategory> groups_and_categories;
|
||||
PrinterTechnology printer_technology;
|
||||
|
||||
std::vector<Option> options{};
|
||||
std::string search_line;
|
||||
Preset::Type search_type = Preset::TYPE_INVALID;
|
||||
PrinterTechnology printer_technology;
|
||||
std::vector<FoundOption> found{};
|
||||
|
||||
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
|
||||
void sort_options();
|
||||
void sort_found()
|
||||
{
|
||||
std::sort(found.begin(), found.end(),
|
||||
[](const FoundOption &f1, const FoundOption &f2) { return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && f1.label < f2.label); });
|
||||
};
|
||||
|
||||
size_t options_size() const { return options.size(); }
|
||||
size_t found_size() const { return found.size(); }
|
||||
|
||||
public:
|
||||
@@ -119,32 +81,26 @@ public:
|
||||
OptionsSearcher();
|
||||
~OptionsSearcher();
|
||||
|
||||
SettingsIndex & index() { return m_index; }
|
||||
const SettingsIndex &index() const { return m_index; }
|
||||
|
||||
// Rebuild the catalog and re-run the current query so the cached results track it.
|
||||
void init(std::vector<InputInfo> input_values);
|
||||
void apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
|
||||
bool search();
|
||||
bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID);
|
||||
|
||||
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category);
|
||||
|
||||
size_t size() const { return found_size(); }
|
||||
|
||||
const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; }
|
||||
const Option & get_option(size_t pos_in_filter) const;
|
||||
const Option & get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
|
||||
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
|
||||
|
||||
const std::vector<FoundOption> &found_options() { return found; }
|
||||
const GroupAndCategory & get_group_and_category(const std::string &opt_key) { return groups_and_categories[opt_key]; }
|
||||
std::string & search_string() { return search_line; }
|
||||
|
||||
void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; }
|
||||
|
||||
void sort_options_by_key()
|
||||
{
|
||||
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
|
||||
}
|
||||
void sort_options_by_label() { sort_options(); }
|
||||
|
||||
void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn);
|
||||
void dlg_sys_color_changed();
|
||||
void dlg_msw_rescale();
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "SettingsIndex.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/nowide/convert.hpp>
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Tab.hpp"
|
||||
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using GUI::into_u8;
|
||||
|
||||
namespace Search {
|
||||
|
||||
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
|
||||
|
||||
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
|
||||
|
||||
template<class T>
|
||||
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
|
||||
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
|
||||
{
|
||||
T *opt_cur = static_cast<T *>(config->option(opt_key));
|
||||
cnt = opt_cur->values.size();
|
||||
return;
|
||||
|
||||
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
|
||||
}
|
||||
|
||||
// Single assembler for an indexed Option, shared by append_options() and create_option(), so a new
|
||||
// Option field is only wired up in one place.
|
||||
static Option make_option(const std::string &key, Preset::Type type, const wxString &label, const GroupAndCategory &gc,
|
||||
ConfigOptionMode mode, const std::string &tooltip, bool rewrite_extruder_category)
|
||||
{
|
||||
wxString suffix;
|
||||
wxString suffix_local;
|
||||
if (gc.category == "Machine limits") {
|
||||
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
|
||||
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
|
||||
suffix_local = " " + _(suffix);
|
||||
suffix = " " + suffix;
|
||||
}
|
||||
|
||||
wxString category = gc.category;
|
||||
if (rewrite_extruder_category && type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
|
||||
std::string opt_idx = key.substr(key.find("#") + 1);
|
||||
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
|
||||
}
|
||||
|
||||
Option option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(),
|
||||
gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(),
|
||||
GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path,
|
||||
// The settings page draws Line::label; carrying it lets the Speed Dial name a setting
|
||||
// the way the page does. `label`/`label_local` stay the search-oriented name.
|
||||
into_u8(gc.line_label)};
|
||||
return option;
|
||||
}
|
||||
|
||||
void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
for (std::string opt_key : config->keys()) {
|
||||
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
|
||||
const bool in_filtered = opt.mode <= mode;
|
||||
|
||||
int cnt = 0;
|
||||
|
||||
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
|
||||
switch (config->option(opt_key)->type()) {
|
||||
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
|
||||
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
|
||||
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
|
||||
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
|
||||
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
|
||||
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
|
||||
// BBS
|
||||
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
|
||||
opt_key += "#0";
|
||||
|
||||
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) {
|
||||
const GroupAndCategory &gc = m_groups_and_categories[k];
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty() || label.IsEmpty()) return;
|
||||
|
||||
const std::string tooltip = into_u8(_(opt.tooltip));
|
||||
if (in_filtered)
|
||||
m_options.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
|
||||
m_all_modes.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
|
||||
};
|
||||
if (cnt == 0)
|
||||
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
|
||||
add(key + "#" + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsIndex::sort_options()
|
||||
{
|
||||
// Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar
|
||||
// search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently
|
||||
// diverging in order or flags.
|
||||
auto sort_and_mark = [](std::vector<Option> &v) {
|
||||
std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
|
||||
Option *last = nullptr;
|
||||
for (auto &opt : v) {
|
||||
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
|
||||
last->multi_category = true;
|
||||
opt.multi_category = true;
|
||||
}
|
||||
last = &opt;
|
||||
}
|
||||
};
|
||||
sort_and_mark(m_options);
|
||||
sort_and_mark(m_all_modes);
|
||||
}
|
||||
|
||||
void SettingsIndex::init(std::vector<InputInfo> input_values)
|
||||
{
|
||||
m_options.clear();
|
||||
m_all_modes.clear();
|
||||
for (auto i : input_values) append_options(i.config, i.type, i.mode);
|
||||
sort_options();
|
||||
}
|
||||
|
||||
bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
|
||||
{
|
||||
// m_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means both
|
||||
// views are empty - the mode-filtered m_options can be empty while m_all_modes is not.
|
||||
if (m_options.empty() && m_all_modes.empty()) return false;
|
||||
|
||||
m_options.erase(std::remove_if(m_options.begin(), m_options.end(), [type](Option opt) { return opt.type == type; }), m_options.end());
|
||||
m_all_modes.erase(std::remove_if(m_all_modes.begin(), m_all_modes.end(), [type](Option opt) { return opt.type == type; }), m_all_modes.end());
|
||||
|
||||
append_options(config, type, mode);
|
||||
|
||||
sort_options();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
|
||||
{
|
||||
auto not_found = [&variant_index]() -> const Option & {
|
||||
static const Option empty_option;
|
||||
variant_index = -2;
|
||||
return empty_option;
|
||||
};
|
||||
|
||||
variant_index = -1;
|
||||
std::string opt_key2 = opt_key;
|
||||
if (auto n = opt_key.find('#'); n != std::string::npos) {
|
||||
variant_index = std::atoi(opt_key.c_str() + n + 1);
|
||||
opt_key2 = opt_key.substr(0, n);
|
||||
}
|
||||
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
|
||||
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({key}));
|
||||
if (it == m_options.end()) return not_found();
|
||||
if (it->key == key) {
|
||||
variant_index = -1;
|
||||
} else {
|
||||
const std::wstring prefix = key + L"#";
|
||||
it = std::lower_bound(it, m_options.end(), Option({prefix}));
|
||||
if (it == m_options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
|
||||
return not_found();
|
||||
// Orca: Copy-parameters dialogs request the base key, without a vector index.
|
||||
if (variant_index < 0) return *it;
|
||||
|
||||
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
|
||||
const bool has_variant =
|
||||
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
|
||||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
|
||||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
|
||||
if (!has_variant || has_mode) {
|
||||
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
|
||||
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
|
||||
boost::nowide::widen(get_key(opt_key, type));
|
||||
it = std::lower_bound(it, m_options.end(), Option({indexed_key}));
|
||||
if (it == m_options.end() || it->key != indexed_key)
|
||||
return not_found();
|
||||
if (!has_variant)
|
||||
variant_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return m_options[it - m_options.begin()];
|
||||
}
|
||||
|
||||
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
|
||||
{
|
||||
return make_option(get_key(opt_key, type), type, label, gc, comSimple, std::string(), true);
|
||||
}
|
||||
|
||||
Option SettingsIndex::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
|
||||
{
|
||||
std::string key = get_key(opt_key, type);
|
||||
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(key)}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
if (it == m_options.end()) return m_options[0];
|
||||
if (it->key == boost::nowide::widen(key)) return m_options[it - m_options.begin()];
|
||||
if (m_groups_and_categories.find(key) == m_groups_and_categories.end()) {
|
||||
size_t pos = key.find('#');
|
||||
if (pos == std::string::npos) return m_options[it - m_options.begin()];
|
||||
|
||||
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
|
||||
|
||||
if (m_groups_and_categories.find(zero_opt_key) == m_groups_and_categories.end()) return m_options[it - m_options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, m_groups_and_categories.at(zero_opt_key));
|
||||
}
|
||||
|
||||
const GroupAndCategory &gc = m_groups_and_categories.at(key);
|
||||
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return m_options[it - m_options.begin()];
|
||||
|
||||
return create_option(opt_key, label, type, gc);
|
||||
}
|
||||
|
||||
void SettingsIndex::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon)
|
||||
{
|
||||
// Update fields in place so a page rebuild (get_option after set_path) doesn't drop the
|
||||
// previously recorded wiki path.
|
||||
GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)];
|
||||
gc.group = group;
|
||||
gc.category = category;
|
||||
gc.icon = icon;
|
||||
}
|
||||
|
||||
void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return;
|
||||
m_groups_and_categories[get_key(opt_key, type)].path = path;
|
||||
}
|
||||
|
||||
void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label)
|
||||
{
|
||||
if (label.IsEmpty())
|
||||
return;
|
||||
m_groups_and_categories[get_key(opt_key, type)].line_label = label;
|
||||
}
|
||||
|
||||
} // namespace Search
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,123 @@
|
||||
#ifndef slic3r_SettingsIndex_hpp_
|
||||
#define slic3r_SettingsIndex_hpp_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <wx/string.h>
|
||||
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/Preset.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Search {
|
||||
|
||||
struct InputInfo
|
||||
{
|
||||
DynamicPrintConfig *config{nullptr};
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
ConfigOptionMode mode{comSimple};
|
||||
};
|
||||
|
||||
struct GroupAndCategory
|
||||
{
|
||||
wxString group;
|
||||
wxString category;
|
||||
wxString icon; // icon of the group's own header, or empty
|
||||
wxString line_label; // label the settings row actually draws (Line::label), or empty
|
||||
std::string path; // wiki path (Line::label_path) of the option's line, or empty
|
||||
};
|
||||
|
||||
// Title for a setting: the row label, qualified with the field leaf when the row packs several
|
||||
// options (e.g. "Cool Plate \u2013 First layer"). Pure; inputs are already localized.
|
||||
inline wxString compose_display_label(const wxString& line_label, const wxString& leaf_label, bool multi)
|
||||
{
|
||||
if (line_label.empty())
|
||||
return leaf_label;
|
||||
if (!multi || leaf_label.empty() || leaf_label == line_label)
|
||||
return line_label;
|
||||
return line_label + L" \u2013 " + leaf_label; // en dash separator
|
||||
}
|
||||
|
||||
// Title to show. A single-option row uses its live label, which can be renamed at runtime
|
||||
// (brim_width -> "Brim ear radius"); otherwise fall back to the precomposed label.
|
||||
inline wxString resolve_setting_title(const wxString& precomposed, const wxString& live_label, bool live_multi)
|
||||
{
|
||||
if (!live_multi && !live_label.empty())
|
||||
return live_label;
|
||||
return precomposed;
|
||||
}
|
||||
|
||||
struct Option
|
||||
{
|
||||
// bool operator<(const Option& other) const { return other.label > this->label; }
|
||||
bool operator<(const Option &other) const { return other.key > this->key; }
|
||||
|
||||
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
|
||||
// though for some languages (Chinese?) it may not work correctly.
|
||||
std::wstring key;
|
||||
Preset::Type type{Preset::TYPE_INVALID};
|
||||
std::wstring label;
|
||||
std::wstring label_local;
|
||||
std::wstring group;
|
||||
std::wstring group_local;
|
||||
std::string group_icon; // SVG base name of the group's own header icon, or empty
|
||||
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 tooltip; // localized ConfigOptionDef::tooltip, or empty
|
||||
std::string wiki_path; // Line::label_path for the option's row, or empty
|
||||
std::string display_label; // label the settings row draws (localized); empty falls back to label
|
||||
|
||||
std::string opt_key() const;
|
||||
};
|
||||
|
||||
// Catalog of settings and their metadata. Owns the group/category registry populated by the
|
||||
// settings pages, plus two views of the options: the mode-filtered view the sidebar search
|
||||
// queries, and every option regardless of mode for the Speed Dial.
|
||||
class SettingsIndex
|
||||
{
|
||||
std::map<std::string, GroupAndCategory> m_groups_and_categories;
|
||||
|
||||
std::vector<Option> m_options; // mode-filtered view used by the sidebar search
|
||||
std::vector<Option> m_all_modes; // every option regardless of mode, for the Speed Dial
|
||||
|
||||
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
void sort_options();
|
||||
|
||||
public:
|
||||
void init(std::vector<InputInfo> input_values);
|
||||
// Rebuild the given type's options; returns false when the index was never initialised.
|
||||
bool apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
|
||||
|
||||
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category,
|
||||
const wxString &icon = wxEmptyString);
|
||||
void set_path(const std::string &opt_key, Preset::Type type, const std::string &path);
|
||||
// Record the label the option's row draws, so the Speed Dial names a setting like the page
|
||||
// (ConfigOptionDef::label/full_label is a search name, not the row text).
|
||||
void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label);
|
||||
|
||||
const std::vector<Option> &options() const { return m_options; }
|
||||
const std::vector<Option> &all_options() const { return m_all_modes; }
|
||||
const Option & option_at(size_t pos) const { return m_options[pos]; }
|
||||
|
||||
const Option &get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
|
||||
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
|
||||
|
||||
const GroupAndCategory &get_group_and_category(const std::string &opt_key) { return m_groups_and_categories[opt_key]; }
|
||||
|
||||
void sort_options_by_key()
|
||||
{
|
||||
std::sort(m_options.begin(), m_options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
|
||||
}
|
||||
void sort_options_by_label() { sort_options(); }
|
||||
};
|
||||
|
||||
} // namespace Search
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SettingsIndex_hpp_
|
||||
@@ -11,9 +11,16 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/display.h>
|
||||
#include <wx/region.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/utils.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <gtk/gtk.h>
|
||||
#endif
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
@@ -21,8 +28,8 @@ namespace {
|
||||
|
||||
// ADJUST WIDTH HERE (DIP px). Fixed dialog width; was 360, now 1.5x. Height is not set here -
|
||||
// the dialog auto-resizes to the page content (see resize_to_content + the list max-height in style.css).
|
||||
constexpr int kPopupWidth = 540;
|
||||
constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content
|
||||
constexpr int kPopupWidth = 540;
|
||||
constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content
|
||||
constexpr int kPopupMaxHeight = 282;
|
||||
|
||||
int json_int_or(const nlohmann::json& j, const char* key, int fallback)
|
||||
@@ -31,47 +38,157 @@ int json_int_or(const nlohmann::json& j, const char* key, int fallback)
|
||||
return it != j.end() && it->is_number() ? it->get<int>() : fallback;
|
||||
}
|
||||
|
||||
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
|
||||
|
||||
}
|
||||
|
||||
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
|
||||
: WebViewHostDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
|
||||
wxBORDER_NONE | wxFRAME_NO_TASKBAR)
|
||||
// Display name of a settings mode, for the mode-switch confirmation.
|
||||
wxString mode_label(ConfigOptionMode mode)
|
||||
{
|
||||
SetBackgroundColour(bg_color());
|
||||
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
|
||||
if (!event.GetActive() && IsShown())
|
||||
Hide();
|
||||
event.Skip();
|
||||
});
|
||||
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString,
|
||||
wxSize(kPopupWidth, kPopupMaxHeight), wxSize(kPopupWidth, kPopupMinHeight))) {
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")),
|
||||
wxSizerFlags().Border(wxALL, 20));
|
||||
SetSizer(sizer);
|
||||
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
|
||||
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.
|
||||
// (browser()->SetFocus() grabs focus on the container and doesn't reach the web content,
|
||||
// so typing only works after the user clicks.) On Linux the native backend is the
|
||||
// WebKitWebView widget; grab focus there directly. Elsewhere SetFocus() is correct.
|
||||
void focus_webview(wxWebView* browser, bool page_ready)
|
||||
{
|
||||
if (!browser)
|
||||
return;
|
||||
#ifdef __linux__
|
||||
if (void* nb = browser->GetNativeBackend())
|
||||
gtk_widget_grab_focus((GtkWidget*) nb);
|
||||
#else
|
||||
browser->SetFocus();
|
||||
#endif
|
||||
if (page_ready)
|
||||
browser->RunScript("focusInput();");
|
||||
}
|
||||
|
||||
// Localized strings for the Speed Dial page, injected as a document-start user script. The page's
|
||||
// T() reads window.ORCA_UI_STRINGS, so these flow through the same .po pipeline as the rest of the
|
||||
// UI (the JS literals are only a fallback before the script runs / in the node vm test).
|
||||
// %% is a literal '%': T() collapses it after substituting %s. Keep the shortcut tokens out of the
|
||||
// translated text so the platform prefix (Alt+/⌥+, Ctrl+/⌘+) stays correct.
|
||||
nlohmann::json speed_dial_ui_strings()
|
||||
{
|
||||
const std::string alt = GUI::shortkey_alt_prefix();
|
||||
const std::string ctrl = GUI::shortkey_ctrl_prefix();
|
||||
return {
|
||||
{"shortcut_alt", alt},
|
||||
{"shortcut_ctrl", ctrl},
|
||||
|
||||
{"sd_search", _u8L("Search actions")},
|
||||
{"sd_clear", _u8L("Clear")},
|
||||
{"sd_search_n", _u8L("Search %s actions")},
|
||||
{"sd_recent", _u8L("Recent")},
|
||||
{"sd_plugins", _u8L("Plugins")},
|
||||
{"sd_other", _u8L("Other")},
|
||||
{"sd_no_match_total", _u8L("No actions match (Total: %s)")},
|
||||
{"sd_no_actions", _u8L("No actions yet")},
|
||||
{"sd_no_tabs_match", _u8L("No tabs match")},
|
||||
{"sd_no_tabs", _u8L("No tabs")},
|
||||
{"sd_result_count", _u8L("Showing %s of %s actions")},
|
||||
{"sd_result_count_all", _u8L("%s actions")},
|
||||
{"sd_tab_count", _u8L("%s tabs")},
|
||||
{"sd_tab_match_count", _u8L("%s matches")},
|
||||
{"sd_favs_full", _u8L("Favourites are full (%s max)")},
|
||||
{"sd_go_to_pct", _u8L("Go to %s%% of the layer range")},
|
||||
{"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")},
|
||||
{"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")},
|
||||
{"sd_go_tab_ph", _u8L("Go to tab")},
|
||||
{"sd_fav_slot", _u8L("Favourite %s (%s)")},
|
||||
{"sd_pin_fav", _u8L("Pin to favourites (%s)")},
|
||||
{"sd_unpin_fav", _u8L("Unpin from favourites (%s)")},
|
||||
{"sd_remove_fav", _u8L("Remove from favourites")},
|
||||
{"sd_move_left", _u8L("Move left")},
|
||||
{"sd_move_right", _u8L("Move right")},
|
||||
{"sd_unpin", _u8L("Unpin")},
|
||||
{"sd_mode_advanced", _u8L("Advanced")},
|
||||
{"sd_mode_expert", _u8L("Expert")},
|
||||
{"sd_mode_develop", _u8L("Developer")},
|
||||
{"sd_wiki_f1", _u8L("Wiki (F1)")},
|
||||
{"sd_no_wiki", _u8L("No wiki page for this action")},
|
||||
{"sd_show_details", _u8L("Show details")},
|
||||
{"sd_hide_details", _u8L("Hide details")},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
|
||||
: WebViewHostDialog(parent,
|
||||
wxID_ANY,
|
||||
wxEmptyString,
|
||||
wxDefaultPosition,
|
||||
wxDefaultSize,
|
||||
wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT | wxFRAME_SHAPED)
|
||||
{
|
||||
SetBackgroundColour(bg_color());
|
||||
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
|
||||
// Focus the WebKit widget exactly when the WM makes the popup the active window
|
||||
// (modeless focus is granted asynchronously, so a focus request made right after
|
||||
// Show() is dropped). Also re-corrects focus on every re-open.
|
||||
if (event.GetActive() && IsShown())
|
||||
focus_webview(browser(), m_page_ready);
|
||||
else if (!event.GetActive() && IsShown())
|
||||
Hide();
|
||||
event.Skip();
|
||||
});
|
||||
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, wxSize(kPopupWidth, kPopupMaxHeight),
|
||||
wxSize(kPopupWidth, kPopupMinHeight))) {
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, 20));
|
||||
SetSizer(sizer);
|
||||
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
|
||||
}
|
||||
// WebView2's browser accelerator keys include Ctrl +/-/0 and Ctrl+wheel zoom, which would resize
|
||||
// the page inside the fixed-size popup. No-op on the other backends (wxWidgets 3.3 base virtual).
|
||||
if (wxWebView* wv = browser())
|
||||
wv->EnableBrowserAcceleratorKeys(false);
|
||||
// Re-cut the shape whenever layout changes the client size. wxOSX SetShape resizes the
|
||||
// NSWindow, which fires this synchronously; apply_rounded_shape() guards re-entry.
|
||||
Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
|
||||
event.Skip();
|
||||
apply_rounded_shape();
|
||||
});
|
||||
apply_rounded_shape();
|
||||
}
|
||||
|
||||
SpeedDialWebDialog::~SpeedDialWebDialog() { m_alive->store(false, std::memory_order_release); }
|
||||
|
||||
// Document-start hook: hand the page its translated strings before speeddial.js runs, so the first
|
||||
// paint is already localized. The table is built when the dialog is created; a live language switch
|
||||
// rebuilds the GUI (and with it this dialog), so the next open re-injects the new locale.
|
||||
void SpeedDialWebDialog::add_user_scripts()
|
||||
{
|
||||
if (wxWebView* wv = browser()) {
|
||||
const std::string js = "window.ORCA_UI_STRINGS = " +
|
||||
speed_dial_ui_strings().dump(-1, ' ', false, nlohmann::json::error_handler_t::ignore) + ";";
|
||||
wv->AddUserScript(wxString::FromUTF8(js));
|
||||
}
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::request_show()
|
||||
{
|
||||
if (IsShown()) {
|
||||
Raise();
|
||||
if (browser())
|
||||
browser()->SetFocus();
|
||||
focus_webview(browser(), m_page_ready);
|
||||
return;
|
||||
}
|
||||
|
||||
Show();
|
||||
Raise();
|
||||
apply_rounded_shape();
|
||||
if (m_page_ready)
|
||||
send_actions();
|
||||
if (browser())
|
||||
browser()->SetFocus();
|
||||
// Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is
|
||||
// what makes typing reach the search field immediately on open.
|
||||
focus_webview(browser(), m_page_ready);
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
|
||||
@@ -95,23 +212,44 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
|
||||
if (command == "request_actions") {
|
||||
m_page_ready = true;
|
||||
send_actions();
|
||||
}
|
||||
else if (command == "toggle_favourite")
|
||||
wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false));
|
||||
else if (command == "reorder_favourites") {
|
||||
} else if (command == "toggle_favourite") {
|
||||
// set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the
|
||||
// pin and show a "favourites are full" hint instead of silently losing the favourite.
|
||||
const std::string fav_id = payload.value("id", "");
|
||||
const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false));
|
||||
if (!ok)
|
||||
call_web_handler({{"command", "favourite_full"}, {"limit", (int) ActionRegistry::kFavLimit}, {"id", fav_id}});
|
||||
} else if (command == "reorder_favourites") {
|
||||
std::vector<std::string> ids;
|
||||
if (payload.contains("ids") && payload["ids"].is_array())
|
||||
for (const auto& id : payload["ids"])
|
||||
if (id.is_string())
|
||||
ids.push_back(id.get<std::string>());
|
||||
wxGetApp().action_registry().reorder_favourites(ids);
|
||||
}
|
||||
} else if (command == "set_tooltip_expanded")
|
||||
wxGetApp().action_registry().set_tooltip_expanded(payload.value("expanded", true));
|
||||
else if (command == "run_action")
|
||||
run_action(payload.value("id", ""), payload.value("title", ""));
|
||||
run_action(payload.value("id", ""), payload.value("title", ""), payload.value("param", ""));
|
||||
else if (command == "open_wiki")
|
||||
open_wiki(payload.value("id", ""));
|
||||
else if (command == "search_tabs")
|
||||
search_tabs();
|
||||
else if (command == "resize")
|
||||
resize_to_content(json_int_or(payload, "height", 0));
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::search_tabs()
|
||||
{
|
||||
// Round-trip is async because the webview delivers script messages synchronously on the
|
||||
// GTK/macOS stack; defer the (cheap) enumeration and push the result back to the page.
|
||||
wxGetApp().CallAfter([this, alive = m_alive]() {
|
||||
if (!alive->load(std::memory_order_acquire))
|
||||
return;
|
||||
auto tabs = wxGetApp().action_registry().tab_options();
|
||||
call_web_handler({{"command", "tab_results"}, {"tabs", std::move(tabs)}});
|
||||
});
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::resize_to_content(int height)
|
||||
{
|
||||
if (height <= 0)
|
||||
@@ -125,17 +263,101 @@ void SpeedDialWebDialog::resize_to_content(int height)
|
||||
const int height_dip = std::max(kPopupMinHeight, std::min(height, max_dip));
|
||||
SetClientSize(FromDIP(wxSize(kPopupWidth, height_dip)));
|
||||
Layout();
|
||||
#ifdef __WXOSX__
|
||||
// WKWebView can lag the dialog's new client size; force the viewport to match so the page is
|
||||
// never painted (and clipped by the rounded layer) below the footer.
|
||||
if (wxWebView* wv = browser()) {
|
||||
const wxSize client = GetClientSize();
|
||||
if (wv->GetSize() != client)
|
||||
wv->SetSize(client);
|
||||
}
|
||||
#endif
|
||||
apply_rounded_shape();
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title)
|
||||
// Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window.
|
||||
// GTK/MSW use a shape region (same mask trick as FilamentPickerDialog, binary edges, no
|
||||
// anti-aliasing); macOS clips the native view layer instead, since SetShape cannot shape there.
|
||||
void SpeedDialWebDialog::apply_rounded_shape()
|
||||
{
|
||||
// wxOSX SetShape resizes the NSWindow (setContentSize 10x10 then back), which synchronously
|
||||
// fires wxEVT_SIZE -> apply_rounded_shape() -> SetShape() and recurses until the stack
|
||||
// overflows. GTK/MSW set a region without resizing, so they are unaffected.
|
||||
if (m_applying_shape)
|
||||
return;
|
||||
|
||||
// BORDER_NONE means the window is all client area, so the client size is the shape size.
|
||||
const wxSize size = GetClientSize();
|
||||
if (size.GetWidth() <= 0 || size.GetHeight() <= 0)
|
||||
return;
|
||||
|
||||
m_applying_shape = true;
|
||||
|
||||
#ifdef __WXOSX__
|
||||
// wxOSX ignores the region (it only clears the window background), so round the native view.
|
||||
set_window_corner_radius(this, FromDIP(m_corner_radius));
|
||||
#else
|
||||
m_shape_bmp.Create(size.GetWidth(), size.GetHeight(), 32);
|
||||
if (m_shape_bmp.IsOk()) {
|
||||
wxMemoryDC dc;
|
||||
dc.SelectObject(m_shape_bmp);
|
||||
dc.SetBackground(wxBrush(wxColour(0, 0, 0)));
|
||||
dc.Clear();
|
||||
dc.SetBrush(wxBrush(wxColour(255, 255, 255, 255)));
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.DrawRoundedRectangle(0, 0, size.GetWidth(), size.GetHeight(), FromDIP(m_corner_radius));
|
||||
dc.SelectObject(wxNullBitmap);
|
||||
|
||||
wxRegion region(m_shape_bmp, wxColour(0, 0, 0));
|
||||
if (region.IsOk())
|
||||
SetShape(region);
|
||||
}
|
||||
#endif
|
||||
|
||||
m_applying_shape = false;
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::on_dpi_changed(const wxRect&)
|
||||
{
|
||||
apply_rounded_shape();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title, const std::string& param)
|
||||
{
|
||||
ActionRegistry& reg = wxGetApp().action_registry();
|
||||
const AppAction* a = reg.by_id(id);
|
||||
const AppAction* a = reg.by_id(id);
|
||||
if (!a)
|
||||
return;
|
||||
|
||||
const bool ask = reg.should_ask(id);
|
||||
// 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().enable_developer_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
|
||||
@@ -143,8 +365,7 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
||||
|
||||
if (ask) {
|
||||
const wxString label = title.empty() ? from_u8(atitle) : from_u8(title);
|
||||
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label),
|
||||
_L("Run plugin"), wxOK | wxCANCEL);
|
||||
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label), _L("Run plugin"), wxOK | wxCANCEL);
|
||||
dlg.ShowCheckBox(_L("Don't ask again for this action"));
|
||||
if (dlg.ShowModal() != wxID_OK)
|
||||
return;
|
||||
@@ -152,27 +373,42 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
|
||||
wxGetApp().action_registry().suppress_ask(id);
|
||||
}
|
||||
|
||||
wxGetApp().CallAfter([id] {
|
||||
wxGetApp().CallAfter([id, param] {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
AppActionRunResult result = wxGetApp().action_registry().run(id);
|
||||
AppActionRunResult result = wxGetApp().action_registry().run(id, param);
|
||||
if (result.level == AppActionRunResult::Level::Busy)
|
||||
return;
|
||||
if (!result.message.IsEmpty() && wxGetApp().plater())
|
||||
wxGetApp().plater()->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
result.level == AppActionRunResult::Level::Error ? NotificationManager::NotificationLevel::ErrorNotificationLevel :
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
into_u8(result.message));
|
||||
wxGetApp()
|
||||
.plater()
|
||||
->get_notification_manager()
|
||||
->push_notification(NotificationType::CustomNotification,
|
||||
result.level == AppActionRunResult::Level::Error ?
|
||||
NotificationManager::NotificationLevel::ErrorNotificationLevel :
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
into_u8(result.message));
|
||||
});
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::open_wiki(const std::string& id)
|
||||
{
|
||||
const AppAction* a = wxGetApp().action_registry().by_id(id);
|
||||
if (!a || a->help_url.empty())
|
||||
return;
|
||||
Hide();
|
||||
wxLaunchDefaultBrowser(from_u8(a->help_url));
|
||||
}
|
||||
|
||||
void SpeedDialWebDialog::send_actions()
|
||||
{
|
||||
nlohmann::json snap = wxGetApp().action_registry().snapshot();
|
||||
call_web_handler({{"command", "list_actions"},
|
||||
{"actions", std::move(snap["actions"])},
|
||||
{"favourites", std::move(snap["favourites"])}});
|
||||
{"favourites", std::move(snap["favourites"])},
|
||||
{"recent", std::move(snap["recent"])},
|
||||
{"user_mode", std::move(snap["user_mode"])},
|
||||
{"tooltip_expanded", std::move(snap["tooltip_expanded"])}});
|
||||
}
|
||||
|
||||
}}
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class SpeedDialWebDialog : public WebViewHostDialog
|
||||
@@ -17,13 +19,23 @@ public:
|
||||
void request_show();
|
||||
|
||||
private:
|
||||
void add_user_scripts() override;
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
void resize_to_content(int height);
|
||||
void run_action(const std::string& id, const std::string& title);
|
||||
void run_action(const std::string& id, const std::string& title, const std::string& param = "");
|
||||
void open_wiki(const std::string& id);
|
||||
void send_actions();
|
||||
void search_tabs();
|
||||
void apply_rounded_shape();
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
bool m_page_ready{false};
|
||||
// Rounded corners (shape region on GTK/MSW, native layer on macOS), since the webview is opaque.
|
||||
int m_corner_radius{7};
|
||||
wxBitmap m_shape_bmp;
|
||||
// wxOSX SetShape resizes the window, which re-enters apply_rounded_shape() through wxEVT_SIZE.
|
||||
bool m_applying_shape{false};
|
||||
// Guards the CallAfter in on_script_message across dialog destruction, same as
|
||||
// PluginsDialog::m_alive (PluginsDialog.hpp:249).
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
|
||||
+42
-12
@@ -1736,16 +1736,46 @@ void Tab::toggle_option(const std::string& opt_key, bool toggle, int opt_index/*
|
||||
|
||||
void Tab::toggle_line(const std::string &opt_key, bool toggle, int opt_index)
|
||||
{
|
||||
if (!m_active_page) return;
|
||||
Line *line = m_active_page->get_line(opt_key, opt_index);
|
||||
if (line) line->toggle_visible = toggle;
|
||||
// Apply to every page that owns the option, not just m_active_page. ConfigManipulation runs while
|
||||
// each tab updates at preset load, so the Speed Dial sees the same visibility regardless of page.
|
||||
for (const PageShp& page : m_pages) {
|
||||
if (!page) continue;
|
||||
if (Line *line = page->get_line(opt_key, opt_index))
|
||||
line->toggle_visible = toggle;
|
||||
}
|
||||
};
|
||||
|
||||
void Tab::set_option_label(const std::string &opt_key, const wxString &label, int opt_index)
|
||||
{
|
||||
if (!m_active_page) return;
|
||||
Line *line = m_active_page->get_line(opt_key, opt_index);
|
||||
if (line) line->set_label(label);
|
||||
// Same as toggle_line: a runtime rename (brim_width -> "Brim ear radius") must reach every page
|
||||
// so the Speed Dial titles the setting before the page has been shown.
|
||||
for (const PageShp& page : m_pages) {
|
||||
if (!page) continue;
|
||||
if (Line *line = page->get_line(opt_key, opt_index))
|
||||
line->set_label(label);
|
||||
}
|
||||
}
|
||||
|
||||
Tab::SettingRowState Tab::setting_row_state(const std::string &opt_id) const
|
||||
{
|
||||
bool found = false;
|
||||
for (const PageShp& page : m_pages) {
|
||||
if (!page) continue;
|
||||
for (const ConfigOptionsGroupShp& group : page->m_optgroups) {
|
||||
if (!group) continue;
|
||||
for (const Line& line : group->get_lines()) {
|
||||
for (const Option& opt : line.get_options()) {
|
||||
if (opt.opt_id != opt_id)
|
||||
continue;
|
||||
if (line.toggle_visible) // shown on any owning page is enough
|
||||
return {true, line.label, line.get_options().size() > 1};
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Never registered on a page -> visible, but with no row label to contribute.
|
||||
return {!found, wxString(), false};
|
||||
}
|
||||
|
||||
// To be called by custom widgets, load a value into a config,
|
||||
@@ -5782,11 +5812,11 @@ if (is_marlin_flavor)
|
||||
} else if (m_extruders_count_old == 1) {
|
||||
first_extruder_title = wxString::Format("Extruder %d", 1);
|
||||
}
|
||||
auto & searcher = wxGetApp().sidebar().get_searcher();
|
||||
auto & index = wxGetApp().sidebar().settings_index();
|
||||
for (auto &group : m_pages[n_before_extruders]->m_optgroups) {
|
||||
group->set_config_category_and_type(first_extruder_title, m_type);
|
||||
for (auto &opt : group->opt_map())
|
||||
searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title);
|
||||
index.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon);
|
||||
}
|
||||
|
||||
Thaw();
|
||||
@@ -7870,10 +7900,10 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent)
|
||||
}));
|
||||
|
||||
{
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
const Search::GroupAndCategory& gc = searcher.get_group_and_category("printable_area");
|
||||
searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category);
|
||||
searcher.add_key("bed_custom_model", m_type, gc.group, gc.category);
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
const Search::GroupAndCategory& gc = index.get_group_and_category("printable_area");
|
||||
index.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon);
|
||||
index.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon);
|
||||
}
|
||||
|
||||
return sizer;
|
||||
|
||||
@@ -401,6 +401,16 @@ public:
|
||||
void toggle_option(const std::string &opt_key, bool toggle, int opt_index = -1);
|
||||
void toggle_line(const std::string &opt_key, bool toggle, int opt_index = -1); // BBS: hide some line
|
||||
void set_option_label(const std::string &opt_key, const wxString &label, int opt_index = -1);
|
||||
|
||||
// Live state of the settings row that owns an option, read from the built pages.
|
||||
struct SettingRowState
|
||||
{
|
||||
bool visible{true}; // false when ConfigManipulation hides the row
|
||||
wxString label; // Line::label the row draws (may change at runtime)
|
||||
bool multi{false}; // row packs several options, so label is precomposed
|
||||
};
|
||||
SettingRowState setting_row_state(const std::string &opt_id) const;
|
||||
|
||||
wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText, wxString text = wxEmptyString);
|
||||
bool current_preset_is_dirty() const;
|
||||
bool saved_preset_is_dirty() const;
|
||||
|
||||
@@ -1485,12 +1485,12 @@ std::string UnsavedChangesDialog::subreplace(std::string resource_str, std::stri
|
||||
|
||||
void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config, int from, int to)
|
||||
{
|
||||
Search::OptionsSearcher &searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.sort_options_by_key();
|
||||
Search::SettingsIndex &index = wxGetApp().sidebar().settings_index();
|
||||
index.sort_options_by_key();
|
||||
|
||||
for (const std::string &opt_key : config->keys()) {
|
||||
int variant_index = -2;
|
||||
Search::Option option = searcher.get_option(opt_key, type, variant_index);
|
||||
Search::Option option = index.get_option(opt_key, type, variant_index);
|
||||
if (variant_index == -2) {
|
||||
// Orca: Every transferred setting must remain visible even when it is absent from the search index.
|
||||
const ConfigOptionDef* def = print_config_def.get(opt_key);
|
||||
@@ -1510,8 +1510,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config
|
||||
|
||||
void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* presets_)
|
||||
{
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.sort_options_by_key();
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
index.sort_options_by_key();
|
||||
|
||||
// list of the presets with unsaved changes
|
||||
std::vector<PresetCollection*> presets_list;
|
||||
@@ -1569,11 +1569,11 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
|
||||
for (const std::string& opt_key : dirty_options) {
|
||||
int variant_index = -2;
|
||||
const Search::Option &option = searcher.get_option(opt_key, type, variant_index);
|
||||
const Search::Option &option = index.get_option(opt_key, type, variant_index);
|
||||
if (variant_index == -2) {
|
||||
// When founded option isn't the correct one.
|
||||
// It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id",
|
||||
// because of they don't exist in searcher
|
||||
// because of they don't exist in the index
|
||||
continue;
|
||||
}
|
||||
wxString category = option.category_local;
|
||||
@@ -1612,8 +1612,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
}
|
||||
}
|
||||
|
||||
// Revert sort of searcher back
|
||||
searcher.sort_options_by_label();
|
||||
// Revert sort of index back
|
||||
index.sort_options_by_label();
|
||||
}
|
||||
|
||||
void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
@@ -2065,8 +2065,8 @@ void DiffPresetDialog::update_bottom_info(wxString bottom_info)
|
||||
|
||||
void DiffPresetDialog::update_tree()
|
||||
{
|
||||
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
|
||||
searcher.sort_options_by_key();
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
index.sort_options_by_key();
|
||||
|
||||
m_tree->Clear();
|
||||
wxString bottom_info = "";
|
||||
@@ -2146,14 +2146,14 @@ void DiffPresetDialog::update_tree()
|
||||
wxString right_val = get_string_value(opt_key, right_congig);
|
||||
|
||||
const std::string lookup_key = get_pure_opt_key(opt_key);
|
||||
Search::Option option = searcher.get_option(lookup_key, get_full_label(lookup_key, left_config), type);
|
||||
Search::Option option = index.get_option(lookup_key, get_full_label(lookup_key, left_config), type);
|
||||
if (get_pure_opt_key(option.opt_key()) != lookup_key)
|
||||
option = searcher.get_option(opt_key, get_full_label(opt_key, left_config), type);
|
||||
option = index.get_option(opt_key, get_full_label(opt_key, left_config), type);
|
||||
if (get_pure_opt_key(option.opt_key()) != lookup_key) {
|
||||
// When the found option is not the requested one.
|
||||
// This can happen for dirty_options such as:
|
||||
// "default_print_profile", "printer_model", "printer_settings_id",
|
||||
// because they do not exist in the searcher.
|
||||
// because they do not exist in the index.
|
||||
continue;
|
||||
}
|
||||
m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
|
||||
@@ -2177,8 +2177,8 @@ void DiffPresetDialog::update_tree()
|
||||
Refresh();
|
||||
}
|
||||
|
||||
// Revert sort of searcher back
|
||||
searcher.sort_options_by_label();
|
||||
// Revert sort of index back
|
||||
index.sort_options_by_label();
|
||||
}
|
||||
|
||||
void DiffPresetDialog::on_dpi_changed(const wxRect&)
|
||||
|
||||
+109
-121
@@ -25,36 +25,29 @@ END_EVENT_TABLE()
|
||||
* calling Refresh()/Update().
|
||||
*/
|
||||
|
||||
Button::Button()
|
||||
: paddingSize(10, 8)
|
||||
Button::Button() : paddingSize(10, 8)
|
||||
{
|
||||
background_color = StateColor(
|
||||
std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
|
||||
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
|
||||
std::make_pair(0x009688, (int) StateColor::Checked),
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
|
||||
std::make_pair(*wxWHITE, (int) StateColor::Normal));
|
||||
text_color = StateColor(
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled),
|
||||
std::make_pair(*wxBLACK, (int) StateColor::Normal));
|
||||
background_color = StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
|
||||
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
|
||||
std::make_pair(0x009688, (int) StateColor::Checked),
|
||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
|
||||
std::make_pair(*wxWHITE, (int) StateColor::Normal));
|
||||
text_color = StateColor(std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled), std::make_pair(*wxBLACK, (int) StateColor::Normal));
|
||||
}
|
||||
|
||||
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
||||
: Button()
|
||||
{
|
||||
Create(parent, text, icon, style, iconSize, btn_id);
|
||||
}
|
||||
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) : Button()
|
||||
{ Create(parent, text, icon, style, iconSize, btn_id); }
|
||||
|
||||
bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
|
||||
{
|
||||
StaticBox::Create(parent, btn_id, wxDefaultPosition, wxDefaultSize, style);
|
||||
state_handler.attach(std::vector<StateColor const*>{&text_color});
|
||||
state_handler.update_binds();
|
||||
//BBS set default font
|
||||
// BBS set default font
|
||||
SetFont(Label::Body_14);
|
||||
wxWindow::SetLabel(text);
|
||||
if (!icon.IsEmpty()) {
|
||||
//BBS set button icon default size to 20
|
||||
// BBS set button icon default size to 20
|
||||
this->active_icon = ScalableBitmap(this, icon.ToStdString(), iconSize > 0 ? iconSize : 20);
|
||||
}
|
||||
messureSize();
|
||||
@@ -82,14 +75,12 @@ void Button::SetIcon(const wxString& icon)
|
||||
{
|
||||
auto tmpBitmap = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt());
|
||||
if (!icon.IsEmpty()) {
|
||||
//BBS set button icon default size to 20
|
||||
// BBS set button icon default size to 20
|
||||
if (!tmpBitmap.bmp().IsSameAs(this->active_icon.bmp())) {
|
||||
this->active_icon = tmpBitmap;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
this->active_icon = ScalableBitmap();
|
||||
Refresh();
|
||||
}
|
||||
@@ -97,7 +88,7 @@ void Button::SetIcon(const wxString& icon)
|
||||
|
||||
void Button::SetIcon(const wxBitmap& icon)
|
||||
{
|
||||
this->active_icon = ScalableBitmap();
|
||||
this->active_icon = ScalableBitmap();
|
||||
this->active_icon.bmp() = icon;
|
||||
messureSize();
|
||||
Refresh();
|
||||
@@ -121,6 +112,13 @@ void Button::SetPaddingSize(const wxSize& size)
|
||||
messureSize();
|
||||
}
|
||||
|
||||
void Button::SetIconSpacing(int spacing)
|
||||
{
|
||||
m_icon_spacing = spacing;
|
||||
messureSize();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Button::SetTextColor(StateColor const& color)
|
||||
{
|
||||
text_color = color;
|
||||
@@ -128,7 +126,7 @@ void Button::SetTextColor(StateColor const& color)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Button::SetTextColorNormal(wxColor const &color)
|
||||
void Button::SetTextColorNormal(wxColor const& color)
|
||||
{
|
||||
text_color.setColorForStates(color, 0);
|
||||
Refresh();
|
||||
@@ -145,22 +143,22 @@ bool Button::Enable(bool enable)
|
||||
return result;
|
||||
}
|
||||
|
||||
void Button::SetCanFocus(bool canFocus) {
|
||||
void Button::SetCanFocus(bool canFocus)
|
||||
{
|
||||
StaticBox::SetCanFocus(canFocus);
|
||||
this->canFocus = canFocus;
|
||||
}
|
||||
|
||||
void Button::SetValue(bool state)
|
||||
{
|
||||
if (GetValue() == state) return;
|
||||
if (GetValue() == state)
|
||||
return;
|
||||
state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked);
|
||||
}
|
||||
|
||||
bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; }
|
||||
|
||||
void Button::SetCenter(bool isCenter)
|
||||
{
|
||||
this->isCenter = isCenter; }
|
||||
void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; }
|
||||
|
||||
void Button::SetIndicator(bool on)
|
||||
{
|
||||
@@ -186,38 +184,33 @@ wxString btn_disabled[10] = {"#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFD
|
||||
|
||||
void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
||||
{
|
||||
if (type == ButtonType::Compact) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(8,3)));
|
||||
if (type == ButtonType::Compact) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(8, 3)));
|
||||
this->SetCornerRadius(this->FromDIP(8));
|
||||
this->SetFont(Label::Body_10);
|
||||
}
|
||||
else if (type == ButtonType::Window) {
|
||||
this->SetSize(FromDIP(wxSize(58,24)));
|
||||
this->SetMinSize(FromDIP(wxSize(58,24)));
|
||||
} else if (type == ButtonType::Window) {
|
||||
this->SetSize(FromDIP(wxSize(58, 24)));
|
||||
this->SetMinSize(FromDIP(wxSize(58, 24)));
|
||||
this->SetCornerRadius(this->FromDIP(12));
|
||||
this->SetFont(Label::Body_12);
|
||||
}
|
||||
else if (type == ButtonType::Choice) {
|
||||
this->SetMinSize(FromDIP(wxSize(100,32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12,8)));
|
||||
} else if (type == ButtonType::Choice) {
|
||||
this->SetMinSize(FromDIP(wxSize(100, 32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
this->SetFont(Label::Body_14);
|
||||
}
|
||||
else if (type == ButtonType::Parameter) {
|
||||
this->SetMinSize(FromDIP(wxSize(120,26)));
|
||||
this->SetSize(FromDIP(wxSize(120,26)));
|
||||
} else if (type == ButtonType::Parameter) {
|
||||
this->SetMinSize(FromDIP(wxSize(120, 26)));
|
||||
this->SetSize(FromDIP(wxSize(120, 26)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
this->SetFont(Label::Body_14);
|
||||
}
|
||||
else if (type == ButtonType::Icon) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(5,5)));
|
||||
this->SetMinSize(FromDIP(wxSize(26,26)));
|
||||
this->SetSize(FromDIP(wxSize(26,26)));
|
||||
} else if (type == ButtonType::Icon) {
|
||||
this->SetPaddingSize(FromDIP(wxSize(5, 5)));
|
||||
this->SetMinSize(FromDIP(wxSize(26, 26)));
|
||||
this->SetSize(FromDIP(wxSize(26, 26)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
}
|
||||
else if (type == ButtonType::Expanded) {
|
||||
this->SetMinSize(FromDIP(wxSize(-1,32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12,8)));
|
||||
} else if (type == ButtonType::Expanded) {
|
||||
this->SetMinSize(FromDIP(wxSize(-1, 32)));
|
||||
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
|
||||
this->SetCornerRadius(this->FromDIP(4));
|
||||
this->SetFont(Label::Body_14);
|
||||
}
|
||||
@@ -226,39 +219,33 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
|
||||
|
||||
bool is_dark = StateColor::darkModeColorFor("#FFFFFF") != wxColour("#FFFFFF");
|
||||
|
||||
auto clr_arr = style == ButtonStyle::Regular ? btn_regular :
|
||||
style == ButtonStyle::Confirm ? btn_confirm :
|
||||
style == ButtonStyle::Alert ? btn_alert :
|
||||
auto clr_arr = style == ButtonStyle::Regular ? btn_regular :
|
||||
style == ButtonStyle::Confirm ? btn_confirm :
|
||||
style == ButtonStyle::Alert ? btn_alert :
|
||||
style == ButtonStyle::Disabled ? btn_disabled :
|
||||
btn_regular ;
|
||||
btn_regular;
|
||||
|
||||
auto bg_color = StateColor(
|
||||
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[1]), (int)StateColor::Pressed),
|
||||
std::pair(wxColour(clr_arr[2]), (int)StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal),
|
||||
std::pair(wxColour(clr_arr[4]), (int)StateColor::Enabled)
|
||||
);
|
||||
auto bg_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[1]), (int) StateColor::Pressed),
|
||||
std::pair(wxColour(clr_arr[2]), (int) StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal),
|
||||
std::pair(wxColour(clr_arr[4]), (int) StateColor::Enabled));
|
||||
bg_color.setTakeFocusedAsHovered(false);
|
||||
this->SetBackgroundColor(bg_color);
|
||||
wxColour focus_clr = clr_arr[is_dark ? 8 : 9];
|
||||
auto border_color = StateColor(
|
||||
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[2]), (int)(StateColor::Hovered | ~StateColor::Focused)),
|
||||
std::pair(wxColour(focus_clr ), (int)StateColor::Focused),
|
||||
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal)
|
||||
);
|
||||
auto border_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[2]), (int) (StateColor::Hovered | ~StateColor::Focused)),
|
||||
std::pair(wxColour(focus_clr), (int) StateColor::Focused),
|
||||
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal));
|
||||
border_color.setTakeFocusedAsHovered(false);
|
||||
this->SetBorderColor(border_color);
|
||||
this->SetTextColor(StateColor(
|
||||
std::pair(wxColour(clr_arr[5]), (int)StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[7]), (int)StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[6]), (int)StateColor::Normal)
|
||||
));
|
||||
this->SetTextColor(StateColor(std::pair(wxColour(clr_arr[5]), (int) StateColor::Disabled),
|
||||
std::pair(wxColour(clr_arr[7]), (int) StateColor::Hovered),
|
||||
std::pair(wxColour(clr_arr[6]), (int) StateColor::Normal)));
|
||||
|
||||
m_has_style = true;
|
||||
m_style = style;
|
||||
m_type = type;
|
||||
m_style = style;
|
||||
m_type = type;
|
||||
}
|
||||
|
||||
void Button::Rescale()
|
||||
@@ -269,7 +256,7 @@ void Button::Rescale()
|
||||
|
||||
messureSize();
|
||||
|
||||
if(m_has_style)
|
||||
if (m_has_style)
|
||||
SetStyle(m_style, m_type);
|
||||
|
||||
Refresh();
|
||||
@@ -290,7 +277,7 @@ void Button::paintEvent(wxPaintEvent& evt)
|
||||
void Button::render(wxDC& dc)
|
||||
{
|
||||
StaticBox::render(dc);
|
||||
int states = state_handler.states();
|
||||
int states = state_handler.states();
|
||||
wxSize size = GetSize();
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
// calc content size
|
||||
@@ -298,15 +285,15 @@ void Button::render(wxDC& dc)
|
||||
wxSize textSize = this->textSize.GetSize();
|
||||
|
||||
const ScalableBitmap& icon = active_icon;
|
||||
wxSize padding = this->paddingSize;
|
||||
int spacing = 5;
|
||||
wxSize padding = this->paddingSize;
|
||||
int spacing = m_icon_spacing;
|
||||
// Wrap text
|
||||
auto text = GetLabel();
|
||||
if (vertical && textSize.x + padding.x * 2 > size.x) {
|
||||
Label::split_lines(dc, size.x - padding.x * 2, text, text, 2);
|
||||
textSize = dc.GetMultiLineTextExtent(text);
|
||||
if (padding.x * 2 + textSize.x > size.x) {
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2);
|
||||
textSize = dc.GetMultiLineTextExtent(text);
|
||||
}
|
||||
}
|
||||
@@ -316,7 +303,7 @@ void Button::render(wxDC& dc)
|
||||
const bool gap_reserved = szContent.y > 0;
|
||||
if (icon.bmp().IsOk()) {
|
||||
if (gap_reserved) {
|
||||
//BBS norrow size between text and icon
|
||||
// BBS norrow size between text and icon
|
||||
if (vertical)
|
||||
szContent.y += spacing;
|
||||
else
|
||||
@@ -325,10 +312,12 @@ void Button::render(wxDC& dc)
|
||||
szIcon = icon.GetBmpSize();
|
||||
if (vertical) {
|
||||
szContent.y += szIcon.y;
|
||||
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
|
||||
if (szIcon.x > szContent.x)
|
||||
szContent.x = szIcon.x;
|
||||
} else {
|
||||
szContent.x += szIcon.x;
|
||||
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
|
||||
if (szIcon.y > szContent.y)
|
||||
szContent.y = szIcon.y;
|
||||
}
|
||||
if (szContent.x > size.x) {
|
||||
int d = std::min(padding.x, (szContent.x - size.x) / 2);
|
||||
@@ -344,10 +333,11 @@ void Button::render(wxDC& dc)
|
||||
szContent.x += dot + FromDIP(6);
|
||||
}
|
||||
// move to center
|
||||
wxRect rcContent = { {0, 0}, size };
|
||||
wxRect rcContent = {{0, 0}, size};
|
||||
if (isCenter) {
|
||||
wxSize offset = (size - szContent) / 2;
|
||||
if (offset.x < 0) offset.x = 0;
|
||||
if (offset.x < 0)
|
||||
offset.x = 0;
|
||||
rcContent.Deflate(offset.x, offset.y);
|
||||
}
|
||||
// start draw
|
||||
@@ -358,7 +348,7 @@ void Button::render(wxDC& dc)
|
||||
else
|
||||
pt.y += (rcContent.height - szIcon.y) / 2;
|
||||
dc.DrawBitmap(icon.bmp(), pt);
|
||||
//BBS norrow size between text and icon
|
||||
// BBS norrow size between text and icon
|
||||
if (vertical) {
|
||||
pt.y += szIcon.y + (gap_reserved ? spacing : 0);
|
||||
pt.x = rcContent.x;
|
||||
@@ -403,19 +393,21 @@ void Button::messureSize()
|
||||
wxSize szContent = textSize.GetSize();
|
||||
if (this->active_icon.bmp().IsOk()) {
|
||||
if (szContent.y > 0) {
|
||||
//BBS norrow size between text and icon
|
||||
// BBS narrow size between text and icon
|
||||
if (vertical)
|
||||
szContent.y += 5;
|
||||
szContent.y += m_icon_spacing;
|
||||
else
|
||||
szContent.x += 5;
|
||||
szContent.x += m_icon_spacing;
|
||||
}
|
||||
wxSize szIcon = this->active_icon.GetBmpSize();
|
||||
if (vertical) {
|
||||
szContent.y += szIcon.y;
|
||||
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
|
||||
if (szIcon.x > szContent.x)
|
||||
szContent.x = szIcon.x;
|
||||
} else {
|
||||
szContent.x += szIcon.x;
|
||||
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
|
||||
if (szIcon.y > szContent.y)
|
||||
szContent.y = szIcon.y;
|
||||
}
|
||||
}
|
||||
if (m_show_indicator) {
|
||||
@@ -467,13 +459,13 @@ void Button::mouseReleased(wxMouseEvent& event)
|
||||
}
|
||||
}
|
||||
|
||||
void Button::mouseCaptureLost(wxMouseCaptureLostEvent &event)
|
||||
void Button::mouseCaptureLost(wxMouseCaptureLostEvent& event)
|
||||
{
|
||||
wxMouseEvent evt;
|
||||
mouseReleased(evt);
|
||||
}
|
||||
|
||||
void Button::keyDownUp(wxKeyEvent &event)
|
||||
void Button::keyDownUp(wxKeyEvent& event)
|
||||
{
|
||||
if (event.GetKeyCode() == WXK_SPACE || event.GetKeyCode() == WXK_RETURN) {
|
||||
wxMouseEvent evt(event.GetEventType() == wxEVT_KEY_UP ? wxEVT_LEFT_UP : wxEVT_LEFT_DOWN);
|
||||
@@ -482,8 +474,8 @@ void Button::keyDownUp(wxKeyEvent &event)
|
||||
return;
|
||||
}
|
||||
if (event.GetEventType() == wxEVT_KEY_DOWN &&
|
||||
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT
|
||||
|| event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
|
||||
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT ||
|
||||
event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
|
||||
HandleAsNavigationKey(event);
|
||||
else
|
||||
event.Skip();
|
||||
@@ -500,7 +492,9 @@ void Button::sendButtonEvent()
|
||||
|
||||
WXLRESULT Button::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||
{
|
||||
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTMESSAGE; }
|
||||
if (nMsg == WM_GETDLGCODE) {
|
||||
return DLGC_WANTMESSAGE;
|
||||
}
|
||||
if (nMsg == WM_KEYDOWN) {
|
||||
wxKeyEvent event(CreateKeyEvent(wxEVT_KEY_DOWN, wParam, lParam));
|
||||
switch (wParam) {
|
||||
@@ -521,8 +515,7 @@ void Button::EnableTooltipEvenDisabled()
|
||||
{
|
||||
#if defined(_MSC_VER) || defined(_WIN32)
|
||||
auto parent = this->GetParent();
|
||||
if (parent)
|
||||
{
|
||||
if (parent) {
|
||||
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
|
||||
parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this);
|
||||
};
|
||||
@@ -532,22 +525,21 @@ void Button::EnableTooltipEvenDisabled()
|
||||
void Button::OnParentMotion(wxMouseEvent& event)
|
||||
{
|
||||
auto parent = this->GetParent();
|
||||
if (!parent) return event.Skip();
|
||||
if (!parent)
|
||||
return event.Skip();
|
||||
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
wxRect screen_rect = this->GetScreenRect();
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos))
|
||||
{
|
||||
if (!tipWindow)
|
||||
{
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos)) {
|
||||
if (!tipWindow) {
|
||||
tipWindow = wxTipWindow::New(this, tip);
|
||||
if (!tipWindow) return event.Skip();
|
||||
if (!tipWindow)
|
||||
return event.Skip();
|
||||
tipWindow->Enable(false);
|
||||
}
|
||||
|
||||
if (tipWindow->GetLabel() != tip)
|
||||
{
|
||||
if (tipWindow->GetLabel() != tip) {
|
||||
tipWindow->SetLabel(tip);
|
||||
}
|
||||
|
||||
@@ -555,11 +547,8 @@ void Button::OnParentMotion(wxMouseEvent& event)
|
||||
// using wxGetMousePosition() which returns (0,0) on Wayland.
|
||||
tipWindow->Position(this->ClientToScreen(wxPoint(0, 0)), this->GetSize());
|
||||
tipWindow->Popup();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tipWindow)
|
||||
{
|
||||
} else {
|
||||
if (tipWindow) {
|
||||
tipWindow->Dismiss();
|
||||
tipWindow->Destroy();
|
||||
tipWindow = nullptr;
|
||||
@@ -572,15 +561,14 @@ void Button::OnParentMotion(wxMouseEvent& event)
|
||||
void Button::OnParentLeave(wxMouseEvent& event)
|
||||
{
|
||||
auto parent = this->GetParent();
|
||||
if (!parent) return event.Skip();
|
||||
if (!parent)
|
||||
return event.Skip();
|
||||
|
||||
if (tipWindow)
|
||||
{
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
if (tipWindow) {
|
||||
wxPoint pos = parent->ClientToScreen(event.GetPosition());
|
||||
wxRect screen_rect = this->GetScreenRect();
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!screen_rect.Contains(pos))
|
||||
{
|
||||
wxString tip = this->GetToolTipText();
|
||||
if (!screen_rect.Contains(pos)) {
|
||||
tipWindow->Dismiss();
|
||||
tipWindow->Destroy();
|
||||
tipWindow = nullptr;
|
||||
|
||||
@@ -36,6 +36,7 @@ class Button : public StaticBox
|
||||
wxRect textSize;
|
||||
wxSize minSize; // set by outer
|
||||
wxSize paddingSize;
|
||||
int m_icon_spacing = 5;
|
||||
ScalableBitmap active_icon;
|
||||
|
||||
StateColor text_color;
|
||||
@@ -70,6 +71,8 @@ public:
|
||||
|
||||
void SetPaddingSize(const wxSize& size);
|
||||
|
||||
void SetIconSpacing(int spacing);
|
||||
|
||||
void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/);
|
||||
|
||||
void SetTextColor(StateColor const& color);
|
||||
|
||||
Reference in New Issue
Block a user