feat: plugin pages

This commit is contained in:
Ian Chua
2026-07-28 19:17:26 +08:00
parent 56c28fc102
commit e00906a833
21 changed files with 622 additions and 79 deletions

View File

@@ -620,6 +620,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostSlicing.cpp plugin/host/PluginHostSlicing.cpp
plugin/host/PluginHostUi.cpp plugin/host/PluginHostUi.cpp
plugin/host/PluginHostUi.hpp plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/CloudPluginService.cpp plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp plugin/PluginFsUtils.cpp
@@ -640,6 +642,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp
plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp

View File

@@ -2799,6 +2799,16 @@ void GUI_App::init_plugin_gui_wiring()
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
return;
wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key);
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
return;
wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key);
});
plugin_mgr.subscribe_on_capability_load_callback( plugin_mgr.subscribe_on_capability_load_callback(
[refresh_plugins_dialog](const PluginCapabilityId& capability) { [refresh_plugins_dialog](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection) if (capability.type == PluginCapabilityType::PrinterConnection)
@@ -2811,11 +2821,15 @@ void GUI_App::init_plugin_gui_wiring()
if (Plater* plater = wxGetApp().plater()) if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing(); plater->revalidate_current_plate_if_plugins_missing();
}); });
if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
wxGetApp().mainframe->plugin_pages().on_cap_register(capability);
}); });
plugin_mgr.subscribe_on_capability_unload_callback( plugin_mgr.subscribe_on_capability_unload_callback(
[refresh_plugins_dialog](const PluginCapabilityId& capability) { [refresh_plugins_dialog](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection) if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability);
refresh_plugins_dialog(); refresh_plugins_dialog();
}); });
} }

View File

@@ -1110,6 +1110,7 @@ void MainFrame::update_edge_panels()
void MainFrame::shutdown() void MainFrame::shutdown()
{ {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter";
m_plugin_pages.shutdown();
#ifdef __WXGTK__ #ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically. // Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr; m_edge_bottom = nullptr;
@@ -1355,6 +1356,9 @@ void MainFrame::init_tabpanel() {
m_calibration->SetBackgroundColour(*wxWHITE); m_calibration->SetBackgroundColour(*wxWHITE);
m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false);
// Plugin pages are appended after the built-in tabs; their ids are namespaced
// (plugin.<plugin_key>.<name>) so they can't collide with the built-in TAB_ID_* constants.
m_plugin_pages.initialize(m_tabpanel);
if (m_plater) { if (m_plater) {
// load initial config // load initial config

View File

@@ -35,6 +35,7 @@
#include "PrinterWebView.hpp" #include "PrinterWebView.hpp"
#include "calib_dlg.hpp" #include "calib_dlg.hpp"
#include "MultiMachinePage.hpp" #include "MultiMachinePage.hpp"
#include "slic3r/plugin/host/PluginPages.hpp"
// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are // Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
// names rather than positional indices so optional pages cannot shift them. // names rather than positional indices so optional pages cannot shift them.
@@ -357,6 +358,7 @@ public:
//SoftFever //SoftFever
void show_device(bool bBBLPrinter); void show_device(bool bBBLPrinter);
void fit_tab_labels(); // ORCA void fit_tab_labels(); // ORCA
PluginPages& plugin_pages() { return m_plugin_pages; }
PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr };
FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr }; FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr };
@@ -382,6 +384,7 @@ public:
CalibrationPanel* m_calibration{ nullptr }; CalibrationPanel* m_calibration{ nullptr };
WebViewPanel* m_webview { nullptr }; WebViewPanel* m_webview { nullptr };
PrinterWebView* m_printer_view{nullptr}; PrinterWebView* m_printer_view{nullptr};
PluginPages m_plugin_pages;
wxLogWindow* m_log_window { nullptr }; wxLogWindow* m_log_window { nullptr };
// BBS // BBS
//wxBookCtrlBase* m_tabpanel { nullptr }; //wxBookCtrlBase* m_tabpanel { nullptr };

View File

@@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale()
void ButtonsListCtrl::SetSelection(int sel) void ButtonsListCtrl::SetSelection(int sel)
{ {
if (m_selection == sel) if (m_selection == sel && sel >= 0 && sel < static_cast<int>(m_pageButtons.size()))
return; return;
// BBS: change button color // BBS: change button color
wxColour selected_btn_bg("#009688"); // Gradient #009688 wxColour selected_btn_bg("#009688"); // Gradient #009688
if (m_selection >= 0) { if (m_selection >= 0 && m_selection < static_cast<int>(m_pageButtons.size())) {
StateColor bg_color = StateColor( StateColor bg_color = StateColor(
std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered}, std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered},
std::pair{wxColour(59, 68, 70), (int) StateColor::Normal}); std::pair{wxColour(59, 68, 70), (int) StateColor::Normal});
@@ -135,6 +135,13 @@ void ButtonsListCtrl::SetSelection(int sel)
m_pageButtons[m_selection]->SetSelected(false); m_pageButtons[m_selection]->SetSelected(false);
m_pageButtons[m_selection]->SetTextColor(text_color); m_pageButtons[m_selection]->SetTextColor(text_color);
} }
if (sel < 0 || sel >= static_cast<int>(m_pageButtons.size())) {
m_selection = -1;
Refresh();
return;
}
m_selection = sel; m_selection = sel;
StateColor bg_color = StateColor( StateColor bg_color = StateColor(
@@ -192,6 +199,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
void ButtonsListCtrl::RemovePage(size_t n) void ButtonsListCtrl::RemovePage(size_t n)
{ {
if (n >= m_pageButtons.size())
return;
if (m_selection == static_cast<int>(n))
m_selection = -1;
else if (m_selection > static_cast<int>(n))
--m_selection;
Button* btn = m_pageButtons[n]; Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n); m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA

View File

@@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI {
namespace { namespace {
// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
// plugin page looks native while any CSS the plugin ships still wins. Built on the
// --orca-* variables the host injects (see WebViewHostDialog); document-start injected
// AFTER the host contract so the variables are defined (shares the base injector's
// WebView2 timing guard).
std::string plugin_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
css += "body{margin:0;}";
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
css += "a{color:var(--orca-accent);}";
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
css += "button:hover{filter:brightness(1.1);}";
css += "button:disabled{opacity:.5;cursor:default;}";
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
css += "table{border-collapse:collapse;}";
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
css += "th{color:var(--orca-muted);font-weight:600;}";
css += "::-webkit-scrollbar{width:12px;height:12px;}";
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
css += "::-webkit-scrollbar-track{background:transparent;}";
css += "</style>";
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
// Injected into the top-level page at document start (before the plugin's own // Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It // scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's // references window.wx lazily (at call time) so it never races the backend's
@@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
void PluginWebDialog::add_user_scripts() void PluginWebDialog::add_user_scripts()
{ {
if (wxWebView* wv = browser()) { if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script())); wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS); wv->AddUserScript(ORCA_BRIDGE_JS);
} }
} }

View File

@@ -57,18 +57,6 @@ std::string host_theme_vars_css()
return s; return s;
} }
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
std::string host_theme_user_script()
{
const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>";
return WebViewHostDialog::document_start_injector(
style, "orca-host-theme-vars", "afterbegin",
"window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";",
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
// JS to re-theme an already-loaded document live (no reload): replace the injected // JS to re-theme an already-loaded document live (no reload): replace the injected
// style's contents and update data-orca-theme. Everything downstream (theme.css // style's contents and update data-orca-theme. Everything downstream (theme.css
// tokens, plugin element defaults, page layout) re-cascades from these values. // tokens, plugin element defaults, page layout) re-cascades from these values.
@@ -87,6 +75,46 @@ if(document.documentElement)
} // namespace } // namespace
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
std::string WebViewHostDialog::theme_user_script()
{
const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>";
return document_start_injector(
style, "orca-host-theme-vars", "afterbegin",
"window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";",
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
std::string WebViewHostDialog::plugin_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
css += "body{margin:0;}";
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
css += "a{color:var(--orca-accent);}";
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
css += "button:hover{filter:brightness(1.1);}";
css += "button:disabled{opacity:.5;cursor:default;}";
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
css += "table{border-collapse:collapse;}";
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
css += "th{color:var(--orca-muted);font-weight:600;}";
css += "::-webkit-scrollbar{width:12px;height:12px;}";
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
css += "::-webkit-scrollbar-track{background:transparent;}";
css += "</style>";
return document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
std::string WebViewHostDialog::document_start_injector(const std::string& markup, std::string WebViewHostDialog::document_start_injector(const std::string& markup,
const char* dom_id, const char* dom_id,
const char* position, const char* position,
@@ -244,7 +272,7 @@ void WebViewHostDialog::register_theme_user_scripts()
// script message handler is registered separately (AddScriptMessageHandler), but on // script message handler is registered separately (AddScriptMessageHandler), but on
// some backends RemoveAllUserScripts() drops it too, which would break // some backends RemoveAllUserScripts() drops it too, which would break
// window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live(). // window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live().
m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script())); m_browser->AddUserScript(wxString::FromUTF8(theme_user_script()));
add_user_scripts(); add_user_scripts();
} }

View File

@@ -49,6 +49,10 @@ public:
const std::string& prelude = {}, const std::string& prelude = {},
const std::string& on_inject = {}); const std::string& on_inject = {});
// Shared by modeless Pages tabs and PluginWebDialog.
static std::string theme_user_script();
static std::string plugin_defaults_user_script();
protected: protected:
wxWebView* browser() const { return m_browser; } wxWebView* browser() const { return m_browser; }

View File

@@ -19,6 +19,7 @@
#include "PyPluginPackage.hpp" #include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp" #include "PyPluginTrampoline.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/pages/PagesPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
@@ -319,17 +320,17 @@ void bind_python_api(pybind11::module_& m)
{ {
m.doc() = "OrcaSlicer plugin API"; m.doc() = "OrcaSlicer plugin API";
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups") py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PrinterConnection", PluginCapabilityType::PrinterConnection) .value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Automation", PluginCapabilityType::Automation) .value("Pages", PluginCapabilityType::Pages)
.value("Analysis", PluginCapabilityType::Analysis) .value("Analysis", PluginCapabilityType::Analysis)
.value("Importer", PluginCapabilityType::Importer) .value("Importer", PluginCapabilityType::Importer)
.value("Exporter", PluginCapabilityType::Exporter) .value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization) .value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script) .value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("Unknown", PluginCapabilityType::Unknown) .value("Unknown", PluginCapabilityType::Unknown)
.export_values(); .export_values();
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code") py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
.value("Success", PluginResult::Success) .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"; BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
// Make sure you register your bindings here // Make sure you register your bindings here
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); PrinterAgentPluginCapability::RegisterBindings(m);
ScriptPluginCapability::RegisterBindings(m, pluginTypes); PagesPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes); ScriptPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m);
PluginHost::RegisterBindings(m); PluginHost::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";

View File

