Add actions speed dial

This commit is contained in:
Andrew
2026-07-09 12:52:30 +08:00
parent fb2c0ef237
commit 6daef171c1
18 changed files with 1309 additions and 147 deletions

View File

@@ -4,10 +4,9 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "PluginScriptRunner.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include "slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp"
#include <libslic3r/Utils.hpp>
@@ -19,8 +18,6 @@
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <pybind11/embed.h>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
@@ -898,147 +895,22 @@ wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
{
if (plugin_key.empty() || capability_name.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring run_script_plugin with an empty plugin key or capability name";
send_plugins();
// Shared with the speed dial; validation, GIL/UI-thread execution, re-entrancy latch
// and error bookkeeping all live in run_script_plugin_capability.
const ScriptRunOutcome outcome = run_script_plugin_capability(plugin_key, capability_name);
if (outcome.level == ScriptRunOutcome::Level::Busy)
return;
}
PluginManager& manager = PluginManager::instance();
auto cap = manager.get_loader().get_plugin_capability_by_name(
plugin_key, Slic3r::PluginCapabilityType::Script, capability_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for missing script capability. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
send_plugins();
return;
}
if (!cap->enabled) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for disabled script capability. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
send_plugins();
return;
}
// A plugin's modal orca.host.ui dialog or the result message box pumps a nested event
// loop; the WebView could re-dispatch this command mid-run. Refuse the overlapping run.
if (m_script_running) {
BOOST_LOG_TRIVIAL(info) << "Ignoring run_script_plugin; a plugin is already running. plugin_key=" << plugin_key;
return;
}
m_script_running = true;
ScopeGuard running_guard([this]() { m_script_running = false; });
BOOST_LOG_TRIVIAL(info) << "Run script plugin requested from Plugins dialog. plugin_key=" << plugin_key
<< " capability_name=" << capability_name;
auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
if (!manager.get_catalog().set_plugin_error(plugin_key, normalized_error))
BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
if (!manager.get_loader().unload_plugin(plugin_key))
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin after script error. plugin_key=" << plugin_key;
send_plugins();
// The row now shows an "Error" status and the Diagnostics tab holds the full text, so surface
// the outcome in the footer status bar instead of a modal box (prefer the friendlier override).
const wxString message = status_message.empty() ? from_u8(normalized_error) : status_message;
show_status(message, "error");
};
PluginDescriptor descriptor;
if (!get_descriptor(plugin_key, descriptor)) {
BOOST_LOG_TRIVIAL(error) << "Cannot run script plugin because manifest was not found. plugin_key=" << plugin_key;
complete_with_error("Plugin manifest was not found.", _L("Plugin manifest was not found."));
return;
}
if (descriptor.has_error()) {
complete_with_error(descriptor.normalized_error(), wxString());
return;
}
// Should not reach here, handle for extra safety
if (!descriptor.is_metadata_valid()) {
std::string plugin_type_str = plugin_capability_type_to_string(descriptor.primary_capability_type());
std::string metadata_valid = descriptor.is_metadata_valid() ? "true" : "false";
const std::string plugin_error = "Cannot run plugin because its metadata is invalid:\n\tplugin type: " + plugin_type_str +
"\n\tmetadata_valid: " + metadata_valid;
BOOST_LOG_TRIVIAL(error) << "Cannot run plugin because its metadata is invalid. plugin_key=" << plugin_key
<< " is_metadata_valid=" << descriptor.is_metadata_valid()
<< " type=" << plugin_capability_type_to_string(descriptor.primary_capability_type());
complete_with_error(plugin_error, _L("Only plugins with valid metadata can be run from this dialog."));
return;
}
// Should not reach here as non-loaded plugins have disabled run buttons, handle for extra safety
if (!manager.get_loader().is_plugin_loaded(plugin_key)) {
BOOST_LOG_TRIVIAL(warning) << "Cannot run script plugin because it is not loaded. plugin_key=" << plugin_key;
complete_with_error("Load the script plugin before running it: Cannot run script plugin because it is not loaded.",
_L("Load the script plugin before running it."));
return;
}
auto plugin = std::dynamic_pointer_cast<Slic3r::ScriptPluginCapability>(cap->instance);
if (!plugin) {
BOOST_LOG_TRIVIAL(error) << "Loaded plugin does not implement ScriptPluginCapability. plugin_key=" << plugin_key;
complete_with_error("The selected plugin is not a runnable script plugin: Loaded plugin does not implement ScriptPluginCapability.",
_L("The selected plugin is not a runnable script plugin."));
return;
}
std::string error;
ExecutionResult result;
// Script plugins run on the main/UI thread (not a worker). They hold live, non-owning
// ModelObject*/ModelVolume*/ModelInstance* aliases into host data and can mint ObjectIDs,
// which libslic3r requires on the main thread (ObjectID.hpp's non-atomic s_last_id). Running
// here makes those reads/instantiations legal and means nothing mutates the model underneath
// a run. The trade-off is that a slow execute() freezes the UI: the contract (see
// plugin_development.md) is to keep execute() quick and offload heavy work to the plugin's own
// threading.Thread. orca.host.ui calls already no-op their main-thread marshaling here.
{
wxBusyCursor busy;
try {
PythonGILState gil;
result = plugin->execute();
} catch (const std::exception& ex) {
error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw exception. plugin_key=" << plugin_key << " error=" << error;
} catch (...) {
error = "Unknown error";
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw unknown exception. plugin_key=" << plugin_key;
}
}
if (!error.empty()) {
plugin.reset();
cap.reset();
complete_with_error(error, wxString());
return;
}
BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
<< " status=" << static_cast<int>(result.status) << " message=" << result.message << " data=" << result.data;
const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError;
if (failed) {
plugin.reset();
cap.reset();
// complete_with_error normalizes an empty message to "Script plugin failed." and reports via the status bar.
complete_with_error(result.message, wxString());
return;
}
manager.clear_plugin_error(plugin_key);
send_plugins();
const bool skipped = result.status == PluginResult::Skipped;
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
const wxString message = result.message.empty() ? fallback : from_u8(result.message);
show_status(message, skipped ? "info" : "success");
// The row shows any "Error" status and the Diagnostics tab holds the full text, so surface
// the outcome in the footer status bar instead of a modal box (prefer the friendlier override).
if (outcome.message.empty())
return;
const char* level = outcome.level == ScriptRunOutcome::Level::Error ? "error" :
outcome.level == ScriptRunOutcome::Level::Success ? "success" :
"info";
show_status(outcome.message, level);
}
void PluginsDialog::update_plugin(const std::string& plugin_key)