mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Merge branch 'main' into feat/printer-agent-impl
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
#include "PyPluginPackage.hpp"
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
|
||||
#include "pluginTypes/pages/PagesPluginCapability.hpp"
|
||||
#include "pluginTypes/script/ScriptPluginCapability.hpp"
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
|
||||
@@ -319,17 +320,17 @@ void bind_python_api(pybind11::module_& m)
|
||||
{
|
||||
m.doc() = "OrcaSlicer plugin API";
|
||||
|
||||
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
|
||||
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
|
||||
.value("Automation", PluginCapabilityType::Automation)
|
||||
.value("Analysis", PluginCapabilityType::Analysis)
|
||||
.value("Importer", PluginCapabilityType::Importer)
|
||||
.value("Exporter", PluginCapabilityType::Exporter)
|
||||
.value("Visualization", PluginCapabilityType::Visualization)
|
||||
.value("Script", PluginCapabilityType::Script)
|
||||
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
|
||||
.value("Unknown", PluginCapabilityType::Unknown)
|
||||
.export_values();
|
||||
py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
|
||||
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
|
||||
.value("Pages", PluginCapabilityType::Pages)
|
||||
.value("Analysis", PluginCapabilityType::Analysis)
|
||||
.value("Importer", PluginCapabilityType::Importer)
|
||||
.value("Exporter", PluginCapabilityType::Exporter)
|
||||
.value("Visualization", PluginCapabilityType::Visualization)
|
||||
.value("Script", PluginCapabilityType::Script)
|
||||
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
|
||||
.value("Unknown", PluginCapabilityType::Unknown)
|
||||
.export_values();
|
||||
|
||||
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
|
||||
.value("Success", PluginResult::Success)
|
||||
@@ -419,9 +420,10 @@ void bind_python_api(pybind11::module_& m)
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
|
||||
|
||||
// Make sure you register your bindings here
|
||||
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PrinterAgentPluginCapability::RegisterBindings(m);
|
||||
PagesPluginCapability::RegisterBindings(m);
|
||||
ScriptPluginCapability::RegisterBindings(m);
|
||||
SlicingPipelinePluginCapability::RegisterBindings(m);
|
||||
PluginHost::RegisterBindings(m);
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
|
||||
struct PluginCapabilityId
|
||||
{
|
||||
@@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
{
|
||||
switch (type) {
|
||||
case PluginCapabilityType::PrinterConnection: return "printer-connection";
|
||||
case PluginCapabilityType::Automation: return "automation";
|
||||
case PluginCapabilityType::Pages: return "pages";
|
||||
case PluginCapabilityType::Analysis: return "analysis";
|
||||
case PluginCapabilityType::Importer: return "importer";
|
||||
case PluginCapabilityType::Exporter: return "exporter";
|
||||
@@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
|
||||
{
|
||||
switch (type) {
|
||||
case PluginCapabilityType::PrinterConnection: return "Printer connection";
|
||||
case PluginCapabilityType::Automation: return "Automation";
|
||||
case PluginCapabilityType::Pages: return "Pages";
|
||||
case PluginCapabilityType::Analysis: return "Analysis";
|
||||
case PluginCapabilityType::Importer: return "Importer";
|
||||
case PluginCapabilityType::Exporter: return "Exporter";
|
||||
@@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
|
||||
|
||||
if (lowered == "printer-connection")
|
||||
return PluginCapabilityType::PrinterConnection;
|
||||
if (lowered == "automation")
|
||||
return PluginCapabilityType::Automation;
|
||||
if (lowered == "pages")
|
||||
return PluginCapabilityType::Pages;
|
||||
if (lowered == "analysis")
|
||||
return PluginCapabilityType::Analysis;
|
||||
if (lowered == "importer")
|
||||
|
||||
@@ -0,0 +1,469 @@
|
||||
#include "PluginPages.hpp"
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/Notebook.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
||||
#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <wx/bookctrl.h>
|
||||
#include <wx/menu.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
||||
(function () {
|
||||
if (window.top !== window.self) return;
|
||||
if (window.orca) return;
|
||||
var handlers = [];
|
||||
function deliver(payload, attempts) {
|
||||
try {
|
||||
if (window.wx && typeof window.wx.postMessage === 'function') {
|
||||
window.wx.postMessage(payload);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* retry while the native handler is being registered */ }
|
||||
if (attempts < 100)
|
||||
window.setTimeout(function () { deliver(payload, attempts + 1); }, 25);
|
||||
}
|
||||
function send(data) {
|
||||
deliver(JSON.stringify({
|
||||
channel: 'orca', kind: 'message', data: (data === undefined ? null : data)
|
||||
}), 0);
|
||||
}
|
||||
window.orca = {
|
||||
postMessage: function (data) { send(data); },
|
||||
onMessage: function (callback) {
|
||||
if (typeof callback === 'function') handlers.push(callback);
|
||||
}
|
||||
};
|
||||
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";
|
||||
|
||||
} // namespace
|
||||
|
||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
||||
, m_cap(std::move(capability))
|
||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||
{
|
||||
auto* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||
SetSizer(topsizer);
|
||||
|
||||
m_browser = WebView::CreateWebView(this, bootstrap_url());
|
||||
if (m_browser == nullptr) {
|
||||
wxLogError("Could not initialize plugin page web view");
|
||||
return;
|
||||
}
|
||||
|
||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this);
|
||||
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this);
|
||||
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
|
||||
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this);
|
||||
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script()));
|
||||
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script()));
|
||||
m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS);
|
||||
|
||||
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
|
||||
m_cap->set_message_sender([lifetime](const std::string& message) {
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
GUI::wxGetApp().CallAfter([lifetime, message] {
|
||||
if (PluginPage* page = lifetime->load(std::memory_order_acquire))
|
||||
page->push_message(message);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
PluginPage::~PluginPage()
|
||||
{
|
||||
detach_capability();
|
||||
if (m_lifetime)
|
||||
m_lifetime->store(nullptr, std::memory_order_release);
|
||||
}
|
||||
|
||||
void PluginPage::detach_capability()
|
||||
{
|
||||
if (m_lifetime)
|
||||
m_lifetime->store(nullptr, std::memory_order_release);
|
||||
if (m_cap)
|
||||
m_cap->clear_message_sender();
|
||||
m_cap.reset();
|
||||
}
|
||||
|
||||
wxString PluginPage::web_base_url() const
|
||||
{
|
||||
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
||||
return wxString("file://") + GUI::from_u8(path) + "/";
|
||||
}
|
||||
|
||||
wxString PluginPage::bootstrap_url() const
|
||||
{
|
||||
const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string();
|
||||
return wxString("file://") + GUI::from_u8(path);
|
||||
}
|
||||
|
||||
void PluginPage::on_bootstrap_event(wxWebViewEvent& event)
|
||||
{
|
||||
load_plugin_content();
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void PluginPage::load_plugin_content()
|
||||
{
|
||||
if (m_content_loaded || m_browser == nullptr || m_cap == nullptr)
|
||||
return;
|
||||
|
||||
m_content_loaded = true;
|
||||
try {
|
||||
m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url());
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
|
||||
detach_capability();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
|
||||
detach_capability();
|
||||
}
|
||||
}
|
||||
|
||||
void PluginPage::on_new_window(wxWebViewEvent& event)
|
||||
{
|
||||
const wxString url = event.GetURL();
|
||||
if (!url.empty() && m_browser != nullptr)
|
||||
m_browser->LoadURL(url);
|
||||
event.Veto();
|
||||
}
|
||||
|
||||
void PluginPage::on_script_message(wxWebViewEvent& event)
|
||||
{
|
||||
if (!m_cap)
|
||||
return;
|
||||
|
||||
const wxString payload = event.GetString();
|
||||
nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false);
|
||||
if (root.is_discarded() || root.value("channel", std::string()) != "orca" ||
|
||||
root.value("kind", std::string()) != "message")
|
||||
return;
|
||||
|
||||
const auto data = root.find("data");
|
||||
try {
|
||||
m_cap->on_message(data == root.end()
|
||||
? "null"
|
||||
: data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
|
||||
}
|
||||
}
|
||||
|
||||
void PluginPage::push_message(const std::string& message)
|
||||
{
|
||||
if (m_browser == nullptr)
|
||||
return;
|
||||
|
||||
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
|
||||
// non-JSON payload needs wrapping as a string literal.
|
||||
const std::string payload = nlohmann::json::accept(message)
|
||||
? message
|
||||
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
|
||||
|
||||
WebView::RunScript(m_browser, wxString::Format(
|
||||
"(function dispatch(payload, attempts) {\n"
|
||||
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
|
||||
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
|
||||
"})({data: %s}, 0);",
|
||||
wxString::FromUTF8(payload)));
|
||||
}
|
||||
|
||||
PluginPages::~PluginPages()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void PluginPages::initialize(Notebook* parent)
|
||||
{
|
||||
shutdown();
|
||||
m_parent = parent;
|
||||
if (m_parent == nullptr)
|
||||
return;
|
||||
|
||||
m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count();
|
||||
|
||||
for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) {
|
||||
if (capability)
|
||||
create_page(capability->identity());
|
||||
}
|
||||
relayout();
|
||||
}
|
||||
|
||||
void PluginPages::shutdown()
|
||||
{
|
||||
while (!m_pages.empty())
|
||||
remove_page(m_pages.begin()->first);
|
||||
m_parent = nullptr;
|
||||
}
|
||||
|
||||
void PluginPages::set_visible_page_count(int count)
|
||||
{
|
||||
const int clamped = std::clamp(count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
|
||||
if (clamped == m_visible_page_count)
|
||||
return;
|
||||
|
||||
m_visible_page_count = clamped;
|
||||
relayout();
|
||||
}
|
||||
|
||||
std::shared_ptr<PagesPluginCapability> PluginPages::get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const
|
||||
{
|
||||
auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false);
|
||||
if (!capability || capability->is_enabled() != is_enabled || capability->type() != PluginCapabilityType::Pages)
|
||||
return nullptr;
|
||||
|
||||
return std::dynamic_pointer_cast<PagesPluginCapability>(capability);
|
||||
}
|
||||
|
||||
bool PluginPages::create_page(const PluginCapabilityId& id)
|
||||
{
|
||||
if (m_pages.find(id) != m_pages.end())
|
||||
return false;
|
||||
|
||||
auto capability = get_pages_cap(id, true);
|
||||
if (!capability)
|
||||
return false;
|
||||
|
||||
std::string icon;
|
||||
try {
|
||||
icon = capability->get_icon();
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key << ": " << error.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key;
|
||||
}
|
||||
|
||||
auto* page = new PluginPage(m_parent, std::move(capability));
|
||||
if (!page->is_valid()) {
|
||||
page->Destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!icon.empty()) {
|
||||
try {
|
||||
boost::filesystem::path icon_path(icon);
|
||||
const std::string extension = icon_path.extension().string();
|
||||
if (extension == ".svg" || extension == ".png")
|
||||
icon_path.replace_extension();
|
||||
|
||||
page->set_icon(create_scaled_bitmap(icon_path.string(), m_parent, 20));
|
||||
} catch (const std::exception& error) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key;
|
||||
}
|
||||
}
|
||||
|
||||
m_pages.emplace(id, page);
|
||||
m_order.push_back(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PluginPages::on_cap_register(const PluginCapabilityId& id)
|
||||
{
|
||||
if (m_parent == nullptr)
|
||||
return;
|
||||
|
||||
if (create_page(id))
|
||||
relayout();
|
||||
}
|
||||
|
||||
void PluginPages::on_cap_deregister(const PluginCapabilityId& id)
|
||||
{
|
||||
remove_page(id);
|
||||
}
|
||||
|
||||
void PluginPages::on_plugin_register(const std::string& plugin_key)
|
||||
{
|
||||
for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) {
|
||||
if (capability)
|
||||
on_cap_register(capability->identity());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginPages::on_plugin_deregister(const std::string& plugin_key)
|
||||
{
|
||||
for (auto it = m_pages.begin(); it != m_pages.end();) {
|
||||
if (it->first.plugin_key != plugin_key) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
const PluginCapabilityId id = it->first;
|
||||
++it;
|
||||
remove_page(id);
|
||||
}
|
||||
}
|
||||
|
||||
void PluginPages::remove_page(const PluginCapabilityId& id)
|
||||
{
|
||||
auto it = m_pages.find(id);
|
||||
if (it == m_pages.end())
|
||||
return;
|
||||
|
||||
PluginPage* page = it->second;
|
||||
page->detach_capability();
|
||||
|
||||
m_pages.erase(it);
|
||||
m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end());
|
||||
|
||||
const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND;
|
||||
if (idx != wxNOT_FOUND)
|
||||
m_parent->RemovePage(idx);
|
||||
|
||||
relayout();
|
||||
page->Destroy();
|
||||
}
|
||||
|
||||
wxString PluginPages::page_tab_id(const PluginCapabilityId& id)
|
||||
{
|
||||
return wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name);
|
||||
}
|
||||
|
||||
void PluginPages::relayout()
|
||||
{
|
||||
if (m_parent == nullptr)
|
||||
return;
|
||||
|
||||
m_order.erase(std::remove_if(m_order.begin(), m_order.end(),
|
||||
[this](const PluginCapabilityId& id) {
|
||||
const bool orphaned = m_pages.find(id) == m_pages.end();
|
||||
if (orphaned)
|
||||
BOOST_LOG_TRIVIAL(error) << "PluginPages::relayout: '" << id.name << "' was in m_order but not m_pages, dropping";
|
||||
return orphaned;
|
||||
}),
|
||||
m_order.end());
|
||||
|
||||
const int visible_slots = std::max(1, m_visible_page_count);
|
||||
const bool need_overflow = static_cast<int>(m_order.size()) > visible_slots;
|
||||
|
||||
// Every visible slot is a normal, individual tab hosting its own page. When there's
|
||||
// overflow, the last slot's page is swappable via m_overflow_button/show_overflow_menu()
|
||||
// rather than being a fixed page — m_swapped_in_id tracks which one currently sits there.
|
||||
std::vector<PluginCapabilityId> tab_ids;
|
||||
if (!need_overflow) {
|
||||
tab_ids = m_order;
|
||||
m_swapped_in_id.reset();
|
||||
} else {
|
||||
const auto overflow_begin = m_order.begin() + (visible_slots - 1);
|
||||
tab_ids.assign(m_order.begin(), overflow_begin);
|
||||
|
||||
if (!m_swapped_in_id || std::find(overflow_begin, m_order.end(), *m_swapped_in_id) == m_order.end())
|
||||
m_swapped_in_id = *overflow_begin;
|
||||
tab_ids.push_back(*m_swapped_in_id);
|
||||
}
|
||||
|
||||
// MainFrame::show_device() relayouts on every printer change and most of those change
|
||||
// nothing, so only touch the notebook when the trailing slots don't already spell out
|
||||
// tab_ids — a rebuild destroys and recreates every tab button and rasterizes every icon.
|
||||
const size_t page_count = m_parent->GetPageCount();
|
||||
bool up_to_date = page_count >= tab_ids.size();
|
||||
for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i)
|
||||
up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]);
|
||||
for (const auto& [id, page] : m_pages) {
|
||||
if (!up_to_date)
|
||||
break;
|
||||
const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end();
|
||||
up_to_date = (m_parent->FindPage(page) != wxNOT_FOUND) == wanted;
|
||||
}
|
||||
|
||||
if (!up_to_date) {
|
||||
const wxString id_to_reselect = m_parent->GetSelectedPageName();
|
||||
|
||||
for (const auto& [id, page] : m_pages) {
|
||||
const int idx = m_parent->FindPage(page);
|
||||
if (idx != wxNOT_FOUND)
|
||||
m_parent->RemovePage(idx);
|
||||
}
|
||||
|
||||
for (const auto& id : tab_ids) {
|
||||
PluginPage* page = m_pages.at(id);
|
||||
m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), "",
|
||||
false, page->icon());
|
||||
}
|
||||
|
||||
if (!id_to_reselect.empty())
|
||||
m_parent->SelectPageByName(id_to_reselect);
|
||||
}
|
||||
|
||||
if (need_overflow) {
|
||||
if (m_overflow_button == nullptr) {
|
||||
auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER);
|
||||
btn->SetCornerRadius(0);
|
||||
const int em = em_unit(m_parent);
|
||||
btn->SetMinSize({40 * em / 10, 36 * em / 10});
|
||||
btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { show_overflow_menu(); });
|
||||
GUI::wxGetApp().UpdateDarkUI(btn);
|
||||
m_overflow_button = btn;
|
||||
}
|
||||
m_parent->SetOverflowButton(m_overflow_button);
|
||||
} else if (m_overflow_button != nullptr) {
|
||||
m_parent->SetOverflowButton(nullptr);
|
||||
m_overflow_button->Destroy();
|
||||
m_overflow_button = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PluginPages::show_overflow_menu()
|
||||
{
|
||||
const int visible_slots = std::max(1, m_visible_page_count);
|
||||
if (m_overflow_button == nullptr || static_cast<int>(m_order.size()) <= visible_slots)
|
||||
return;
|
||||
|
||||
const std::vector<PluginCapabilityId> overflow_ids(m_order.begin() + (visible_slots - 1), m_order.end());
|
||||
|
||||
wxMenu menu;
|
||||
for (size_t i = 0; i < overflow_ids.size(); ++i)
|
||||
menu.AppendRadioItem(static_cast<int>(wxID_HIGHEST + 1 + i), wxString::FromUTF8(overflow_ids[i].name));
|
||||
if (m_swapped_in_id) {
|
||||
const auto it = std::find(overflow_ids.begin(), overflow_ids.end(), *m_swapped_in_id);
|
||||
if (it != overflow_ids.end())
|
||||
menu.Check(static_cast<int>(wxID_HIGHEST + 1 + (it - overflow_ids.begin())), true);
|
||||
}
|
||||
|
||||
menu.Bind(wxEVT_MENU, [this, overflow_ids](wxCommandEvent& evt) {
|
||||
const size_t index = static_cast<size_t>(evt.GetId() - (wxID_HIGHEST + 1));
|
||||
if (index >= overflow_ids.size())
|
||||
return;
|
||||
m_swapped_in_id = overflow_ids[index];
|
||||
relayout();
|
||||
m_parent->SelectPageByName(page_tab_id(*m_swapped_in_id));
|
||||
});
|
||||
m_overflow_button->PopupMenu(&menu);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/webview.h>
|
||||
|
||||
class Notebook;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginPage : public wxPanel
|
||||
{
|
||||
public:
|
||||
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
|
||||
~PluginPage() override;
|
||||
|
||||
PluginPage() = delete;
|
||||
|
||||
bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; }
|
||||
void detach_capability();
|
||||
void on_bootstrap_event(wxWebViewEvent& event);
|
||||
void on_new_window(wxWebViewEvent& event);
|
||||
void on_script_message(wxWebViewEvent& event);
|
||||
void push_message(const std::string& message);
|
||||
void set_icon(const wxBitmap& icon) { m_icon = icon; }
|
||||
const wxBitmap& icon() const { return m_icon; }
|
||||
|
||||
private:
|
||||
void load_plugin_content();
|
||||
wxString bootstrap_url() const;
|
||||
wxString web_base_url() const;
|
||||
|
||||
wxWebView* m_browser{nullptr};
|
||||
std::shared_ptr<PagesPluginCapability> m_cap;
|
||||
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
|
||||
bool m_content_loaded{false};
|
||||
wxBitmap m_icon;
|
||||
};
|
||||
|
||||
class PluginPages
|
||||
{
|
||||
public:
|
||||
PluginPages() = default;
|
||||
~PluginPages();
|
||||
|
||||
PluginPages(const PluginPages&) = delete;
|
||||
PluginPages& operator=(const PluginPages&) = delete;
|
||||
|
||||
void initialize(Notebook* parent);
|
||||
void shutdown();
|
||||
|
||||
void on_cap_register(const PluginCapabilityId& id);
|
||||
void on_cap_deregister(const PluginCapabilityId& id);
|
||||
void on_plugin_register(const std::string& plugin_key);
|
||||
void on_plugin_deregister(const std::string& plugin_key);
|
||||
|
||||
void set_visible_page_count(int count);
|
||||
|
||||
void relayout();
|
||||
|
||||
private:
|
||||
std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const;
|
||||
bool create_page(const PluginCapabilityId& id);
|
||||
void remove_page(const PluginCapabilityId& id);
|
||||
|
||||
void show_overflow_menu();
|
||||
static wxString page_tab_id(const PluginCapabilityId& id);
|
||||
|
||||
std::map<PluginCapabilityId, PluginPage*> m_pages;
|
||||
std::vector<PluginCapabilityId> m_order;
|
||||
Notebook* m_parent{nullptr};
|
||||
|
||||
int m_visible_page_count{0};
|
||||
|
||||
std::optional<PluginCapabilityId> m_swapped_in_id;
|
||||
wxWindow* m_overflow_button{nullptr};
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "PagesPluginCapability.hpp"
|
||||
#include "PagesPluginCapabilityTrampoline.hpp"
|
||||
|
||||
#include "../../PluginFsUtils.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void PagesPluginCapability::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering orca.pages bindings";
|
||||
|
||||
auto pages = module.def_submodule("pages", "Plugin page API");
|
||||
|
||||
py::class_<PagesPluginCapability, PluginCapabilityInterface, PyPagesPluginCapabilityTrampoline,
|
||||
std::shared_ptr<PagesPluginCapability>>(pages, "PagesPluginCapabilityBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &PagesPluginCapability::get_type)
|
||||
.def("get_ui", &PagesPluginCapability::get_ui)
|
||||
.def("get_icon", &PagesPluginCapability::get_icon)
|
||||
.def("on_message", &PagesPluginCapability::on_message)
|
||||
.def(
|
||||
"post_message",
|
||||
[](PagesPluginCapability& capability, py::object data) {
|
||||
capability.post_message(py_to_json(data).dump());
|
||||
},
|
||||
py::arg("data"), "Send a JSON-compatible value to the page's window.orca.onMessage handlers.");
|
||||
}
|
||||
|
||||
void PagesPluginCapability::post_message(std::string message)
|
||||
{
|
||||
std::function<void(const std::string&)> sender;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_message_mutex);
|
||||
sender = m_message_sender;
|
||||
}
|
||||
|
||||
if (sender)
|
||||
sender(message);
|
||||
}
|
||||
|
||||
void PagesPluginCapability::set_message_sender(std::function<void(const std::string&)> sender)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_message_mutex);
|
||||
m_message_sender = std::move(sender);
|
||||
}
|
||||
|
||||
void PagesPluginCapability::clear_message_sender()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_message_mutex);
|
||||
m_message_sender = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef slic3r_PagesPluginCapability_hpp_
|
||||
#define slic3r_PagesPluginCapability_hpp_
|
||||
|
||||
#include "../../PythonPluginInterface.hpp"
|
||||
#include "pybind11/pybind11.h"
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
class PagesPluginCapability : public PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::Pages; }
|
||||
|
||||
virtual std::string get_ui() = 0;
|
||||
virtual void on_message(std::string message) { (void) message; }
|
||||
virtual std::string get_icon() { return {}; }
|
||||
|
||||
void post_message(std::string message);
|
||||
void set_message_sender(std::function<void(const std::string&)> sender);
|
||||
void clear_message_sender();
|
||||
|
||||
private:
|
||||
mutable std::mutex m_message_mutex;
|
||||
std::function<void(const std::string&)> m_message_sender;
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "PagesPluginCapability.hpp"
|
||||
#include "../../PluginFsUtils.hpp"
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PyPagesPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PagesPluginCapability>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<PagesPluginCapability>::PyPluginCommonTrampoline;
|
||||
|
||||
std::string get_icon() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
std::string,
|
||||
PagesPluginCapability,
|
||||
get_icon);
|
||||
}
|
||||
|
||||
std::string get_ui() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
std::string,
|
||||
PagesPluginCapability,
|
||||
get_ui);
|
||||
}
|
||||
|
||||
void on_message(std::string message) override
|
||||
{
|
||||
PluginCapabilityInterface::RefCounter ref_counter(*this);
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
throw std::runtime_error("Python interpreter is shutting down");
|
||||
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
|
||||
pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message");
|
||||
if (!override)
|
||||
return;
|
||||
|
||||
nlohmann::json data = nlohmann::json::parse(message, nullptr, false);
|
||||
if (data.is_discarded())
|
||||
data = message;
|
||||
|
||||
ORCA_PY_LOGGED_OVERRIDE_BODY(override(::Slic3r::json_to_py(data)));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -13,10 +13,8 @@ namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||||
void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
(void) pluginTypes;
|
||||
|
||||
auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API");
|
||||
|
||||
py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode")
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Slic3r {
|
||||
class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }
|
||||
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
void ScriptPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||||
void ScriptPluginCapability::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
(void) pluginTypes;
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings";
|
||||
|
||||
auto script = module.def_submodule("script", "Script Plugins API");
|
||||
|
||||
@@ -11,8 +11,7 @@ public:
|
||||
|
||||
virtual ExecutionResult execute() = 0;
|
||||
|
||||
static void RegisterBindings(pybind11::module_ &module,
|
||||
pybind11::enum_<PluginCapabilityType> &pluginTypes);
|
||||
static void RegisterBindings(pybind11::module_ &module);
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ namespace Slic3r {
|
||||
|
||||
bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); }
|
||||
|
||||
void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_<PluginCapabilityType>& pluginTypes) {
|
||||
(void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum.
|
||||
void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module) {
|
||||
auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental).");
|
||||
|
||||
py::enum_<SlicingPipelineStepPlugin>(slicing, "Step")
|
||||
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
// Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be
|
||||
// blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock.
|
||||
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
|
||||
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
Reference in New Issue
Block a user