@@ -12,7 +12,7 @@
namespace Slic3r { 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 struct PluginCapabilityId
{ {
@@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{ {
switch (type) { switch (type) {
case PluginCapabilityType::PrinterConnection: return "printer-connection"; case PluginCapabilityType::PrinterConnection: return "printer-connection";
case PluginCapabilityType::Automation: return "automation"; case PluginCapabilityType::Pages: return "pages";
case PluginCapabilityType::Analysis: return "analysis"; case PluginCapabilityType::Analysis: return "analysis";
case PluginCapabilityType::Importer: return "importer"; case PluginCapabilityType::Importer: return "importer";
case PluginCapabilityType::Exporter: return "exporter"; case PluginCapabilityType::Exporter: return "exporter";
@@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
{ {
switch (type) { switch (type) {
case PluginCapabilityType::PrinterConnection: return "Printer connection"; case PluginCapabilityType::PrinterConnection: return "Printer connection";
case PluginCapabilityType::Automation: return "Automation"; case PluginCapabilityType::Pages: return "Pages";
case PluginCapabilityType::Analysis: return "Analysis"; case PluginCapabilityType::Analysis: return "Analysis";
case PluginCapabilityType::Importer: return "Importer"; case PluginCapabilityType::Importer: return "Importer";
case PluginCapabilityType::Exporter: return "Exporter"; case PluginCapabilityType::Exporter: return "Exporter";
@@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
if (lowered == "printer-connection") if (lowered == "printer-connection")
return PluginCapabilityType::PrinterConnection; return PluginCapabilityType::PrinterConnection;
if (lowered == "automation") if (lowered == "pages")
return PluginCapabilityType::Automation; return PluginCapabilityType::Pages;
if (lowered == "analysis") if (lowered == "analysis")
return PluginCapabilityType::Analysis; return PluginCapabilityType::Analysis;
if (lowered == "importer") if (lowered == "importer")

View File

@@ -0,0 +1,298 @@
#include "PluginPages.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Notebook.hpp"
#include "slic3r/GUI/GUI_App.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 <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#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 nlohmann::json data = root.contains("data") ? root["data"] : nlohmann::json();
try {
m_cap->on_message(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;
nlohmann::json data = nlohmann::json::parse(message, nullptr, false);
if (data.is_discarded())
data = message;
const wxString script = wxString("(function dispatch(payload, attempts) {\n") +
wxString(" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n") +
wxString(" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n") +
wxString("})({data: ") +
wxString::FromUTF8(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)) +
wxString("}, 0);");
WebView::RunScript(m_browser, script);
}
PluginPages::~PluginPages()
{
shutdown();
}
void PluginPages::initialize(Notebook* parent)
{
shutdown();
m_parent = parent;
if (m_parent == nullptr)
return;
for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) {
if (capability)
on_cap_register(capability->identity());
}
}
void PluginPages::shutdown()
{
while (!m_pages.empty())
remove_page(m_pages.begin()->first);
m_parent = nullptr;
}
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);
}
void PluginPages::on_cap_register(const PluginCapabilityId& id)
{
if (m_parent == nullptr || m_pages.find(id) != m_pages.end())
return;
auto capability = get_pages_cap(id, true);
if (!capability)
return;
auto* page = new PluginPage(m_parent, std::move(capability));
if (!page->is_valid()) {
page->Destroy();
return;
}
const wxString title = wxString::FromUTF8(id.name);
const wxString page_id = wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name);
if (!m_parent->AddPage(page_id, page, title, "tab_auxiliary_active", "tab_auxiliary_active", false)) {
page->Destroy();
return;
}
m_pages.emplace(id, page);
}
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();
if (m_parent != nullptr) {
const int index = m_parent->FindPage(page);
if (index != wxNOT_FOUND)
m_parent->RemovePage(static_cast<size_t>(index));
}
page->Destroy();
m_pages.erase(it);
}
} // namespace Slic3r

View File

@@ -0,0 +1,69 @@
#pragma once
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
#include <atomic>
#include <map>
#include <memory>
#include <string>
#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);
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};
};
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);
private:
std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const;
void remove_page(const PluginCapabilityId& id);
std::map<PluginCapabilityId, PluginPage*> m_pages;
Notebook* m_parent{nullptr};
};
} // namespace Slic3r

View File

@@ -0,0 +1,59 @@
#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("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

View File

@@ -0,0 +1,32 @@
#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; }
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

View File

@@ -0,0 +1,48 @@
#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_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

View File

@@ -13,10 +13,8 @@ namespace py = pybind11;
namespace Slic3r { 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"); auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API");
py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode") py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode")

View File

@@ -19,7 +19,7 @@ namespace Slic3r {
class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent
{ {
public: 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; } PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }

View File

@@ -9,9 +9,8 @@
namespace py = pybind11; namespace py = pybind11;
namespace Slic3r { 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"; BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings";
auto script = module.def_submodule("script", "Script Plugins API"); auto script = module.def_submodule("script", "Script Plugins API");

View File

@@ -11,8 +11,7 @@ public:
virtual ExecutionResult execute() = 0; virtual ExecutionResult execute() = 0;
static void RegisterBindings(pybind11::module_ &module, static void RegisterBindings(pybind11::module_ &module);
pybind11::enum_<PluginCapabilityType> &pluginTypes);
}; };
} // namespace Slic3r } // namespace Slic3r

View File

@@ -8,8 +8,7 @@ namespace Slic3r {
bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); } bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); }
void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_<PluginCapabilityType>& pluginTypes) { void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module) {
(void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum.
auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental)."); auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental).");
py::enum_<SlicingPipelineStepPlugin>(slicing, "Step") py::enum_<SlicingPipelineStepPlugin>(slicing, "Step")

View File

@@ -37,7 +37,7 @@ public:
// Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be // 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. // blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock.
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0; virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes); static void RegisterBindings(pybind11::module_& module);
}; };
} // namespace Slic3r } // namespace Slic3r