Files
OrcaSlicer/src/slic3r/GUI/PluginsDialog.cpp
Andrew a11e442f1d Add version sorting for plugins
Enhance plugin dialog with semver-aware sorting
by version, enabling users to sort plugins based
on version comparisons.
2026-07-06 15:25:09 +08:00

1273 lines
54 KiB
C++

#include "PluginsDialog.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "OrcaCloudServiceAgent.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>
#include <slic3r/GUI/NotificationManager.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <slic3r/GUI/format.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <pybind11/embed.h>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <memory>
#include <mutex>
#include <vector>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/filedlg.h>
#include <wx/msgdlg.h>
#include <wx/progdlg.h>
#include <wx/timer.h>
#include <wx/utils.h>
namespace Slic3r { namespace GUI {
namespace {
const wxString kDeletePluginTitle = _L("Delete Plugin");
const wxString kUnsubscribeTitle = _L("Unsubscribe");
const wxString kOverwritePluginTitle = _L("Overwrite Plugin");
std::string s_selected_plugin_install_action = "explore";
struct PluginContextAction
{
std::string id;
std::string label;
bool enabled = true;
bool danger = false;
};
struct PluginAvailableActions
{
bool can_toggle = false;
bool toggle_installs_cloud_plugin = false;
std::vector<PluginContextAction> context_actions;
};
struct PluginCapabilityView
{
std::string name;
std::string type_label;
std::string type_key;
bool enabled = false;
bool can_toggle = false;
bool can_run = false;
};
struct PluginChangelogView
{
std::string version;
std::string changelog;
long long created_time = 0;
};
// View-model for one plugin row in the dialog
struct PluginDialogItem
{
// Identity and display text
std::string plugin_key;
std::string plugin_id;
std::string display_name;
std::string description;
std::string author;
std::string version;
std::string installed_version;
std::string latest_version;
std::string sort_version; // Version shown in the row (installed if installed, else latest); used by the Version sort.
std::string type_label;
std::string type_key;
std::string sharing_token;
std::string thumbnail_url;
std::vector<std::string> type_labels;
std::vector<PluginChangelogView> changelog;
// Derived UI state
PluginSource source = PluginSource::Local;
PluginStatus status = PluginStatus::Inactive;
PluginUpdateStatus update_status = PluginUpdateStatus::Normal;
std::string error_text;
bool has_error = false;
bool is_loaded = false;
bool loading = false;
// Installation and capability flags
bool is_cloud_plugin = false;
bool has_local_package = false;
bool unauthorized = false;
bool has_script_capability = false;
bool can_run_script = false;
// Runtime capabilities in registration order, or descriptor-only type rows when unloaded.
std::vector<PluginCapabilityView> capabilities;
// Row-level actions
PluginAvailableActions available_actions;
};
constexpr bool kFetchCloudCatalog = true;
constexpr bool kUseCurrentCloudCatalog = false;
std::vector<PluginDescriptor> current_cloud_catalog_snapshot()
{
const auto& catalog = PluginManager::instance().get_catalog();
std::vector<PluginDescriptor> cloud_entries;
auto append_cloud_entries = [&cloud_entries](const std::vector<PluginDescriptor>& entries) {
for (const PluginDescriptor& entry : entries)
if (entry.is_cloud_plugin())
cloud_entries.push_back(entry);
};
append_cloud_entries(catalog.get_all_plugin_descriptors());
append_cloud_entries(catalog.get_invalid_plugins());
return cloud_entries;
}
PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
{
descriptor.plugin_root.clear();
descriptor.entry_path.clear();
descriptor.installed_version.clear();
if (descriptor.cloud.has_value()) {
descriptor.cloud->installed = false;
descriptor.cloud->update_available = false;
}
return descriptor;
}
// rescan_plugins() clears the whole catalog and rediscovers only local packages. When
// callers do not need fresh cloud data, reuse the current cloud rows after the local
// rescan so cloud-only rows and cloud-derived UI state do not disappear.
void refresh_plugin_catalog_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_catalog = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_catalog_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.get_catalog().update_cloud_catalog(current_cloud_catalog);
return;
}
manager.fetch_plugins_from_cloud(&not_found, &unauthorized);
auto plater = wxGetApp().plater();
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));
}
}
}
std::string to_string(PluginUpdateStatus status);
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions);
nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
{
nlohmann::json payload_item;
payload_item["plugin_key"] = dialog_item.plugin_key;
payload_item["plugin_id"] = dialog_item.plugin_id;
payload_item["name"] = dialog_item.display_name;
payload_item["description"] = dialog_item.description;
payload_item["author"] = dialog_item.author;
payload_item["version"] = dialog_item.version;
payload_item["type"] = dialog_item.type_label;
payload_item["type_key"] = dialog_item.type_key;
payload_item["types"] = dialog_item.type_labels;
nlohmann::json caps = nlohmann::json::array();
for (const PluginCapabilityView& capability : dialog_item.capabilities) {
nlohmann::json c;
c["name"] = capability.name;
c["type"] = capability.type_label;
c["type_key"] = capability.type_key;
c["enabled"] = capability.enabled;
c["can_toggle"] = capability.can_toggle;
c["can_run"] = capability.can_run;
caps.push_back(std::move(c));
}
payload_item["capabilities"] = std::move(caps);
nlohmann::json changelog = nlohmann::json::array();
for (const PluginChangelogView& entry : dialog_item.changelog) {
nlohmann::json c;
c["version"] = entry.version;
c["changelog"] = entry.changelog;
c["created_time"] = entry.created_time;
changelog.push_back(std::move(c));
}
payload_item["changelog"] = std::move(changelog);
payload_item["label"] = dialog_item.display_name;
payload_item["source"] = to_string(dialog_item.source);
payload_item["status"] = to_string(dialog_item.status);
payload_item["error"] = dialog_item.error_text;
payload_item["update_status"] = to_string(dialog_item.update_status);
payload_item["unauthorized"] = dialog_item.unauthorized;
payload_item["context_actions"] = build_context_actions_payload(dialog_item.available_actions);
payload_item["update_available"] = dialog_item.update_status == PluginUpdateStatus::UpdateAvailable;
payload_item["can_toggle"] = dialog_item.available_actions.can_toggle;
payload_item["has_script_capability"] = dialog_item.has_script_capability;
payload_item["can_run_script"] = dialog_item.can_run_script;
payload_item["sharing_token"] = dialog_item.sharing_token;
payload_item["thumbnail_url"] = dialog_item.thumbnail_url;
payload_item["installed"] = dialog_item.has_local_package;
payload_item["installed_version"] = dialog_item.installed_version;
payload_item["latest_version"] = dialog_item.latest_version;
return payload_item;
}
std::string to_string(PluginUpdateStatus status)
{
switch (status) {
case PluginUpdateStatus::Normal: return "normal";
case PluginUpdateStatus::UpdateAvailable: return "update_available";
case PluginUpdateStatus::Unauthorized: return "unauthorized";
}
return "normal";
}
PluginSource derive_plugin_source(const PluginDescriptor& descriptor)
{
const bool has_cloud_meta = descriptor.cloud.has_value();
const bool is_cloud = descriptor.is_cloud_plugin();
const bool is_mine = is_cloud && has_cloud_meta && descriptor.cloud->is_mine;
// Source is ownership/locality only; issue states never replace this badge.
if (is_mine)
return PluginSource::Mine;
if (is_cloud)
return PluginSource::Subscribed;
return PluginSource::Local;
}
PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
{
PluginAvailableActions available_actions;
const bool is_loading = item.status == PluginStatus::Loading;
const bool is_cloud = item.is_cloud_plugin;
const bool is_mine = item.source == PluginSource::Mine;
const bool has_local = item.has_local_package;
const bool authorized_for_install = !item.unauthorized;
available_actions.toggle_installs_cloud_plugin = is_cloud && !has_local && authorized_for_install;
available_actions.can_toggle = !is_loading && (has_local || available_actions.toggle_installs_cloud_plugin);
auto add_action = [&available_actions](const char* id, const char* label, bool enabled = true, bool danger = false) {
available_actions.context_actions.push_back(PluginContextAction{id, label, enabled, danger});
};
if (is_cloud) {
if (is_mine)
add_action("delete_mine_plugin", "Delete", true, true);
else
add_action("unsubscribe_plugin", "Unsubscribe", true, true);
} else if (has_local) {
add_action("delete_plugin", "Delete", true, true);
}
add_action("open_folder", "Show in folder", has_local);
add_action("reinstall_plugin", "Reinstall");
return available_actions;
}
PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
{
PluginDialogItem item;
const auto& loader = PluginManager::instance().get_loader();
item.plugin_key = descriptor.plugin_key;
item.display_name = descriptor.name;
item.description = !descriptor.is_metadata_valid() && descriptor.has_error() ?
descriptor.normalized_error() :
(descriptor.description.empty() ? "No description." : descriptor.description);
item.author = descriptor.author;
item.version = descriptor.version;
// Installed version: the cloud merge overwrites `version` with the latest cloud version, so prefer
// the preserved local version, falling back to `version` for local-only / pre-merge descriptors.
item.installed_version = descriptor.has_local_package() ?
(descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version) :
std::string{};
item.latest_version = descriptor.latest_available_version();
// why: sort by the same version the row displays (GetDisplayVersion in index.js) - installed when
// installed, otherwise latest - so the Version sort matches what the user sees.
item.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
item.type_label = descriptor.type_label();
item.type_key = plugin_capability_type_to_string(descriptor.primary_capability_type());
// "types" is the display-only compatibility list. Cloud plugins show the raw labels the
// service returned (which may not map to real capability types); local plugins derive them
// from the capabilities actually discovered/loaded.
if (descriptor.is_cloud_plugin() && !descriptor.display_types.empty()) {
for (const std::string& label : descriptor.display_types)
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
item.type_labels.push_back(label);
} else {
for (PluginCapabilityType type : descriptor.capability_types) {
const std::string label = plugin_capability_type_display_name(type);
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
item.type_labels.push_back(label);
}
}
for (const PluginChangelog& entry : descriptor.changelog)
item.changelog.push_back({entry.version, entry.changelog, entry.created_time});
item.source = derive_plugin_source(descriptor);
item.update_status = descriptor.get_update_status();
item.error_text = descriptor.normalized_error();
item.has_error = descriptor.has_error();
item.is_cloud_plugin = descriptor.is_cloud_plugin();
item.has_local_package = descriptor.has_local_package();
item.unauthorized = descriptor.is_unauthorized();
item.has_script_capability = descriptor.has_capability_type(Slic3r::PluginCapabilityType::Script);
item.is_loaded = loader.is_plugin_loaded(descriptor.plugin_key);
item.loading = loader.is_plugin_load_in_progress(descriptor.plugin_key);
if (item.is_loaded) {
for (const auto& cap : loader.get_loaded_plugin_capabilities(descriptor.plugin_key)) {
if (cap) {
item.capabilities.push_back({cap->name, plugin_capability_type_display_name(cap->type),
plugin_capability_type_to_string(cap->type), cap->enabled, true, false});
if (cap->type == PluginCapabilityType::Script)
item.has_script_capability = true;
}
}
}
if (item.capabilities.empty()) {
for (PluginCapabilityType type : descriptor.capability_types) {
const std::string type_key = plugin_capability_type_to_string(type);
const auto existing = std::find_if(item.capabilities.begin(), item.capabilities.end(),
[&type_key](const PluginCapabilityView& capability) {
return capability.type_key == type_key;
});
if (existing != item.capabilities.end())
continue;
item.capabilities.push_back({std::string{}, plugin_capability_type_display_name(type), type_key, false, false, false});
}
}
item.sharing_token = descriptor.sharing_token;
item.thumbnail_url = descriptor.thumbnail_url;
if (item.loading)
item.status = PluginStatus::Loading;
else if (item.has_error)
item.status = PluginStatus::Error;
else if (item.is_loaded)
item.status = PluginStatus::Activated;
else
item.status = PluginStatus::Inactive;
item.available_actions = evaluate_action_policy(item);
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
[](const PluginCapabilityView& capability) {
return capability.type_key == "script" && capability.enabled;
});
item.can_run_script = descriptor.is_metadata_valid() && !descriptor.has_error() && item.has_script_capability && item.is_loaded &&
!item.loading && has_enabled_script;
for (PluginCapabilityView& capability : item.capabilities) {
capability.can_run = item.can_run_script && capability.type_key == "script" && capability.enabled;
}
return item;
}
const PluginContextAction* find_context_action(const PluginAvailableActions& available_actions, const std::string& action_id)
{
const auto it = std::find_if(available_actions.context_actions.begin(), available_actions.context_actions.end(),
[&action_id](const PluginContextAction& action) { return action.id == action_id; });
return it != available_actions.context_actions.end() ? &(*it) : nullptr;
}
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions)
{
nlohmann::json payload = nlohmann::json::array();
for (const PluginContextAction& action : available_actions.context_actions) {
nlohmann::json item;
item["id"] = action.id;
item["label"] = action.label;
item["enabled"] = action.enabled;
item["danger"] = action.danger;
payload.push_back(std::move(item));
}
return payload;
}
struct PluginOperationState
{
std::mutex mutex;
bool succeeded = false;
std::string error;
};
void store_plugin_operation_result(const std::shared_ptr<PluginOperationState>& state, bool succeeded, std::string error)
{
std::lock_guard<std::mutex> lock(state->mutex);
state->succeeded = succeeded;
state->error = std::move(error);
}
bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& state, std::string& error)
{
std::lock_guard<std::mutex> lock(state->mutex);
error = std::move(state->error);
return state->succeeded;
}
} // namespace
PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style)
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
PluginsDialog::~PluginsDialog() = default;
void PluginsDialog::set_open_terminal_dlg_fn()
{
m_open_terminal_dlg_fn = [] { wxGetApp().open_terminal_dialog(); };
}
void PluginsDialog::update_plugin_dialog_ui()
{
// Called after the shared catalog is already updated, for example from the
// cloud-plugin state callback. Do not fetch here or the callback can re-enter.
send_plugins();
resolve_pending_activation();
}
void PluginsDialog::resolve_pending_activation()
{
if (m_activating_plugin_key.empty())
return;
PluginLoader& loader = PluginManager::instance().get_loader();
if (loader.is_plugin_load_in_progress(m_activating_plugin_key))
return; // Still loading: keep the "Activating..." message until the load resolves.
const std::string plugin_key = m_activating_plugin_key;
m_activating_plugin_key.clear();
// Mirror the row's status precedence (Error before Activated) so the message matches the list.
PluginDescriptor descriptor;
if (get_descriptor(plugin_key, descriptor) && descriptor.has_error())
show_status(wxString::Format(_L("Failed to activate \"%s\"."), plugin_display_name(plugin_key)), "error");
else if (loader.is_plugin_loaded(plugin_key))
show_status(wxString::Format(_L("Activated \"%s\"."), plugin_display_name(plugin_key)), "success");
// Otherwise it ended up inactive (toggled off or cancelled mid-load): stay silent.
}
void PluginsDialog::on_script_message(const nlohmann::json& payload)
{
if (handle_common_script_command(payload))
return;
const std::string command = payload.value("command", "");
if (command == "request_plugins") {
// The web page finished loading and is asking for the current catalog. Plugin
// discovery already runs at startup and on login, so the shared catalog is up to
// date by the time the dialog opens. Just render it here - no blocking fetch on
// open. The Refresh button (refresh_plugins) is what triggers a fresh discovery.
send_plugins();
} else if (command == "refresh_plugins") {
refresh_plugins();
} else if (command == "toggle_plugin") {
toggle_plugin(payload.value("plugin_key", ""), payload.value("enabled", false));
} else if (command == "toggle_plugin_capability") {
toggle_plugin_capability(payload.value("plugin_key", ""), plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""), payload.value("enabled", false));
} else if (command == "install_local_plugin") {
install_plugin_from_file();
} else if (command == "plugin_menu_action") {
handle_plugin_menu_action(payload.value("plugin_key", ""), payload.value("action", ""));
} else if (command == "run_script_plugin") {
run_script_plugin(payload.value("plugin_key", ""), payload.value("capability_name", ""));
} else if (command == "open_terminal") {
m_open_terminal_dlg_fn();
} else if (command == "update_plugin") {
update_plugin(payload.value("plugin_key", ""));
} else if (command == "open_plugin_on_cloud") {
open_plugin_on_cloud(payload.value("sharing_token", ""));
} else if (command == "open_plugin_hub") {
open_plugin_hub();
} else if (command == "set_plugin_sort") {
set_plugin_sort(payload.value("sort_key", ""), payload.value("sort_order", ""));
} else if (command == "set_plugin_install_action") {
const std::string action = payload.value("action", "");
if (action == "explore" || action == "install-local")
s_selected_plugin_install_action = action;
}
}
void PluginsDialog::send_plugins() { call_web_handler(build_plugins_payload()); }
void PluginsDialog::set_plugin_sort(const std::string& sort_key, const std::string& sort_order)
{
m_plugin_sort_key = plugin_sort_key_from_string(sort_key, m_plugin_sort_key);
m_plugin_sort_order = plugin_sort_order_from_string(sort_order, m_plugin_sort_order);
send_plugins();
}
nlohmann::json PluginsDialog::build_plugins_payload() const
{
nlohmann::json response;
response["command"] = "list_plugins";
response["install_action"] = s_selected_plugin_install_action;
response["sort_key"] = to_string(m_plugin_sort_key);
response["sort_order"] = to_string(m_plugin_sort_order);
response["data"] = nlohmann::json::array();
const auto& catalog = PluginManager::instance().get_catalog();
const auto valid = catalog.get_all_plugin_descriptors();
const auto invalid = catalog.get_invalid_plugins();
BOOST_LOG_TRIVIAL(info) << "Prepared " << valid.size() + invalid.size() << " plugin rows for Plugins dialog";
std::vector<PluginDialogItem> items;
items.reserve(valid.size() + invalid.size());
for (const PluginDescriptor& row : valid)
items.push_back(build_plugin_dialog_item(row));
for (const PluginDescriptor& row : invalid)
items.push_back(build_plugin_dialog_item(row));
// In-place sort
sort_plugin_items_for_dialog(items, m_plugin_sort_key, m_plugin_sort_order);
for (const PluginDialogItem& item : items)
response["data"].push_back(build_plugin_payload_item(item));
return response;
}
bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
{
const auto& catalog = PluginManager::instance().get_catalog();
if (const PluginDescriptor* valid_descriptor = catalog.find_valid_plugin_descriptor(plugin_key)) {
descriptor = *valid_descriptor;
return true;
}
return catalog.try_get_invalid_plugin_descriptor(plugin_key, descriptor);
}
std::shared_ptr<LoadedPluginCapability> PluginsDialog::get_capability(const std::string& plugin_key,
PluginCapabilityType type,
const std::string& capability_name) const
{
return PluginManager::instance().get_loader().get_plugin_capability_by_name(plugin_key, type, capability_name);
}
void PluginsDialog::refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud)
{
run_with_dialog([fetch_cloud]() { refresh_plugin_catalog_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
}
void PluginsDialog::refresh_plugins()
{
BOOST_LOG_TRIVIAL(info) << "Refreshing plugins from Plugins dialog";
refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kFetchCloudCatalog);
}
void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
{
if (plugin_key.empty())
return;
BOOST_LOG_TRIVIAL(info) << "Toggle plugin request. plugin_key=" << plugin_key << " enabled=" << enabled;
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
// The row no longer maps to a catalog entry (the catalog changed under the UI).
// Toggling is a local action, so just re-render from the current catalog; a cloud
// fetch (or clearing rescan) adds nothing here.
send_plugins();
return;
}
PluginDialogItem dialog_item = build_plugin_dialog_item(row_data);
PluginAvailableActions available_actions = dialog_item.available_actions;
PluginManager& manager = PluginManager::instance();
if (!enabled) {
if (!manager.get_loader().unload_plugin(plugin_key)) {
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin from Plugins dialog: " << plugin_key;
show_status(_L("Failed to unload plugin."), "warn");
send_plugins();
return;
}
BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
if (m_activating_plugin_key == plugin_key)
m_activating_plugin_key.clear();
send_plugins();
show_status(wxString::Format(_L("Deactivated \"%s\"."), plugin_display_name(plugin_key)), "success");
return;
}
// Check for capabilities that are currently in use
auto loaded_capabilities = manager.get_loader().get_loaded_plugin_capabilities(plugin_key);
if (!available_actions.can_toggle) {
if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) {
const std::string install_error = "Unauthorized cloud plugins cannot be installed.";
manager.set_plugin_error(plugin_key, install_error);
show_status(from_u8(install_error), "warn");
}
send_plugins();
return;
}
if (available_actions.toggle_installs_cloud_plugin) {
if (!install_cloud_plugin(row_data.plugin_key, row_data.version, from_u8(row_data.name))) {
BOOST_LOG_TRIVIAL(warning) << "Cloud plugin install was not completed for " << plugin_key;
send_plugins();
return;
}
BOOST_LOG_TRIVIAL(info) << "Cloud plugin installed locally from Plugins dialog: " << plugin_key;
// download_and_install_cloud_plugin updates the descriptor with the installed
// local package state, so no rescan or cloud fetch is needed before loading.
if (!get_descriptor(plugin_key, row_data)) {
send_plugins();
return;
}
dialog_item = build_plugin_dialog_item(row_data);
if (dialog_item.has_error) {
send_plugins();
return;
}
}
if (manager.get_loader().is_plugin_loaded(plugin_key)) {
send_plugins();
return;
}
const PluginDescriptor* descriptor_ptr = manager.get_catalog().find_valid_plugin_descriptor(plugin_key);
if (!descriptor_ptr) {
show_status(_L("Plugin manifest was not found."), "warn");
send_plugins();
return;
}
if (manager.get_loader().is_plugin_load_in_progress(plugin_key)) {
send_plugins();
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Starting plugin load from web dialog: " << plugin_key;
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key);
send_plugins();
// Loading is asynchronous: show a pending message now; resolve_pending_activation() turns it into
// Activated/Failed once the loader's completion callback runs update_plugin_dialog_ui().
m_activating_plugin_key = plugin_key;
show_status(wxString::Format(_L("Activating \"%s\"..."), plugin_display_name(plugin_key)), "info");
}
void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type,
const std::string& capability_name, bool enabled)
{
if (plugin_key.empty() || capability_name.empty() || type == PluginCapabilityType::Unknown)
return;
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
// The row no longer maps to a catalog entry (the catalog changed under the UI).
// Toggling is a local action, so just re-render from the current catalog; a cloud
// fetch (or clearing rescan) adds nothing here.
send_plugins();
return;
}
if (!get_capability(plugin_key, type, capability_name)) {
BOOST_LOG_TRIVIAL(warning) << "Cannot toggle missing plugin capability: " << plugin_key << " | " << capability_name;
send_plugins();
return;
}
PluginLoader& loader = PluginManager::instance().get_loader();
if (enabled) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Enabling plugin capability: " << plugin_key << " | " << capability_name;
loader.enable_capability(plugin_key, capability_name, type);
} else {
// check if the capability is currently in use here.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Disabling plugin capability: " << plugin_key << " | " << capability_name;
loader.disable_capability(plugin_key, capability_name, type);
}
send_plugins();
show_status(wxString::Format(enabled ? _L("Enabled \"%s\".") : _L("Disabled \"%s\"."), from_u8(capability_name)), "success");
}
void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, const std::string& action)
{
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data))
return;
const PluginDialogItem dialog_item = build_plugin_dialog_item(row_data);
const PluginContextAction* context_action = find_context_action(dialog_item.available_actions, action);
if (!context_action || !context_action->enabled)
return;
if (action == "open_folder") {
open_plugin_folder(row_data);
} else if (action == "delete_plugin") {
delete_local_plugin(row_data);
} else if (action == "unsubscribe_plugin") {
unsubscribe_cloud_plugin(row_data);
} else if (action == "delete_mine_plugin") {
delete_mine_local_and_cloud_plugin(plugin_key);
} else if (action == "reinstall_plugin") {
if (row_data.is_cloud_plugin())
reinstall_cloud_plugin(row_data);
else
reinstall_local_plugin(plugin_key);
}
}
void PluginsDialog::install_plugin_from_file()
{
wxFileDialog dialog(this, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK)
return;
if (!install_plugin_package(dialog.GetPath().ToUTF8().data())) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
}
}
bool PluginsDialog::install_plugin_package(const std::string& package_path)
{
if (package_path.empty())
return false;
BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path;
std::string error;
const boost::filesystem::path package_file(package_path);
const wxString package_name = from_u8(package_file.filename().string());
std::string extension = package_file.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") {
show_status(_L("Select a .py or .whl plugin package."), "info");
return false;
}
PluginDescriptor plugin_descriptor;
bool existing_installation = false;
auto report_inspection_failure = [&]() {
BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error;
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
send_plugins();
return false;
};
try {
if (!PluginManager::instance().get_loader().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation,
error))
return report_inspection_failure();
} catch (const std::exception& ex) {
error = ex.what();
return report_inspection_failure();
} catch (...) {
error = "Unknown error";
return report_inspection_failure();
}
if (existing_installation) {
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
wxMessageDialog dialog(
this,
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
if (dialog.ShowModal() != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
<< " plugin=" << plugin_descriptor.name;
return false;
}
}
bool installed = false;
try {
installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); },
_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (!installed) {
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error;
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
send_plugins();
return false;
}
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
refresh_plugin_catalog_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudCatalog);
return true;
}
bool PluginsDialog::install_cloud_plugin(const std::string& plugin_key, const std::string& version, const wxString& name)
{
if (plugin_key.empty())
return false;
BOOST_LOG_TRIVIAL(info) << "Downloading cloud plugin. plugin_key=" << plugin_key << " name=" << into_u8(name);
std::string error;
bool downloaded = false;
try {
downloaded = run_with_dialog_wait(
[plugin_key, &error, version]() {
return PluginManager::instance().download_and_install_cloud_plugin(plugin_key, version, error);
},
_L("Downloading plugin"), _L("Downloading") + ": " + name);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (!downloaded) {
BOOST_LOG_TRIVIAL(error) << "Cloud plugin download failed. plugin_key=" << plugin_key << " error=" << error;
PluginManager::instance().set_plugin_error(plugin_key, error);
show_status(error.empty() ? _L("Plugin download failed.") : from_u8(error), "error");
return false;
}
BOOST_LOG_TRIVIAL(info) << "Cloud plugin downloaded successfully. plugin_key=" << plugin_key;
return true;
}
void PluginsDialog::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);
}
wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
{
PluginDescriptor descriptor;
if (get_descriptor(plugin_key, descriptor) && !descriptor.name.empty())
return from_u8(descriptor.name);
return from_u8(plugin_key);
}
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();
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");
}
void PluginsDialog::update_plugin(const std::string& plugin_key)
{
if (plugin_key.empty())
return;
BOOST_LOG_TRIVIAL(info) << "Updating cloud plugin. plugin_key=" << plugin_key;
PluginDescriptor descriptor;
const wxString name = get_descriptor(plugin_key, descriptor) ? from_u8(descriptor.name) : from_u8(plugin_key);
// update_cloud_plugin unloads the old plugin, deletes its local package, then downloads and
// reinstalls the latest version. Each of those steps already runs off the main thread in the
// delete/install paths, so run the whole operation on the worker and pump a progress dialog.
std::string error;
bool updated = false;
try {
updated = run_with_dialog_wait(
[plugin_key, &error]() { return PluginManager::instance().update_cloud_plugin(plugin_key, error); },
_L("Updating plugin"), _L("Updating") + ": " + name);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (!updated) {
BOOST_LOG_TRIVIAL(error) << "Cloud plugin update failed. plugin_key=" << plugin_key << " error=" << error;
show_status(error.empty() ? _L("Failed to update plugin.") : from_u8(error), "error");
send_plugins();
return;
}
// update_cloud_plugin installs the package and updates the in-memory descriptor
// (installed=true, update_available=false) on success.
send_plugins();
show_status(wxString::Format(_L("Updated \"%s\"."), name), "success");
}
void PluginsDialog::open_plugin_folder(const PluginDescriptor& plugin)
{
const boost::filesystem::path plugin_folder = resolve_plugin_root_from_descriptor(plugin);
if (plugin_folder.empty()) {
show_status(_L("Plugin folder could not be determined."), "warn");
return;
}
desktop_open_any_folder(plugin_folder.string());
}
void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token)
{
if (sharing_token.empty())
return;
if (!wxGetApp().getAgent())
return;
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (!orca_agent)
return;
wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token));
}
void PluginsDialog::open_plugin_hub()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (wxGetApp().getAgent()) {
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
cloud_base_url.pop_back();
if (cloud_base_url.empty())
cloud_base_url = "https://cloud.orcaslicer.com";
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
}
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
{
const wxString plugin_name = from_u8(plugin.name);
const int rc = wxMessageBox(wxString::Format(_L("Delete plugin \"%s\"?\n\nThis permanently removes the plugin folder."), plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
if (rc != wxYES)
return;
auto state = std::make_shared<PluginOperationState>();
run_with_dialog(
[plugin_key = plugin.plugin_key, refresh_catalog = plugin.is_cloud_plugin(), state]() {
std::string error;
const bool succeeded = PluginManager::instance().delete_plugin(plugin_key, error);
if (succeeded && refresh_catalog)
refresh_plugin_catalog_blocking(kFetchCloudCatalog);
store_plugin_operation_result(state, succeeded, std::move(error));
},
[this, state, plugin_name]() {
std::string error;
if (!take_plugin_operation_result(state, error)) {
show_status(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), "error");
return;
}
send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
},
_L("Deleting plugin"), _L("Deleting plugin..."));
}
void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
{
const wxString plugin_name = from_u8(plugin.name);
const int rc = wxMessageBox(
wxString::Format(_L("Unsubscribe plugin \"%s\"?\n\nThis will stop tracking the plugin and delete any local plugin files."),
plugin_name),
kUnsubscribeTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
if (rc != wxYES)
return;
auto state = std::make_shared<PluginOperationState>();
run_with_dialog(
[plugin_key = plugin.plugin_key, state]() {
std::string error;
const bool succeeded = PluginManager::instance().delete_and_unsubscribe_cloud_plugin(plugin_key, error);
store_plugin_operation_result(state, succeeded, std::move(error));
},
[this, state, plugin_name]() {
std::string error;
if (!take_plugin_operation_result(state, error)) {
show_status(error.empty() ? _L("Failed to unsubscribe plugin.") : from_u8(error), "error");
return;
}
send_plugins();
show_status(wxString::Format(_L("Unsubscribed \"%s\"."), plugin_name), "success");
},
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
}
void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
{
if (plugin_key.empty())
return;
PluginManager& manager = PluginManager::instance();
const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key);
if (!manager.get_loader().unload_plugin(plugin_key)) {
show_status(_L("Failed to unload plugin."), "warn");
send_plugins();
return;
}
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key, false);
if (!was_loaded && !manager.get_loader().unload_plugin(plugin_key)) {
show_status(_L("Plugin reloaded, but failed to restore the inactive state."), "warn");
send_plugins();
return;
}
send_plugins();
show_status(wxString::Format(_L("Reloaded \"%s\"."), plugin_display_name(plugin_key)), "success");
}
void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
{
const std::string plugin_key = plugin.plugin_key;
if (plugin_key.empty())
return;
PluginManager& manager = PluginManager::instance();
const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key);
std::string error;
if (plugin.has_local_package()) {
if (!manager.delete_plugin(plugin_key, error)) {
show_status(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), "error");
send_plugins();
return;
}
manager.get_catalog().update_cloud_catalog(std::vector<PluginDescriptor>{as_cloud_only_descriptor(plugin)});
}
if (!install_cloud_plugin(plugin_key, plugin.version, from_u8(plugin.name))) {
send_plugins();
return;
}
if (was_loaded) {
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key);
}
send_plugins();
show_status(wxString::Format(_L("Reloaded \"%s\"."), plugin_display_name(plugin_key)), "success");
}
void PluginsDialog::delete_mine_local_and_cloud_plugin(const std::string& plugin_key)
{
PluginDescriptor descriptor;
const std::string display = get_descriptor(plugin_key, descriptor) ? descriptor.name : std::string{};
const wxString plugin_name = from_u8(display.empty() ? plugin_key : display);
const int rc = wxMessageBox(
wxString::Format(_L("Delete plugin \"%s\" from local and cloud?\n\nThis permanently removes the local plugin files and "
"deletes the plugin from the cloud. This action cannot be undone."),
plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
if (rc != wxYES)
return;
std::string error;
if (!PluginManager::instance().delete_mine_local_and_cloud_plugin(plugin_key, error)) {
show_status(error.empty() ? _L("Failed to delete plugin from local and cloud.") : from_u8(error), "error");
return;
}
// delete_mine_local_and_cloud_plugin already updated the in-memory catalog
// (finalize_cloud_plugin_removal removes the row and, when a local package existed,
// re-syncs the cloud list itself), so a UI refresh is sufficient here - an extra
// clearing rescan + cloud fetch would be redundant.
send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
}
}} // namespace Slic3r::GUI