Merge branch 'main' into feature/bambu_printer_housekeeping

# Conflicts:
#	tests/slic3rutils/CMakeLists.txt
This commit is contained in:
SoftFever
2026-07-18 02:08:13 +08:00
291 changed files with 59581 additions and 2738 deletions
+23 -1
View File
@@ -495,7 +495,29 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
simple_render(shader, model_objects, colors);
return;
}
// 0th. render pass, render the model using stencil buffer
glsafe(::glEnable(GL_STENCIL_TEST));
glsafe(::glStencilMask(0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE));
glsafe(::glClearStencil(0));
glsafe(::glClear(GL_STENCIL_BUFFER_BIT));
glsafe(::glStencilFunc(GL_ALWAYS, 0xFF, 0xFF));
if (tverts_range == std::make_pair<size_t, size_t>(0, -1))
model.render(shader);
else
model.render(this->tverts_range, shader);
glsafe(::glStencilFunc(GL_NOTEQUAL, 0xFF, 0xFF));
glsafe(::glStencilMask(0x00));
shader->set_uniform("is_outline", true);
shader->set_uniform("screen_size", Vec2f{cnv_size.get_width(), cnv_size.get_height()});
if (tverts_range == std::make_pair<size_t, size_t>(0, -1))
model.render(shader);
else
model.render(this->tverts_range, shader);
shader->set_uniform("is_outline", false);
glsafe(::glStencilMask(0xFF));
glsafe(::glDisable(GL_STENCIL_TEST));
// render the outline using depth buffer and discard the pixels that are not on the outline
// 1st. render pass, render the model into a separate render target that has only depth buffer
GLuint depth_fbo = 0;
GLuint depth_tex = 0;
+390
View File
@@ -0,0 +1,390 @@
#include "ActionRegistry.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/AppConfig.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/thread.h>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <unordered_map>
namespace Slic3r { namespace GUI {
namespace {
constexpr const char* kConfigSection = "speed_dial";
nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallback)
{
nlohmann::json parsed = nlohmann::json::parse(value, nullptr, false);
return parsed.is_discarded() ? std::move(fallback) : parsed;
}
nlohmann::json read_section(const char* key, nlohmann::json 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());
}
std::vector<std::string> read_string_array(const char* key)
{
auto j = read_section(key, nlohmann::json::array());
std::vector<std::string> v;
for (auto& e : j)
if (e.is_string())
v.push_back(e.get<std::string>());
return v;
}
// frecency = frequency + recency; score halves every 30 idle days.
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);
return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
}
// ---- script-plugin action source (the one and only source) ------------------
std::string find_loaded_source_name(PluginManager& manager, const std::string& plugin_key)
{
PluginDescriptor descriptor;
if (manager.try_get_plugin_descriptor(plugin_key, descriptor) && !descriptor.name.empty())
return descriptor.name;
return plugin_key;
}
// A runnable script capability exposed as a speed-dial action. source_key = plugin_key
// (identity), so a plugin display-name change does not re-key the action.
struct PluginScriptAction : AppAction
{
static constexpr const char* kIdPrefix = "plugin_script_action";
std::string plugin_key;
std::string capability;
// 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);
}
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))
{}
AppActionRunResult run() 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 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)};
}
};
// 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)
{
PluginManager& manager = PluginManager::instance();
if (!manager.is_plugin_loaded(plugin_key))
return nullptr;
// only_enabled defaults true, so a disabled capability resolves to nullptr here.
if (!manager.get_plugin_capability({PluginCapabilityType::Script, capability, plugin_key}))
return nullptr;
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
}
} // namespace
void ActionRegistry::init()
{
assert(wxThread::IsMain());
assert(!m_started);
m_started = true;
PluginManager& manager = PluginManager::instance();
auto on_source = [this](const std::string& plugin_key, ActionChange change) {
if (!wxTheApp || wxGetApp().is_closing())
return;
wxGetApp().CallAfter([this, plugin_key, change] {
if (!wxGetApp().is_closing())
this->refresh_source(plugin_key, change);
});
};
auto on_capability = [this](const PluginCapabilityId& capability, ActionChange change) {
if (capability.type != PluginCapabilityType::Script || !wxTheApp || wxGetApp().is_closing())
return;
const std::string plugin_key = capability.plugin_key;
const std::string name = capability.name;
wxGetApp().CallAfter([this, plugin_key, name, change] {
if (!wxGetApp().is_closing())
this->refresh_capability(plugin_key, name, change);
});
};
// 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_capability_load_callback(
[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);
});
// enumerate current script capabilities
std::unordered_map<std::string, std::string> source_names;
for (const PluginDescriptor& desc : manager.get_plugin_descriptors())
if (!desc.name.empty())
source_names.emplace(desc.plugin_key, desc.name);
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& source_name = it == source_names.end() ? key : it->second;
if (auto action = make_action(key, capability->name(), source_name))
upsert(std::move(action));
}
}
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
{
assert(wxThread::IsMain());
// Remove this source's current actions. Collect first - erasing from m_actions while
// iterating invalidates the iterator. why: m_actions (not the loader) is the source of
// truth, so this is correct even after the plugin has already unloaded.
std::vector<std::string> stale;
for (const auto& [id, action] : m_actions)
if (action->source_key() == plugin_key)
stale.push_back(id);
for (const std::string& id : stale)
remove(id);
if (change == ActionChange::Removed)
return;
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)
continue;
if (auto action = make_action(plugin_key, capability->name(), source_name))
upsert(std::move(action));
}
}
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
ActionChange change)
{
assert(wxThread::IsMain());
const std::string id = PluginScriptAction::id_for(plugin_key, capability);
if (change == ActionChange::Removed) {
remove(id);
return;
}
PluginManager& manager = PluginManager::instance();
if (auto action = make_action(plugin_key, capability, find_loaded_source_name(manager, plugin_key)))
upsert(std::move(action));
else
remove(id);
}
void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
{
assert(wxThread::IsMain());
if (!action)
return;
seed_state(*action);
std::string id = action->id();
std::shared_ptr<AppAction> stored = std::move(action);
m_actions.insert_or_assign(std::move(id), std::move(stored));
}
void ActionRegistry::remove(const std::string& id)
{
assert(wxThread::IsMain());
m_actions.erase(id);
}
void ActionRegistry::seed_state(AppAction& a) const
{
auto favs = read_string_array("favourite_actions");
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());
if (it != stats.end() && it->is_object()) {
a.count = it->value("count", 0);
a.last = it->value("last", 0LL);
} else {
a.count = 0;
a.last = 0;
}
}
// ---- read surface -----------------------------------------------------------
const AppAction* ActionRegistry::by_id(const std::string& id) const
{
assert(wxThread::IsMain());
auto it = m_actions.find(id);
return it == m_actions.end() ? nullptr : it->second.get();
}
AppAction* ActionRegistry::find(const std::string& id)
{
return const_cast<AppAction*>(by_id(id));
}
// ---- dispatch + write-through ----------------------------------------------
AppActionRunResult ActionRegistry::run(const std::string& id)
{
assert(wxThread::IsMain());
auto it = m_actions.find(id);
if (it == m_actions.end())
return {}; // default Info, empty message
// why: hold a shared_ptr keep-alive, never a bare map entry. A runner may pump a
// 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();
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
stats = nlohmann::json::object();
nlohmann::json& e = stats[id];
if (!e.is_object())
e = nlohmann::json::object();
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"]; }
return o;
}
void ActionRegistry::set_favourite(const std::string& id, bool on)
{
assert(wxThread::IsMain());
auto favs = read_string_array("favourite_actions");
auto it = std::find(favs.begin(), favs.end(), id);
if (on && it == favs.end())
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;
}
void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
{
assert(wxThread::IsMain());
auto cur = read_string_array("favourite_actions");
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())
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);
write_section("favourite_actions", nlohmann::json(next));
}
bool ActionRegistry::should_ask(const std::string& id) const
{
assert(wxThread::IsMain());
auto arr = read_string_array("ask_suppressed");
return std::find(arr.begin(), arr.end(), id) == arr.end();
}
void ActionRegistry::suppress_ask(const std::string& id)
{
assert(wxThread::IsMain());
auto arr = read_string_array("ask_suppressed");
if (std::find(arr.begin(), arr.end(), id) == arr.end())
arr.push_back(id);
write_section("ask_suppressed", nlohmann::json(arr));
}
// ---- snapshot ---------------------------------------------------------------
nlohmann::json ActionRegistry::snapshot() const
{
assert(wxThread::IsMain());
std::vector<const AppAction*> sorted;
sorted.reserve(m_actions.size());
for (const auto& entry : m_actions)
sorted.push_back(entry.second.get());
const long long now = (long long) std::time(nullptr);
std::sort(sorted.begin(), sorted.end(), [&](const AppAction* a, const AppAction* b) {
double sa = frecency_score(a->count, a->last, now);
double sb = frecency_score(b->count, b->last, now);
if (sa != sb)
return sa > sb;
if (a->title() != b->title())
return a->title() < b->title();
if (a->source_name() != b->source_name())
return a->source_name() < b->source_name();
return a->id() < b->id();
});
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", ""}});
// 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)}};
}
}} // namespace Slic3r::GUI
+135
View File
@@ -0,0 +1,135 @@
#pragma once
#include <nlohmann/json.hpp>
#include <wx/string.h>
#include <wx/thread.h>
#include <cassert>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Slic3r { namespace GUI {
// How a source's action set changed. Drives the registry's refresh handlers.
enum class ActionChange { Added, Removed };
// 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"
};
// A speed-dial action: identity + user-state seeded from config + how to run itself.
// Abstract base - the only virtual is run(); concrete subclasses know how to run
// and what their source is.
// note: named AppAction, not Action - Slic3r::GUI::Action is already taken by
// UnsavedChangesDialog's exit-action enum, and this header reaches most GUI TUs.
struct AppAction
{
const std::string& id() const { return m_id; }
const std::string& title() const { return m_title; }
const std::string& source_key() const { return m_source_key; } // stable source identity
const std::string& source_name() const { return m_source_name; } // source display name
// Builds the stable id "<prefix>:<title>:<source_key>". why: one place owns the
// format - both the base ctor and the one raw-key lookup (removing a capability
// without an action object) go through this; ids are never parsed back apart.
static std::string compose_id(std::string_view prefix, std::string_view title, std::string_view source_key)
{
std::string out;
out.reserve(prefix.size() + title.size() + source_key.size() + 2);
out.append(prefix).append(1, ':').append(title).append(1, ':').append(source_key);
return out;
}
// seeded from AppConfig for the snapshot / sort:
bool favourite = false;
int count = 0;
long long last = 0; // epoch seconds
virtual ~AppAction() = default;
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread)
protected:
// The definition is constructor-set and immutable. Refreshes replace an action
// instead of mutating identity after the registry has indexed it by id.
// 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)) {}
private:
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
std::string m_title; // display name
std::string m_source_key; // stable identity of the action's source (e.g. plugin_key)
std::string m_source_name; // display name of the action's source
};
// 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.
// 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.
//
// note: there is exactly one source (script plugins), so it lives inline here rather
// than behind a polymorphic source interface.
class ActionRegistry
{
public:
// 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();
// 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);
// Removes the action with this id. Missing ids are a harmless no-op.
void remove(const std::string& id);
// Always-clean read surface. UI thread only.
const AppAction* by_id(const std::string& id) const;
// 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
// 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;
private:
void seed_state(AppAction& a) const; // favourite/stats from config
AppAction* find(const std::string& id);
// 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
};
}} // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1440,7 +1440,7 @@ PageTemperatures::PageTemperatures(ConfigWizard *parent)
spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0);
append_text(_L("Enter the nozzle_temperature needed for extruding your filament."));
append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS."));
append_text(_L("A rule of thumb is 160 to 230℃ for PLA, and 215 to 250℃ for ABS."));
#endif
auto *sizer_extr = new wxFlexGridSizer(3, 5, 5);
@@ -1455,7 +1455,7 @@ PageTemperatures::PageTemperatures(ConfigWizard *parent)
append_spacer(VERTICAL_SPACING);
append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed."));
append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed."));
append_text(_L("A rule of thumb is 60℃ for PLA and 110℃ for ABS. Leave zero if you have no heated bed."));
auto *sizer_bed = new wxFlexGridSizer(3, 5, 5);
auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:"));
+150
View File
@@ -839,4 +839,154 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
}
}
static DevAms::AmsType ams_type_from_string(const std::string& s)
{
if (s == "ams_lite" || s == "ams-lite") return DevAms::AMS_LITE;
if (s == "n3f") return DevAms::N3F;
if (s == "n3s") return DevAms::N3S;
return DevAms::AMS; // default
}
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
{
if (!system || !data.is_object())
return;
// --- AMS units ---
if (data.contains("units") && data["units"].is_array())
{
std::set<std::string> seen_units;
for (const auto& u : data["units"])
{
if (!u.is_object() || !u.contains("id"))
continue;
const std::string ams_id = u.value("id", std::string());
if (ams_id.empty())
continue;
seen_units.insert(ams_id);
const int ext_id = u.value("extruder", MAIN_EXTRUDER_ID);
const DevAms::AmsType type = ams_type_from_string(u.value("type", std::string("ams")));
DevAms* ams = nullptr;
auto it = system->amsList.find(ams_id);
if (it == system->amsList.end())
{
ams = new DevAms(ams_id, ext_id, type);
system->amsList.insert(std::make_pair(ams_id, ams));
}
else
{
ams = it->second;
ams->m_ext_id = ext_id;
ams->SetAmsType(type);
}
ams->m_exist = true;
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
ams->m_humidity_percent = u.value("humidity_percent", -1);
ams->m_left_dry_time = u.value("dry_time_min", 0);
// --- slots / trays ---
std::set<std::string> seen_slots;
if (u.contains("slots") && u["slots"].is_array())
{
for (const auto& s : u["slots"])
{
if (!s.is_object())
continue;
const std::string tray_id = std::to_string(s.value("index", -1));
seen_slots.insert(tray_id);
DevAmsTray* tray = nullptr;
auto tit = ams->m_trays.find(tray_id);
if (tit == ams->m_trays.end())
{
tray = new DevAmsTray(tray_id);
ams->m_trays.insert(std::make_pair(tray_id, tray));
}
else
{
tray = tit->second;
}
tray->is_exists = s.value("loaded", false);
tray->m_fila_type = s.value("material", std::string());
tray->setting_id = s.value("preset_id", std::string());
tray->UpdateColorFromStr(s.value("color", std::string()));
tray->nozzle_temp_min = std::to_string(s.value("nozzle_temp_min", 0));
tray->nozzle_temp_max = std::to_string(s.value("nozzle_temp_max", 0));
tray->remain = s.value("remain_percent", -1);
tray->k = s.value("k", 0.0f);
if (s.contains("diameter_mm") && s["diameter_mm"].is_number())
tray->diameter = std::to_string(s["diameter_mm"].get<double>());
if (s.contains("weight_g") && s["weight_g"].is_number())
tray->weight = std::to_string(s["weight_g"].get<int>());
tray->cols.clear();
if (s.contains("colors") && s["colors"].is_array())
{
for (const auto& c : s["colors"])
if (c.is_string())
tray->cols.push_back(c.get<std::string>());
}
tray->ctype = tray->cols.size() > 1 ? 1 : 0;
}
}
// prune trays no longer reported
for (auto tit = ams->m_trays.begin(); tit != ams->m_trays.end();)
{
if (seen_slots.count(tit->first) == 0)
{
delete tit->second;
tit = ams->m_trays.erase(tit);
}
else
{
++tit;
}
}
}
// prune units no longer reported
for (auto it = system->amsList.begin(); it != system->amsList.end();)
{
if (seen_units.count(it->first) == 0)
{
delete it->second;
it = system->amsList.erase(it);
}
else
{
++it;
}
}
}
// --- external / direct spools -> obj->vt_slot ---
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
if (obj && data.contains("external") && data["external"].is_array())
{
obj->vt_slot.clear();
for (const auto& e : data["external"])
{
if (!e.is_object())
continue;
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
DevAmsTray tray(std::to_string(vt_id));
tray.is_exists = e.value("loaded", false);
tray.m_fila_type = e.value("material", std::string());
tray.setting_id = e.value("preset_id", std::string());
tray.UpdateColorFromStr(e.value("color", std::string()));
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
tray.remain = e.value("remain_percent", -1);
obj->vt_slot.push_back(tray);
}
}
}
}
@@ -415,6 +415,8 @@ class DevFilaSystemParser
{
public:
static void ParseV1_0(const json& print_json, MachineObject* obj, DevFilaSystem* system, bool key_field_only);
static void ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system);
};
struct DevFilamentDryingPreset
+23 -89
View File
@@ -1,5 +1,4 @@
#include "ExportPresetBundleDialog.hpp"
#include <slic3r/GUI/Widgets/WebView.hpp>
#include "GUI_App.hpp"
#include "ConfigWizard.hpp"
#include "I18N.hpp"
@@ -12,17 +11,14 @@
#include <wx/sizer.h>
#include <libslic3r/PresetBundle.hpp>
#include <wx/string.h>
#include <slic3r/GUI/Widgets/WebView.hpp>
#include <miniz.h>
#include <slic3r/GUI/MsgDialog.hpp>
namespace Slic3r { namespace GUI {
ExportPresetBundleDialog::ExportPresetBundleDialog(
wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style)
: DPIDialog(parent, id, _L("ExportPresetBundle"), pos, size, style)
: WebViewHostDialog(parent, id, _L("ExportPresetBundle"), pos, size, style)
{
SetBackgroundColour(*wxWHITE);
SetMinSize(DESIGN_WINDOW_SIZE);
Init();
wxGetApp().UpdateDlgDarkUI(this);
}
@@ -38,97 +34,36 @@ ExportPresetBundleDialog::~ExportPresetBundleDialog()
}
}
void ExportPresetBundleDialog::LoadUrl(wxString& url)
{
if (!m_browser)
return;
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " enter, url=" << url.ToStdString();
WebView::LoadUrl(m_browser, url);
m_browser->SetFocus();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " exit";
}
void ExportPresetBundleDialog::on_dpi_changed(const wxRect& suggested_rect) { this->Refresh(); }
void ExportPresetBundleDialog::Init()
{
wxString TargetUrl = from_u8(
(boost::filesystem::path(resources_dir()) / "web/dialog/ExportPresetDialog/index.html").make_preferred().string());
wxString strlang = wxGetApp().current_language_code_safe();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", strlang=%1%") % into_u8(strlang);
if (strlang != "")
TargetUrl = wxString::Format("%s?lang=%s", std::string(TargetUrl.mb_str()), strlang);
TargetUrl = "file://" + TargetUrl;
// Create the webview
m_browser = WebView::CreateWebView(this, TargetUrl);
if (m_browser == nullptr) {
wxLogError("Could not init m_browser");
return;
}
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
SetTitle(_L("Export Preset Bundle"));
SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
// Set a more sensible size for web browsing
wxSize pSize = FromDIP(wxSize(820, 660));
SetSize(pSize);
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY);
Move(tmpPT);
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ExportPresetBundleDialog::OnScriptMessage, this, m_browser->GetId());
LoadUrl(TargetUrl);
create_webview("web/dialog/ExportPresetDialog/index.html", _L("Export Preset Bundle"),
wxSize(820, 660), wxSize(640, 640));
}
void ExportPresetBundleDialog::RunScript(const wxString& s)
void ExportPresetBundleDialog::on_script_message(const nlohmann::json& j)
{
if (!m_browser)
if (handle_common_script_command(j))
return;
WebView::RunScript(m_browser, s);
}
void ExportPresetBundleDialog::OnScriptMessage(wxWebViewEvent& e)
{
try {
wxString strInput = e.GetString();
BOOST_LOG_TRIVIAL(trace) << "ExportPresetBundleDialog::OnScriptMessage;OnRecv:" << strInput.c_str();
json j = json::parse(strInput.utf8_string());
wxString strCmd = j["command"];
BOOST_LOG_TRIVIAL(trace) << "ExportPresetBundleDialog::OnScriptMessage;Command:" << strCmd;
if (strCmd == "close_page") {
this->EndModal(wxID_CANCEL);
} else if (strCmd == "request_export_preset_profile") {
InitExportData();
OnRequestPresets();
} else if (strCmd == "export_local") {
wxFileDialog dlg(this, _L("Save preset bundle"), "", "export.orca_bundle", "Orca Preset Bundle (*.orca_bundle)|*.orca_bundle",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
wxString path;
wxString name;
if (dlg.ShowModal() == wxID_OK) {
path = dlg.GetPath();
wxFileName file_name(path);
name = file_name.GetName();
if (file_name.GetExt().empty()) {
file_name.SetExt("orca_bundle");
path = file_name.GetFullPath();
}
const std::string strCmd = j.value("command", "");
if (strCmd == "request_export_preset_profile") {
InitExportData();
OnRequestPresets();
} else if (strCmd == "export_local") {
wxFileDialog dlg(this, _L("Save preset bundle"), "", "export.orca_bundle",
"Orca Preset Bundle (*.orca_bundle)|*.orca_bundle", wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
wxString path;
wxString name;
if (dlg.ShowModal() == wxID_OK) {
path = dlg.GetPath();
wxFileName file_name(path);
name = file_name.GetName();
if (file_name.GetExt().empty()) {
file_name.SetExt("orca_bundle");
path = file_name.GetFullPath();
}
OnExportData(path, name, j["data"]);
}
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(trace) << "ExportPresetBundleDialog::OnScriptMessage;Error:" << e.what();
OnExportData(path, name, j.value("data", json()));
}
}
@@ -331,8 +266,7 @@ void ExportPresetBundleDialog::OnRequestPresets()
}
}
wxString strJS = wxString::Format("HandleStudio(%s)", wxString::FromUTF8(res.dump(-1, ' ', false, json::error_handler_t::ignore)));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
call_web_handler(res);
}
void ExportPresetBundleDialog::OnExportData(const wxString& path, const wxString& filename, json data)
+3 -10
View File
@@ -3,6 +3,7 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include "libslic3r/AppConfig.hpp"
#include <slic3r/GUI/GUI.hpp>
@@ -11,7 +12,6 @@
#include <wx/language.h>
#include <wx/string.h>
#include <wx/fswatcher.h>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
@@ -37,7 +37,7 @@ enum ExportCase {
CASE_COUNT,
};
class ExportPresetBundleDialog : public Slic3r::GUI::DPIDialog
class ExportPresetBundleDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
ExportPresetBundleDialog(wxWindow* parent,
@@ -52,16 +52,12 @@ public:
// Utilities
bool seq_top_layer_only_changed() const { return m_seq_top_layer_only_changed; }
bool recreate_GUI() const { return m_recreate_GUI; }
void on_dpi_changed(const wxRect& suggested_rect) override;
void show_export_result(const ExportCase& e);
void Init();
void InitExportData();
// Webview
void LoadUrl(wxString& url);
void OnScriptMessage(wxWebViewEvent& e);
void RunScript(const wxString& s);
void on_script_message(const nlohmann::json& payload) override;
void OnRequestPresets();
void OnExportData(const wxString& path, const wxString& name, json data);
@@ -69,9 +65,6 @@ protected:
bool m_seq_top_layer_only_changed{false};
bool m_recreate_GUI{false};
// Webview
wxWebView* m_browser{nullptr};
// Export Preset
std::unordered_map<std::string, Preset*> m_printer_presets; // first: printer name, second: printer presets have same printer name
std::unordered_map<std::string, std::vector<const Preset*>>
+574 -44
View File
@@ -10,7 +10,9 @@
#include "libslic3r/PrintConfig.hpp"
#include <algorithm>
#include <regex>
#include <utility>
#include <cstdint>
#include <wx/numformatter.h>
#include <wx/tooltip.h>
@@ -21,6 +23,8 @@
#include "OG_CustomCtrl.hpp"
#include "MsgDialog.hpp"
#include "BitmapComboBox.hpp"
#include "PluginsConfigDialog.hpp"
#include "Widgets/Button.hpp"
// BBS
#include "Notebook.hpp"
@@ -116,8 +120,8 @@ wxString get_formatted_tooltip_text(const ConfigOptionDef& opt, const t_config_o
tooltip += (tooltip.empty() ? "" : "\n\n") + _(L("parameter name")) + ": " + opt_id;
// Orca:
// We can't use Orca's default values as-is because they sometimes depend on other values.
// Orca:
// We can't use Orca's default values as-is because they sometimes depend on other values.
// Parent preset configuration values will be used instead.
if (const Preset* print_parent_preset = wxGetApp().preset_bundle->prints.get_selected_preset_parent()) {
const DynamicPrintConfig& parent_config = print_parent_preset->config;
@@ -155,8 +159,8 @@ wxString get_formatted_tooltip_text(const ConfigOptionDef& opt, const t_config_o
tooltip += "\n\n" + _(L("Default")) + ": " + _(double_to_string(default_value)) + _(side_text);
if (opt.min > -FLT_MAX && opt.max < FLT_MAX) {
tooltip += "\n" + _(L("Range")) + ": [" +
_(double_to_string(opt.min)) + _(side_text) + ", " +
tooltip += "\n" + _(L("Range")) + ": [" +
_(double_to_string(opt.min)) + _(side_text) + ", " +
_(double_to_string(opt.max)) + _(side_text) + "]";
}
} else if (opt.type == coBool || opt.type == coString) {
@@ -484,17 +488,18 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
}
const std::string sidetext = m_opt.sidetext.rfind("mm/s") != std::string::npos ? "mm/s" : "mm";
const wxString stVal = double_to_string(val, 2);
const wxString msg_text = from_u8((boost::format(_utf8(L("Is it %s%% or %s %s?\n"
"YES for %s%%, \n"
"NO for %s %s."))) % stVal % stVal % sidetext % stVal % stVal % sidetext).str());
const wxString stVal = double_to_string(val, 2);
const wxString msg_text = from_u8((boost::format(_utf8(L("Is it %s%% or %s %s?\n"
"YES for %s%%, \n"
"NO for %s %s."))) %
stVal % stVal % sidetext % stVal % stVal % sidetext)
.str());
WarningDialog dialog(m_parent, msg_text, _L("Parameter validation") + ": " + m_opt_id, wxYES | wxNO);
if ((val > 100) && dialog.ShowModal() == wxID_YES) {
set_value(from_u8((boost::format("%s%%") % stVal).str()), false/*true*/);
set_value(from_u8((boost::format("%s%%") % stVal).str()), false /*true*/);
str += "%%";
}
else
set_value(stVal, false); // it's no needed but can be helpful, when inputted value contained "," instead of "."
} else
set_value(stVal, false); // it's no needed but can be helpful, when inputted value contained "," instead of "."
}
}
if (m_opt.opt_key == "thumbnails") {
@@ -1326,6 +1331,39 @@ using choice_ctrl = ::ComboBox; // BBS
static std::map<std::string, DynamicList*> dynamic_lists;
static bool is_plugin_printer_agent_key(const std::string& value)
{
return value.rfind("plugin:", 0) == 0;
}
static int printer_agent_item_for_enum_index(const choice_ctrl* field, int enum_index)
{
if (!field)
return -1;
const unsigned int count = field->GetCount();
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
const int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1;
if (stored == enum_index)
return static_cast<int>(idx);
}
}
return -1;
}
static int printer_agent_enum_index_for_item(const choice_ctrl* field, int item_index, int fallback)
{
if (!field || item_index < 0)
return fallback;
if (void* data = field->GetClientData(item_index))
return static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1;
return fallback;
}
void Choice::register_dynamic_list(std::string const &optname, DynamicList *list) { dynamic_lists.emplace(optname, list); }
void DynamicList::update()
@@ -1408,7 +1446,33 @@ void Choice::BUILD()
window = dynamic_cast<wxWindow*>(temp);
if (! m_opt.enum_labels.empty() || ! m_opt.enum_values.empty()) {
if (m_opt.enum_labels.empty()) {
if (m_opt_id == "printer_agent") {
const bool has_builtin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(),
[](const std::string& value) { return !is_plugin_printer_agent_key(value); });
const bool has_plugin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(),
[](const std::string& value) { return is_plugin_printer_agent_key(value); });
auto append_agent_rows = [this, temp](bool plugins) {
for (size_t i = 0; i < m_opt.enum_values.size(); ++i) {
const bool is_plugin = is_plugin_printer_agent_key(m_opt.enum_values[i]);
if (is_plugin != plugins)
continue;
const wxString label = i < m_opt.enum_labels.size() ? _(m_opt.enum_labels[i]) : wxString(m_opt.enum_values[i]);
const int item = temp->Append(label);
temp->SetClientData(item, reinterpret_cast<void*>(static_cast<uintptr_t>(i + 1)));
}
};
if (has_builtin_agents) {
temp->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(false);
}
if (has_plugin_agents) {
temp->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(true);
}
} else if (m_opt.enum_labels.empty()) {
// Append non-localized enum_values
for (auto el : m_opt.enum_values)
temp->Append(el);
@@ -1511,10 +1575,11 @@ void Choice::set_selection()
wxString text_value = wxString("");
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
switch (m_opt.type) {
case coEnum:{
field->SetSelection(m_opt.default_value->getInt());
const int val = m_opt.default_value->getInt();
field->SetSelection(m_opt_id == "printer_agent" ? printer_agent_item_for_enum_index(field, val) : val);
break;
}
case coFloat:
@@ -1563,9 +1628,15 @@ void Choice::set_value(const std::string& value, bool change_event) //! Redunda
++idx;
}
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
idx == m_opt.enum_values.size() ?
field->SetValue(value) :
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
if (m_opt_id == "printer_agent") {
const int enum_index = idx == m_opt.enum_values.size() ?
(m_opt.default_value ? m_opt.default_value->getInt() : 0) :
static_cast<int>(idx);
field->SetSelection(printer_agent_item_for_enum_index(field, enum_index));
} else if (idx == m_opt.enum_values.size())
field->SetValue(value);
else
field->SetSelection(idx);
m_disable_change_event = false;
@@ -1629,32 +1700,55 @@ void Choice::set_value(const boost::any& value, bool change_event)
case coEnum:
// BBS
case coEnums: {
int val = boost::any_cast<int>(value);
int selection = val;
auto printer_agent_index_from_key = [this](const std::string& key) {
auto it = std::find(m_opt.enum_values.begin(), m_opt.enum_values.end(), key);
if (it != m_opt.enum_values.end())
return static_cast<int>(it - m_opt.enum_values.begin());
return m_opt.default_value ? m_opt.default_value->getInt() : 0;
};
if (m_opt_id == "input_shaping_type") {
if (field != nullptr) {
const unsigned int count = field->GetCount();
int match_index = -1;
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data));
if (stored == val) {
match_index = static_cast<int>(idx);
break;
}
}
}
if (match_index >= 0)
selection = match_index;
else if (val >= 0 && val < static_cast<int>(count))
selection = val;
else if (count > 0)
selection = 0;
else
selection = -1;
}
} else {
int val = 0;
if (m_opt_id == "printer_agent") {
if (const int* int_value = boost::any_cast<int>(&value))
val = *int_value;
else if (const wxString* wx_value = boost::any_cast<wxString>(&value))
val = printer_agent_index_from_key(into_u8(*wx_value));
else if (const std::string* string_value = boost::any_cast<std::string>(&value))
val = printer_agent_index_from_key(*string_value);
else {
m_disable_change_event = false;
return;
}
} else
val = boost::any_cast<int>(value);
int selection = val;
if (m_opt_id == "printer_agent") {
selection = printer_agent_item_for_enum_index(field, val);
} else if (m_opt_id == "input_shaping_type") {
if (field != nullptr) {
const unsigned int count = field->GetCount();
int match_index = -1;
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data));
if (stored == val) {
match_index = static_cast<int>(idx);
break;
}
}
}
if (match_index >= 0)
selection = match_index;
else if (val >= 0 && val < static_cast<int>(count))
selection = val;
else if (count > 0)
selection = 0;
else
selection = -1;
}
} else {
// Support ThirdPartyPrinter
if (m_opt_id.compare("host_type") == 0 && val != 0 &&
m_opt.enum_values.size() > field->GetCount()) // for case, when PrusaLink isn't used as a HostType
@@ -1749,11 +1843,17 @@ boost::any& Choice::get_value()
if (m_opt_id == rp_option)
return m_value = boost::any(ret_str);
// BBS
// BBS
if (m_opt.type == coEnum || m_opt.type == coEnums)
{
if (m_opt.nullable && field->GetSelection() == -1)
m_value = ConfigOptionEnumsGenericNullable::nil_value();
else if (m_opt_id == "printer_agent")
{
const int selection = field->GetSelection();
const int fallback = m_opt.default_value ? m_opt.default_value->getInt() : 0;
m_value = printer_agent_enum_index_for_item(field, selection, fallback);
}
else if (m_opt_id == "input_shaping_type")
{
int selection = field->GetSelection();
@@ -1894,6 +1994,436 @@ void Choice::msw_rescale()
#endif
}
void PluginField::BUILD()
{
auto* panel = new wxPanel(m_parent, wxID_ANY);
wxGetApp().UpdateDarkUI(panel);
window = panel;
m_main_sizer = new wxBoxSizer(wxVERTICAL);
panel->SetSizer(m_main_sizer);
if (m_opt.type == coStrings) {
const ConfigOptionStrings* vec = m_opt.get_default_value<ConfigOptionStrings>();
if (vec != nullptr && !vec->values.empty()) {
m_values = vec->values;
}
}
rebuild_ui();
m_value = m_values;
}
void PluginField::set_selector(std::function<std::string()> selector)
{
m_selector = std::move(selector);
}
static void remove_empty_plugin_values(std::vector<std::string>& values)
{
for (auto it = values.begin(); it != values.end();) {
if (it->empty())
it = values.erase(it);
else
++it;
}
}
void PluginField::rebuild_ui()
{
remove_empty_plugin_values(m_values);
if (m_main_sizer) {
m_main_sizer->Clear(true);
}
m_rows.clear();
m_standalone_add_btn = nullptr;
if (m_values.empty()) {
add_empty_state_row();
} else {
for (size_t i = 0; i < m_values.size(); ++i)
add_plugin_row(display_name_for_value(m_values[i]), i == m_values.size() - 1);
}
if (window) {
// Report the stacked rows' size so the option-group / OG_CustomCtrl allocate the right height.
window->SetMinSize(m_main_sizer->CalcMin());
window->Layout();
window->Refresh();
}
if (m_parent)
m_parent->Layout();
}
void PluginField::add_empty_state_row()
{
const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1);
auto row_sizer = new wxBoxSizer(wxHORIZONTAL);
wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, _L("No plugin selected"),
wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord),
wxTE_READONLY);
display->SetEditable(false);
wxGetApp().UpdateDarkUI(display);
display->SetToolTip(_L("No plugin selected"));
auto add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(add_btn);
add_btn->SetToolTip(_L("Add plugin"));
add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_clicked(); });
row_sizer->Add(display, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
row_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL);
m_main_sizer->Add(row_sizer, 0, wxEXPAND);
PluginRow row;
row.display = display;
row.add_btn = add_btn;
row.sizer = row_sizer;
m_rows.push_back(row);
m_standalone_add_btn = add_btn;
}
void PluginField::add_plugin_row(const wxString& value, bool is_last)
{
const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1);
auto row_sizer = new wxBoxSizer(wxHORIZONTAL);
ScalableButton* select_btn = new ScalableButton(window, wxID_ANY, "search", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(select_btn);
select_btn->SetToolTip(_L("Select plugin"));
wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, value,
wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord),
wxTE_READONLY);
display->SetEditable(false);
wxGetApp().UpdateDarkUI(display);
display->SetToolTip(get_tooltip_text(value));
ScalableButton* remove_btn = nullptr;
if (!m_opt.readonly) {
remove_btn = new ScalableButton(window, wxID_ANY, "cross", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(remove_btn);
remove_btn->SetToolTip(_L("Remove plugin"));
}
ScalableButton* add_btn = nullptr;
if (is_last && !m_opt.readonly) {
add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(add_btn);
add_btn->SetToolTip(_L("Add plugin"));
add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_clicked(); });
}
const size_t row_index = m_rows.size();
select_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_select_clicked(row_index); });
if (remove_btn)
remove_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_remove_clicked(row_index); });
row_sizer->Add(select_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
row_sizer->Add(display, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
if (remove_btn)
row_sizer->Add(remove_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
if (add_btn)
row_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL);
else if (!m_opt.readonly) {
// Reserve space equal to the add button so all rows align.
row_sizer->Add(button_size.GetWidth(), button_size.GetHeight(), 0, wxALIGN_CENTER_VERTICAL);
}
const int bottom_gap = is_last ? 0 : 4;
m_main_sizer->Add(row_sizer, 0, wxEXPAND | (bottom_gap > 0 ? wxBOTTOM : 0), bottom_gap);
PluginRow row;
row.select_btn = select_btn;
row.display = display;
row.remove_btn = remove_btn;
row.add_btn = add_btn;
row.sizer = row_sizer;
m_rows.push_back(row);
}
wxString PluginField::display_name_for_value(const std::string& value) const
{
if (value.empty())
return _L("No plugin selected");
return from_u8(value);
}
void PluginField::on_select_clicked(size_t index)
{
if (index >= m_rows.size())
return;
if (!m_selector)
return;
std::string selected = m_selector();
if (selected.empty())
return;
if (index >= m_values.size())
m_values.resize(index + 1);
if (m_values[index] == selected)
return;
m_values[index] = selected;
set_row_value(index, display_name_for_value(selected));
m_value = m_values;
on_change_field();
}
void PluginField::on_add_clicked()
{
if (m_opt.readonly)
return;
if (!m_selector)
return;
std::string selected = m_selector();
if (selected.empty())
return;
m_values.push_back(selected);
m_value = m_values;
rebuild_ui();
on_change_field();
}
void PluginField::on_remove_clicked(size_t index)
{
if (m_opt.readonly || index >= m_values.size())
return;
m_values.erase(m_values.begin() + index);
m_value = m_values;
rebuild_ui();
on_change_field();
}
wxString PluginField::get_row_value(size_t index) const
{
if (index >= m_rows.size() || !m_rows[index].display)
return wxEmptyString;
return m_rows[index].display->GetValue();
}
void PluginField::set_row_value(size_t index, const wxString& value)
{
if (index >= m_rows.size() || !m_rows[index].display)
return;
m_rows[index].display->ChangeValue(value);
m_rows[index].display->SetToolTip(get_tooltip_text(value));
}
void PluginField::set_value(const boost::any& value, bool change_event)
{
m_disable_change_event = !change_event;
if (value.empty()) {
m_values.clear();
} else if (value.type() == typeid(std::vector<std::string>)) {
m_values = boost::any_cast<std::vector<std::string>>(value);
} else if (value.type() == typeid(wxString)) {
m_values.clear();
wxString text = boost::any_cast<wxString>(value);
if (!text.IsEmpty())
m_values.push_back(into_u8(text));
} else if (value.type() == typeid(std::string)) {
m_values.clear();
std::string text = boost::any_cast<std::string>(value);
if (!text.empty())
m_values.push_back(text);
}
rebuild_ui();
m_value = m_values;
m_disable_change_event = false;
if (change_event)
on_change_field();
}
boost::any& PluginField::get_value()
{
m_value = m_values;
return m_value;
}
void PluginField::enable()
{
for (auto& row : m_rows) {
if (row.select_btn)
row.select_btn->Enable();
if (row.display)
row.display->Enable();
if (row.remove_btn)
row.remove_btn->Enable();
if (row.add_btn)
row.add_btn->Enable();
}
if (m_standalone_add_btn)
m_standalone_add_btn->Enable();
}
void PluginField::disable()
{
for (auto& row : m_rows) {
if (row.select_btn)
row.select_btn->Disable();
if (row.display)
row.display->Disable();
if (row.remove_btn)
row.remove_btn->Disable();
if (row.add_btn)
row.add_btn->Disable();
}
if (m_standalone_add_btn)
m_standalone_add_btn->Disable();
}
void PluginField::msw_rescale()
{
Field::msw_rescale();
rebuild_ui();
}
namespace {
// The stored text as a document, or an empty array when it is absent or unparseable.
nlohmann::json plugin_overrides_as_json(const std::string& text)
{
if (text.empty())
return nlohmann::json::array();
return nlohmann::json::parse(text, nullptr, /* allow_exceptions */ false);
}
} // namespace
void PluginConfigField::BUILD()
{
m_button = new ::Button(m_parent, _L("Configure"));
// ButtonType::Parameter gives the button the same height as the parameter fields above it.
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
wxSize size(def_width_wider() * m_em_unit, m_button->GetMinSize().GetHeight());
if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
m_button->SetMinSize(size);
m_button->SetSize(size);
m_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { open_dialog(); });
m_button->SetToolTip(get_tooltip_text(_L("Configure")));
window = m_button;
if (const ConfigOptionString* def = m_opt.get_default_value<ConfigOptionString>())
m_json = def->value;
update_button_label();
m_value = m_json;
}
void PluginConfigField::update_button_label()
{
if (m_button == nullptr)
return;
const nlohmann::json entries = plugin_overrides_as_json(m_json);
const size_t count = entries.is_array() ? entries.size() : 0;
m_button->SetLabel(count == 0 ? _L("Configure")
: wxString::Format(_L("Configure (%d)"), int(count)));
}
void PluginConfigField::open_dialog()
{
const Preset::Type type = static_cast<Preset::Type>(m_preset_type);
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_FILAMENT && type != Preset::TYPE_PRINTER)
return;
std::string edited;
{
PluginsConfigDialog dlg(m_button, type, m_json);
dlg.ShowModal();
edited = dlg.overrides_json();
}
// Compare semantically: a round-trip through the serializer can reorder keys or drop whitespace,
// and that alone must not dirty the preset.
if (plugin_overrides_as_json(m_json) == plugin_overrides_as_json(edited))
return;
m_json = edited;
m_value = m_json;
update_button_label();
on_change_field();
}
void PluginConfigField::set_value(const boost::any& value, bool change_event)
{
m_disable_change_event = !change_event;
if (value.type() == typeid(wxString))
m_json = into_u8(boost::any_cast<wxString>(value));
else if (value.type() == typeid(std::string))
m_json = boost::any_cast<std::string>(value);
m_value = m_json;
update_button_label();
m_disable_change_event = false;
}
boost::any& PluginConfigField::get_value()
{
// std::string, not wxString: change_opt_value any_casts a coString to std::string.
m_value = m_json;
return m_value;
}
void PluginConfigField::enable()
{
if (m_button)
m_button->Enable();
}
void PluginConfigField::disable()
{
if (m_button)
m_button->Disable();
}
void PluginConfigField::msw_rescale()
{
Field::msw_rescale();
if (m_button == nullptr)
return;
m_button->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
wxSize size(def_width_wider() * m_em_unit, m_button->GetMinSize().GetHeight());
if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
m_button->SetMinSize(size);
}
void ColourPicker::BUILD()
{
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
+92
View File
@@ -32,6 +32,9 @@
#define wxMSW false
#endif
// Orca's styled button (Widgets/Button.hpp), used by PluginConfigField. It lives at global scope.
class Button;
namespace Slic3r { namespace GUI {
class Field;
@@ -466,6 +469,95 @@ public:
void suppress_scroll();
};
class PluginField : public Field {
using Field::Field;
public:
PluginField(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
PluginField(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~PluginField() {}
void BUILD() override;
void set_selector(std::function<std::string()> selector);
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void enable() override;
void disable() override;
// The rows live in one container panel (the base `window`), so the field exposes a window instead
// of a bare sizer and focus, sizing and teardown apply to the whole field.
wxWindow* getWindow() override { return window; }
void msw_rescale() override;
private:
struct PluginRow {
ScalableButton* select_btn { nullptr };
wxTextCtrl* display { nullptr };
ScalableButton* remove_btn { nullptr };
ScalableButton* add_btn { nullptr };
wxBoxSizer* sizer { nullptr };
};
void rebuild_ui();
void add_empty_state_row();
void add_plugin_row(const wxString& value = wxEmptyString, bool is_last = false);
wxString display_name_for_value(const std::string& value) const;
void on_select_clicked(size_t index);
void on_add_clicked();
void on_remove_clicked(size_t index);
wxString get_row_value(size_t index) const;
void set_row_value(size_t index, const wxString& value);
wxWindow* window { nullptr }; // container panel that hosts m_main_sizer
wxBoxSizer* m_main_sizer { nullptr };
std::vector<PluginRow> m_rows;
std::vector<std::string> m_values;
ScalableButton* m_standalone_add_btn { nullptr };
std::function<std::string()> m_selector;
};
// A settings row whose value is a raw JSON document nobody types by hand: the button opens
// PluginsConfigDialog and the document it hands back becomes the field's value. The edit goes through
// the ordinary Field value/on_change_field path, so the row gets the same dirty state and revert arrow
// as any other setting — the dialog never touches the preset.
class PluginConfigField : public Field {
using Field::Field;
public:
PluginConfigField(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
PluginConfigField(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~PluginConfigField() {}
void BUILD() override;
// Which preset's capabilities the dialog lists; set by the option group. An int for the same
// reason OptionsGroup::m_config_type is one: it keeps Preset.hpp out of this header.
void set_preset_type(int type) { m_preset_type = type; }
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void enable() override;
void disable() override;
// The button is the whole field, so it is the window the option group sizes and positions (the
// ColourPicker idiom). A container panel would be sized but never laid out, collapsing the row.
wxWindow* getWindow() override { return window; }
void msw_rescale() override;
private:
void open_dialog();
void update_button_label();
wxWindow* window { nullptr }; // == m_button; the base class hands this to the option group
::Button* m_button { nullptr };
std::string m_json; // the option's raw text; "" when the preset overrides nothing
int m_preset_type { -1 };
};
class ColourPicker : public Field {
using Field::Field;
+2 -2
View File
@@ -445,7 +445,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
add_row(_u8L("Flow rate"), buff);
sprintf(buff, "%.0f %%", vertex.fan_speed);
add_row(_u8L("Fan speed"), buff);
sprintf(buff, ("%.0f " + _u8L("°C")).c_str(), vertex.temperature);
sprintf(buff, ("%.0f " + _u8L("\u2103" /* °C */)).c_str(), vertex.temperature);
add_row(_u8L("Temperature"), buff);
sprintf(buff, "%.4f", vertex.pressure_advance);
add_row(_u8L("Pressure Advance"), buff);
@@ -3711,7 +3711,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
break;
}
case libvgcode::EViewType::FanSpeed: { imgui.title(_u8L("Fan speed (%)")); break; }
case libvgcode::EViewType::Temperature: { imgui.title(_u8L("Temperature (°C)")); break; }
case libvgcode::EViewType::Temperature: { imgui.title(_u8L("Temperature (℃)")); break; }
// ORCA: Add Pressure Advance visualization support
case libvgcode::EViewType::PressureAdvance:{ imgui.title(_u8L("Pressure Advance")); break; }
case libvgcode::EViewType::VolumetricFlowRate:
+31 -15
View File
@@ -1034,6 +1034,7 @@ 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);
@@ -2338,11 +2339,6 @@ void GLCanvas3D::remove_curr_plate_all()
m_dirty = true;
}
void GLCanvas3D::update_plate_thumbnails()
{
_update_imgui_select_plate_toolbar();
}
void GLCanvas3D::select_all()
{
if (!m_gizmos.is_allow_select_all()) {
@@ -3013,6 +3009,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
_set_warning_notification(EWarning::MixtureFilamentIncompatible, !filament_mixture_compatible);
bool model_fits = contained_min_one && !m_model->objects.empty() && !partlyOut && object_results.filaments.empty() && tpu_valid && filament_printable;
// Honor the missing-plugin block so this geometry-only path does not re-enable slicing
// that Plater::validate_current_plate disabled (otherwise moving an object would briefly
// re-enable the Slice button while required plugins are still missing).
if (wxGetApp().plater() && wxGetApp().plater()->plugins_block_slicing())
model_fits = false;
post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, model_fits));
ppl.get_curr_plate()->update_slice_ready_status(model_fits);
}
@@ -3223,7 +3224,6 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
// BBS
//m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
_update_imgui_select_plate_toolbar();
bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
m_dirty |= mouse3d_controller_applied;
m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this);
@@ -3511,6 +3511,11 @@ 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':
{
@@ -4833,11 +4838,6 @@ void GLCanvas3D::force_set_focus() {
void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
{
m_tooltip_enabled = false;
if (m_canvas_type == ECanvasType::CanvasPreview) {
// update thumbnails and update plate toolbar
wxGetApp().plater()->update_all_plate_thumbnails();
_update_imgui_select_plate_toolbar();
}
_refresh_if_shown_on_screen();
m_tooltip_enabled = true;
m_is_touchpad_navigation = wxGetApp().app_config->get_bool("camera_navigation_style");
@@ -6920,13 +6920,28 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
{
bool result = true;
if (!m_sel_plate_toolbar.is_enabled() || m_sel_plate_toolbar.is_render_finish) return false;
if (!m_sel_plate_toolbar.is_enabled()) {
return false;
}
const auto& p_plater = wxGetApp().plater();
if (!p_plater) {
return false;
}
if (!p_plater->is_plate_toolbar_image_dirty()) {
return false;
}
if (!p_plater->is_gcode_3mf()) {
p_plater->update_all_plate_thumbnails(true);
}
_update_select_plate_toolbar_stats_item();
m_sel_plate_toolbar.del_all_item();
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
PartPlateList& plate_list = p_plater->get_partplate_list();
for (int i = 0; i < plate_list.get_plate_count(); i++) {
IMToolbarItem* item = new IMToolbarItem();
PartPlate* plate = plate_list.get_plate(i);
@@ -6939,7 +6954,7 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
}
m_sel_plate_toolbar.m_items.push_back(item);
}
p_plater->clear_plate_toolbar_image_dirty();
m_sel_plate_toolbar.is_display_scrollbar = false;
return result;
}
@@ -8797,6 +8812,8 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
return;
}
_update_imgui_select_plate_toolbar();
IMToolbarItem* all_plates_stats_item = m_sel_plate_toolbar.m_all_plates_stats_item;
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
@@ -9213,7 +9230,6 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered;
imgui.end();
m_sel_plate_toolbar.is_render_finish = true;
}
//BBS: GUI refactor: GLToolbar adjust
+1 -1
View File
@@ -166,6 +166,7 @@ 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);
@@ -997,7 +998,6 @@ public:
void select_curr_plate_all();
void select_object_from_idx(std::vector<int>& object_idxs);
void remove_curr_plate_all();
void update_plate_thumbnails();
void select_all();
void deselect_all();
+10
View File
@@ -174,6 +174,16 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt
config.option<ConfigOptionStrings>(opt_key)->values =
boost::any_cast<std::vector<std::string>>(value);
}
else if (config.def()->get(opt_key)->gui_type == ConfigOptionDef::GUIType::plugin_picker) {
if (value.type() == typeid(std::vector<std::string>)) {
config.option<ConfigOptionStrings>(opt_key)->values =
boost::any_cast<std::vector<std::string>>(value);
} else {
std::string str = boost::any_cast<std::string>(value);
config.option<ConfigOptionStrings>(opt_key)->values = str.empty() ?
std::vector<std::string>() : std::vector<std::string>{str};
}
}
else if (config.def()->get(opt_key)->gui_flags.compare("serialized") == 0) {
std::string str = boost::any_cast<std::string>(value);
std::vector<std::string> values {};
+277 -75
View File
@@ -1,6 +1,7 @@
#include "ExportPresetBundleDialog.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp"
#include "GUI_App.hpp"
#include "GUI_Init.hpp"
#include "GUI_ObjectList.hpp"
@@ -13,6 +14,7 @@
#include <boost/log/detail/native_typeof.hpp>
#include <libslic3r/Config.hpp>
#include <mutex>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/event.h>
// Localization headers: include libslic3r version first so everything in this file
@@ -76,6 +78,9 @@
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/host/PluginHostUi.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include "GUI.hpp"
#include "GUI_Utils.hpp"
@@ -135,6 +140,10 @@
#include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include "slic3r/Utils/bambu_networking.hpp"
#include "PluginsDialog.hpp"
#include "SpeedDialDialog.hpp"
#include "TerminalDialog.hpp"
//#ifdef WIN32
//#include "BaseException.h"
//#endif
@@ -1113,6 +1122,12 @@ void GUI_App::shutdown()
if (m_is_recreating_gui) return;
stop_http_server();
set_closing(true);
Slic3r::PluginManager::instance().set_shutting_down();
if (m_agent)
m_agent->set_printer_agent(nullptr);
NetworkAgentFactory::clear_printer_agent_cache();
BOOST_LOG_TRIVIAL(info) << "GUI_App::shutdown exit";
}
@@ -2272,6 +2287,14 @@ void GUI_App::init_networking_callbacks()
GUI_App::~GUI_App()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": enter");
if (m_agent)
m_agent->set_printer_agent(nullptr);
NetworkAgentFactory::clear_printer_agent_cache();
Slic3r::PluginManager::instance().shutdown();
Slic3r::PythonInterpreter::instance().shutdown();
if (app_config != nullptr) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy app_config");
delete app_config;
@@ -2709,6 +2732,55 @@ std::string get_system_info()
return out.str();
}
// wx/app-level plugin wiring, kept in one place: subscriptions to plugin
// loader events that drive GUI policy (plugins dialog refresh, network-agent
// registration, plate revalidation). The libslic3r dispatch hooks are NOT
// wired here -- PluginManager::initialize() installs those via
// plugin_hooks::install().
void GUI_App::init_plugin_gui_wiring()
{
PluginManager& plugin_mgr = PluginManager::instance();
auto refresh_plugins_dialog = [] {
if (!wxTheApp)
return;
GUI_App* app = &GUI::wxGetApp();
if (app->is_closing())
return;
app->CallAfter([app] {
if (!app->is_closing() && app->m_plugins_dlg)
app->m_plugins_dlg->update_plugin_dialog_ui();
});
};
plugin_mgr.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin);
plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.subscribe_on_capability_load_callback(
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
// current plate (on the UI thread) so the notification clears once its plugin is available.
if (wxTheApp && !wxGetApp().is_closing())
wxGetApp().CallAfter([]() {
if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
});
plugin_mgr.subscribe_on_capability_unload_callback(
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
});
}
bool GUI_App::on_init_inner()
{
wxLog::SetActiveTarget(new wxBoostLog());
@@ -3102,13 +3174,70 @@ bool GUI_App::on_init_inner()
wxMessageBox("Force using legacy bambu networking plugin because debugger is attached! If the app terminates itself immediately, please delete installed plugin and try again!");
}
} */
copy_network_if_available();
if (scrn) {
scrn->SetText(_L("Loading Plugins") + dots, 20);
wxYield();
}
on_init_network();
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically.
// initialize() also installs the libslic3r hooks (capability resolver,
// slicing-pipeline dispatcher) via plugin_hooks::install() -- no
// per-capability wiring belongs here.
PluginManager& plugin_mgr = PluginManager::instance();
plugin_mgr.initialize();
// Set cloud plugin directory from previous session so cloud-installed
// plugins are discovered even before the network agent is ready.
const std::string preset_folder = app_config->get("preset_folder");
if (!preset_folder.empty()) {
plugin_mgr.set_cloud_user(preset_folder);
}
plugin_mgr.discover_plugins(false, true);
init_plugin_gui_wiring();
// Subscribe to the plugin loader and enumerate current actions (UI thread, once).
m_action_registry.init();
for (const std::string& plugin_key : plugin_mgr.get_enabled_plugin_keys()) {
if (!plugin_mgr.is_plugin_loaded(plugin_key)) {
plugin_mgr.load_plugin(plugin_key, false);
BOOST_LOG_TRIVIAL(info) << "Auto-loading plugin on startup: " << plugin_key;
}
}
if (m_agent)
plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()));
if (m_agent && m_agent->is_user_login()) {
enable_user_preset_folder(true);
plugin_mgr.set_cloud_user(m_agent->get_user_id());
// If there is a user logged in we do an immediate sync.
std::vector<std::string> not_found, unauthorized;
plugin_mgr.fetch_plugins_from_cloud(&not_found, &unauthorized);
if (plater()) {
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));
}
}
} else {
enable_user_preset_folder(false);
plugin_mgr.set_cloud_user("");
}
// BBS if load user preset failed
@@ -3680,69 +3809,69 @@ void GUI_App::switch_printer_agent()
// Read printer_agent from config, falling back to default
std::string effective_agent_id = ORCA_PRINTER_AGENT_ID;
std::string cloud_agent_id = ORCA_CLOUD_PROVIDER;
if (preset_bundle->is_bbl_vendor()) {
if (preset_bundle->is_bbl_vendor())
effective_agent_id = BBL_PRINTER_AGENT_ID;
cloud_agent_id = BBL_CLOUD_PROVIDER;
} else {
const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config;
if (config.has("printer_agent")) {
const std::string& value = config.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
effective_agent_id = value;
}
const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config;
if (config.has("printer_agent")) {
const std::string& value = config.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
effective_agent_id = value;
}
// Check if agent is registered
if (!NetworkAgentFactory::is_printer_agent_registered(effective_agent_id)) {
const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id);
if (!agent_info_ptr) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id
<< "', keeping current agent";
// Keep current agent, don't switch
return;
}
const PrinterAgentInfo agent_info = *agent_info_ptr;
std::string current_agent_id;
if (m_agent->get_printer_agent())
current_agent_id = m_agent->get_printer_agent()->get_agent_info().id;
std::string log_dir = data_dir();
std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
if (current_agent_id != effective_agent_id) {
std::string log_dir = data_dir();
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
// Create new printer agent via registry
std::shared_ptr<IPrinterAgent> new_printer_agent =
NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir);
// Create new printer agent via registry
std::shared_ptr<IPrinterAgent> new_printer_agent =
NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir);
if (!new_printer_agent) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent";
return;
}
if (!new_printer_agent) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent";
return;
}
// Swap the agent
m_agent->set_printer_agent(new_printer_agent);
sidebar().update_all_preset_comboboxes();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;
// Auto-switch MachineObject (new agent has empty device_info, so always re-select)
select_machine(effective_agent_id);
} else if (effective_agent_id != BBL_PRINTER_AGENT_ID) {
// The factory caches agents per ID, so an identical pointer means the agent type is unchanged.
if (m_agent->get_printer_agent() == new_printer_agent) {
// Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper
// printer presets), so the selected machine and the agent's cached device_info still
// point at the previously active printer preset. Re-select the machine when the new
// preset targets a different host, otherwise filament sync keeps hitting the old
// printer. (#12506)
if (m_device_manager && preset_bundle) {
const DynamicPrintConfig& cfg = preset_bundle->printers.get_edited_preset().config;
const std::string print_host = cfg.opt_string("print_host");
if (effective_agent_id != BBL_PRINTER_AGENT_ID && m_device_manager && preset_bundle) {
const std::string print_host = config.opt_string("print_host");
if (!print_host.empty()) {
const std::string dev_id = MachineObject::dev_id_from_address(print_host, cfg.opt_string("printhost_port"));
MachineObject* sel = m_device_manager->get_selected_machine();
const std::string dev_id = MachineObject::dev_id_from_address(print_host, config.opt_string("printhost_port"));
MachineObject* sel = m_device_manager->get_selected_machine();
if (!sel || sel->get_dev_id() != dev_id)
select_machine(effective_agent_id);
}
}
return;
}
// Swap the agent
m_agent->set_printer_agent(new_printer_agent);
sidebar().update_all_preset_comboboxes();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;
// Start discovery so Python agents can populate the device list via SSDP callback
m_agent->start_discovery(true, false);
// Auto-switch MachineObject (new agent has empty device_info, so always re-select)
select_machine(effective_agent_id);
}
void GUI_App::select_machine(const std::string& agent_id)
@@ -4740,9 +4869,16 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
remove_user_presets();
enable_user_preset_folder(false);
Slic3r::PluginManager::instance().unload_cloud_plugins();
Slic3r::PluginManager::instance().clear_cloud_plugin_metadata();
Slic3r::PluginManager::instance().set_cloud_user("");
preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable);
mainframe->update_side_preset_ui();
// keep this here. refresh_from_catalog is meant to update the dialog UI.
if (m_plugins_dlg)
m_plugins_dlg->update_plugin_dialog_ui();
GUI::wxGetApp().stop_sync_user_preset();
}
@@ -5281,10 +5417,12 @@ void GUI_App::enable_user_preset_folder(bool enable)
std::string user_id = m_agent->get_user_id();
app_config->set("preset_folder", user_id);
GUI::wxGetApp().preset_bundle->update_user_presets_directory(user_id);
PluginManager::instance().set_cloud_user(user_id);
} else {
BOOST_LOG_TRIVIAL(info) << "preset_folder: set to empty";
app_config->set("preset_folder", "");
GUI::wxGetApp().preset_bundle->update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
PluginManager::instance().set_cloud_user("");
}
}
@@ -5322,12 +5460,27 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
});
if (online_login && provider == ORCA_CLOUD_PROVIDER) {
// The steps below run synchronously on the UI thread (cloud plugin fetch and
// user-preset load both block on network/disk). Show an indeterminate progress
// dialog so the window isn't frozen without feedback. Percentages are cosmetic
// milestones, not measured progress.
ProgressDialog dlg(_L("Loading"), _L("Syncing your account…"), 100, mainframe, wxPD_AUTO_HIDE | wxPD_APP_MODAL);
dlg.Update(10, _L("Migrating presets…"));
maybe_migrate_user_presets_on_login();
remove_user_presets();
enable_user_preset_folder(true);
dlg.Update(40, _L("Fetching plugins…"));
PluginManager::instance().fetch_plugins_from_cloud();
if (m_plugins_dlg)
m_plugins_dlg->update_plugin_dialog_ui();
dlg.Update(70, _L("Loading user presets…"));
preset_bundle->load_user_presets(m_agent->get_user_id(provider), ForwardCompatibilitySubstitutionRule::Enable);
mainframe->update_side_preset_ui();
dlg.Update(100);
GUI::wxGetApp().mainframe->show_sync_dialog();
}
@@ -5489,40 +5642,6 @@ struct UpdaterQuery
std::string os_info;
};
std::string detect_updater_os()
{
#if defined(_WIN32)
return "win";
#elif defined(__APPLE__)
return "macos";
#elif defined(__linux__) || defined(__LINUX__)
return "linux";
#else
return "unknown";
#endif
}
std::string detect_updater_arch()
{
#if defined(__aarch64__) || defined(_M_ARM64)
return "arm64";
#elif defined(__x86_64__) || defined(_M_X64)
return "x86_64";
#elif defined(__i386__) || defined(_M_IX86)
return "i386";
#else
std::string arch = wxPlatformInfo::Get().GetArchName().ToStdString();
boost::algorithm::to_lower(arch);
if (arch.find("aarch64") != std::string::npos || arch.find("arm64") != std::string::npos)
return "arm64";
if (arch.find("x86_64") != std::string::npos || arch.find("amd64") != std::string::npos)
return "x86_64";
if (arch.find("i686") != std::string::npos || arch.find("i386") != std::string::npos || arch.find("x86") != std::string::npos)
return "i386";
return "unknown";
#endif
}
std::string detect_updater_os_info()
{
wxString description = wxPlatformInfo::Get().GetOperatingSystemDescription();
@@ -5751,8 +5870,8 @@ void GUI_App::check_new_version_sf(bool show_tips, int by_user)
UpdaterQuery query{
detect_updater_iid(app_config),
detect_updater_version(),
detect_updater_os(),
detect_updater_arch(),
platform_os_type(),
platform_architecture(),
detect_updater_os_info()
};
@@ -8191,6 +8310,89 @@ void GUI_App::open_presetbundledialog(size_t open_on_tab, const std::string& hig
}
}
void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlight_option)
{
if (m_plugins_dlg) {
m_plugins_dlg->Show();
m_plugins_dlg->Raise();
return;
}
try {
m_plugins_dlg = new PluginsDialog(mainframe, wxID_ANY, _L("Plugins"));
m_plugins_dlg->set_open_terminal_dlg_fn();
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
if (event.GetEventObject() == m_plugins_dlg)
m_plugins_dlg = nullptr;
event.Skip();
});
m_plugins_dlg->Show();
m_plugins_dlg->Raise();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "open_plugins_dialog failed: " << e.what();
if (m_plugins_dlg) {
m_plugins_dlg->Destroy();
m_plugins_dlg = nullptr;
}
wxMessageBox(wxString::Format(_L("Failed to open the Plugins dialog:\n%s"), from_u8(e.what())), _L("Plugins"),
wxOK | wxICON_ERROR, mainframe);
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "open_plugins_dialog failed with a non-standard exception";
if (m_plugins_dlg) {
m_plugins_dlg->Destroy();
m_plugins_dlg = nullptr;
}
wxMessageBox(_L("Failed to open the Plugins dialog (unknown error)."), _L("Plugins"), wxOK | wxICON_ERROR, mainframe);
}
}
void GUI_App::open_terminal_dialog()
{
// Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
// inside the webview script-message callback, which GTK/macOS deliver synchronously
// (see ui_create_window in PluginHostUi.cpp). TerminalDialog hosts a webview of its
// own, so creating or presenting it on that stack is the same class as the Linux
// gtk_window_present crash — defer all window work to a clean main-loop iteration.
CallAfter([this]() {
if (m_terminal_dlg) {
// Re-front the existing window; guard Show() per #13657 (GTK re-enters
// layout when showing an already-visible window).
if (!m_terminal_dlg->IsShown())
m_terminal_dlg->Show();
m_terminal_dlg->Raise();
return;
}
m_terminal_dlg = new TerminalDialog(mainframe, wxID_ANY, _L("Plugin Terminal"),
wxDefaultPosition, wxSize(820, 600));
m_terminal_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
if (event.GetEventObject() == m_terminal_dlg)
m_terminal_dlg = nullptr;
event.Skip();
});
// Show() alone activates and fronts a freshly created window on every platform.
m_terminal_dlg->Show();
});
}
void GUI_App::open_speed_dial()
{
if (!mainframe)
return;
if (!m_speed_dial_dialog) {
m_speed_dial_dialog = new SpeedDialWebDialog(mainframe);
m_speed_dial_dialog->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
if (event.GetEventObject() == m_speed_dial_dialog)
m_speed_dial_dialog = nullptr;
event.Skip();
});
}
m_speed_dial_dialog->request_show();
}
void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::string& highlight_option)
{
bool app_layout_changed = false;
+16
View File
@@ -3,6 +3,7 @@
#include <memory>
#include <string>
#include "ActionRegistry.hpp"
#include "ImGuiWrapper.hpp"
#include "ConfigWizard.hpp"
#include "OpenGLManager.hpp"
@@ -85,6 +86,9 @@ class HMSQuery;
class ModelMallDialog;
class PingCodeBindDialog;
class NetworkErrorDialog;
class PluginsDialog;
class SpeedDialWebDialog;
class TerminalDialog;
enum FileType
@@ -550,6 +554,11 @@ public:
void update_single_bundle(wxCommandEvent& evt);
PresetBundleDialog* m_preset_bundle_dlg{nullptr};
PluginsDialog* m_plugins_dlg{nullptr};
SpeedDialWebDialog* m_speed_dial_dialog{nullptr};
TerminalDialog* m_terminal_dlg{nullptr};
ActionRegistry m_action_registry;
void start_http_server(const std::string& provider = ORCA_CLOUD_PROVIDER);
void start_http_server(int port, const std::string& provider = ORCA_CLOUD_PROVIDER);
@@ -617,6 +626,10 @@ 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());
void open_terminal_dialog();
void open_speed_dial();
ActionRegistry& action_registry() { return m_action_registry; }
void open_exportpresetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
virtual bool OnExceptionInMainLoop() override;
// Calls wxLaunchDefaultBrowser if user confirms in dialog.
@@ -766,6 +779,9 @@ private:
bool on_init_network(bool try_backup = false);
void init_networking_callbacks();
void init_app_config();
// GUI-side subscriptions to plugin loader events (dialog refresh,
// network-agent registration, plate revalidation).
void init_plugin_gui_wiring();
void remove_old_networking_plugins();
void drain_pending_events(int timeout_ms);
bool wait_for_network_idle(int timeout_ms);
-2
View File
@@ -61,8 +61,6 @@ void IMToolbar::del_stats_item()
void IMToolbar::set_enabled(bool enable)
{
m_enabled = enable;
if (!m_enabled)
is_render_finish = false;
}
bool IMReturnToolbar::init()
-1
View File
@@ -51,7 +51,6 @@ public:
float icon_height;
bool is_display_scrollbar;
bool show_stats_item{ false };
bool is_render_finish{false};
IMToolbar() {
icon_width = DEFAULT_TOOLBAR_BUTTON_WIDTH;
icon_height = DEFAULT_TOOLBAR_BUTTON_HEIGHT;
+4
View File
@@ -3158,6 +3158,9 @@ void ImGuiWrapper::render_draw_data(ImDrawData *draw_data)
shader->set_uniform("Texture", 0);
shader->set_uniform("ProjMtx", ortho_projection);
const uint8_t stage = 0;
shader->set_uniform("s_texture", stage);
// Will project scissor/clipping rectangles into framebuffer space
const ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
const ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
@@ -3222,6 +3225,7 @@ void ImGuiWrapper::render_draw_data(ImDrawData *draw_data)
glsafe(::glScissor((int)clip_min.x, (int)(fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y)));
// Bind texture, Draw
glsafe(::glActiveTexture(GL_TEXTURE0 + stage));
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID()));
glsafe(::glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx))));
}
+2
View File
@@ -348,6 +348,8 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr)
}
m_plater->update();
}
m_plater->mark_plate_toolbar_image_dirty();
}
}} // namespace Slic3r::GUI
+1
View File
@@ -267,6 +267,7 @@ void KBShortcutsDialog::fill_shortcuts()
{ "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") },
{ L("Tab"), L("Switch between Prepare/Preview") },
{ L("Space"), L("Open actions speed dial") },
};
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
+22 -7
View File
@@ -46,6 +46,7 @@
#include "Widgets/ProgressDialog.hpp"
#include "BindDialog.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#include "../Utils/PrintHost.hpp"
#include <fstream>
@@ -3274,10 +3275,11 @@ void MainFrame::init_menubar_as_editor()
},
"", nullptr, []() { return true; }, this);
m_topbar->GetTopMenu()->AppendSeparator();
auto top_menu = m_topbar->GetTopMenu();
top_menu->AppendSeparator();
append_menu_item(
m_topbar->GetTopMenu(), wxID_ANY, _L("Preset Bundle") + "\t", "",
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
wxGetApp().open_presetbundledialog();
@@ -3286,7 +3288,7 @@ void MainFrame::init_menubar_as_editor()
"", nullptr, []() { return true; }, this);
append_menu_item(
m_topbar->GetTopMenu(), wxID_ANY, _L("Sync Presets"), _L("Pull and apply the latest presets from OrcaCloud"),
top_menu, wxID_ANY, _L("Sync Presets"), _L("Pull and apply the latest presets from OrcaCloud"),
[this](wxCommandEvent&) {
if (!wxGetApp().is_user_login()) {
MessageDialog info_dlg(this, _L("You must be logged in to sync presets from cloud."),
@@ -3303,12 +3305,19 @@ void MainFrame::init_menubar_as_editor()
return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode();
}, this);
m_topbar->GetTopMenu()->AppendSeparator();
top_menu->AppendSeparator();
append_menu_item(
top_menu, wxID_ANY, _L("Plugins") + "\t", "",
[this](wxCommandEvent &) {
wxGetApp().open_plugins_dialog();
},
"", nullptr, []() { return true; }, this);
//m_topbar->AddDropDownMenuItem(preference_item);
//m_topbar->AddDropDownMenuItem(printer_item);
//m_topbar->AddDropDownMenuItem(language_item);
//m_topbar->AddDropDownMenuItem(config_item);
top_menu->AppendSeparator();
m_topbar->AddDropDownSubMenu(helpMenu, _L("Help"));
// SoftFever calibrations
@@ -3409,7 +3418,7 @@ void MainFrame::init_menubar_as_editor()
fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
[this](wxCommandEvent &) {
[this](wxCommandEvent&) {
wxGetApp().open_presetbundledialog();
plater()->get_current_canvas3D()->force_set_focus();
},
@@ -3433,6 +3442,13 @@ void MainFrame::init_menubar_as_editor()
return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode();
}, this);
fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Plugins"), "", [this](wxCommandEvent&) { wxGetApp().open_plugins_dialog(); }, "", nullptr,
[]() { return true; }, this);
fileMenu->AppendSeparator();
m_menubar->Append(fileMenu, wxString::Format("&%s", _L("File")));
if (editMenu)
m_menubar->Append(editMenu, wxString::Format("&%s", _L("Edit")));
@@ -4237,7 +4253,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey)
void MainFrame::load_printer_url()
{
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.use_bbl_device_tab())
if (preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin())
return;
auto cfg = preset_bundle.printers.get_edited_preset().config;
@@ -4445,4 +4461,3 @@ void SettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
} // GUI
} // Slic3r
+87
View File
@@ -2462,6 +2462,93 @@ void NotificationManager::push_orca_sync_conflict_notification(const std::string
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback), conflict_code), 0);
}
void NotificationManager::PluginMissingNotification::init()
{
PopNotification::init();
// Reserve body rows, an optional spacer, and a dedicated action row for the two links.
m_lines_count = m_lines_count + m_body.size() + (m_body.empty() ? 0 : 1) + 1;
}
void NotificationManager::PluginMissingNotification::render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y)
{
float x_offset = m_left_indentation;
float shift_y = m_line_height;
float starting_y = m_line_height / 2;
int last_end = 0;
std::string line;
for (size_t i = 0; i < m_endlines.size(); i++) {
if (m_text1.size() >= m_endlines[i]) {
line = m_text1.substr(last_end, m_endlines[i] - last_end);
last_end = m_endlines[i];
if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
ImGui::SetCursorPosX(x_offset);
ImGui::SetCursorPosY(starting_y + i * shift_y);
imgui.text(line.c_str());
}
}
const size_t body_start_row = m_endlines.size();
const std::string jump_text = _u8L("Jump to");
for (size_t i = 0; i < m_body.size(); ++i) {
const JumpTo& item = m_body[i];
const std::string item_text = item.text.empty() ? item.opt : item.text;
const std::string prefix = "- " + item_text + " ";
const float row_y = starting_y + (body_start_row + i) * shift_y;
ImGui::SetCursorPosX(x_offset);
ImGui::SetCursorPosY(row_y);
imgui.text(prefix.c_str());
std::string button_id = "##plugin_missing_jump_" + std::to_string(i);
const float jump_x = x_offset + ImGui::CalcTextSize(prefix.c_str()).x;
render_hyperlink_action(imgui, jump_x, row_y, jump_text, button_id.c_str(), [item] {
// Defer the jump: jump_to_option switches the settings tab/page, which must not run
// inside this notification's ImGui render pass. item is captured by value.
if (!item.opt.empty())
wxGetApp().CallAfter([item]() { wxGetApp().sidebar().jump_to_option(item.opt, item.opt_type, L""); });
});
}
const size_t action_row = body_start_row + m_body.size() + (m_body.empty() ? 0 : 1);
const float action_y = starting_y + action_row * shift_y;
render_hyperlink_action(imgui, x_offset, action_y, m_resolve_label, "##plugin_missing_resolve",
[this] { if (m_resolve_callback && m_resolve_callback(m_evt_handler)) close(); });
}
void NotificationManager::PluginMissingNotification::bbl_render_block_notif_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y)
{
const ImVec4 hyper_text_color = m_HyperTextColor;
const ImVec4 hyper_text_color_hover = m_HyperTextColorHover;
m_HyperTextColor = ImVec4(1.f, 1.f, 1.f, 1.f);
m_HyperTextColorHover = ImVec4(1.f, 1.f, 1.f, 0.75f);
render_text(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
m_HyperTextColor = hyper_text_color;
m_HyperTextColorHover = hyper_text_color_hover;
}
void NotificationManager::push_plugin_missing_notification(NotificationType type,
const std::string& text,
const std::string& resolve_label,
std::vector<JumpTo> body,
std::function<bool(wxEvtHandler*)> resolve_callback)
{
m_pop_notifications.erase(std::remove_if(m_pop_notifications.begin(), m_pop_notifications.end(),
[type](const std::unique_ptr<PopNotification>& notification) {
return notification && notification->get_type() == type;
}), m_pop_notifications.end());
NotificationData data{ type, NotificationLevel::ErrorNotificationLevel, 0, text };
push_notification_data(std::make_unique<NotificationManager::PluginMissingNotification>(
data, m_id_provider, m_evt_handler, resolve_label, std::move(body), std::move(resolve_callback)), 0);
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
{
// If already exists
+66
View File
@@ -21,6 +21,8 @@
#include <deque>
#include <unordered_set>
#include <libslic3r/Preset.hpp>
namespace Slic3r {
namespace GUI {
@@ -44,6 +46,13 @@ enum class InfoItemType;
#define BBL_NOTICE_MAX_INTERVAL 86400 * 10
struct JumpTo
{
std::string text;
std::string opt;
Preset::Type opt_type;
};
enum class NotificationType
{
CustomNotification = 0,
@@ -166,6 +175,16 @@ enum class NotificationType
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
// Active preset requires plugin capabilities that are not installed/loadable. Persistent,
// non-modal; offers install (cloud) / OrcaCloud search (local) and blocks slicing.
OrcaLocalPluginMissingError,
OrcaCloudPluginMissingError,
// Active preset references capabilities that are installed but not active (plugin not loaded, or
// capability disabled). Resolved locally by activating them; persistent, blocks slicing.
OrcaPluginInactiveError,
// Active preset references a capability the installed+loaded plugin does not provide (outdated
// plugin). Informational; cannot be auto-resolved; persistent, blocks slicing.
OrcaPluginCapabilityUnavailableError,
NotificationTypeCount
};
@@ -282,6 +301,13 @@ public:
int conflict_code,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback);
// Non-closable, persistent missing-plugin notification with a single resolve action (install /
// open OrcaCloud). The callback returns true to close the notification, or false to keep it
// visible while resolution continues.
void push_plugin_missing_notification(NotificationType type, const std::string& text,
const std::string& resolve_label,
std::vector<JumpTo> body,
std::function<bool(wxEvtHandler*)> resolve_callback);
// Download URL progress notif
void push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback);
@@ -925,6 +951,46 @@ private:
std::function<bool(wxEvtHandler*)> m_force_push_callback;
int conflict_code;
};
// Persistent, non-closable notification for preset plugin capabilities that are required but
// unavailable. Offers per-capability "Jump to" links and a single resolve action; it stays up
// until every missing plugin is resolved.
class PluginMissingNotification : public PopNotification
{
public:
PluginMissingNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler,
std::string resolve_label,
std::vector<JumpTo> body,
std::function<bool(wxEvtHandler*)> resolve_callback)
: PopNotification(n, id_provider, evt_handler)
, m_resolve_label(std::move(resolve_label))
, m_body(std::move(body))
, m_resolve_callback(std::move(resolve_callback))
{
m_multiline = true;
}
protected:
void init() override;
void render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
// Non-closable: the notification stays up until the missing plugins are resolved.
void render_close_button(ImGuiWrapper& /*imgui*/,
const float /*win_size_x*/, const float /*win_size_y*/,
const float /*win_pos_x*/, const float /*win_pos_y*/) override {}
void render_minimize_button(ImGuiWrapper& /*imgui*/,
const float /*win_pos_x*/, const float /*win_pos_y*/) override { m_minimize_b_visible = false; }
void bbl_render_block_notif_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
void bbl_render_block_notif_buttons(ImGuiWrapper& /*imgui*/,
ImVec2 /*win_size*/, ImVec2 /*win_pos*/) override {}
std::string m_resolve_label;
std::vector<JumpTo> m_body;
std::function<bool(wxEvtHandler*)> m_resolve_callback;
};
class SlicingProgressNotification;
// in HintNotification.hpp
+6 -1
View File
@@ -266,7 +266,12 @@ bool OpenGLManager::init_gl(bool popup_error)
else
s_compressed_textures_supported = false;
if (GLAD_GL_ARB_framebuffer_object) {
if (s_gl_info.is_version_greater_or_equal_to(3, 0)) {
// ARB framebuffer became a mandatory part of core OpenGL 3.0
s_framebuffers_type = EFramebufferType::Arb;
BOOST_LOG_TRIVIAL(info) << "Opengl version >= 30, FrameBuffer Type ARB." << std::endl;
}
else if (GLAD_GL_ARB_framebuffer_object) {
s_framebuffers_type = EFramebufferType::Arb;
BOOST_LOG_TRIVIAL(info) << "Found Framebuffer Type ARB."<< std::endl;
}
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -106,6 +106,7 @@ public:
wxWindow * stb;
const wxString icon;
const wxString title;
bool m_labels_hidden{false};
size_t label_width = 20 ;// {200};
wxSizer* sizer {nullptr};
OG_CustomCtrl* custom_ctrl{ nullptr };
@@ -185,7 +186,7 @@ public:
void clear_fields_except_of(const std::vector<std::string> left_fields);
void hide_labels() { label_width = 0; }
void hide_labels() { label_width = 0; m_labels_hidden = true; }
OptionsGroup(wxWindow *_parent, const wxString &title, const wxString &icon, bool is_tab_opt = false,
column_t extra_clmn = nullptr);
@@ -243,6 +244,9 @@ protected:
public:
static wxString get_url(const std::string& path_end);
static bool launch_browser(const std::string& path_end);
protected:
std::string pick_plugin(const ConfigOptionDef& opt);
};
class ConfigOptionsGroup: public OptionsGroup {
+11
View File
@@ -2433,6 +2433,9 @@ void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height
m_depth = depth;
m_height = height;
if (with_instance_move && m_plater)
m_plater->mark_plate_toolbar_image_dirty();
return;
}
@@ -2780,6 +2783,8 @@ int PartPlate::add_instance(int obj_id, int instance_id, bool move_position, Bou
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": plate %1% , m_ready_for_slice changes to %2%") % m_plate_index %m_ready_for_slice;
if (m_plater)
m_plater->mark_plate_toolbar_image_dirty();
return 0;
}
@@ -5344,6 +5349,9 @@ int PartPlateList::notify_instance_removed(int obj_id, int instance_id)
unprintable_plate.update_object_index(obj_id, m_model->objects.size());
}
if (m_plater)
m_plater->mark_plate_toolbar_image_dirty();
return 0;
}
@@ -6265,6 +6273,9 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
}
#endif
if (m_plater)
m_plater->mark_plate_toolbar_image_dirty();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after rebuild, plates count %1%") % m_plate_list.size();
return ret;
}
+280 -16
View File
@@ -1,15 +1,25 @@
#include "Plater.hpp"
#include "../Utils/NetworkAgent.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r_version.h"
#include <cstddef>
#include <algorithm>
#include <chrono>
#include <numeric>
#include <limits>
#include <optional>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PluginResolver.hpp>
#include <vector>
#include <string>
#include <regex>
#include <future>
#include <thread>
#include <atomic>
#include <mutex>
#include <boost/algorithm/string.hpp>
#include <boost/iterator/counting_iterator.hpp>
#include <boost/optional.hpp>
@@ -21,6 +31,7 @@
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <wx/msgdlg.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/button.h>
@@ -37,6 +48,8 @@
#include <wx/debug.h>
#include <wx/busyinfo.h>
#include <wx/event.h>
#include <wx/evtloop.h>
#include <wx/timer.h>
#include <wx/wrapsizer.h>
#ifdef _WIN32
#include <wx/richtooltip.h>
@@ -277,6 +290,21 @@ void Plater::show_illegal_characters_warning(wxWindow* parent)
show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\"");
}
void Plater::mark_plate_toolbar_image_dirty()
{
m_b_plate_toolbar_image_dirty = true;
}
bool Plater::is_plate_toolbar_image_dirty() const
{
return m_b_plate_toolbar_image_dirty;
}
void Plater::clear_plate_toolbar_image_dirty()
{
m_b_plate_toolbar_image_dirty = false;
}
static std::map<BedType, std::string> bed_type_thumbnails = {
{BedType::btPC, "bed_cool" },
{BedType::btEP, "bed_engineering" },
@@ -3199,6 +3227,7 @@ void Sidebar::update_all_preset_comboboxes()
auto p_mainframe = wxGetApp().mainframe;
auto cfg = preset_bundle.printers.get_edited_preset().config;
const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin();
if (preset_bundle.use_bbl_network()) {
//only show connection button for not-BBL printer
@@ -3235,7 +3264,8 @@ void Sidebar::update_all_preset_comboboxes()
print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode;
}
p_mainframe->load_printer_url(url, apikey);
if (!use_native_device_tab)
p_mainframe->load_printer_url(url, apikey);
p_mainframe->set_print_button_to_default(print_btn_type);
@@ -3305,8 +3335,7 @@ void Sidebar::update_all_preset_comboboxes()
update_printer_thumbnail();
}
// Orca:: show device tab based on vendor type
p_mainframe->show_device(preset_bundle.use_bbl_device_tab());
p_mainframe->show_device(use_native_device_tab);
p_mainframe->m_tabpanel->SetSelection(p_mainframe->m_tabpanel->GetSelection());
}
@@ -5201,6 +5230,14 @@ struct Plater::priv
bool m_ignore_event{false};
bool m_slice_all{false};
bool m_is_slicing {false};
// Missing-plugin set signatures (sorted full refs joined by '\n'), one per notification. They
// gate plugin-load re-validation and avoid needlessly recreating the notification when the set
// is unchanged. Whether missing plugins block slicing is derived directly from PluginResolver
// (has_missing_plugins()), not cached here.
std::string m_local_missing_shown_sig;
std::string m_cloud_missing_shown_sig;
std::string m_inactive_shown_sig;
std::string m_broken_shown_sig;
bool auto_reslice_pending {false};
bool auto_reslice_after_cancel {false};
bool m_is_publishing {false};
@@ -5992,6 +6029,10 @@ 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(); });
@@ -6326,10 +6367,8 @@ void Plater::priv::update(unsigned int flags)
//BBS assemble view
this->assemble_view->reload_scene(false, flags);
if (current_panel && is_preview_shown()) {
q->force_update_all_plate_thumbnails();
//update_fff_scene_only_shells(true);
}
// todo: better to mark thumbnail dirty here
q->mark_plate_toolbar_image_dirty();
if (force_background_processing_restart)
this->restart_background_process(update_status);
@@ -7884,6 +7923,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
q->schedule_background_process(true);
q->mark_plate_toolbar_image_dirty();
return obj_idxs;
}
@@ -8263,6 +8303,8 @@ void Plater::priv::object_list_changed()
main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice);
wxGetApp().params_panel()->notify_object_config_changed();
q->mark_plate_toolbar_image_dirty();
}
void Plater::priv::select_curr_plate_all()
@@ -9166,6 +9208,8 @@ void Plater::priv::update_fff_scene()
view3D->reload_scene(true);
//BBS: add assemble view related logic
assemble_view->reload_scene(true);
q->mark_plate_toolbar_image_dirty();
}
//BBS: add print project related logic
@@ -10074,9 +10118,6 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice)
preview->get_canvas3d()->enable_select_plate_toolbar(true);
}
}
else {
preview->get_canvas3d()->enable_select_plate_toolbar(false);
}
if (current_panel == panel)
{
@@ -10935,6 +10976,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":finished, reload print soon");
m_is_slicing = false;
this->preview->reload_print(false);
q->mark_plate_toolbar_image_dirty();
/* BBS if in publishing progress */
if (m_is_publishing) {
if (m_publish_dlg && !m_publish_dlg->was_cancelled()) {
@@ -11146,8 +11188,13 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
update_sidebar();
int old_sel = e.GetOldSelection();
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_device_tab() && new_sel == MainFrame::tpMonitor) {
if (!Slic3r::NetworkAgent::is_network_module_loaded()) {
const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin();
const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin);
if (use_native_device_tab && new_sel == MainFrame::tpMonitor) {
// BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
e.Veto();
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel;
if (q) {
@@ -13929,8 +13976,13 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i
_obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
_obj->config.set_key_value("gap_fill_target", new ConfigOptionEnum<GapFillTarget>(GapFillTarget::gftNowhere));
print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0));
// ORCA: print the top surface spiral from the center outwards, so the tiles are comparable.
_obj->config.set_key_value("top_surface_fill_order", new ConfigOptionEnum<SurfaceFillOrder>(SurfaceFillOrder::Outward));
// ORCA: request the calibration's special toolpath order (chords first, center spiral
// last and inside-out) so opposing directions collide into the tactile lip the test
// reads. The special order only applies while the fill order is Default, so reset the
// profile's fill order on the calibration objects; changing the setting on the object
// afterwards deliberately overrides the special order.
_obj->config.set_key_value("calib_flowrate_topinfill_special_order", new ConfigOptionBool(true));
_obj->config.set_key_value("top_surface_fill_order", new ConfigOptionEnum<SurfaceFillOrder>(SurfaceFillOrder::Default));
// extract flowrate from name, filename format: flowrate_xxx
std::string obj_name = _obj->name;
@@ -14667,6 +14719,7 @@ void Plater::invalid_all_plate_thumbnails()
plate->thumbnail_data.reset();
plate->no_light_thumbnail_data.reset();
}
mark_plate_toolbar_image_dirty();
}
void Plater::force_update_all_plate_thumbnails()
@@ -14677,7 +14730,6 @@ void Plater::force_update_all_plate_thumbnails()
invalid_all_plate_thumbnails();
update_all_plate_thumbnails(true);
}
get_preview_canvas3D()->update_plate_thumbnails();
}
// BBS: backup
@@ -16749,7 +16801,19 @@ void Plater::reslice()
// and notify user that he should leave it first.
if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
return;
// Enforce the missing-plugin block at the slicing choke point: menu/keyboard/queued triggers can
// carry a stale enabled state while plugins load asynchronously. refresh_missing_plugin_block
// rebuilds the missing sets and notifications from the active presets without running
// Print::validate; all other validation keeps upstream behavior and is surfaced by
// update_background_process() below.
if (refresh_missing_plugin_block()) {
p->partplate_list.get_curr_plate()->update_slice_ready_status(false);
p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": required plugins missing; slicing blocked.";
return;
}
// Stop the running (and queued) UI jobs and only proceed if they actually
// get stopped.
unsigned timeout_ms = 10000;
@@ -18680,12 +18744,212 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error)
}*/
}
// Missing-plugin validation (both technologies): block slicing while the active preset(s)
// reference plugin capabilities that are not installed/loadable here. The helper rebuilds the
// missing sets, manages the notifications, and reports whether slicing is blocked. plugins_block_changed
// is set when the block toggled, so the Slice button can be refreshed below.
bool plugins_block_changed = false;
if (refresh_missing_plugin_block(&plugins_block_changed)) {
model_fits = false;
validate_error = true;
}
PartPlate* part_plate = p->partplate_list.get_curr_plate();
part_plate->update_slice_ready_status(model_fits);
// The toolbar Slice button is normally refreshed only by the canvas
// (EVT_GLCANVAS_ENABLE_ACTION_BUTTONS) on geometry updates. When the missing-plugin block toggles
// without a geometry change (e.g. a plugin finishing loading, or a setting edit that drops the
// last missing plugin), refresh it here — AFTER update_slice_ready_status set the plate's
// can_slice() flag that get_enable_slice_status() reads — so the button doesn't lag until the
// next bed click.
if (plugins_block_changed && !p->background_process.running())
p->main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, model_fits);
return;
}
bool Plater::refresh_missing_plugin_block(bool* block_toggled)
{
// PluginResolver owns the per-preset-type missing sets; rebuild them from each preset's own
// "plugins" manifest so the state is always fresh. A plugin is resolved when it is
// installed/loaded, or when no active setting references it any more (refresh drops it then).
// The block is derived solely from PluginResolver; snapshot it before the refresh to detect a
// toggle (so the caller can refresh the Slice button).
const bool was_blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
refresh_missing_plugins(*wxGetApp().preset_bundle);
const auto missing_refs = [](const std::vector<MissingPlugin>& missing) {
std::vector<std::string> refs;
refs.reserve(missing.size());
for (const MissingPlugin& m : missing)
refs.emplace_back(create_full_ref(m.ref));
return refs;
};
const auto signature = [&missing_refs](const std::vector<MissingPlugin>& missing) {
std::vector<std::string> refs = missing_refs(missing);
std::sort(refs.begin(), refs.end());
std::string sig;
for (const std::string& r : refs) { sig += r; sig += '\n'; }
return sig;
};
// Show/refresh the non-closable notification for one missing set. Only (re)create it when the
// set changes; pushing every validate would close+recreate it (flicker, reset hover) since
// validate runs on many triggers. shown_sig also gates plugin-load re-validation.
const auto update = [&](NotificationType type, const std::vector<MissingPlugin>& missing,
std::string* shown_sig, const std::string& header,
const std::string& resolve_label,
std::function<bool(wxEvtHandler*)> resolve_action) {
if (missing.empty()) {
if (!shown_sig->empty()) {
p->notification_manager->close_notification_of_type(type);
shown_sig->clear();
}
return;
}
const std::string sig = signature(missing);
if (*shown_sig != sig) {
std::vector<JumpTo> body;
for (const auto& m : missing)
body.emplace_back(JumpTo{m.ref.capability_name, m.opt, m.opt_type});
p->notification_manager->push_plugin_missing_notification(
type, header, resolve_label, std::move(body), std::move(resolve_action));
*shown_sig = sig;
}
};
const std::vector<MissingPlugin> missing_cloud = get_missing_cloud_plugins();
const std::vector<MissingPlugin> missing_local = get_missing_local_plugins();
const std::vector<std::string> missing_cloud_refs = missing_refs(missing_cloud);
const std::vector<std::string> missing_local_refs = missing_refs(missing_local);
update(NotificationType::OrcaCloudPluginMissingError, missing_cloud,
&p->m_cloud_missing_shown_sig,
_u8L("OrcaCloud plugins required by the current preset are not installed:"),
_u8L("Install Plugins"),
[this, missing_cloud_refs](wxEvtHandler*) { install_missing_cloud_plugins(missing_cloud_refs); return false; });
// "Find on OrcaCloud" is only a suggestion: it opens the browser but cannot resolve the missing
// plugin in-session, so it never closes the notification or unblocks slicing. The user resolves a
// local plugin by installing it or by changing the setting that needs it.
update(NotificationType::OrcaLocalPluginMissingError, missing_local,
&p->m_local_missing_shown_sig,
_u8L("Local plugins required by the current preset are missing:"),
_u8L("Find on OrcaCloud"),
[missing_local_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(missing_local_refs); return false; });
const std::vector<MissingPlugin> inactive = get_inactive_plugins();
const std::vector<MissingPlugin> broken = get_broken_plugins();
const std::vector<std::string> inactive_refs = missing_refs(inactive);
const std::vector<std::string> broken_refs = missing_refs(broken);
update(NotificationType::OrcaPluginInactiveError, inactive,
&p->m_inactive_shown_sig,
_u8L("Plugins required by the current preset are not activated:"),
_u8L("Activate Now"),
[this, inactive_refs](wxEvtHandler*) { enable_inactive_plugins(inactive_refs); return false; });
update(NotificationType::OrcaPluginCapabilityUnavailableError, broken,
&p->m_broken_shown_sig,
_u8L("The installed plugin does not provide the required capability — it may be outdated:"),
_u8L("Find on OrcaCloud"),
[broken_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(broken_refs); return false; });
const bool blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
if (block_toggled)
*block_toggled = (was_blocked != blocked);
return blocked;
}
void Plater::revalidate_current_plate_if_plugins_missing()
{
// Only do work while a missing-plugin notification is up, so the plugin-load hook does not
// trigger a full validation for every plugin that loads during normal startup/use.
if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() &&
p->m_inactive_shown_sig.empty() && p->m_broken_shown_sig.empty())
return;
bool model_fits = true, validate_error = false;
validate_current_plate(model_fits, validate_error);
}
void Plater::install_missing_cloud_plugins(const std::vector<std::string>& cloud_refs)
{
if (cloud_refs.empty())
return;
// Shared between the UI-thread dialog/timer and the resolver's worker thread.
struct InstallProgressState
{
std::atomic<bool> cancel{false};
std::atomic<bool> finished{false};
std::atomic<bool> torn_down{false};
std::mutex mtx;
std::string message;
};
auto state = std::make_shared<InstallProgressState>();
state->message = _u8L("Preparing to install plugins...");
wxWindow* parent = wxGetApp().mainframe;
auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100,
parent, wxPD_APP_MODAL | wxPD_CAN_ABORT);
dialog->Pulse();
// UI-thread timer: animate the pulse, observe the Cancel button, and tear down when the worker
// signals completion. The timer is deleted via CallAfter so it is never freed inside its own
// handler.
auto* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [this, dialog, timer, state](wxTimerEvent&) {
std::string msg;
{
std::lock_guard<std::mutex> lock(state->mtx);
msg = state->message;
}
// Once cancellation is requested, the in-flight plugin still has to finish; reflect that.
if (state->cancel)
msg = _u8L("Cancelling — finishing the current plugin...");
if (!dialog->Pulse(from_u8(msg)))
state->cancel = true;
// Tear down exactly once: Stop() prevents further ticks, but guard so a stale queued tick
// can never double-Destroy the dialog or double-delete the timer.
if (state->finished && !state->torn_down.exchange(true)) {
timer->Stop();
dialog->Destroy();
wxGetApp().CallAfter([timer]() { delete timer; });
revalidate_current_plate_if_plugins_missing();
}
});
timer->Start(100);
PluginInstallProgress progress;
progress.on_plugin_begin = [state](const std::string& name, std::size_t /*index*/, std::size_t /*total*/) {
std::lock_guard<std::mutex> lock(state->mtx);
state->message = (boost::format(_u8L("Installing %1%...")) % name).str();
};
progress.is_cancelled = [state]() { return state->cancel.load(); };
progress.on_finished = [state]() { state->finished = true; };
resolve_missing_plugins(cloud_refs, std::move(progress));
}
void Plater::enable_inactive_plugins(const std::vector<std::string>& refs)
{
if (refs.empty())
return;
// Local and instant — load the plugin and/or enable the capability. The plugin-load callback
// re-validates the plate and clears (or reclassifies) the notification; no progress dialog needed.
resolve_inactive_plugins(refs);
}
bool Plater::plugins_block_slicing() const
{
// Single source of truth: slicing is blocked while PluginResolver still has unresolved plugin
// references — missing (download), inactive (activate), or broken (capability unavailable).
return has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
}
void Plater::open_platesettings_dialog(wxCommandEvent& evt) {
int plate_index = evt.GetInt();
PlateSettingsDialog dlg(this, _L("Plate Settings"), evt.GetString() == "only_layer_sequence");
+24
View File
@@ -719,6 +719,25 @@ public:
//BBS: partplate list related functions
PartPlateList& get_partplate_list();
void validate_current_plate(bool& model_fits, bool& validate_error);
// Rebuild the missing-plugin sets from the active presets and (re)show/close their notifications.
// Returns true when slicing must be blocked (a referenced plugin is still missing); sets
// *block_toggled when the blocked state changed since the previous call (so the caller can refresh
// the Slice button). Helper for validate_current_plate and the reslice() gate.
bool refresh_missing_plugin_block(bool* block_toggled = nullptr);
// Re-run plate validation when a plugin finishes loading, but only while a missing-plugin
// notification is active, so it clears automatically once the required plugin is available.
void revalidate_current_plate_if_plugins_missing();
// Install the given missing cloud plugin refs, showing an app-modal pulsing progress dialog
// (Cancel stops before the next plugin). Runs the blocking install on a worker thread; the
// dialog is driven from the UI thread. Clears the missing-plugin notification once resolved.
void install_missing_cloud_plugins(const std::vector<std::string>& cloud_refs);
// Activate the given inactive plugin refs (load the plugin / enable the capability). No progress
// dialog; the loads run on a background worker that re-validates the plate once they settle, so
// the notification clears (or flips to broken if the plugin lacks the capability).
void enable_inactive_plugins(const std::vector<std::string>& refs);
// True when the active preset references plugins that are missing and not yet acknowledged, as
// of the last validate_current_plate. Other slice-ready writers consult this to stay consistent.
bool plugins_block_slicing() const;
//BBS: select the plate by index
int select_plate(int plate_index, bool need_slice = false);
//BBS: update progress result
@@ -936,6 +955,10 @@ public:
bool is_loading_project() const { return m_loading_project; }
void mark_plate_toolbar_image_dirty();
bool is_plate_toolbar_image_dirty() const;
void clear_plate_toolbar_image_dirty();
private:
struct priv;
std::unique_ptr<priv> p;
@@ -956,6 +979,7 @@ private:
std::string m_preview_only_filename;
int m_valid_plates_count { 0 };
int m_check_status = 0; // 0 not check, 1 check success, 2 check failed
bool m_b_plate_toolbar_image_dirty{ true };
void suppress_snapshots();
void allow_snapshots();
+190
View File
@@ -0,0 +1,190 @@
#include "PluginPickerDialog.hpp"
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/valgen.h>
#include "GUI.hpp"
#include "I18N.hpp"
namespace Slic3r { namespace GUI {
PluginPickerDialog::PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
const std::vector<Slic3r::PluginDescriptor>& plugins)
: wxDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label))
, m_plugins(plugins)
, m_capability_mode(false)
{
build_ui(plugin_type_label);
CentreOnParent();
}
PluginPickerDialog::PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
std::vector<CapabilityEntry> capabilities)
: wxDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label))
, m_capabilities(std::move(capabilities))
, m_capability_mode(true)
{
build_capability_ui(plugin_type_label);
CentreOnParent();
}
void PluginPickerDialog::build_ui(const wxString& plugin_type_label)
{
const bool has_plugins = !m_plugins.empty();
auto* top_sizer = new wxBoxSizer(wxVERTICAL);
auto* info_text = new wxStaticText(this, wxID_ANY,
wxString::Format(_L("Choose a %s plugin from the list below."), plugin_type_label));
top_sizer->Add(info_text, 0, wxALL | wxEXPAND, 10);
wxArrayString choices;
choices.reserve(m_plugins.size());
for (const auto& plugin : m_plugins) {
wxString label = from_u8(plugin.name);
if (!plugin.version.empty())
label += wxString::Format(" (%s)", from_u8(plugin.version));
choices.Add(label);
}
m_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
if (has_plugins) {
m_choice->SetSelection(0);
m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) {
update_description(evt.GetSelection());
});
} else {
m_choice->Enable(false);
}
top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, 10);
m_description = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_description->Wrap(400);
top_sizer->Add(m_description, 0, wxALL | wxEXPAND, 10);
if (has_plugins)
update_description(0);
else
m_description->SetLabel(_L("No plugins found for this type."));
auto* button_sizer = new wxStdDialogButtonSizer();
auto* ok_button = new wxButton(this, wxID_OK);
ok_button->Enable(has_plugins);
button_sizer->AddButton(ok_button);
button_sizer->AddButton(new wxButton(this, wxID_CANCEL));
button_sizer->Realize();
top_sizer->Add(button_sizer, 0, wxALL | wxALIGN_RIGHT, 10);
SetSizerAndFit(top_sizer);
}
void PluginPickerDialog::build_capability_ui(const wxString& plugin_type_label)
{
const bool has_capabilities = !m_capabilities.empty();
auto* top_sizer = new wxBoxSizer(wxVERTICAL);
auto* info_text = new wxStaticText(this, wxID_ANY,
wxString::Format(_L("Choose a %s plugin from the list below."), plugin_type_label));
top_sizer->Add(info_text, 0, wxALL | wxEXPAND, 10);
wxArrayString choices;
choices.reserve(m_capabilities.size());
for (const auto& cap : m_capabilities)
choices.Add(cap.label);
m_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
if (has_capabilities) {
m_choice->SetSelection(0);
m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) {
update_capability_description(evt.GetSelection());
});
} else {
m_choice->Enable(false);
}
top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, 10);
m_description = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_description->Wrap(400);
top_sizer->Add(m_description, 0, wxALL | wxEXPAND, 10);
if (has_capabilities)
update_capability_description(0);
else
m_description->SetLabel(_L("No plugins found for this type."));
auto* button_sizer = new wxStdDialogButtonSizer();
auto* ok_button = new wxButton(this, wxID_OK);
ok_button->Enable(has_capabilities);
button_sizer->AddButton(ok_button);
button_sizer->AddButton(new wxButton(this, wxID_CANCEL));
button_sizer->Realize();
top_sizer->Add(button_sizer, 0, wxALL | wxALIGN_RIGHT, 10);
SetSizerAndFit(top_sizer);
}
PluginPickerDialog::CapabilityEntry PluginPickerDialog::selected_capability() const
{
if (!m_choice || !m_choice->IsEnabled())
return {};
int sel = m_choice->GetSelection();
if (sel < 0 || static_cast<size_t>(sel) >= m_capabilities.size())
return {};
return m_capabilities[static_cast<size_t>(sel)];
}
void PluginPickerDialog::update_capability_description(int selection)
{
if (!m_description)
return;
if (selection < 0 || static_cast<size_t>(selection) >= m_capabilities.size()) {
m_description->SetLabel(wxEmptyString);
return;
}
const auto& cap = m_capabilities[static_cast<size_t>(selection)];
m_description->SetLabel(cap.description.empty() ? cap.label : cap.description);
m_description->Wrap(400);
Layout();
}
std::string PluginPickerDialog::selected_plugin_key() const
{
if (!m_choice || !m_choice->IsEnabled())
return {};
int selection = m_choice->GetSelection();
if (selection < 0 || static_cast<size_t>(selection) >= m_plugins.size())
return {};
const auto& plugin = m_plugins[static_cast<size_t>(selection)];
return plugin.plugin_key;
}
void PluginPickerDialog::update_description(int selection)
{
if (!m_description)
return;
if (selection < 0 || static_cast<size_t>(selection) >= m_plugins.size()) {
m_description->SetLabel(wxEmptyString);
return;
}
const auto& plugin = m_plugins[static_cast<size_t>(selection)];
wxString desc;
if (!plugin.description.empty())
desc = from_u8(plugin.description);
else
desc = wxString::Format(_L("Plugin file: %s"), from_u8(plugin.entry_path));
m_description->SetLabel(desc);
m_description->Wrap(400);
Layout();
}
}} // namespace Slic3r::GUI
+58
View File
@@ -0,0 +1,58 @@
#ifndef SLIC3R_GUI_PLUGINPICKERDIALOG_HPP
#define SLIC3R_GUI_PLUGINPICKERDIALOG_HPP
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
# include <wx/wx.h>
#endif
#include <vector>
#include <string>
#include "slic3r/plugin/PluginManager.hpp"
namespace Slic3r { namespace GUI {
class PluginPickerDialog : public wxDialog
{
public:
// Entry for capability-level selection (plugin_type non-empty path).
struct CapabilityEntry {
std::string plugin_key;
std::string name;
wxString label;
wxString description;
};
// Existing constructor: offer all loaded plugin packages (plugin_type empty path).
PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
const std::vector<Slic3r::PluginDescriptor>& plugins);
// New constructor: offer a list of capabilities (plugin_type non-empty path).
PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
std::vector<CapabilityEntry> capabilities);
// Returns the plugin_key of the selected plugin package (package path).
std::string selected_plugin_key() const;
// Returns the {plugin_key, name} of the selected capability (capability path).
CapabilityEntry selected_capability() const;
private:
void build_ui(const wxString& plugin_type_label);
void build_capability_ui(const wxString& plugin_type_label);
void update_description(int selection);
void update_capability_description(int selection);
wxChoice* m_choice { nullptr };
wxStaticText* m_description { nullptr };
std::vector<Slic3r::PluginDescriptor> m_plugins;
std::vector<CapabilityEntry> m_capabilities;
bool m_capability_mode { false };
};
}} // namespace Slic3r::GUI
#endif // SLIC3R_GUI_PLUGINPICKERDIALOG_HPP
+133
View File
@@ -0,0 +1,133 @@
#include "PluginProgressDialog.hpp"
#include <utility>
namespace Slic3r { namespace GUI {
PluginProgressDialog::PluginProgressDialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed)
: ProgressDialog(title, message, maximum, parent, style)
, m_pulse_message(message)
, m_on_destroyed(std::move(on_destroyed))
{
// The base ProgressDialog already binds wxEVT_CLOSE_WINDOW (its OnClose vetoes
// a non-cancelable dialog and marks a cancelable one Canceled). We deliberately
// don't add a second handler: a user-initiated close surfaces to the plugin as
// update()/pulse() returning false, and programmatic close() calls Destroy()
// directly, so no extra wiring is needed here.
}
PluginProgressDialog::~PluginProgressDialog()
{
stop_pulse();
if (m_on_destroyed)
m_on_destroyed();
}
PluginProgressDialog* PluginProgressDialog::create_dialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed)
{
return new PluginProgressDialog(parent, title, message, maximum, style, std::move(on_destroyed));
}
bool PluginProgressDialog::pulse(PluginProgressDialog* dialog, const wxString& message)
{
return dialog != nullptr ? dialog->pulse(message) : false;
}
bool PluginProgressDialog::update(PluginProgressDialog* dialog, int value, const wxString& message)
{
return dialog != nullptr ? dialog->update(value, message) : false;
}
void PluginProgressDialog::start_pulse(PluginProgressDialog* dialog, int interval_ms, const wxString& message)
{
if (dialog != nullptr)
dialog->start_pulse(interval_ms, message);
}
void PluginProgressDialog::stop_pulse(PluginProgressDialog* dialog)
{
if (dialog != nullptr)
dialog->stop_pulse();
}
void PluginProgressDialog::request_close(PluginProgressDialog* dialog)
{
if (dialog != nullptr)
dialog->close();
}
bool PluginProgressDialog::pulse(const wxString& message)
{
if (!m_open)
return false;
if (!message.empty())
m_pulse_message = message;
return Pulse(message);
}
bool PluginProgressDialog::update(int value, const wxString& message)
{
if (!m_open)
return false;
if (!message.empty())
m_pulse_message = message;
return Update(value, message);
}
void PluginProgressDialog::start_pulse(int interval_ms, const wxString& message)
{
if (!m_open)
return;
if (!message.empty())
m_pulse_message = message;
stop_pulse();
m_timer = std::make_unique<wxTimer>(this);
Bind(wxEVT_TIMER, &PluginProgressDialog::on_timer, this, m_timer->GetId());
m_timer->Start(interval_ms > 0 ? interval_ms : 1);
}
void PluginProgressDialog::stop_pulse()
{
if (!m_timer)
return;
Unbind(wxEVT_TIMER, &PluginProgressDialog::on_timer, this, m_timer->GetId());
m_timer->Stop();
m_timer.reset();
}
void PluginProgressDialog::close()
{
if (!m_open)
return;
m_open = false;
stop_pulse();
Destroy();
}
void PluginProgressDialog::on_timer(wxTimerEvent& /*event*/)
{
if (m_open)
Pulse(m_pulse_message);
}
}} // namespace Slic3r::GUI
+72
View File
@@ -0,0 +1,72 @@
#ifndef slic3r_GUI_PluginProgressDialog_hpp_
#define slic3r_GUI_PluginProgressDialog_hpp_
#include <functional>
#include <memory>
#include <wx/event.h>
#include <wx/string.h>
#include <wx/timer.h>
#include <wx/window.h>
#include "Widgets/ProgressDialog.hpp"
namespace Slic3r { namespace GUI {
// A host-owned progress dialog for Python plugins. This class is deliberately
// Python-agnostic; the plugin layer owns any pybind/GIL concerns and marshals
// all calls to the UI thread.
//
// It derives from OrcaSlicer's own Slic3r::GUI::ProgressDialog (a real wxDialog
// with themable wx children) rather than the native wxProgressDialog: on Windows
// wxProgressDialog is a comctl32 TaskDialog running on a worker thread with no
// recolorable wx surface, so it cannot follow OrcaSlicer's (OS-independent) dark
// theme. The base themes itself via UpdateDlgDarkUI(this) in its Create().
class PluginProgressDialog : public ProgressDialog
{
public:
using CloseHandler = std::function<void()>;
// on_destroyed runs from the destructor on every path and must touch
// host-side state only (no Python / no derived members).
PluginProgressDialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed = nullptr);
~PluginProgressDialog() override;
// Convenience helpers for plugin-host callers. MAIN-THREAD ONLY.
static PluginProgressDialog* create_dialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed);
static bool pulse(PluginProgressDialog* dialog, const wxString& message = wxEmptyString);
static bool update(PluginProgressDialog* dialog, int value, const wxString& message = wxEmptyString);
static void start_pulse(PluginProgressDialog* dialog, int interval_ms, const wxString& message = wxEmptyString);
static void stop_pulse(PluginProgressDialog* dialog);
static void request_close(PluginProgressDialog* dialog);
// MAIN-THREAD ONLY.
bool pulse(const wxString& message = wxEmptyString);
bool update(int value, const wxString& message = wxEmptyString);
void start_pulse(int interval_ms, const wxString& message = wxEmptyString);
void stop_pulse();
void close();
bool is_open() const { return m_open; }
private:
void on_timer(wxTimerEvent& event);
bool m_open{true};
wxString m_pulse_message;
std::unique_ptr<wxTimer> m_timer;
CloseHandler m_on_destroyed;
};
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_PluginProgressDialog_hpp_
+210
View File
@@ -0,0 +1,210 @@
#pragma once
#include "PluginSource.hpp"
#include "PluginStatus.hpp"
#include "libslic3r/Semver.hpp"
#include <algorithm>
#include <cctype>
#include <string>
#include <string_view>
#include <vector>
namespace Slic3r::GUI
{
enum class PluginSortKey
{
Status,
Name,
Source,
Version,
// why: neutral "no column selected" state - clearing a header sort returns here and the
// list falls to compare_plugin_base_order only. Header UI reaches it via the asc/desc/clear cycle.
None
};
enum class PluginSortOrder
{
Asc,
Desc
};
inline std::string to_string(PluginSortKey sort_key)
{
switch (sort_key)
{
case PluginSortKey::Status: return "status";
case PluginSortKey::Name: return "name";
case PluginSortKey::Source: return "source";
case PluginSortKey::Version: return "version";
case PluginSortKey::None: return "none";
}
return "status";
}
inline std::string to_string(PluginSortOrder sort_order)
{
return sort_order == PluginSortOrder::Desc ? "desc" : "asc";
}
inline PluginSortKey plugin_sort_key_from_string(const std::string& sort_key, PluginSortKey fallback)
{
if (sort_key == "status")
return PluginSortKey::Status;
if (sort_key == "name")
return PluginSortKey::Name;
if (sort_key == "source")
return PluginSortKey::Source;
if (sort_key == "version")
return PluginSortKey::Version;
if (sort_key == "none")
return PluginSortKey::None;
return fallback;
}
inline PluginSortOrder plugin_sort_order_from_string(const std::string& sort_order, PluginSortOrder fallback)
{
if (sort_order == "asc")
return PluginSortOrder::Asc;
if (sort_order == "desc")
return PluginSortOrder::Desc;
return fallback;
}
// Natural, case-insensitive ASCII compare returning -1 / 0 / +1. Digit runs compare by
// numeric value; other chars compare lowercased; on a prefix tie the shorter string is less.
// e.g. "item2" < "item10" (2 < 10, not '2' > '1')
// "Camera" == "camera" (case ignored)
// "app" < "apple" (prefix is shorter)
// "1" < "01" (equal value, fewer leading zeros wins the tie)
// note: ASCII only - no locale/Unicode; accented or non-Latin names fall back to byte order.
inline int compare_ascii_case_insensitive_natural(const std::string& lhs, const std::string& rhs)
{
std::size_t li = 0;
std::size_t ri = 0;
while (li < lhs.size() && ri < rhs.size())
{
const unsigned char lc = static_cast<unsigned char>(lhs[li]);
const unsigned char rc = static_cast<unsigned char>(rhs[ri]);
if (std::isdigit(lc) && std::isdigit(rc))
{
const std::size_t lhs_digit_begin = li;
const std::size_t rhs_digit_begin = ri;
while (li < lhs.size() && std::isdigit(static_cast<unsigned char>(lhs[li])))
++li;
while (ri < rhs.size() && std::isdigit(static_cast<unsigned char>(rhs[ri])))
++ri;
const std::string_view lhs_run(lhs.data() + lhs_digit_begin, li - lhs_digit_begin);
const std::string_view rhs_run(rhs.data() + rhs_digit_begin, ri - rhs_digit_begin);
// why: digit runs compare numerically; leading zeros only break exact ties ("1" < "01").
const std::string_view lhs_num = lhs_run.substr(std::min(lhs_run.find_first_not_of('0'), lhs_run.size()));
const std::string_view rhs_num = rhs_run.substr(std::min(rhs_run.find_first_not_of('0'), rhs_run.size()));
if (lhs_num.size() != rhs_num.size())
return lhs_num.size() < rhs_num.size() ? -1 : 1;
if (const int cmp = lhs_num.compare(rhs_num); cmp != 0)
return cmp;
// note: fewer-leading-zeros-first is our convention, not an industry standard (impls
// diverge here); it only matters as a deterministic total order for unstable std::sort.
if (lhs_run.size() != rhs_run.size())
return lhs_run.size() < rhs_run.size() ? -1 : 1;
continue;
}
const int lower_lhs = std::tolower(lc);
const int lower_rhs = std::tolower(rc);
if (lower_lhs != lower_rhs)
return lower_lhs < lower_rhs ? -1 : 1;
++li;
++ri;
}
if (li == lhs.size() && ri == rhs.size())
return 0;
return li == lhs.size() ? -1 : 1;
}
// Neutral baseline order: the whole order when no column is sorted, and the tie-breaker under
// every primary sort key. Name-first so the default view is intuitively alphabetical:
// name, then source, then status, then type, with plugin_key as the final deterministic tie.
// e.g. with no column sorted the list reads A..Z by name.
template <class PluginItem>
int compare_plugin_base_order(const PluginItem& lhs, const PluginItem& rhs)
{
if (const int cmp = compare_ascii_case_insensitive_natural(lhs.display_name, rhs.display_name); cmp != 0)
return cmp;
if (const int cmp = static_cast<int>(lhs.source) - static_cast<int>(rhs.source); cmp != 0)
return cmp;
if (const int cmp = static_cast<int>(lhs.status) - static_cast<int>(rhs.status); cmp != 0)
return cmp;
if (const int cmp = lhs.type_key.compare(rhs.type_key); cmp != 0)
return cmp;
return lhs.plugin_key.compare(rhs.plugin_key);
}
// Compares two version strings returning -1 / 0 / +1. Uses Slic3r::Semver (the same parser the
// plugin catalog's update-available check uses); on unparseable input falls back to the natural
// compare so the order stays deterministic.
// e.g. "1.2.0" < "1.10.0" (numeric), "1.0.0-rc1" < "1.0.0" (semver prerelease rule).
inline int compare_plugin_version(const std::string& lhs, const std::string& rhs)
{
const auto lhs_semver = Semver::parse(lhs);
const auto rhs_semver = Semver::parse(rhs);
if (lhs_semver && rhs_semver)
{
if (*lhs_semver < *rhs_semver) return -1;
if (*rhs_semver < *lhs_semver) return 1;
return 0;
}
return compare_ascii_case_insensitive_natural(lhs, rhs);
}
// Compares two items by the chosen primary key, returning -1 / 0 / +1. Status and Source
// rank by enum ordinal (the declared dialog priority); Name uses the natural compare above.
// e.g. Status: an enabled item (lower ordinal) sorts before a disabled one.
// Name: "Plugin 2" sorts before "Plugin 10".
template <class PluginItem>
int compare_plugin_sort_key(const PluginItem& lhs, const PluginItem& rhs, PluginSortKey sort_key)
{
switch (sort_key)
{
case PluginSortKey::Status:
// why: PluginStatus/PluginSource declare the dialog sort priority as their ordinal order.
return static_cast<int>(lhs.status) - static_cast<int>(rhs.status);
case PluginSortKey::Name:
return compare_ascii_case_insensitive_natural(lhs.display_name, rhs.display_name);
case PluginSortKey::Source:
return static_cast<int>(lhs.source) - static_cast<int>(rhs.source);
case PluginSortKey::Version:
return compare_plugin_version(lhs.sort_version, rhs.sort_version);
case PluginSortKey::None:
// why: no primary key - every pair ties here so sort_plugin_items_for_dialog falls
// straight to the ascending base order (direction is irrelevant for the baseline).
return 0;
}
return 0;
}
// Sorts the dialog list in place by primary key + direction. Ties always fall back to the
// ascending base order, so the result is deterministic regardless of the primary direction.
// e.g. sort_key=Name, order=Desc -> names Z..A, but equal names keep the stable base order.
template <class PluginItem>
void sort_plugin_items_for_dialog(std::vector<PluginItem>& items, PluginSortKey sort_key,
PluginSortOrder sort_order)
{
std::sort(items.begin(), items.end(),
[sort_key, sort_order](const PluginItem& lhs, const PluginItem& rhs)
{
if (const int cmp = compare_plugin_sort_key(lhs, rhs, sort_key); cmp != 0)
return sort_order == PluginSortOrder::Asc ? cmp < 0 : cmp > 0;
// why: ties fall back to ascending base order regardless of the primary direction.
return compare_plugin_base_order(lhs, rhs) < 0;
});
}
} // namespace Slic3r::GUI
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <string>
namespace Slic3r
{
namespace GUI
{
enum class PluginSource
{
// IMPORTANT: ordinal order is the Plugins dialog Source sort priority.
Mine,
Subscribed,
Local
};
inline std::string to_string(PluginSource source)
{
switch (source)
{
case PluginSource::Mine: return "mine";
case PluginSource::Subscribed: return "subscribed";
case PluginSource::Local: return "local";
}
return "local";
}
}
} // namespace Slic3r::GUI
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <string>
namespace Slic3r
{
namespace GUI
{
enum class PluginStatus
{
// IMPORTANT: ordinal order is the Plugins dialog Status sort priority.
Activated,
Error,
Inactive,
Loading
};
inline std::string to_string(PluginStatus status)
{
switch (status)
{
case PluginStatus::Activated: return "Activated";
case PluginStatus::Error: return "Error";
case PluginStatus::Inactive: return "Inactive";
case PluginStatus::Loading: return "Loading";
}
return "Inactive";
}
}
} // namespace Slic3r::GUI
+277
View File
@@ -0,0 +1,277 @@
#include "PluginWebDialog.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <wx/event.h>
#include <utility>
namespace Slic3r { namespace GUI {
namespace {
// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
// plugin page looks native while any CSS the plugin ships still wins. Built on the
// --orca-* variables the host injects (see WebViewHostDialog); document-start injected
// AFTER the host contract so the variables are defined (shares the base injector's
// WebView2 timing guard).
std::string plugin_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
css += "body{margin:0;}";
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
css += "a{color:var(--orca-accent);}";
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
css += "button:hover{filter:brightness(1.1);}";
css += "button:disabled{opacity:.5;cursor:default;}";
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
css += "table{border-collapse:collapse;}";
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
css += "th{color:var(--orca-muted);font-weight:600;}";
css += "::-webkit-scrollbar{width:12px;height:12px;}";
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
css += "::-webkit-scrollbar-track{background:transparent;}";
css += "</style>";
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
// Injected into every page at document start (before the plugin's own scripts).
// Defines window.orca as the only host surface the page may use. It references
// window.wx lazily (at call time) so it never races the backend's deferred
// registration of the "wx" message handler. Guarded against double-injection so
// it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
window.__orcaDispatch = function (payload) {
var data = payload ? payload.data : null;
for (var i = 0; i < handlers.length; i++) {
try { handlers[i](data); } catch (e) {}
}
};
})();
)JS";
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
// relative URLs resolve against the bundled web resources directory.
wxString web_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
} // namespace
PluginWebDialog::PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style)
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
, m_html(html)
, m_on_message(std::move(on_message))
, m_on_submit(std::move(on_submit))
, m_on_close(std::move(on_close))
, m_on_destroyed(std::move(on_destroyed))
{
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
// is swapped in via SetPage once the bootstrap finishes loading.
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
// Paint the window/webview in the themed background so there is no white
// flash before the (transparent) bootstrap page and plugin HTML render.
SetBackgroundColour(wxGetApp().get_window_default_clr());
if (wxWebView* wv = browser()) {
wv->SetBackgroundColour(wxGetApp().get_window_default_clr());
// Theme contract + plugin defaults + bridge are registered by the base
// create_webview() via add_user_scripts(); nothing to add here.
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
}
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
}
void PluginWebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
}
}
PluginWebDialog::~PluginWebDialog()
{
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
// that event is sent from the base ~wxDialog(), after this subclass's members
// are already destroyed. Here the members are still alive, and the callback
// only touches the host-side registry (no Python), so this is safe.
if (m_on_destroyed)
m_on_destroyed();
}
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
{
if (dialog != nullptr && dialog->is_open())
dialog->push_message(data);
}
void PluginWebDialog::request_close(PluginWebDialog* dialog)
{
if (dialog != nullptr)
dialog->Close();
}
void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
{
if (dialog == nullptr)
return;
// Forced plugin teardown must not invoke Python close callbacks. End a modal
// loop first, otherwise destroying the window can leave ShowModal() running.
if (dialog->IsModal()) {
dialog->m_open = false;
dialog->EndModal(wxID_CANCEL);
}
dialog->Destroy();
}
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
{
// The first bootstrap load (or its error) triggers the swap to plugin HTML;
// the resulting plugin-page load is ignored (guarded by m_content_loaded).
load_plugin_content();
event.Skip();
}
void PluginWebDialog::load_plugin_content()
{
if (m_content_loaded)
return;
m_content_loaded = true;
if (wxWebView* wv = browser())
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
}
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
{
if (payload.value("channel", std::string()) == "orca") {
const std::string kind = payload.value("kind", std::string());
const nlohmann::json data = payload.contains("data") ? payload["data"] : nlohmann::json();
if (kind == "message") {
if (m_on_message)
m_on_message(data);
} else if (kind == "submit") {
finish(true, data);
} else if (kind == "close") {
finish(false, nlohmann::json());
}
return;
}
// Fall back to the shared shell commands (e.g. "close_page").
handle_common_script_command(payload);
}
void PluginWebDialog::push_message(const nlohmann::json& data)
{
if (!m_open)
return;
nlohmann::json envelope;
envelope["data"] = data;
call_web_handler(envelope, wxT("__orcaDispatch"));
}
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
{
if (!m_open)
return;
m_open = false;
if (submitted) {
m_result = data;
fire_submit(data);
} else {
m_result.reset();
fire_close();
}
if (IsModal())
EndModal(submitted ? wxID_OK : wxID_CANCEL);
else
Close();
}
void PluginWebDialog::on_close_window(wxCloseEvent&)
{
if (!m_open) {
// finish() already dispatched submit/close and requested the close.
// Modeless windows still need to be destroyed after that request.
if (!IsModal())
Destroy();
return;
}
m_open = false;
m_result.reset();
fire_close();
if (IsModal()) {
EndModal(wxID_CANCEL);
return;
}
Destroy();
}
void PluginWebDialog::fire_submit(const nlohmann::json& data)
{
if (m_on_submit) {
SubmitHandler cb = std::move(m_on_submit);
cb(data);
}
}
void PluginWebDialog::fire_close()
{
if (m_close_fired)
return;
m_close_fired = true;
if (m_on_close) {
CloseHandler cb = m_on_close;
m_on_close = nullptr;
cb();
}
}
}} // namespace Slic3r::GUI
+86
View File
@@ -0,0 +1,86 @@
#ifndef slic3r_GUI_PluginWebDialog_hpp_
#define slic3r_GUI_PluginWebDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
#include <functional>
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
// A host-owned webview window that renders plugin-supplied raw HTML and bridges
// messages to/from the page through a small injected `window.orca` API.
//
// This class is deliberately Python-agnostic: it talks to the plugin layer only
// through std::function hooks. Those hooks must NOT capture bare pybind11
// objects, because the dialog can be destroyed on the main thread without the
// GIL held; the plugin layer wraps any Python callables in a GIL-safe holder.
//
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
using MessageHandler = std::function<void(const nlohmann::json& data)>;
using SubmitHandler = std::function<void(const nlohmann::json& data)>;
using CloseHandler = std::function<void()>;
// on_submit fires once for window.orca.submit(). on_close fires only on a
// user/JS-initiated close (while the window is alive). on_destroyed runs from
// the destructor on every path and must touch host-side state only (no Python
// / no derived members).
PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~PluginWebDialog() override;
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
static void request_close(PluginWebDialog* dialog);
static void destroy_for_plugin(PluginWebDialog* dialog);
// Push a payload to the page; delivered to handlers registered via
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
void push_message(const nlohmann::json& data);
bool is_open() const { return m_open; }
// The payload submitted via window.orca.submit() (modal use), if any.
const std::optional<nlohmann::json>& result() const { return m_result; }
protected:
void on_script_message(const nlohmann::json& payload) override;
// Plugin HTML is loaded as a raw string, not a localized resource URL.
bool append_language_to_url() const override { return false; }
void add_user_scripts() override;
private:
void on_bootstrap_event(wxWebViewEvent& event);
void load_plugin_content();
void on_close_window(wxCloseEvent& event);
void fire_submit(const nlohmann::json& data);
void fire_close();
void finish(bool submitted, const nlohmann::json& data);
std::string m_html;
bool m_content_loaded{false};
bool m_open{true};
bool m_close_fired{false};
std::optional<nlohmann::json> m_result;
MessageHandler m_on_message;
SubmitHandler m_on_submit;
CloseHandler m_on_close;
CloseHandler m_on_destroyed;
};
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_PluginWebDialog_hpp_
+221
View File
@@ -0,0 +1,221 @@
#include "PluginsConfigDialog.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include <libslic3r/Preset.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PluginResolver.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace GUI {
namespace {
wxString preset_type_title(Preset::Type type)
{
switch (type) {
case Preset::TYPE_PRINT: return _L("Process plugins");
case Preset::TYPE_FILAMENT: return _L("Filament plugins");
case Preset::TYPE_PRINTER: return _L("Printer plugins");
default: return _L("Plugins");
}
}
} // namespace
PluginsConfigDialog::PluginsConfigDialog(wxWindow* parent, Preset::Type type, const std::string& overrides_json)
: WebViewHostDialog(parent, wxID_ANY, preset_type_title(type))
, m_type(type)
{
// On failure the document stays empty and every row goes read-only (see m_parse_error), so a preset
// we cannot understand is never silently overwritten.
if (!parse_plugin_overrides(overrides_json, m_overrides, m_parse_error))
BOOST_LOG_TRIVIAL(error) << "Plugins Config dialog: " << m_parse_error;
create_webview("web/dialog/PluginsConfigDialog/index.html", preset_type_title(type), wxSize(820, 660),
wxSize(640, 520));
}
PluginsConfigDialog::~PluginsConfigDialog() { m_alive->store(false, std::memory_order_release); }
const Preset* PluginsConfigDialog::current_preset() const
{
const PresetBundle* bundle = wxGetApp().preset_bundle;
if (bundle == nullptr)
return nullptr;
switch (m_type) {
case Preset::TYPE_PRINT: return &bundle->prints.get_edited_preset();
case Preset::TYPE_PRINTER: return &bundle->printers.get_edited_preset();
case Preset::TYPE_FILAMENT: return &bundle->filaments.get_edited_preset();
default: return nullptr;
}
}
PluginCapabilityId PluginsConfigDialog::identifier_from(const nlohmann::json& payload) const
{
return {plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""),
payload.value("plugin_key", "")};
}
void PluginsConfigDialog::on_script_message(const nlohmann::json& payload)
{
if (handle_common_script_command(payload))
return;
// Defer command handling out of the webview script-message callback, exactly as PluginsDialog
// does: GTK and macOS deliver it synchronously inside the native webview callback, and window
// work on that stack is the crash class fixed in b779a7bfed/f2ccbfc8b5. remove_preset_override
// puts a modal message box on that stack, which is the same bug.
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
if (alive->load(std::memory_order_acquire))
handle_web_command(payload);
});
}
void PluginsConfigDialog::handle_web_command(const nlohmann::json& payload)
{
const std::string command = payload.value("command", "");
if (command == "request_capabilities") {
send_capabilities();
return;
}
const PluginCapabilityId id = identifier_from(payload);
if (command == "get_capability_config") {
send_capability_config(id);
} else if (command == "save_capability_config") {
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
nlohmann::json value = payload.contains("config") ? payload.at("config") : nlohmann::json::object();
if (value.is_string()) {
value = nlohmann::json::parse(value.get<std::string>(), nullptr, /* allow_exceptions */ false);
if (value.is_discarded()) {
send_save_error(id, into_u8(_L("The configuration is not valid JSON. Your changes were not saved.")));
return;
}
}
const MutationResult result = m_service.set_preset_override(m_overrides, id, value);
if (!result.ok) {
send_save_error(id, result.error);
return;
}
send_capability_config(id);
show_status(_L("Configuration updated. Save the preset to persist it."), "success");
} else if (command == "remove_preset_override") {
// "Restore defaults" for a preset means holding no override at all: the capability falls back
// to the global configuration, not to the plugin's own get_default_config().
if (!m_parse_error.empty()) {
send_save_error(id, m_parse_error);
return;
}
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
"This discards the preset's override and uses the global configuration."),
from_u8(id.name)),
_L("Restore defaults"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
if (rc != wxYES)
return;
const MutationResult result = m_service.remove_preset_override(m_overrides, id);
if (!result.ok) {
send_save_error(id, result.error);
return;
}
send_capability_config(id);
show_status(_L("Using global configuration. Save the preset to persist it."), "success");
}
}
void PluginsConfigDialog::send_capabilities()
{
const Preset* preset = current_preset();
if (preset == nullptr)
return;
nlohmann::json response;
response["command"] = "list_capabilities";
response["preset_type"] = static_cast<int>(m_type);
response["preset_name"] = preset->name;
response["data"] = PluginConfig::capabilities_payload(capabilities_in_use(m_type, *preset));
BOOST_LOG_TRIVIAL(info) << "Prepared " << response["data"].size() << " capability rows for the Plugins Config dialog";
call_web_handler(response);
}
void PluginsConfigDialog::send_capability_config(const PluginCapabilityId& id)
{
const Preset* preset = current_preset();
nlohmann::json response;
response["command"] = "capability_config";
response["plugin_key"] = id.plugin_key;
response["capability_name"] = id.name;
response["capability_type"] = plugin_capability_type_to_string(id.type);
response["config"] = nlohmann::json::object();
response["custom_html"] = "";
response["error"] = "";
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
if (!cap || preset == nullptr) {
response["error"] = into_u8(_L("This capability is no longer available."));
call_web_handler(response);
return;
}
const EffectiveCapabilityConfig effective = m_service.get_effective_config(m_overrides, id);
response["config"] = effective.config;
response["has_preset_override"] = effective.has_preset_override;
response["has_base_config"] = effective.has_base_config;
response["stored_plugin_version"] = effective.stored_plugin_version;
response["running_plugin_version"] = effective.running_plugin_version;
response["read_only"] = !m_parse_error.empty();
if (!m_parse_error.empty())
response["error"] = m_parse_error;
if (cap->config_ui_available()) {
try {
wxBusyCursor busy;
PythonGILState gil;
response["custom_html"] = cap->get_config_ui();
} catch (const std::exception& ex) {
response["error"] = into_u8(GUI::format_wxstr(_L("The plugin's configuration UI failed to load (%1%). Showing the default editor."),
from_u8(ex.what())));
}
}
call_web_handler(response);
}
void PluginsConfigDialog::send_save_error(const PluginCapabilityId& id, const std::string& error)
{
call_web_handler({{"command", "capability_config_saved"},
{"plugin_key", id.plugin_key},
{"capability_name", id.name},
{"capability_type", plugin_capability_type_to_string(id.type)},
{"ok", false},
{"error", error}});
}
void PluginsConfigDialog::show_status(const wxString& message, const char* level)
{
nlohmann::json payload;
payload["command"] = "status_message";
payload["level"] = level;
payload["message"] = into_u8(message);
call_web_handler(payload);
}
}} // namespace Slic3r::GUI
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
#include <libslic3r/Preset.hpp>
#include <slic3r/plugin/PluginConfig.hpp>
#include <atomic>
#include <memory>
#include <string>
namespace Slic3r { namespace GUI {
// Lists the plugin capabilities the edited preset of `m_type` uses (see capabilities_in_use) and edits
// each one's config, falling back to the global config where the preset has no override.
//
// A pure editor over a JSON document: it never writes to the preset and never writes to the base config
// file. The caller seeds it with the preset's raw override text and reads the edited text back from
// overrides_json(); PluginConfigField owns the value and feeds it through the normal field/dirty pipeline.
class PluginsConfigDialog : public WebViewHostDialog
{
public:
PluginsConfigDialog(wxWindow* parent, Preset::Type type, const std::string& overrides_json);
~PluginsConfigDialog() override;
// The edited overrides as compact JSON text; "" once no override remains.
std::string overrides_json() const { return serialize_plugin_overrides(m_overrides); }
private:
void on_script_message(const nlohmann::json& payload) override;
// Runs one web command on a clean main-loop stack; see on_script_message.
void handle_web_command(const nlohmann::json& payload);
const Preset* current_preset() const;
void send_capabilities();
void send_capability_config(const PluginCapabilityId& id);
void send_save_error(const PluginCapabilityId& id, const std::string& error);
void show_status(const wxString& message, const char* level);
PluginCapabilityId identifier_from(const nlohmann::json& payload) const;
Preset::Type m_type = Preset::TYPE_INVALID;
PresetPluginConfigService m_service;
// The working copy the dialog edits. Seeded from the preset's raw text, read back by the caller.
CapabilityConfigDocument m_overrides;
// Set when the preset's stored text could not be parsed: the rows are shown read-only rather
// than silently replacing data we did not understand.
std::string m_parse_error;
// Guards the deferred command handlers against the dialog being destroyed while one is queued.
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
};
}} // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+261
View File
@@ -0,0 +1,261 @@
#ifndef slic3r_PluginsDialog_hpp_
#define slic3r_PluginsDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
#include "PluginSource.hpp"
#include "PluginStatus.hpp"
#include "PluginSort.hpp"
#include "slic3r/plugin/PluginDescriptor.hpp"
#include <atomic>
#include <boost/log/trivial.hpp>
#include <exception>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <type_traits>
#include <utility>
#include <wx/evtloop.h>
#include <wx/app.h>
#include <wx/progdlg.h>
#include <wx/string.h>
#include <wx/timer.h>
class wxTimer;
namespace Slic3r {
class PluginCapabilityInterface;
struct PluginCapabilityId;
enum class PluginCapabilityType;
namespace GUI {
class PluginsDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
PluginsDialog(wxWindow* parent,
wxWindowID id = wxID_ANY,
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~PluginsDialog();
void set_open_terminal_dlg_fn();
void update_plugin_dialog_ui();
private:
void open_plugin_on_cloud(const std::string& sharing_token);
void open_plugin_hub();
void on_script_message(const nlohmann::json& payload) override;
// Runs one web command on a clean main-loop stack; see on_script_message.
void handle_web_command(const nlohmann::json& payload);
// Re-raises this dialog after a transient modal it opened (file dialog, message box,
// progress dialog). Native macOS panels end by re-activating the app's main window
// (the mainframe) instead of this webview-hosting dialog, burying it; wx only
// compensates for generic wxDialog modals (wxDialog::EndModal raises the parent).
void restore_z_order();
void send_plugins();
void set_plugin_sort(const std::string& sort_key, const std::string& sort_order);
nlohmann::json build_plugins_payload() const;
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) const;
void refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud);
void refresh_plugins();
void toggle_plugin(const std::string& plugin_key, bool enabled);
void toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name, bool enabled);
void handle_plugin_menu_action(const std::string& plugin_key, const std::string& action);
void install_plugin_from_file();
bool install_plugin_package(const std::string& package_path);
bool install_cloud_plugin(const std::string& uuid, const std::string& version, const wxString& name);
void run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name);
// Config tab. Both are scoped to the full capability ID: a request naming a
// capability that is gone or not configurable is refused rather than served from, or written
// to, some other entry.
void send_capability_config(const PluginCapabilityId& id);
void save_capability_config(const PluginCapabilityId& id, const nlohmann::json& config);
void restore_capability_config(const PluginCapabilityId& id);
// Pushes a one-line result into the web footer status bar (level: "success" | "warn" | "error" | "info"),
// used for every plugin/capability operation instead of a modal box so the dialog stays non-disruptive.
void show_status(const wxString& message, const char* level);
// Best-effort human-readable name for a plugin_key (falls back to the key itself).
wxString plugin_display_name(const std::string& plugin_key) const;
// Turns the pending "Activating..." status into "Activated"/"Failed to activate" once an
// asynchronous plugin load reported via update_plugin_dialog_ui() finishes. No-op otherwise.
void resolve_pending_activation();
void update_plugin(const std::string& plugin_key);
void open_plugin_folder(const Slic3r::PluginDescriptor& plugin);
void delete_local_plugin(const Slic3r::PluginDescriptor& plugin);
void unsubscribe_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void reinstall_local_plugin(const std::string& plugin_key);
void reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void delete_mine_local_and_cloud_plugin(const std::string& plugin_key);
// In the future, we can allow users to choose which plugin version they want to install.
template<typename Run, typename OnFinish>
void run_with_dialog(Run&& run,
OnFinish&& on_finish,
const wxString& title,
const wxString& message,
int maximum = 100,
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
bool finish_after_dialog_destroyed = false)
{
const auto alive = m_alive;
wxProgressDialog* progress = new wxProgressDialog(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();
}
template<typename Run>
std::invoke_result_t<std::decay_t<Run>&> run_with_dialog_wait(Run&& run,
const wxString& title,
const wxString& message,
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);
}
}
std::function<void()> m_open_terminal_dlg_fn;
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
PluginSortKey m_plugin_sort_key = PluginSortKey::None;
PluginSortOrder m_plugin_sort_order = PluginSortOrder::Asc;
// Plugin whose asynchronous activation is in flight, awaited by resolve_pending_activation().
// Empty when no activation is pending.
std::string m_activating_plugin_key;
};
} // namespace GUI
} // namespace Slic3r
#endif
+495
View File
@@ -0,0 +1,495 @@
#include "PostProcessor.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r_version.h"
#include "I18N.hpp"
// Post-processing plugins are executed through the embedded-Python plugin system, which is why this
// file lives in the GUI layer (libslic3r must not depend on pybind11 / PluginManager).
#include "libslic3r/Config.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
// BBS
#include <algorithm>
#include <iostream>
#include <fstream>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#ifdef WIN32
// The standard Windows includes.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <shellapi.h>
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
// This routine appends the given argument to a command line such that CommandLineToArgvW will return the argument string unchanged.
// Arguments in a command line should be separated by spaces; this function does not add these spaces.
// Argument - Supplies the argument to encode.
// CommandLine - Supplies the command line to which we append the encoded argument string.
static void quote_argv_winapi(const std::wstring& argument, std::wstring& commmand_line_out)
{
// Don't quote unless we actually need to do so --- hopefully avoid problems if programs won't parse quotes properly.
if (argument.empty() == false && argument.find_first_of(L" \t\n\v\"") == argument.npos)
commmand_line_out.append(argument);
else {
commmand_line_out.push_back(L'"');
for (auto it = argument.begin();; ++it) {
unsigned number_backslashes = 0;
while (it != argument.end() && *it == L'\\') {
++it;
++number_backslashes;
}
if (it == argument.end()) {
// Escape all backslashes, but let the terminating double quotation mark we add below be interpreted as a metacharacter.
commmand_line_out.append(number_backslashes * 2, L'\\');
break;
} else if (*it == L'"') {
// Escape all backslashes and the following double quotation mark.
commmand_line_out.append(number_backslashes * 2 + 1, L'\\');
commmand_line_out.push_back(*it);
} else {
// Backslashes aren't special here.
commmand_line_out.append(number_backslashes, L'\\');
commmand_line_out.push_back(*it);
}
}
commmand_line_out.push_back(L'"');
}
}
static DWORD execute_process_winapi(const std::wstring& command_line)
{
// Extract the current environment to be passed to the child process.
std::wstring envstr;
{
wchar_t* env = GetEnvironmentStrings();
assert(env != nullptr);
const wchar_t* var = env;
size_t totallen = 0;
size_t len;
while ((len = wcslen(var)) > 0) {
totallen += len + 1;
var += len + 1;
}
envstr = std::wstring(env, totallen);
FreeEnvironmentStrings(env);
}
STARTUPINFOW startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(STARTUPINFO);
#if 0
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
#endif
PROCESS_INFORMATION process_info;
if (!::CreateProcessW(nullptr /* lpApplicationName */, (LPWSTR) command_line.c_str(), nullptr /* lpProcessAttributes */,
nullptr /* lpThreadAttributes */, false /* bInheritHandles */,
CREATE_UNICODE_ENVIRONMENT /* | CREATE_NEW_CONSOLE */ /* dwCreationFlags */, (LPVOID) envstr.c_str(),
nullptr /* lpCurrentDirectory */, &startup_info, &process_info))
throw Slic3r::RuntimeError(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) +
", Win32 error: " + std::to_string(int(::GetLastError())));
::WaitForSingleObject(process_info.hProcess, INFINITE);
ULONG rc = 0;
::GetExitCodeProcess(process_info.hProcess, &rc);
::CloseHandle(process_info.hThread);
::CloseHandle(process_info.hProcess);
return rc;
}
// Run the script. If it is a perl script, run it through the bundled perl interpreter.
// If it is a batch file, run it through the cmd.exe.
// Otherwise run it directly.
static int run_script(const std::string& script, const std::string& gcode, std::string& /*std_err*/)
{
// Unpack the argument list provided by the user.
int nArgs;
LPWSTR* szArglist = CommandLineToArgvW(boost::nowide::widen(script).c_str(), &nArgs);
if (szArglist == nullptr || nArgs <= 0) {
// CommandLineToArgvW failed. Maybe the command line escapment is invalid?
throw Slic3r::RuntimeError(std::string("Post processing script ") + script + " on file " + gcode +
" failed. CommandLineToArgvW() refused to parse the command line path.");
}
std::wstring command_line;
std::wstring command = szArglist[0];
if (!boost::filesystem::exists(boost::filesystem::path(command)))
throw Slic3r::RuntimeError(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command));
if (boost::iends_with(command, L".pl")) {
// This is a perl script. Run it through the perl interpreter.
// The current process may be slic3r.exe or slic3r-console.exe.
// Find the path of the process:
wchar_t wpath_exe[_MAX_PATH + 1];
::GetModuleFileNameW(nullptr, wpath_exe, _MAX_PATH);
boost::filesystem::path path_exe(wpath_exe);
boost::filesystem::path path_perl = path_exe.parent_path() / "perl" / "perl.exe";
if (!boost::filesystem::exists(path_perl)) {
LocalFree(szArglist);
throw Slic3r::RuntimeError(std::string("Perl interpreter ") + path_perl.string() + " does not exist.");
}
// Replace it with the current perl interpreter.
quote_argv_winapi(boost::nowide::widen(path_perl.string()), command_line);
command_line += L" ";
} else if (boost::iends_with(command, ".bat")) {
// Run a batch file through the command line interpreter.
command_line = L"cmd.exe /C ";
}
for (int i = 0; i < nArgs; ++i) {
quote_argv_winapi(szArglist[i], command_line);
command_line += L" ";
}
LocalFree(szArglist);
quote_argv_winapi(boost::nowide::widen(gcode), command_line);
return (int) execute_process_winapi(command_line);
}
#else
// POSIX
#include <cstdlib> // getenv()
#include <boost/process.hpp>
namespace process = boost::process;
static int run_script(const std::string& script, const std::string& gcode, std::string& std_err)
{
// Try to obtain user's default shell
const char* shell = ::getenv("SHELL");
if (shell == nullptr) {
shell = "/bin/sh";
}
// Quote and escape the gcode path argument
std::string command{script};
command.append(" '");
for (char c : gcode) {
if (c == '\'') {
command.append("'\\''");
} else {
command.push_back(c);
}
}
command.push_back('\'');
BOOST_LOG_TRIVIAL(debug) << boost::format("Executing script, shell: %1%, command: %2%") % shell % command;
process::ipstream istd_err;
process::child child(shell, "-c", command, process::std_err > istd_err);
std_err.clear();
std::string line;
while (child.running() && std::getline(istd_err, line)) {
std_err.append(line);
std_err.push_back('\n');
}
child.wait();
return child.exit_code();
}
#endif
namespace Slic3r {
// BBS
void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& config)
{
const ConfigOptionBool* opt = config.opt<ConfigOptionBool>("gcode_add_line_number");
if (!opt->getBool())
return;
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
return;
std::fstream fs;
std::string new_gcode;
fs.open(gcode_file.c_str(), std::fstream::in | std::fstream::out);
size_t line_number = 1;
std::string gcode_line;
while (std::getline(fs, gcode_line)) {
char num_str[128];
memset(num_str, 0, sizeof(num_str));
snprintf(num_str, sizeof(num_str), "%zd", line_number);
new_gcode += std::string("N") + num_str + " " + gcode_line + "\n";
line_number++;
}
fs.clear();
fs.seekp(0, std::ios_base::beg);
fs.write(new_gcode.c_str(), new_gcode.length());
fs.close();
}
// Run the configured slicing-pipeline plugins on `gcode_path` in place, at their Step.psGCodePostProcess
// seam. This is the same capability that runs at the geometry seams inside Print::process(); here it is
// dispatched a final time on the exported G-code, so a plugin can edit slices AND the final G-code from
// one class. Plugins are executed in-process through the embedded Python interpreter. Throws
// Slic3r::RuntimeError on any failure; the caller removes the working copy (see run_post_process_scripts'
// catch block). Entries are bare capability names; the top-level plugins manifest carries the full refs.
// A geometry-only plugin simply returns success here (it filters on ctx.step), so it costs nothing beyond
// one no-op call, but note any configured pipeline plugin still engages this post-process path (i.e. the
// non-BBL ".pp" working copy) even if it does no G-code work.
static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
const ConfigOptionStrings* plugins,
const std::string& gcode_path,
const std::string& host,
const std::string& output_name,
const DynamicPrintConfig& config)
{
// Let plugins observe the (possibly script-updated) target file name, mirroring the script env.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
const boost::filesystem::path gcode_file(gcode_path);
auto execute_fn = [&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
SlicingPipelineContext ctx;
ctx.orca_version = SoftFever_VERSION;
ctx.step = SlicingPipelineStepPlugin::psGCodePostProcess;
ctx.gcode_path = gcode_path;
ctx.host = host;
ctx.output_name = output_name;
ctx.full_config = &config; // no live Print here; config_value() reads this
ExecutionResult exec_result;
try {
PythonGILState gil;
exec_result = cap->execute(ctx);
} catch (const std::exception& ex) {
const std::string msg =
(boost::format("Post-processing plugin %1% raised an exception.\nError: %2%") % ref.capability_name % ex.what()).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
if (exec_result.status == PluginResult::RecoverableError || exec_result.status == PluginResult::FatalError) {
const std::string msg =
(boost::format("Post-processing plugin %1% failed.\nError: %2%") % ref.capability_name % exec_result.message).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
if (!exec_result.message.empty())
BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << ": " << exec_result.message;
if (!boost::filesystem::exists(gcode_file)) {
const std::string msg = (boost::format(
_utf8(L("Post-processing plugin %1% failed.\n\n"
"The post-processing plugin is expected to modify the G-code file %2% in place, but "
"the G-code file was deleted.\nPlease check the plugin implementation.\n"))) %
ref.capability_name % gcode_path)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << " completed successfully";
};
execute_capabilities_from_refs<SlicingPipelinePluginCapability>(capabilities, plugins, PluginCapabilityType::SlicingPipeline, execute_fn);
}
// Run post-processing scripts ("post_process") and/or the slicing-pipeline plugins' psGCodePostProcess
// step ("slicing_pipeline_plugin") if defined. Both run on the same working copy of the G-code (the
// ".pp" temp when make_copy), so a
// plugin never opens the original file the G-code viewer keeps memory-mapped (a writable open of the
// mapped file fails on Windows with a sharing violation).
// Returns true if a script or plugin was executed.
// Returns false if neither a post-processing script nor plugin was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// For a "File" target, a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created.
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
bool run_post_process_scripts(
std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config)
{
// post_process / slicing_pipeline_plugin are absent in SLA mode, hence the null checks. G-code
// post-processing is now the psGCodePostProcess step of the slicing-pipeline plugin, so the same
// slicing_pipeline_plugin option drives both the geometry seams and this final G-code seam.
const auto* post_process = config.opt<ConfigOptionStrings>("post_process");
const auto* slicing_pipeline_plugin = config.opt<ConfigOptionStrings>("slicing_pipeline_plugin");
const bool have_scripts = post_process != nullptr && !post_process->values.empty();
const bool have_plugins = slicing_pipeline_plugin != nullptr && !slicing_pipeline_plugin->values.empty();
if (!have_scripts && !have_plugins)
return false;
std::string path;
if (make_copy) {
// Don't run the post-processing script/plugin on the input file, it will be memory mapped by the G-code viewer.
// Make a copy.
path = src_path + ".pp";
// First delete an old file if it exists.
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception& err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format(
"Failed deleting an old temporary file %1% before running a post-processing script: %2%", path, err.what());
}
// Second make a copy.
std::string error_message;
if (copy_file(src_path, path, error_message, false) != SUCCESS)
throw Slic3r::RuntimeError(
Slic3r::format("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%", src_path,
error_message));
} else {
// Don't make a copy of the G-code before running the post-processing script.
path = src_path;
}
auto delete_copy = [&path, &src_path, make_copy]() {
if (make_copy)
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception& err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a temporary copy %1% of a G-code file %2% : %3%", path,
src_path, err.what());
}
};
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file"));
// Store print configuration into environment variables.
config.setenv_();
// Let the post-processing script know the target host ("File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet",
// "AstroBox" ...)
boost::nowide::setenv("SLIC3R_PP_HOST", host.c_str(), 1);
// Let the post-processing script know the final file name. For "File" host, it is a full path of the target file name and its location,
// for example pointing to an SD card. For "PrusaLink" or "OctoPrint", it is a file name optionally with a directory on the target host.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
// Path to an optional file that the post-processing script may create and populate it with a single line containing the output_name replacement.
std::string path_output_name = path + ".output_name";
auto remove_output_name_file = [&path_output_name, &src_path]() {
try {
if (boost::filesystem::exists(path_output_name))
boost::filesystem::remove(path_output_name);
} catch (const std::exception& err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format(
"Failed deleting a file %1% carrying the final name / path of a G-code file %2%: %3%", path_output_name, src_path,
err.what());
}
};
// Remove possible stalled path_output_name of the previous run.
remove_output_name_file();
try {
if (have_scripts) {
for (const std::string& scripts : post_process->values) {
std::vector<std::string> lines;
boost::split(lines, scripts, boost::is_any_of("\r\n"));
for (std::string script : lines) {
// Ignore empty post processing script lines.
boost::trim(script);
if (script.empty())
continue;
BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path;
std::string std_err;
const int result = run_script(script, gcode_file.string(), std_err);
if (result != 0) {
const std::string msg = std_err.empty() ?
(boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") %
script % path % result)
.str() :
(boost::format(
"Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") %
script % path % result % std_err)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
delete_copy();
throw Slic3r::RuntimeError(msg);
}
if (!boost::filesystem::exists(gcode_file)) {
const std::string msg = (boost::format(
_utf8(L("Post-processing script %1% failed.\n\n"
"The post-processing script is expected to change the G-code file %2% in place, but "
"the G-code file was deleted and likely saved under a new name.\n"
"Please adjust the post-processing script to change the G-code in place and consult "
"the manual on how to optionally rename the post-processed G-code file.\n"))) %
script % path)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
}
}
}
if (boost::filesystem::exists(path_output_name)) {
try {
// Read a single line from path_output_name, which should contain the new output name of the post-processed G-code.
boost::nowide::fstream f;
f.open(path_output_name, std::ios::in);
std::string new_output_name;
std::getline(f, new_output_name);
f.close();
if (host == "File") {
namespace fs = boost::filesystem;
fs::path op(new_output_name);
if (op.is_relative() && op.has_filename() && op.parent_path().empty()) {
// Is this just a filename? Make it an absolute path.
auto outpath = fs::path(output_name).parent_path();
outpath /= op.string();
new_output_name = outpath.string();
} else {
if (!op.is_absolute() || !op.has_filename())
throw Slic3r::RuntimeError("Unable to parse desired new path from output name file");
}
if (!fs::exists(fs::path(new_output_name).parent_path()))
throw Slic3r::RuntimeError(
Slic3r::format("Output directory does not exist: %1%", fs::path(new_output_name).parent_path().string()));
}
BOOST_LOG_TRIVIAL(trace) << "Post-processing script changed the file name from " << output_name << " to "
<< new_output_name;
output_name = new_output_name;
} catch (const std::exception& err) {
throw Slic3r::RuntimeError(Slic3r::format("run_post_process_scripts: Failed reading a file %1% "
"carrying the final name / path of a G-code file: %2%",
path_output_name, err.what()));
}
remove_output_name_file();
}
// Run plugins after the scripts so they observe any output_name the scripts produced. A thrown
// exception is handled by the catch below, which removes the temp copy.
if (have_plugins) {
run_post_process_plugins(*slicing_pipeline_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name, config);
}
} catch (...) {
remove_output_name_file();
delete_copy();
throw;
}
src_path = std::move(path);
return true;
}
} // namespace Slic3r
+40
View File
@@ -0,0 +1,40 @@
#ifndef slic3r_GUI_PostProcessor_hpp_
#define slic3r_GUI_PostProcessor_hpp_
#include <functional>
#include <string>
#include "libslic3r/libslic3r.h"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
// Run post-processing scripts (the "post_process" option) and/or the slicing-pipeline plugins'
// Step.psGCodePostProcess seam (the "slicing_pipeline_plugin" option) if defined. Lives in the GUI
// layer because plugins are executed through the embedded-Python PluginManager, which libslic3r must
// not depend on.
// Returns true if a script or plugin was executed.
// Returns false if neither a post-processing script nor plugin was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// If make_copy, then a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created. Scripts and plugins always
// run on this working copy so they never touch the original G-code the viewer keeps memory-mapped
// (a writable open of the mapped file fails on Windows with a sharing violation).
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
extern bool run_post_process_scripts(
std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config);
inline bool run_post_process_scripts(std::string& src_path, const DynamicPrintConfig& config)
{
std::string src_path_name = src_path;
return run_post_process_scripts(src_path, false, "File", src_path_name, config);
}
// BBS
extern void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& config);
} // namespace Slic3r
#endif /* slic3r_GUI_PostProcessor_hpp_ */
+36 -111
View File
@@ -12,7 +12,6 @@
#include <libslic3r/PresetBundle.hpp>
#include <wx/string.h>
#include "MainFrame.hpp"
#include <slic3r/GUI/Widgets/WebView.hpp>
#include <miniz.h>
#include <OrcaCloudServiceAgent.hpp>
#include <wx/event.h>
@@ -21,11 +20,9 @@ namespace Slic3r { namespace GUI {
PresetBundleDialog::PresetBundleDialog(
wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style)
: DPIDialog(parent, id, _L("PresetBundle"), pos, size, style)
: WebViewHostDialog(parent, id, _L("PresetBundle"), pos, size, style)
{
wxGetApp().preset_bundle->bundles.PauseRead(); // for the entirety of the preset bundle dialog, we want the update thread to yield.
SetBackgroundColour(*wxWHITE);
SetMinSize(DESIGN_WINDOW_SIZE);
create();
wxGetApp().UpdateDlgDarkUI(this);
@@ -186,55 +183,11 @@ void PresetBundleDialog::RefreshBundleMap()
wxGetApp().preset_bundle->bundles.ReadUnlock();
}
void PresetBundleDialog::load_url(wxString& url)
{
if (!m_browser)
return;
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " enter, url=" << url.ToStdString();
WebView::LoadUrl(m_browser, url);
m_browser->SetFocus();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " exit";
}
void PresetBundleDialog::create()
{
app_config = get_app_config();
wxString TargetUrl = from_u8(
(boost::filesystem::path(resources_dir()) / "web/dialog/PresetBundleDialog/index.html").make_preferred().string());
wxString strlang = wxGetApp().current_language_code_safe();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", strlang=%1%") % into_u8(strlang);
if (strlang != "")
TargetUrl = wxString::Format("%s?lang=%s", std::string(TargetUrl.mb_str()), strlang);
TargetUrl = "file://" + TargetUrl;
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
SetTitle(_L("Preset Bundle"));
m_browser = WebView::CreateWebView(this, TargetUrl);
if (m_browser == nullptr) {
wxLogError("Could not init m_browser");
return;
}
SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
// Set a more sensible size for web browsing
wxSize pSize = FromDIP(wxSize(820, 660));
SetSize(pSize);
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY);
Move(tmpPT);
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PresetBundleDialog::OnScriptMessage, this, m_browser->GetId());
load_url(TargetUrl);
create_webview("web/dialog/PresetBundleDialog/index.html", _L("Preset Bundle"),
wxSize(820, 660), wxSize(640, 640));
}
bool PresetBundleDialog::DeleteBundleById(const wxString& id)
@@ -292,68 +245,41 @@ bool PresetBundleDialog::DeleteBundleById(const wxString& id)
bool PresetBundleDialog::UnsubscribeBundleById(const std::string& id) { return wxGetApp().unsubscribe_bundle(id); }
void PresetBundleDialog::on_dpi_changed(const wxRect& suggested_rect) { this->Refresh(); }
void PresetBundleDialog::RunScript(const wxString& s)
void PresetBundleDialog::on_script_message(const nlohmann::json& j)
{
if (!m_browser)
if (handle_common_script_command(j))
return;
WebView::RunScript(m_browser, s);
}
void PresetBundleDialog::OnScriptMessage(wxWebViewEvent& e)
{
try {
wxString strInput = e.GetString();
BOOST_LOG_TRIVIAL(trace) << "PresetBundleDialog::OnScriptMessage;OnRecv:" << strInput.c_str();
json j = json::parse(strInput.utf8_string());
wxString strCmd = j["command"];
BOOST_LOG_TRIVIAL(trace) << "PresetBundleDialog::OnScriptMessage;Command:" << strCmd;
if (strCmd == "request_bundles") {
ListBundles();
} else if (strCmd == "refresh_bundles") {
// use the thread to check for updates.
m_check_update_pending.store(true, std::memory_order_relaxed);
} else if (strCmd == "update_bundle") {
std::string id = j["bundle_id"];
auto* evt = new wxCommandEvent(EVT_UPDATE_PRESET_BUNDLE);
evt->SetString(wxString::FromUTF8(id));
wxQueueEvent(&wxGetApp(), evt); // dialog -> GUI_App
} else if (strCmd == "set_auto_update") {
bool enabled = j.value("enabled", false);
// Example persistence location. Adjust key name if you already have one.
app_config->set_bool("preset_bundle_auto_update", enabled ? true : false);
app_config->save();
} else if (strCmd == "close_page") {
this->EndModal(wxID_CANCEL);
} else if (strCmd == "export_page") {
wxGetApp().CallAfter([this]() {
ExportPresetBundleDialog dlg(this);
dlg.ShowModal();
});
} else if (strCmd == "top_row_menu_action") {
if (j["action"] == "open_folder") {
std::string id = j["bundle_id"];
OpenFolder(id);
} else if (j["action"] == "delete_bundle") {
std::string id = j["bundle_id"];
DeleteBundle(id);
} else if (j["action"] == "unsubscribe_bundle") {
std::string id = j["bundle_id"];
UnsubscribeBundle(id);
}
} else if (strCmd == "open_bundle_on_cloud") {
std::string bundle_id = j["bundle_id"];
OpenBundleOnCloud(bundle_id);
}
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(trace) << "PresetBundleDialog::OnScriptMessage;Error:" << e.what();
const std::string strCmd = j.value("command", "");
if (strCmd == "request_bundles") {
ListBundles();
} else if (strCmd == "refresh_bundles") {
m_check_update_pending.store(true, std::memory_order_relaxed);
} else if (strCmd == "update_bundle") {
std::string id = j.value("bundle_id", "");
auto* evt = new wxCommandEvent(EVT_UPDATE_PRESET_BUNDLE);
evt->SetString(wxString::FromUTF8(id));
wxQueueEvent(&wxGetApp(), evt);
} else if (strCmd == "set_auto_update") {
bool enabled = j.value("enabled", false);
app_config->set_bool("preset_bundle_auto_update", enabled ? true : false);
app_config->save();
} else if (strCmd == "export_page") {
wxGetApp().CallAfter([this]() {
ExportPresetBundleDialog dlg(this);
dlg.ShowModal();
});
} else if (strCmd == "top_row_menu_action") {
const std::string action = j.value("action", "");
const std::string id = j.value("bundle_id", "");
if (action == "open_folder")
OpenFolder(id);
else if (action == "delete_bundle")
DeleteBundle(id);
else if (action == "unsubscribe_bundle")
UnsubscribeBundle(id);
} else if (strCmd == "open_bundle_on_cloud") {
OpenBundleOnCloud(j.value("bundle_id", ""));
}
}
@@ -396,8 +322,7 @@ void PresetBundleDialog::ListBundles()
res["data"].push_back(std::move(temp));
}
wxString strJS = wxString::Format("HandleStudio(%s)", wxString::FromUTF8(res.dump(-1, ' ', false, json::error_handler_t::ignore)));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
call_web_handler(res);
}
void PresetBundleDialog::OpenFolder(const std::string& id)
+3 -9
View File
@@ -3,6 +3,7 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include "libslic3r/AppConfig.hpp"
#include <boost/thread/detail/thread.hpp>
#include <libslic3r/PresetBundle.hpp>
@@ -15,7 +16,6 @@
#include <wx/language.h>
#include <wx/string.h>
#include <wx/fswatcher.h>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
#define DESIGN_GRAY900_COLOR wxColour("#363636") // Label color
@@ -28,7 +28,7 @@ namespace Slic3r { namespace GUI {
#define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1)
#define DESIGN_LEFT_MARGIN 25
#define VERTICAL_GAP_SIZE FromDIP(4)
class PresetBundleDialog : public Slic3r::GUI::DPIDialog
class PresetBundleDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
PresetBundleDialog(wxWindow* parent,
@@ -47,10 +47,8 @@ public:
bool seq_top_layer_only_changed() const { return m_seq_top_layer_only_changed; }
bool recreate_GUI() const { return m_recreate_GUI; }
void on_dpi_changed(const wxRect& suggested_rect) override;
// webview utilities
void load_url(wxString& url);
void ListBundles();
void OpenFolder(const std::string& id);
void DeleteBundle(const std::string& id);
@@ -59,11 +57,8 @@ public:
void OnPresetBundlePage();
// sends command to webview
void RunScript(const wxString& s);
// webview events
void OnScriptMessage(wxWebViewEvent& e);
void on_script_message(const nlohmann::json& payload) override;
void StartDialogWorker();
void StopDialogWorker();
@@ -86,7 +81,6 @@ protected:
bool m_recreate_GUI{false};
// Webview
wxWebView* m_browser{nullptr};
std::unordered_map<std::string, BundleMetadata> bundle_copy;
boost::thread m_dialog_worker_thread;
+254
View File
@@ -0,0 +1,254 @@
#include "ProcessRunner.hpp"
#include <boost/process/env.hpp>
#include <boost/process.hpp>
#ifdef _WIN32
#include <boost/process/windows.hpp>
#endif
namespace Slic3r { namespace GUI {
ProcessRunner::ProcessRunner()
: m_poll_timer(this, wxID_ANY)
{
Bind(wxEVT_TIMER, &ProcessRunner::on_timer, this);
}
ProcessRunner::~ProcessRunner()
{
terminate();
}
void ProcessRunner::join_launch_thread()
{
if (m_launch_thread.joinable())
m_launch_thread.join();
}
void ProcessRunner::start_reader_threads()
{
join_reader_threads();
if (m_stdout_pipe) {
m_stdout_thread = std::thread([this]() {
std::string line;
while (std::getline(*m_stdout_pipe, line))
enqueue_output(std::move(line), false);
});
}
if (m_stderr_pipe) {
m_stderr_thread = std::thread([this]() {
std::string line;
while (std::getline(*m_stderr_pipe, line))
enqueue_output(std::move(line), true);
});
}
}
void ProcessRunner::join_reader_threads()
{
if (m_stdout_thread.joinable())
m_stdout_thread.join();
if (m_stderr_thread.joinable())
m_stderr_thread.join();
}
void ProcessRunner::enqueue_output(std::string line, bool is_stderr)
{
std::lock_guard<std::mutex> lock(m_output_mutex);
m_output_queue.push_back({std::move(line), is_stderr});
}
std::vector<ProcessRunner::OutputLine> ProcessRunner::drain_output_queue()
{
std::lock_guard<std::mutex> lock(m_output_mutex);
std::vector<OutputLine> batch;
batch.swap(m_output_queue);
return batch;
}
// Clean up stored process state after the child has exited or has already been stopped.
void ProcessRunner::finish_process(int exit_code)
{
join_launch_thread();
join_reader_threads();
std::vector<OutputLine> final_batch = drain_output_queue();
if (!final_batch.empty() && m_on_output)
m_on_output(final_batch);
m_process.reset();
m_stdout_pipe.reset();
m_stderr_pipe.reset();
m_stdin_pipe.reset();
if (m_on_done) {
auto cb = std::move(m_on_done);
m_on_done = nullptr;
cb(exit_code);
}
}
// Stop a running child process, then use finish_process() for shared cleanup.
void ProcessRunner::terminate()
{
m_poll_timer.Stop();
join_launch_thread();
if (m_process && m_process->running()) {
std::error_code ec;
m_process->terminate(ec);
m_process->wait(ec);
}
if (m_process) {
finish_process(m_process->exit_code());
} else if (m_launch_failed.exchange(false) || m_on_done) {
finish_process(-1);
}
}
bool ProcessRunner::run_command_line_async(const std::string& command_line,
OutputCallback on_output,
DoneCallback on_done)
{
if (!m_launching.load())
join_launch_thread();
if (is_running())
return false;
m_on_output = std::move(on_output);
m_on_done = std::move(on_done);
drain_output_queue();
m_launch_failed.store(false);
m_launching.store(true);
m_launch_thread = std::thread([this, command_line]() {
namespace bp = boost::process;
try {
auto stdout_pipe = std::make_unique<bp::ipstream>();
auto stderr_pipe = std::make_unique<bp::ipstream>();
auto stdin_pipe = std::make_unique<bp::opstream>();
auto process = std::make_unique<bp::child>(
bp::cmd = command_line,
bp::env["PYTHONUNBUFFERED"] = "1",
bp::env["PYTHONIOENCODING"] = "utf-8",
bp::env["PYTHONUTF8"] = "1",
#ifdef _WIN32
bp::windows::create_no_window,
#endif
bp::std_in < *stdin_pipe,
bp::std_out > *stdout_pipe,
bp::std_err > *stderr_pipe);
m_stdout_pipe = std::move(stdout_pipe);
m_stderr_pipe = std::move(stderr_pipe);
m_stdin_pipe = std::move(stdin_pipe);
m_process = std::move(process);
start_reader_threads();
} catch (const std::exception&) {
m_stdout_pipe.reset();
m_stderr_pipe.reset();
m_stdin_pipe.reset();
m_process.reset();
m_launch_failed.store(true);
}
m_launching.store(false);
});
m_poll_timer.Start(50);
return true;
}
void ProcessRunner::on_timer(wxTimerEvent& /*event*/)
{
if (m_launching.load())
return;
join_launch_thread();
if (m_launch_failed.exchange(false)) {
m_poll_timer.Stop();
finish_process(-1);
return;
}
if (!m_process) {
m_poll_timer.Stop();
return;
}
std::vector<OutputLine> batch = drain_output_queue();
if (!batch.empty() && m_on_output)
m_on_output(batch);
// Check if process exited
if (!m_process->running()) {
m_poll_timer.Stop();
m_process->wait();
const int exit_code = m_process->exit_code();
finish_process(exit_code);
}
}
void ProcessRunner::write_stdin(const std::string& data)
{
if (m_launching.load() || !m_stdin_pipe || !m_process || !m_process->running())
return;
*m_stdin_pipe << data;
m_stdin_pipe->flush();
}
bool ProcessRunner::is_running() const
{
return m_launching.load() || (m_process && m_process->running());
}
ProcessRunner::SyncResult ProcessRunner::run_sync(const std::string& executable,
const std::vector<std::string>& args)
{
SyncResult result;
try {
namespace bp = boost::process;
bp::ipstream std_out;
bp::ipstream std_err;
bp::child child(executable, bp::args(args),
bp::std_out > std_out,
bp::std_err > std_err);
// Read both streams on separate threads while the process runs
std::thread stdout_reader([&]() {
std::string line;
while (std::getline(std_out, line))
result.stdout_output += line + '\n';
});
std::thread stderr_reader([&]() {
std::string line;
while (std::getline(std_err, line))
result.stderr_output += line + '\n';
});
child.wait();
stdout_reader.join();
stderr_reader.join();
result.exit_code = child.exit_code();
} catch (const std::exception&) {
result.exit_code = -1;
}
return result;
}
}} // namespace Slic3r::GUI
+93
View File
@@ -0,0 +1,93 @@
#ifndef slic3r_GUI_ProcessRunner_hpp_
#define slic3r_GUI_ProcessRunner_hpp_
#include <wx/event.h>
#include <wx/timer.h>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <functional>
#include <boost/process.hpp>
namespace Slic3r { namespace GUI {
class ProcessRunner : public wxEvtHandler
{
public:
ProcessRunner();
~ProcessRunner() override;
struct OutputLine {
std::string text;
bool is_stderr;
};
using OutputCallback = std::function<void(const std::vector<OutputLine>& lines)>;
using DoneCallback = std::function<void(int exit_code)>;
// Non-blocking command-line style launch. Boost parses command_line into
// executable + arguments.
// Output is batched per timer tick (50ms) and delivered via on_output on the GUI thread.
// on_done fires on the GUI thread with the exit code.
// Returns false if a process is already running.
bool run_command_line_async(const std::string& command_line,
OutputCallback on_output,
DoneCallback on_done);
struct SyncResult {
int exit_code = -1;
std::string stdout_output;
std::string stderr_output;
};
// Blocking: runs the process and waits for it to finish.
// Safe to call from any thread (no wx dependency).
static SyncResult run_sync(const std::string& executable,
const std::vector<std::string>& args);
// Write data to the running process's stdin (async mode only).
void write_stdin(const std::string& data);
// True while an async process is launching or running.
bool is_running() const;
// Terminate the running process (async mode only).
void terminate();
private:
void on_timer(wxTimerEvent& event);
void finish_process(int exit_code);
void start_reader_threads();
void join_reader_threads();
void enqueue_output(std::string line, bool is_stderr);
std::vector<OutputLine> drain_output_queue();
// Join any completed background launcher thread before reusing or destroying it.
void join_launch_thread();
wxTimer m_poll_timer;
// True while the launcher thread is still creating the child process; join only after this becomes false.
std::atomic_bool m_launching{false};
std::atomic_bool m_launch_failed{false};
std::thread m_launch_thread;
std::thread m_stdout_thread;
std::thread m_stderr_thread;
std::mutex m_output_mutex;
std::vector<OutputLine> m_output_queue;
std::unique_ptr<boost::process::child> m_process;
std::unique_ptr<boost::process::ipstream> m_stdout_pipe;
std::unique_ptr<boost::process::ipstream> m_stderr_pipe;
std::unique_ptr<boost::process::opstream> m_stdin_pipe;
OutputCallback m_on_output;
DoneCallback m_on_done;
};
}} // namespace Slic3r::GUI
#endif
+178
View File
@@ -0,0 +1,178 @@
#include "SpeedDialDialog.hpp"
#include "ActionRegistry.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include "NotificationManager.hpp"
#include "Plater.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <algorithm>
#include <wx/display.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
namespace Slic3r { namespace GUI {
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 kPopupMaxHeight = 282;
int json_int_or(const nlohmann::json& j, const char* key, int fallback)
{
auto it = j.find(key);
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)
{
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)));
}
}
SpeedDialWebDialog::~SpeedDialWebDialog() { m_alive->store(false, std::memory_order_release); }
void SpeedDialWebDialog::request_show()
{
if (IsShown()) {
Raise();
if (browser())
browser()->SetFocus();
return;
}
Show();
Raise();
if (m_page_ready)
send_actions();
if (browser())
browser()->SetFocus();
}
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
{
if (handle_common_script_command(payload))
return;
// Defer command handling out of the webview script-message callback: GTK and macOS deliver
// it synchronously inside the native webview callback, and window work on that stack is the
// crash class fixed in b779a7bfed/f2ccbfc8b5 (see PluginsDialog::on_script_message).
// run_action puts a modal confirm on that stack, which is the same bug.
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
if (alive->load(std::memory_order_acquire))
handle_web_command(payload);
});
}
void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
{
const std::string command = payload.value("command", "");
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") {
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 == "run_action")
run_action(payload.value("id", ""), payload.value("title", ""));
else if (command == "resize")
resize_to_content(json_int_or(payload, "height", 0));
}
void SpeedDialWebDialog::resize_to_content(int height)
{
if (height <= 0)
return;
int display_index = wxDisplay::GetFromWindow(this);
if (display_index == wxNOT_FOUND)
display_index = 0;
const int screen_dip = ToDIP(wxDisplay(display_index).GetClientArea().GetHeight());
const int max_dip = std::max(kPopupMinHeight, screen_dip * 85 / 100);
const int height_dip = std::max(kPopupMinHeight, std::min(height, max_dip));
SetClientSize(FromDIP(wxSize(kPopupWidth, height_dip)));
Layout();
}
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title)
{
ActionRegistry& reg = wxGetApp().action_registry();
const AppAction* a = reg.by_id(id);
if (!a)
return;
const bool ask = reg.should_ask(id);
const std::string atitle = a->title();
if (IsModal())
EndModal(wxID_CANCEL);
else
Hide();
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);
dlg.ShowCheckBox(_L("Don't ask again for this action"));
if (dlg.ShowModal() != wxID_OK)
return;
if (dlg.IsCheckBoxChecked())
wxGetApp().action_registry().suppress_ask(id);
}
wxGetApp().CallAfter([id] {
if (wxGetApp().is_closing())
return;
AppActionRunResult result = wxGetApp().action_registry().run(id);
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));
});
}
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"])}});
}
}}
+34
View File
@@ -0,0 +1,34 @@
#ifndef slic3r_GUI_SpeedDialDialog_hpp_
#define slic3r_GUI_SpeedDialDialog_hpp_
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <string>
namespace Slic3r { namespace GUI {
class SpeedDialWebDialog : public WebViewHostDialog
{
public:
explicit SpeedDialWebDialog(wxWindow* parent);
~SpeedDialWebDialog() override;
void request_show();
private:
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 send_actions();
bool m_page_ready{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);
};
}}
#endif
+53 -17
View File
@@ -1074,6 +1074,28 @@ void add_correct_opts_to_options_list(const std::string &opt_key, std::map<std::
map.emplace(opt_key + "#0", value);
}
std::string Tab::options_list_storage_key(const std::string& opt_key) const
{
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" ||
opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area")
return opt_key;
if (m_config == nullptr || !m_config->has(opt_key))
return opt_key;
const ConfigOption* option = m_config->option(opt_key);
if (option == nullptr || !option->is_vector())
return opt_key;
const ConfigOptionDef* def = m_config->def()->get(opt_key);
if (def == nullptr)
return opt_key;
const bool serialized = def->gui_flags == "serialized";
const bool is_plugin_field = def->gui_type == ConfigOptionDef::GUIType::plugin_picker;
return (serialized || is_plugin_field) ? opt_key : opt_key + "#0";
}
void Tab::update_all_extruder_options_status()
{
if (!m_extruder_switch && !m_variant_combo) {
@@ -1242,23 +1264,14 @@ void Tab::check_extruder_options_status(int index, bool &sys_extruder, bool &mod
}
}
}
void Tab::init_options_list()
{
if (!m_options_list.empty())
m_options_list.clear();
for (const std::string& opt_key : m_config->keys())
{
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") {
m_options_list.emplace(opt_key, m_opt_status_value);
continue;
}
const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
if (m_config->option(opt_key)->is_vector() && !(opt_def && opt_def->gui_flags == "serialized"))
m_options_list.emplace(opt_key + "#0", m_opt_status_value);
else
m_options_list.emplace(opt_key, m_opt_status_value);
}
m_options_list.emplace(options_list_storage_key(opt_key), m_opt_status_value);
}
void TabPrinter::init_options_list()
@@ -1780,6 +1793,12 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
return;
}
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so full_config() and
// save_to_json() always find resolved "name;uuid;capability" references and rebuild it nowhere else.
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
opt_def && opt_def->is_plugin_backed())
m_config->update_plugin_manifest();
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
if (auto printer_tab = dynamic_cast<TabPrinter*>(this))
printer_tab->on_gcode_flavor_changed();
@@ -2071,7 +2090,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
bool is_safe_to_rotate = _sparse_infill_pattern == ipRectilinear || _sparse_infill_pattern == ipLine ||
_sparse_infill_pattern == ipZigZag || _sparse_infill_pattern == ipCrossZag ||
_sparse_infill_pattern == ipLockedZag;
auto new_value = boost::any_cast<std::string>(value);
is_safe_to_rotate = is_safe_to_rotate || new_value.empty();
const bool had_previous_value = !m_last_sparse_infill_rotate_template_value.empty();
@@ -3097,6 +3116,17 @@ void TabPrint::build()
option.opt.height = 15;
optgroup->append_single_option_line(option, "others_settings_post_processing_scripts");
optgroup = page->new_optgroup(L("Slicing Pipeline Plugin"), L"param_gcode", 0);
optgroup->hide_labels();
option = optgroup->get_option("slicing_pipeline_plugin");
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
// Its own group: the one above hides its labels, and this row needs its label — and the revert
// arrow beside it — to show. No label-width override either, as a 0 there means "no label column".
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
optgroup->append_single_option_line("plugin_config_overrides");
optgroup = page->new_optgroup(L("Notes"), "note", 0);
option = optgroup->get_option("notes");
option.opt.full_width = true;
@@ -4180,7 +4210,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
if (og_ironing_it != page->m_optgroups.end())
{
ConfigOptionsGroupShp ironing_optgroup = *og_ironing_it;
std::vector<std::string> ironing_opt_keys = {
"filament_ironing_flow",
"filament_ironing_spacing",
@@ -4192,7 +4222,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
{
if (m_overrides_options.find(opt_key) == m_overrides_options.end())
continue;
bool is_checked = !dynamic_cast<ConfigOptionVectorBase*>(m_config->option(opt_key))->is_nil(extruder_idx);
m_overrides_options[opt_key]->Enable(true);
m_overrides_options[opt_key]->SetValue(is_checked);
@@ -4498,6 +4528,9 @@ void TabFilament::build()
option.opt.height = gcode_field_height;// 150;
optgroup->append_single_option_line(option);
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
optgroup->append_single_option_line("plugin_config_overrides");
page = add_options_page(L("Multimaterial"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders
optgroup = page->new_optgroup(L("Wipe tower parameters"), "param_tower");
optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters");
@@ -4506,7 +4539,7 @@ void TabFilament::build()
optgroup->append_single_option_line("filament_tower_ironing_area", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_tower_interface_purge_volume", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_tower_interface_print_temp", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup = page->new_optgroup(L("Multi Filament"));
// optgroup->append_single_option_line("filament_flush_temp", "", 0);
// optgroup->append_single_option_line("filament_flush_volumetric_speed", "", 0);
@@ -5004,6 +5037,9 @@ void TabPrinter::build_fff()
// optgroup->append_single_option_line("spaghetti_detector");
optgroup->append_single_option_line("time_cost", "printer_basic_information_advanced#time-cost");
optgroup = page->new_optgroup(L("Plugin Configuration"), L"param_gcode");
optgroup->append_single_option_line("plugin_config_overrides");
optgroup = page->new_optgroup(L("Cooling Fan"), "param_cooling_fan");
Line line = Line{ L("Fan speed-up time"), optgroup->get_option("fan_speedup_time").opt.tooltip };
line.label_path = "printer_basic_information_cooling_fan#fan-speed-up-time";
@@ -5989,7 +6025,7 @@ void TabPrinter::toggle_options()
toggle_line("support_air_filtration", !m_config->opt_bool("support_cooling_filter"));
toggle_line("cooling_filter_enabled", m_config->opt_bool("support_cooling_filter"));
}
if (m_active_page->title() == L("Machine G-code")) {
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
@@ -7881,7 +7917,7 @@ bool Tab::validate_filament_temperature_pairs()
if (delta <= rule.max_delta)
continue;
const wxString deg_c = wxString::FromUTF8("°C");
const wxString deg_c = wxString::FromUTF8("℃");
const wxString bullet = wxString::FromUTF8("•");
invalid_pairs += wxString::Format(_L(" - %s:\n %s first layer %d %s, other layers %d %s\n %s max delta %d %s, current delta %d %s\n"),
rule.label, bullet, first_temp, deg_c, other_temp, deg_c, bullet, rule.max_delta, deg_c, delta, deg_c);
+1
View File
@@ -383,6 +383,7 @@ public:
virtual void update() = 0;
virtual void toggle_options() = 0;
virtual void init_options_list();
std::string options_list_storage_key(const std::string& opt_key) const;
virtual void update_custom_dirty(std::vector<std::string> &dirty_options, std::vector<std::string> &nonsys_options) {}
void load_initial_data();
void update_dirty();
+163
View File
@@ -0,0 +1,163 @@
#include "TerminalDialog.hpp"
#include "ProcessRunner.hpp"
#include "I18N.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include <wx/sizer.h>
#include <cctype>
namespace Slic3r { namespace GUI {
namespace {
bool consume_command_prefix(const std::string& command, const std::string& prefix, std::string& remainder)
{
size_t pos = 0;
while (pos < command.size() && std::isspace(static_cast<unsigned char>(command[pos])))
++pos;
if (command.compare(pos, prefix.size(), prefix) != 0)
return false;
pos += prefix.size();
if (pos < command.size() && !std::isspace(static_cast<unsigned char>(command[pos])))
return false;
while (pos < command.size() && std::isspace(static_cast<unsigned char>(command[pos])))
++pos;
remainder = command.substr(pos);
return true;
}
std::string quote_command_line_arg(const std::string& value)
{
std::string quoted = "\"";
for (const char ch : value) {
if (ch == '"')
quoted += "\\\"";
else
quoted += ch;
}
quoted += "\"";
return quoted;
}
} // namespace
TerminalDialog::TerminalDialog(wxWindow* parent, wxWindowID id, const wxString& title,
const wxPoint& pos, const wxSize& size, long style)
: WebViewHostDialog(parent, id, title, pos, size, style)
{
m_runner = std::make_unique<ProcessRunner>();
create_webview("web/dialog/TerminalDialog/index.html", _L("Terminal"),
wxSize(820, 600), wxSize(640, 480));
}
TerminalDialog::~TerminalDialog()
{
if (m_runner && m_runner->is_running())
m_runner->terminate();
}
void TerminalDialog::on_script_message(const nlohmann::json& payload)
{
const std::string command = payload.value("command", "");
if (command == "run_command") {
std::string cmd = payload.value("cmd", "");
if (!cmd.empty())
resolve_and_run(cmd);
}
else if (command == "write_stdin") {
std::string data = payload.value("data", "");
if (!data.empty() && m_runner && m_runner->is_running())
m_runner->write_stdin(data);
}
else {
handle_common_script_command(payload);
}
}
void TerminalDialog::resolve_and_run(const std::string& cmd)
{
if (m_runner->is_running()) {
nlohmann::json err;
err["command"] = "process_error";
err["message"] = "A command is already running.";
call_web_handler(err);
return;
}
std::string executable;
std::string arg_string;
auto send_process_error = [this](const std::string& message) {
nlohmann::json err;
err["command"] = "process_error";
err["message"] = message;
call_web_handler(err);
};
// Parse command: must start with "python" or "uv"
if (consume_command_prefix(cmd, "python", arg_string)) {
const std::string python_path = PythonInterpreter::bundled_python_executable();
if (python_path.empty()) {
send_process_error("Bundled Python executable not found.");
return;
}
executable = python_path;
}
else if (consume_command_prefix(cmd, "uv", arg_string)) {
const std::string uv_path = PythonInterpreter::bundled_uv_path();
if (uv_path.empty()) {
send_process_error("uv executable not found.");
return;
}
executable = uv_path;
}
else {
send_process_error("Only 'python' and 'uv' commands are supported.");
return;
}
std::string command_line = quote_command_line_arg(executable);
if (!arg_string.empty()) {
command_line += " ";
command_line += arg_string;
}
bool started = m_runner->run_command_line_async(
command_line,
[this](const std::vector<ProcessRunner::OutputLine>& lines) {
nlohmann::json msg;
msg["command"] = "output";
nlohmann::json lines_arr = nlohmann::json::array();
for (const auto& line : lines) {
nlohmann::json l;
l["text"] = line.text;
l["is_stderr"] = line.is_stderr;
lines_arr.push_back(std::move(l));
}
msg["lines"] = std::move(lines_arr);
call_web_handler(msg);
},
[this](int exit_code) {
nlohmann::json msg;
msg["command"] = "process_done";
msg["exit_code"] = exit_code;
call_web_handler(msg);
});
if (!started) {
nlohmann::json err;
err["command"] = "process_error";
err["message"] = "Failed to start process.";
call_web_handler(err);
}
}
}} // namespace Slic3r::GUI
+36
View File
@@ -0,0 +1,36 @@
#ifndef slic3r_TerminalDialog_hpp_
#define slic3r_TerminalDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
#include <memory>
#include <string>
#include <vector>
namespace Slic3r { namespace GUI {
class ProcessRunner;
class TerminalDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
TerminalDialog(wxWindow* parent,
wxWindowID id = wxID_ANY,
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
~TerminalDialog() override;
private:
void on_script_message(const nlohmann::json& payload) override;
void resolve_and_run(const std::string& cmd);
std::unique_ptr<ProcessRunner> m_runner;
};
}} // namespace Slic3r::GUI
#endif
+19
View File
@@ -231,6 +231,17 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int
m_sizer_main->Add(m_gauge, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28));
}
// Optional elapsed/estimated/remaining time labels, created only when the
// caller opts in via the wxPD_*_TIME style flags (so callers that don't set
// them are unaffected). Update()/Pulse() already refresh these once non-null.
if (HasPDFlag(wxPD_ELAPSED_TIME | wxPD_ESTIMATED_TIME | wxPD_REMAINING_TIME)) {
wxFlexGridSizer *sizer_times = new wxFlexGridSizer(2, FromDIP(2), FromDIP(8));
if (HasPDFlag(wxPD_ELAPSED_TIME)) m_elapsed = CreateLabel(GetElapsedLabel(), sizer_times);
if (HasPDFlag(wxPD_ESTIMATED_TIME)) m_estimated = CreateLabel(GetEstimatedLabel(), sizer_times);
if (HasPDFlag(wxPD_REMAINING_TIME)) m_remaining = CreateLabel(GetRemainingLabel(), sizer_times);
m_sizer_main->Add(sizer_times, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, FromDIP(12));
}
#ifdef __WXMSW__
//m_block_left = new wxWindow(m_gauge, wxID_ANY, wxPoint(0, 0), wxSize(FromDIP(2), PROGRESSDIALOG_GAUGE_SIZE.y * 2));
//m_block_left->SetBackgroundColour(PROGRESSDIALOG_DEF_BK);
@@ -490,6 +501,14 @@ wxStaticText *ProgressDialog::CreateLabel(const wxString &text, wxSizer *sizer)
wxStaticText *label = new wxStaticText(this, wxID_ANY, text);
wxStaticText *value = new wxStaticText(this, wxID_ANY, wxGetTranslation("unknown"));
// Match the message label's look so the times theme with the rest of the
// dialog: PROGRESSDIALOG_GREY_700 is a key in the dark-mode colour map, so
// UpdateDlgDarkUI() (called at the end of Create()) remaps it in dark mode.
for (wxStaticText *st : {label, value}) {
st->SetFont(::Label::Body_13);
st->SetForegroundColour(PROGRESSDIALOG_GREY_700);
}
// select placement most native or nice on target GUI
#if defined(__WXMSW__) || defined(__WXMAC__) || defined(__WXGTK20__)
// value and time centered in one row
+7 -1
View File
@@ -407,6 +407,12 @@ void WebView::RecreateAll()
for (auto webView : g_webviews) {
webView->SetUserAgent(wxString::Format("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) BBL-Slicer/v%s (%s) BBL-Language/%s",
Slic3r::GUI::wxGetApp().get_bbl_client_version(), dark ? "dark" : "light", language_code.mb_str()));
webView->Reload();
// A host-themed WebViewHostDialog re-themes in place (no reload). If it handles
// the event, skip the reload; legacy pages fall through and reload as before
// (their own dark.css swap re-themes them on reload).
wxCommandEvent evt(EVT_WEBVIEW_RECREATED);
evt.SetEventObject(webView);
if (!webView->GetEventHandler()->ProcessEvent(evt))
webView->Reload();
}
}
+3
View File
@@ -2,6 +2,9 @@
#define slic3r_GUI_WebView_hpp_
#include <wx/webview.h>
#include <wx/event.h>
wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
class WebView
{
@@ -0,0 +1,265 @@
#include "WebViewHostDialog.hpp"
#include "WebView.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <nlohmann/json.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <wx/log.h>
#include <wx/sizer.h>
#include <algorithm>
namespace Slic3r { namespace GUI {
namespace {
// CSS "#rrggbb" for a wxColour (portable accessor used throughout the codebase).
std::string css_color(const wxColour& c) { return c.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); }
// "dark"/"light" for the live app theme — the value of both data-orca-theme and color-scheme.
std::string host_theme_name() { return wxGetApp().dark_mode() ? "dark" : "light"; }
// The host theme "contract": CSS custom properties filled from the LIVE app theme,
// plus color-scheme. Consumed by resources/web/dialog/css/theme.css and by plugin
// content. Variables only — no element styling — so it never fights a page's CSS.
std::string host_theme_vars_css()
{
GUI_App& app = wxGetApp();
const wxColour bg = app.get_window_default_clr();
const wxColour fg = app.get_label_clr_default();
const wxColour muted = app.get_label_clr_sys();
const wxColour border = app.get_highlight_default_clr();
const wxColour accent = StateColor::darkModeColorFor(wxColour("#009688"));
std::string font = app.normal_font().GetFaceName().ToStdString();
// Strip characters that could break out of the CSS value / <style> block.
font.erase(std::remove_if(font.begin(), font.end(), [](char c) {
return c == '\'' || c == '"' || c == '<' || c == '>' || c == '{' || c == '}' || c == ';';
}),
font.end());
std::string s;
s += ":root{";
s += "--orca-bg:" + css_color(bg) + ";";
s += "--orca-fg:" + css_color(fg) + ";";
s += "--orca-muted:" + css_color(muted) + ";";
s += "--orca-border:" + css_color(border) + ";";
s += "--orca-accent:" + css_color(accent) + ";";
s += "--orca-accent-fg:#ffffff;";
s += "--orca-font:" + (font.empty() ? std::string() : "'" + font + "',") +
"system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;";
s += "color-scheme:" + host_theme_name() + ";";
s += "}";
return s;
}
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
std::string host_theme_user_script()
{
const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>";
return WebViewHostDialog::document_start_injector(
style, "orca-host-theme-vars", "afterbegin",
"window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";",
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
// JS to re-theme an already-loaded document live (no reload): replace the injected
// style's contents and update data-orca-theme. Everything downstream (theme.css
// tokens, plugin element defaults, page layout) re-cascades from these values.
std::string host_theme_apply_js()
{
const std::string vars_literal = nlohmann::json(host_theme_vars_css()).dump();
const std::string theme = host_theme_name();
return "(function(){var css=" + vars_literal + ";var theme=\"" + theme + "\";" + R"JS(
var el=document.getElementById('orca-host-theme-vars');
if(el){el.textContent=css;}
else if(document.head){document.head.insertAdjacentHTML('afterbegin','<style id="orca-host-theme-vars"></style>');var e2=document.getElementById('orca-host-theme-vars');if(e2)e2.textContent=css;}
if(document.documentElement)
document.documentElement.setAttribute('data-orca-theme',theme);
})();)JS";
}
} // namespace
std::string WebViewHostDialog::document_start_injector(const std::string& markup,
const char* dom_id,
const char* position,
const std::string& prelude,
const std::string& on_inject)
{
const std::string literal = nlohmann::json(markup).dump();
std::string s;
s += "(function(){";
s += prelude;
s += "var css=" + literal + ";";
s += "function inject(){";
s += "var root=document.head||document.documentElement;if(!root)return false;";
s += "if(!document.getElementById('" + std::string(dom_id) + "'))root.insertAdjacentHTML('" +
std::string(position) + "',css);";
s += on_inject;
s += "return true;}";
s += "if(inject())return;";
s += "var obs=new MutationObserver(function(){if(inject())obs.disconnect();});";
s += "obs.observe(document,{childList:true});})();";
return s;
}
WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
wxWindowID id,
const wxString& title,
const wxPoint& pos,
const wxSize& size,
long style)
: DPIDialog(parent, id, title, pos, size, style)
{
SetBackgroundColour(*wxWHITE);
}
bool WebViewHostDialog::create_webview(const std::string& resource_path,
const wxString& title,
const wxSize& dialog_size,
const wxSize& min_size)
{
SetTitle(title);
SetMinSize(FromDIP(min_size));
const wxString target_url = build_resource_url(resource_path);
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
m_browser = WebView::CreateWebView(this, target_url);
if (m_browser == nullptr) {
wxLogError("Could not init m_browser");
delete topsizer;
return false;
}
SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
SetSize(FromDIP(dialog_size));
CenterOnParent();
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewHostDialog::on_script_message_event, this, m_browser->GetId());
// Inject the shared host theme contract BEFORE the first load so the page paints in
// the app theme with no flash, and re-theme live when the app theme toggles.
register_theme_user_scripts();
m_browser->Bind(EVT_WEBVIEW_RECREATED, &WebViewHostDialog::on_webview_recreated, this);
load_url(target_url);
wxGetApp().UpdateDlgDarkUI(this);
return true;
}
wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) const
{
wxString target_url = from_u8((boost::filesystem::path(resources_dir()) / resource_path).make_preferred().string());
if (append_language_to_url()) {
const wxString lang = wxGetApp().current_language_code_safe();
if (!lang.empty()) {
target_url += wxT("?lang=");
target_url += lang;
}
}
return wxString("file://") + target_url;
}
void WebViewHostDialog::load_url(const wxString& url)
{
if (!m_browser)
return;
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " enter, url=" << into_u8(url);
WebView::LoadUrl(m_browser, url);
m_browser->SetFocus();
}
bool WebViewHostDialog::run_script(const wxString& script)
{
if (!m_browser)
return false;
return WebView::RunScript(m_browser, script);
}
void WebViewHostDialog::call_web_handler(const nlohmann::json& payload, const wxString& handler)
{
const wxString payload_text = wxString::FromUTF8(payload.dump(-1, ' ', false, nlohmann::json::error_handler_t::ignore));
const wxString script = handler + wxT("(") + payload_text + wxT(")");
wxGetApp().CallAfter([this, script] { run_script(script); });
}
bool WebViewHostDialog::handle_common_script_command(const nlohmann::json& payload, int close_return_code)
{
const std::string command = payload.value("command", "");
if (command == "close_page") {
if (IsModal())
EndModal(close_return_code);
else
Close();
return true;
}
return false;
}
void WebViewHostDialog::on_dpi_changed(const wxRect&)
{
Refresh();
}
void WebViewHostDialog::on_script_message_parse_error(const wxString& payload, const std::exception& error)
{
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << "; payload=" << into_u8(payload) << "; error=" << error.what();
}
void WebViewHostDialog::on_script_message_event(wxWebViewEvent& event)
{
const wxString payload = event.GetString();
try {
on_script_message(nlohmann::json::parse(payload.utf8_string()));
} catch (const std::exception& e) {
on_script_message_parse_error(payload, e);
}
}
void WebViewHostDialog::register_theme_user_scripts()
{
if (!m_browser)
return;
// Added once, at creation. Deliberately no RemoveAllUserScripts() here: the "wx"
// script message handler is registered separately (AddScriptMessageHandler), but on
// some backends RemoveAllUserScripts() drops it too, which would break
// window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live().
m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script()));
add_user_scripts();
}
void WebViewHostDialog::apply_theme_live()
{
if (!m_browser)
return;
// Update the already-loaded document in place (no reload, no flash) by rewriting the
// injected :root variables + data-orca-theme; the whole cascade re-flows from these.
// The document-start script keeps the creation-time theme for any later reload, and
// these dialogs are not reloaded on a theme toggle (see WebView::RecreateAll).
run_script(wxString::FromUTF8(host_theme_apply_js()));
}
void WebViewHostDialog::on_webview_recreated(wxCommandEvent&)
{
// Handled: do NOT Skip(), so WebView::RecreateAll skips the redundant reload.
apply_theme_live();
}
}} // namespace Slic3r::GUI
@@ -0,0 +1,88 @@
#ifndef slic3r_GUI_Widgets_WebViewHostDialog_hpp_
#define slic3r_GUI_Widgets_WebViewHostDialog_hpp_
#include <nlohmann/json.hpp>
#include <slic3r/GUI/GUI_Utils.hpp>
#include <exception>
#include <string>
#include <wx/string.h>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
// Shared shell for local HTML dialogs that communicate through window.wx.postMessage().
class WebViewHostDialog : public Slic3r::GUI::DPIDialog
{
public:
WebViewHostDialog(wxWindow* parent,
wxWindowID id = wxID_ANY,
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
// wxRESIZE_BORDER is required for a resizable frame on MSW/GTK; macOS derives
// one from wxMAXIMIZE_BOX alone, which is why these dialogs used to resize only there.
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~WebViewHostDialog() override = default;
bool create_webview(const std::string& resource_path,
const wxString& title,
const wxSize& dialog_size = wxSize(820, 660),
const wxSize& min_size = wxSize(640, 640));
void load_url(const wxString& url);
bool run_script(const wxString& script);
void call_web_handler(const nlohmann::json& payload, const wxString& handler = wxT("HandleStudio"));
// Wraps `markup` (an HTML fragment, usually a <style> block) in a document-start user
// script that inserts it once — guarded by element id `dom_id`, at `position` (an
// insertAdjacentHTML target such as "afterbegin"/"beforeend") — retrying via a
// MutationObserver until a root node exists. On WebView2 a document-start script can run
// before <html> exists (document.head and document.documentElement both null), so a bare
// insert would throw and silently never apply. `prelude` is emitted once before the
// injector (extra var/flag declarations); `on_inject` runs inside inject() after each
// successful insert. Both default to empty.
static std::string document_start_injector(const std::string& markup,
const char* dom_id,
const char* position,
const std::string& prelude = {},
const std::string& on_inject = {});
protected:
wxWebView* browser() const { return m_browser; }
wxString build_resource_url(const std::string& resource_path) const;
bool handle_common_script_command(const nlohmann::json& payload, int close_return_code = wxID_CANCEL);
void on_dpi_changed(const wxRect& suggested_rect) override;
virtual void on_script_message(const nlohmann::json& payload) = 0;
virtual void on_script_message_parse_error(const wxString& payload, const std::exception& error);
virtual bool append_language_to_url() const { return true; }
// Registers all document-start user scripts: the shared host theme contract first,
// then subclass scripts from add_user_scripts(). Called ONCE, at creation. Live
// re-theme goes through apply_theme_live() (RunScript), not a re-registration —
// calling this again would append duplicate scripts.
void register_theme_user_scripts();
// Subclasses override to add page-specific document-start user scripts (e.g. the
// plugin bridge / unstyled-content defaults). Called AFTER the theme contract is
// added, by register_theme_user_scripts(). Default: none.
virtual void add_user_scripts() {}
// Pushes the current app theme into the already-loaded document without a reload
// (updates the injected :root variables and the data-orca-theme attribute).
void apply_theme_live();
private:
void on_script_message_event(wxWebViewEvent& event);
void on_webview_recreated(wxCommandEvent& event);
wxWebView* m_browser{nullptr};
};
}} // namespace Slic3r::GUI
#endif