From 0709c9931e3fb1bcf02567687278b02638425e34 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 9 Sep 2026 17:36:21 +0800 Subject: [PATCH] Slight refactor for opening Plugins in speed dial --- src/slic3r/GUI/GUI_App.cpp | 44 ++++ src/slic3r/GUI/GUI_App.hpp | 3 + src/slic3r/GUI/NativeCommands.cpp | 19 ++ src/slic3r/GUI/PluginsDialog.cpp | 328 +++++++++++++++++++----------- src/slic3r/GUI/PluginsDialog.hpp | 18 ++ 5 files changed, 290 insertions(+), 122 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 3500493329..c5d59d9185 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -8532,6 +8532,50 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig } } +void GUI_App::refresh_plugins() +{ + // The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker + // and report completion through the notification manager -- the speed dial needs no dialog. + std::thread([]() { + refresh_plugin_metadata_blocking(/*fetch_cloud=*/true); + wxTheApp->CallAfter([]() { + if (wxGetApp().is_closing()) + return; + Plater* plater = wxGetApp().plater(); + if (plater == nullptr) + return; + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + into_u8(_L("Plugins refreshed."))); + }); + }).detach(); +} + +void GUI_App::install_local_plugin() +{ + if (mainframe == nullptr) + return; + + wxFileDialog dialog(mainframe, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + wxString message; + const bool ok = install_local_plugin_package(boost::filesystem::path(dialog.GetPath().ToUTF8().data()), mainframe, message); + if (message.IsEmpty()) + return; // user cancelled the overwrite prompt + + Plater* plater = this->plater(); + if (plater == nullptr) + return; + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + ok ? NotificationManager::NotificationLevel::RegularNotificationLevel : NotificationManager::NotificationLevel::ErrorNotificationLevel, + into_u8(message)); +} + void GUI_App::open_terminal_dialog() { // Reached from the plugins dialog's webview ("open_terminal" command), i.e. from diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8bf32df64c..b1ee96d468 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -633,6 +633,9 @@ public: void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string()); + // Dialog-free plugin actions used by the speed dial: they never require the Plugins dialog to be open. + void refresh_plugins(); + void install_local_plugin(); void open_terminal_dialog(); void open_speed_dial(); ActionRegistry& action_registry() { return m_action_registry; } diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index e4c4449cf6..94edd4898e 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -10,6 +10,7 @@ #include "IMSlider.hpp" #include "MainFrame.hpp" #include "Plater.hpp" +#include "PluginsDialog.hpp" #include "PlateSettingsDialog.hpp" #include "DeviceCore/DevManager.h" @@ -475,6 +476,24 @@ std::vector build_command_catalog() return AppActionRunResult{AppActionRunResult::Level::Success}; }); + // ---- Plugins ---- + add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) { + wxGetApp().open_plugins_dialog(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) { + wxGetApp().refresh_plugins(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) { + open_plugin_hub(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) { + wxGetApp().install_local_plugin(); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); + return out; } diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp index a39fcbc535..4f97a7d089 100644 --- a/src/slic3r/GUI/PluginsDialog.cpp +++ b/src/slic3r/GUI/PluginsDialog.cpp @@ -125,15 +125,6 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type(); } -std::vector current_cloud_metadata_snapshot() -{ - std::vector cloud_entries; - for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true)) - if (entry.is_cloud_plugin()) - cloud_entries.push_back(entry); - return cloud_entries; -} - PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor) { descriptor.plugin_root.clear(); @@ -149,41 +140,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor) return descriptor; } -void refresh_plugin_metadata_blocking(bool fetch_cloud) -{ - PluginManager& manager = PluginManager::instance(); - - std::vector not_found, unauthorized; - const std::vector current_cloud_metadata = fetch_cloud ? std::vector{} : - current_cloud_metadata_snapshot(); - - manager.rescan_plugins(); - - if (!fetch_cloud) { - manager.update_cloud_metadata(current_cloud_metadata); - return; - } - - manager.fetch_plugins_from_cloud(¬_found, &unauthorized); - - wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() { - if (wxGetApp().is_closing()) - return; - Plater* plater = wxGetApp().plater(); - if (plater == nullptr) - return; - - for (const auto& uuid : not_found) - plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotificationLevel, - format(_L("Plugin %s is no longer available."), uuid)); - for (const auto& uuid : unauthorized) - plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, - NotificationManager::NotificationLevel::RegularNotificationLevel, - format(_L("Plugin %s access is unauthorized."), uuid)); - }); -} - std::string to_string(PluginUpdateStatus status); nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions); @@ -447,6 +403,200 @@ bool take_plugin_operation_result(const std::shared_ptr& s } } // namespace +// ── Dialog-independent plugin actions (also used by the speed dial) ─────────────────────────── + +namespace { + +// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a +// network round-trip (kUseCurrentCloudMeta). +std::vector current_cloud_metadata_snapshot() +{ + std::vector cloud_entries; + for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true)) + if (entry.is_cloud_plugin()) + cloud_entries.push_back(entry); + return cloud_entries; +} + +} // namespace + +void refresh_plugin_metadata_blocking(bool fetch_cloud) +{ + PluginManager& manager = PluginManager::instance(); + + std::vector not_found, unauthorized; + const std::vector current_cloud_metadata = fetch_cloud ? std::vector{} : + current_cloud_metadata_snapshot(); + + manager.rescan_plugins(); + + if (!fetch_cloud) { + manager.update_cloud_metadata(current_cloud_metadata); + return; + } + + manager.fetch_plugins_from_cloud(¬_found, &unauthorized); + + wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() { + if (wxGetApp().is_closing()) + return; + Plater* plater = wxGetApp().plater(); + if (plater == nullptr) + return; + + for (const auto& uuid : not_found) + plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Plugin %s is no longer available."), uuid)); + for (const auto& uuid : unauthorized) + plater->get_notification_manager()->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Plugin %s access is unauthorized."), uuid)); + }); +} + +void open_plugin_hub() +{ + std::string cloud_base_url = "https://cloud.orcaslicer.com"; + + if (wxGetApp().getAgent()) { + auto orca_agent = std::dynamic_pointer_cast(wxGetApp().getAgent()->get_cloud_agent()); + if (orca_agent && !orca_agent->get_cloud_base_url().empty()) + cloud_base_url = orca_agent->get_cloud_base_url(); + } + + while (!cloud_base_url.empty() && cloud_base_url.back() == '/') + cloud_base_url.pop_back(); + if (cloud_base_url.empty()) + cloud_base_url = "https://cloud.orcaslicer.com"; + + wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub")); +} + +bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message) +{ + message.clear(); + if (package_file.empty()) + return false; + + // ---- pre-flight (main thread): validate + inspect + overwrite prompt ---- + const wxString package_name = from_u8(package_file.filename().string()); + + std::string extension = package_file.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + if (extension != ".py" && extension != ".whl") { + message = _L("Select a .py or .whl plugin package."); + return false; + } + + PluginDescriptor plugin_descriptor; + bool existing_installation = false; + std::string error; + try { + if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error; + message = _L("Failed to install plugin package. See the log for details."); + return false; + } + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what(); + message = _L("Failed to install plugin package. See the log for details."); + return false; + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file; + message = _L("Failed to install plugin package. See the log for details."); + return false; + } + + if (existing_installation) { + const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); + wxMessageDialog dialog(parent, + wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."), + plugin_name), + kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); + dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel")); + if (dialog.ShowModal() != wxID_OK) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file + << " plugin=" << plugin_descriptor.name; + return false; // cancelled: message stays empty so callers stay silent + } + } + + // ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ---- + bool installed = false; + { + struct Result + { + std::mutex mutex; + bool ok = false; + std::string error; + }; + auto state = std::make_shared(); + + wxProgressDialog* progress = new wxProgressDialog(_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, + 100, parent, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); + wxTimer* timer = new wxTimer(); + timer->Bind(wxEVT_TIMER, [progress](wxTimerEvent&) { + if (progress) + progress->Pulse(); + }); + timer->Start(100); + + bool finished = false; + wxEventLoop loop; + auto on_finish = [&finished, &loop]() { + finished = true; + if (loop.IsRunning()) + loop.Exit(); + }; + + std::thread([state, package_file, on_finish]() mutable { + std::string error; + bool ok = false; + try { + ok = PluginManager::instance().install_plugin(package_file, error); + } catch (const std::exception& ex) { + error = ex.what(); + } catch (...) { + error = "Unknown error"; + } + if (ok) { + // Reflect the new package in discovery/cloud metadata without blocking the caller. + try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {} + } + { + std::lock_guard lock(state->mutex); + state->ok = ok; + state->error = std::move(error); + } + wxTheApp->CallAfter(on_finish); + }).detach(); + + if (!finished) + loop.Run(); + + timer->Stop(); + delete timer; + progress->Destroy(); + + std::lock_guard lock(state->mutex); + installed = state->ok; + error = std::move(state->error); + } + + if (!installed) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error; + message = _L("Failed to install plugin package. See the log for details."); + return false; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file; + const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); + message = wxString::Format(_L("Installed \"%s\"."), installed_name); + return true; +} + PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style) : WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style) { create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); } @@ -818,78 +968,28 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path) { if (package_path.empty()) return false; - BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path; - std::string error; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path; + const boost::filesystem::path package_file(package_path); - const wxString package_name = from_u8(package_file.filename().string()); + wxString message; + const bool installed = install_local_plugin_package(package_file, this, message); - std::string extension = package_file.extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), - [](unsigned char ch) { return static_cast(std::tolower(ch)); }); - if (extension != ".py" && extension != ".whl") { - show_status(_L("Select a .py or .whl plugin package."), "info"); - return false; - } - - PluginDescriptor plugin_descriptor; - bool existing_installation = false; - auto report_inspection_failure = [&]() { - BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error; - show_status(_L("Failed to install plugin package. See the log for details."), "warn"); + // The shared helper reports a user-cancelled overwrite with an empty message: stay silent. + if (message.IsEmpty()) { send_plugins(); return false; - }; - - try { - if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) - return report_inspection_failure(); - } catch (const std::exception& ex) { - error = ex.what(); - return report_inspection_failure(); - } catch (...) { - error = "Unknown error"; - return report_inspection_failure(); - } - - if (existing_installation) { - const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); - wxMessageDialog dialog( - this, - wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."), - plugin_name), - kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); - dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel")); - const int overwrite_rc = dialog.ShowModal(); - restore_z_order(); - if (overwrite_rc != wxID_OK) { - BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path - << " plugin=" << plugin_descriptor.name; - return false; - } - } - - bool installed = false; - try { - installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); }, - _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100, - wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); - } catch (const std::exception& ex) { - error = ex.what(); - } catch (...) { - error = "Unknown error"; } if (!installed) { - BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error; - show_status(_L("Failed to install plugin package. See the log for details."), "warn"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package."; + show_status(message, "warn"); send_plugins(); return false; } - BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path; - const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name); - show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success"); - refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta); + show_status(message, "success"); + prompt_for_missing_plugins(); + send_plugins(); return true; } @@ -1085,23 +1185,7 @@ void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token) wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token)); } -void PluginsDialog::open_plugin_hub() -{ - std::string cloud_base_url = "https://cloud.orcaslicer.com"; - - if (wxGetApp().getAgent()) { - auto orca_agent = std::dynamic_pointer_cast(wxGetApp().getAgent()->get_cloud_agent()); - if (orca_agent && !orca_agent->get_cloud_base_url().empty()) - cloud_base_url = orca_agent->get_cloud_base_url(); - } - - while (!cloud_base_url.empty() && cloud_base_url.back() == '/') - cloud_base_url.pop_back(); - if (cloud_base_url.empty()) - cloud_base_url = "https://cloud.orcaslicer.com"; - - wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub")); -} +void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); } void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin) { diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp index e663de79e4..2c4a5925d9 100644 --- a/src/slic3r/GUI/PluginsDialog.hpp +++ b/src/slic3r/GUI/PluginsDialog.hpp @@ -24,6 +24,8 @@ #include #include +#include + class wxTimer; namespace Slic3r { @@ -34,6 +36,22 @@ enum class PluginCapabilityType; namespace GUI { +// Dialog-independent plugin-management actions, shared by the Plugins dialog and the speed dial: +// they never require the webview dialog to be open. + +// Rescans local plugins and (optionally) re-fetches cloud metadata. Blocking: run off the UI +// thread. Used by PluginsDialog (behind its progress dialog) and GUI_App::refresh_plugins(). +void refresh_plugin_metadata_blocking(bool fetch_cloud); + +// Opens the Cloud plugin hub in the default browser. No dialog needed. +void open_plugin_hub(); + +// Synchronously installs a local plugin package (.py/.whl). Runs on the UI thread but keeps it +// responsive by performing the install on a worker behind a modal progress dialog. `parent` owns +// the overwrite prompt and the progress dialog. On success `message` carries the localized +// confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason. +bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message); + class PluginsDialog : public Slic3r::GUI::WebViewHostDialog { public: