mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Merge branch 'main' into feature/bambu_printer_housekeeping
# Conflicts: # tests/slic3rutils/CMakeLists.txt
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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:"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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(¬_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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+558
-552
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"])}});
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user