Compare commits

..

2 Commits

Author SHA1 Message Date
Ian Chua
b8e0f9cdfb Merge branch 'main' into feat/plater-notification-api 2026-08-25 13:45:57 +08:00
Ian Chua
790a010615 feat: plater notification API for plugins 2026-08-21 14:40:35 +08:00
7 changed files with 98 additions and 69 deletions

View File

@@ -631,7 +631,7 @@ void parse_metadata_rfc822(const std::string& content,
bool is_ignored_plugin_directory(const boost::filesystem::path& path)
{
const std::string name = path.filename().string();
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR || name == PLUGIN_DATA_DIR;
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
}
bool is_safe_relative_path(const boost::filesystem::path& path)

View File

@@ -12,7 +12,6 @@
#include <vector>
#define PLUGIN_SUBSCRIBED_DIR "_subscribed"
#define PLUGIN_DATA_DIR "plugin_data"
namespace Slic3r {

View File

@@ -522,38 +522,6 @@ bool PluginManager::try_get_plugin_descriptor_for_capability(const std::string&
return false;
}
std::string PluginManager::get_storage_dir(const std::string& plugin_key) const
{
namespace fs = boost::filesystem;
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor))
throw std::runtime_error("The current plugin is not registered");
const fs::path base_storage_dir = fs::path(get_orca_plugins_dir()) / PLUGIN_DATA_DIR;
if (!descriptor.is_cloud_plugin()) {
const fs::path local_storage_dir = base_storage_dir / plugin_key;
fs::create_directories(local_storage_dir);
return local_storage_dir.string();
}
auto agent = m_cloud_service.get_cloud_agent();
if (!agent)
throw std::runtime_error("Cloud plugin storage is unavailable before networking is initialized");
const std::string user_id = agent->get_user_id();
if (user_id.empty())
throw std::runtime_error("Cloud plugin storage is unavailable without a logged-in user");
if (!is_valid_plugin_id(plugin_key))
throw std::runtime_error("The current cloud plugin key is not a valid folder name");
const fs::path cloud_storage_dir = base_storage_dir / PLUGIN_SUBSCRIBED_DIR / user_id / plugin_key;
fs::create_directories(cloud_storage_dir);
return cloud_storage_dir.string();
}
// ── Capability instances ────────────────────────────────────────────────────────────────────
std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugin_capabilities(const std::string& plugin_key,

View File

@@ -143,10 +143,6 @@ public:
bool try_get_plugin_descriptor_for_capability(const std::string& capability_name,
PluginCapabilityType type,
PluginDescriptor& out) const;
// Per-plugin storage directory under orca_plugins/plugin_data, created if missing. Throws
// std::runtime_error if the plugin is unregistered, the key is invalid, or (cloud plugins)
// no user is logged in yet.
std::string get_storage_dir(const std::string& plugin_key) const;
std::vector<std::shared_ptr<PluginCapabilityInterface>> get_plugin_capabilities(
const std::string& plugin_key = "", // "" => all plugins

View File

@@ -1,31 +1,9 @@
#include "PluginHost.hpp"
#include "PluginHostBindings.hpp"
#include "PluginHostUi.hpp"
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <stdexcept>
namespace Slic3r {
namespace host_bindings {
void register_plugin(pybind11::module_& host)
{
auto plugin_host = host.def_submodule("plugin", "Plugin host API");
plugin_host.def(
"storage",
[]() -> std::string {
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
if (plugin_key.empty())
throw std::runtime_error("plugin.storage() must be called from a plugin callback");
return PluginManager::instance().get_storage_dir(plugin_key);
},
"Return the installed folder of the current plugin.");
}
} // namespace host_bindings
void PluginHost::RegisterBindings(pybind11::module_& module)
{
auto host = module.def_submodule("host", "Host application API");
@@ -37,7 +15,6 @@ void PluginHost::RegisterBindings(pybind11::module_& module)
host_bindings::register_presets(host);
host_bindings::register_model(host);
host_bindings::register_app(host);
host_bindings::register_plugin(host);
// UI: native dialogs and interactive HTML windows for plugins.
PluginHostUi::RegisterBindings(host);

View File

@@ -12,5 +12,5 @@ void register_presets(pybind11::module_& host); // PluginHostPresets.cpp
void register_model(pybind11::module_& host); // PluginHostModel.cpp
void register_app(pybind11::module_& host); // PluginHostApp.cpp
void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp
void register_plugin(pybind11::module_& host); // PluginHost.cpp
} // namespace Slic3r::host_bindings

View File

@@ -9,6 +9,7 @@
#include <slic3r/GUI/MsgDialog.hpp>
#include <slic3r/GUI/PluginProgressDialog.hpp>
#include <slic3r/GUI/PluginWebDialog.hpp>
#include <slic3r/GUI/NotificationManager.hpp>
#include <nlohmann/json.hpp>
#include <pybind11/pybind11.h>
@@ -19,6 +20,7 @@
#include <wx/defs.h>
#include <wx/window.h>
#include <atomic>
#include <cstdint>
#include <future>
#include <memory>
@@ -44,16 +46,20 @@ namespace {
struct GilSafeCallable
{
py::object fn;
std::atomic_bool active{true};
explicit GilSafeCallable(py::object f) : fn(std::move(f)) {}
~GilSafeCallable()
void disable()
{
if (fn) {
active.store(false, std::memory_order_release);
PythonGILState gil;
if (gil)
fn = py::object();
else
(void) fn.release();
}
~GilSafeCallable()
{
disable();
}
};
using CallablePtr = std::shared_ptr<GilSafeCallable>;
@@ -166,11 +172,34 @@ public:
}
return out;
}
void bind_callback(const CallablePtr& callback, const std::string& plugin_key)
{
if (!callback)
return;
std::lock_guard<std::mutex> lk(m_mtx);
m_callbacks[plugin_key].push_back(callback);
}
std::vector<CallablePtr> take_callbacks_for_plugin(const std::string& plugin_key)
{
std::lock_guard<std::mutex> lk(m_mtx);
auto it = m_callbacks.find(plugin_key);
if (it == m_callbacks.end())
return {};
std::vector<CallablePtr> callbacks;
callbacks.reserve(it->second.size());
for (const std::weak_ptr<GilSafeCallable>& weak_callback : it->second) {
if (auto callback = weak_callback.lock())
callbacks.push_back(std::move(callback));
}
m_callbacks.erase(it);
return callbacks;
}
private:
std::mutex m_mtx;
std::unordered_map<int, wxWindow*> m_resources;
std::unordered_map<int, std::string> m_owners;
std::unordered_map<std::string, std::vector<std::weak_ptr<GilSafeCallable>>> m_callbacks;
int m_next_id{1};
};
@@ -448,6 +477,46 @@ void progress_close(int id)
});
}
void plater_notification(NotificationManager::NotificationLevel notification_level, const std::string& text,
const std::string& hypertext, py::object on_click)
{
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
CallablePtr holder = make_holder(std::move(on_click));
if (holder)
UiRegistry::instance().bind_callback(holder, plugin_key);
std::function<bool(wxEvtHandler*)> callback;
if (holder) {
callback = [holder](wxEvtHandler*) -> bool {
if (!holder->active.load(std::memory_order_acquire))
return false;
PythonGILState gil;
if (!gil)
return false;
try {
py::object result = holder->fn();
return result.is_none() || result.cast<bool>();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
PyErr_Clear();
return false;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised an unknown exception";
return false;
}
};
}
run_on_ui_blocking([notification_level, text, hypertext, callback = std::move(callback)]() mutable {
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::CustomNotification, notification_level, text,
hypertext, std::move(callback));
});
}
} // namespace
void PluginHostUi::RegisterBindings(pybind11::module_& host)
@@ -530,6 +599,23 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"),
py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
"Create a native progress dialog and return a ProgressDialog handle.");
py::enum_<NotificationManager::NotificationLevel>(ui, "NotificationLevel")
.value("ProgressBarNotificationLevel", NotificationManager::NotificationLevel::ProgressBarNotificationLevel)
.value("HintNotificationLevel", NotificationManager::NotificationLevel::HintNotificationLevel)
.value("RegularNotificationLevel", NotificationManager::NotificationLevel::RegularNotificationLevel)
.value("PrintInfoNotificationLevel", NotificationManager::NotificationLevel::PrintInfoNotificationLevel)
.value("PrintInfoShortNotificationLevel", NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel)
.value("ImportantNotificationLevel", NotificationManager::NotificationLevel::ImportantNotificationLevel)
.value("WarningNotificationLevel", NotificationManager::NotificationLevel::WarningNotificationLevel)
.value("SeriousWarningNotificationLevel", NotificationManager::NotificationLevel::SeriousWarningNotificationLevel)
.value("ErrorNotificationLevel", NotificationManager::NotificationLevel::ErrorNotificationLevel)
.export_values();
ui.def("push_notification", &plater_notification, py::arg("notification_level"), py::arg("text"),
py::arg("hyper_text") = "", py::arg("on_click") = py::none(),
"Push a plater notification. hyper_text is an underlined label; on_click() is called when it is clicked "
"and may return True to close the notification.");
}
void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
@@ -538,6 +624,9 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
return;
auto teardown = [plugin_key]() {
for (auto& callback : UiRegistry::instance().take_callbacks_for_plugin(plugin_key))
callback->disable();
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
// forced teardown (intended); the resource destructor still cleans the registry.
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {