Merge branch 'main' into feat/printer-agent-infra

This commit is contained in:
Ian Chua
2026-09-22 20:55:29 +08:00
88 changed files with 13028 additions and 6155 deletions
+23 -12
View File
@@ -122,17 +122,19 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
// Put the prime tower where the GUI and CLI would before slicing. The config default (x 15, y 220)
// lies off any bed shallower than the tower, and generation rejects an off-plate tower instead of
// exporting it. Beside the centred cube, clear of the edge exclusion strips some beds carry, then
// pulled inside the printable outline by the tower's own estimated footprint, with a few mm of
// clearance so the conflict checker never sees the two touch.
void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
// exporting it. Beside the cube at the bed centre, clear of the edge exclusion strips some beds
// carry, with a few mm of clearance so the conflict checker never sees the two touch. The cube and
// the tower's estimated footprint are then pulled inside the printable outline as one rigid pair:
// moving the tower alone would push it back onto the cube on a narrow bed. Returns that move for
// the cube.
Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
{
const auto *area = cfg.option<ConfigOptionPoints>("printable_area");
if (area == nullptr || area->values.size() < 3)
return;
return Vec2d::Zero();
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(cfg, resolve_wipe_tower_type(cfg), {0, 1}, cfg.opt_float("layer_height"), 10.);
if (footprint.depth < EPSILON)
return;
return Vec2d::Zero();
const double margin = WIPE_TOWER_MARGIN + footprint.brim_width;
// The position is the tower's own origin; a rotated tower extends from it in another
// direction, so place the rotated box's extents rather than the origin.
@@ -143,13 +145,22 @@ void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
const Vec2d size = unscale(local.max) - lo;
Vec2d pos(center.x() + 5. + margin + 5. - lo.x(), center.y() - size.y() / 2. - lo.y());
box.translate(Point::new_scale(pos.x(), pos.y()));
const Vec2f move = WipeTower::move_box_inside_polygon(get_extents(box), Polygons{Polygon::new_scale(area->values)}, scaled<coord_t>(margin));
pos += move.cast<double>();
// A bed too small for the pair keeps the cube at its centre and places the tower alone.
const Polygons bed{Polygon::new_scale(area->values)};
const BoundingBox tower = get_extents(box);
BoundingBox pair = tower;
pair.merge(Point::new_scale(center.x() - 5., center.y() - 5.));
pair.merge(Point::new_scale(center.x() + 5., center.y() + 5.));
const Point room = get_extents(bed).size() - Point::new_scale(2. * margin, 2. * margin);
const bool rigid = pair.size().x() < room.x() && pair.size().y() < room.y();
const Vec2d move = WipeTower::move_box_inside_polygon(rigid ? pair : tower, bed, scaled<coord_t>(margin)).cast<double>();
pos += move;
cfg.option<ConfigOptionFloats>("wipe_tower_x", true)->values = {pos.x()};
cfg.option<ConfigOptionFloats>("wipe_tower_y", true)->values = {pos.y()};
return rigid ? move : Vec2d::Zero();
}
// Slice one centered cube that switches from filament 1 to filament 2 partway up, so exactly one
// Slice one cube that switches from filament 1 to filament 2 partway up, so exactly one
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
@@ -157,10 +168,10 @@ void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
// Slic3r::PlaceholderParserError from export.
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
{
const Vec2d center = printable_area_center(cfg);
place_wipe_tower(cfg, center);
const Vec2d center = printable_area_center(cfg);
const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
Model model;
Print print;
+10 -2
View File
@@ -139,8 +139,16 @@ set(SLIC3R_GUI_SOURCES
GUI/TerminalDialog.hpp
GUI/PluginProgressDialog.cpp
GUI/PluginProgressDialog.hpp
GUI/PluginWebDialog.cpp
GUI/PluginWebDialog.hpp
GUI/WebDialog.cpp
GUI/WebDialog.hpp
GUI/DockPanel.cpp
GUI/DockPanel.hpp
GUI/WebPanel.cpp
GUI/WebPanel.hpp
GUI/Widgets/WebHosting.cpp
GUI/Widgets/WebHosting.hpp
GUI/AuiPaneLayout.cpp
GUI/AuiPaneLayout.hpp
GUI/DragCanvas.cpp
GUI/DragCanvas.hpp
GUI/EditGCodeDialog.cpp
+20
View File
@@ -0,0 +1,20 @@
#include "AuiPaneLayout.hpp"
namespace Slic3r { namespace GUI {
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name)
{
// Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|".
const std::string prefix = "name=" + pane_name + ";";
size_t begin = 0;
for (size_t i = 0; i <= layout.size(); ++i) {
if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\')))
continue;
if (layout.compare(begin, prefix.size(), prefix) == 0)
return layout.substr(begin, i - begin);
begin = i + 1;
}
return {};
}
}} // namespace Slic3r::GUI
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace Slic3r { namespace GUI {
// The part a wxAuiManager layout string (wxAuiManager::SavePerspective) holds for `pane_name`, in the
// form wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name);
}} // namespace Slic3r::GUI
+103
View File
@@ -0,0 +1,103 @@
#include "DockPanel.hpp"
#include "GUI_App.hpp"
#include "Plater.hpp"
#include "Widgets/WebHosting.hpp"
#include <wx/weakref.h>
#include <algorithm>
#include <utility>
namespace Slic3r { namespace GUI {
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title)
{
std::string name = "plugin:" + plugin_key + ":" + title;
std::replace_if(name.begin(), name.end(), [](char c) { return c == '|' || c == ';' || c == '=' || c == '\\'; }, '_');
return name;
}
DockPanel::DockPanel(wxWindow* parent,
const std::string& html,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed)
: WebPanel(parent, web_hosting::orca_bridge_script())
, m_html(html)
, m_on_message(std::move(on_message))
, m_on_close(std::move(on_close))
, m_on_destroyed(std::move(on_destroyed))
{
// A link asking for a new window has nowhere to open from a docked panel.
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
}
DockPanel::~DockPanel()
{
if (m_on_destroyed)
m_on_destroyed();
}
bool DockPanel::on_page_message(const std::string& kind, const nlohmann::json& data)
{
if (kind == "message") {
if (m_on_message)
m_on_message(data);
return true;
}
if (kind == "close") {
request_close();
return true;
}
return false;
}
void DockPanel::push_message(const nlohmann::json& data)
{
if (!m_closing)
post_to_page(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
}
void DockPanel::fire_close()
{
if (m_closing)
return;
m_closing = true;
if (m_on_close) {
CloseHandler on_close = std::move(m_on_close);
m_on_close = nullptr;
on_close();
}
}
void DockPanel::request_close()
{
if (m_closing)
return;
fire_close();
// A page-requested close arrives inside the web view's script callback, so destroy later; another
// close path may have destroyed the panel by then.
wxWeakRef<DockPanel> self(this);
CallAfter([self]() {
if (self)
self->remove_pane();
});
}
void DockPanel::destroy_silently()
{
m_closing = true;
m_on_close = nullptr;
remove_pane();
}
void DockPanel::remove_pane()
{
if (Plater* plater = wxGetApp().plater())
plater->remove_dock_pane(this);
else
Destroy();
}
}} // namespace Slic3r::GUI
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "WebPanel.hpp"
#include <functional>
#include <string>
namespace Slic3r { namespace GUI {
// Stable across sessions so the saved layout finds the pane; free of wxAuiManager layout delimiters.
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title);
// A WebPanel docked in the Plater, on the plugin-window bridge minus submit. It can be destroyed
// without the GIL, so its hooks must not capture pybind11 objects.
class DockPanel : public WebPanel
{
public:
using MessageHandler = std::function<void(const nlohmann::json& data)>;
using CloseHandler = std::function<void()>;
// on_close fires once, on a user or page close. on_destroyed runs on every destruction and must
// touch host-side state only.
DockPanel(wxWindow* parent,
const std::string& html,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed);
~DockPanel() override;
// Main thread only.
void push_message(const nlohmann::json& data);
// Fires on_close, then removes the pane.
void request_close();
// Removes the pane without on_close, for plugin unload. Destroys at once: unload always comes from
// the host, never from this panel's own callbacks.
void destroy_silently();
// Fires on_close at most once; also run by the pane's own close button.
void fire_close();
protected:
std::optional<std::string> page_html() override { return m_html; }
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
private:
void remove_pane();
std::string m_html;
bool m_closing{false};
MessageHandler m_on_message;
CloseHandler m_on_close;
CloseHandler m_on_destroyed;
};
}} // namespace Slic3r::GUI
+4 -7
View File
@@ -1189,6 +1189,8 @@ void MainFrame::shutdown()
if (m_project != nullptr)
m_project->shutdown();
m_plugin_pages.shutdown();
if (m_plater != nullptr)
m_plater->remove_dock_panes();
#ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr;
@@ -3424,11 +3426,6 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().open_preferences();
},
"", nullptr, []() { return true; }, this, 1);
parent_menu->AppendSeparator();
append_shortcut_item(
parent_menu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
//parent_menu->Insert(1, preference_item);
#endif
// Help menu
@@ -3454,7 +3451,7 @@ void MainFrame::init_menubar_as_editor()
top_menu->AppendSeparator();
append_shortcut_item(
top_menu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
top_menu, Shortcut::SpeedDial, false, _L("Open Speed Dial"), "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
top_menu->AppendSeparator();
@@ -3561,7 +3558,7 @@ void MainFrame::init_menubar_as_editor()
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
fileMenu->AppendSeparator();
append_shortcut_item(
fileMenu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
fileMenu, Shortcut::SpeedDial, false, _L("Open Speed Dial"), "",
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
append_menu_item(
+133
View File
@@ -87,6 +87,7 @@
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
#endif
#include "AuiPaneLayout.hpp"
#include "GUI_Utils.hpp"
#include "GUI_Factories.hpp"
#include "wxExtensions.hpp"
@@ -6808,6 +6809,14 @@ struct Plater::priv
// GUI elements
AuiMgr m_aui_mgr;
// Live dock panes. `on_close` runs when the user closes one from its close button; `shown` is
// what the owner asked for.
struct DockPane
{
std::function<void()> on_close;
bool shown{true};
};
std::map<wxWindow*, DockPane> m_dock_panes;
wxString m_default_window_layout;
wxPanel* current_panel{ nullptr };
std::vector<wxPanel*> panels;
@@ -6980,6 +6989,11 @@ struct Plater::priv
void update_sidebar(bool force_update = false);
void reset_window_layout();
Sidebar::DockingState get_sidebar_docking_state();
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close);
void remove_dock_pane(wxWindow* window);
void show_dock_pane(wxWindow* window, bool show);
bool dock_pane_visible(const DockPane& dock_pane, const wxAuiPaneInfo& pane) const;
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
@@ -7560,6 +7574,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
panel_3d->SetSizer(panel_sizer);
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& evt) {
const wxAuiPaneInfo* pane = evt.GetPane();
auto it = pane != nullptr ? m_dock_panes.find(pane->window) : m_dock_panes.end();
if (it != m_dock_panes.end()) {
const std::function<void()> on_close = std::move(it->second.on_close);
m_dock_panes.erase(it);
if (on_close)
on_close();
}
evt.Skip();
});
m_default_window_layout = m_aui_mgr.SavePerspective();
{
@@ -8225,6 +8251,14 @@ void Plater::priv::update_sidebar(bool force_update) {
}
}
for (const auto& [window, dock_pane] : m_dock_panes) {
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (pane.IsOk() && pane.IsShown() != dock_pane_visible(dock_pane, pane)) {
pane.Show(!pane.IsShown());
needs_update = true;
}
}
if (needs_update) {
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
m_aui_mgr.Update();
@@ -8234,10 +8268,96 @@ void Plater::priv::update_sidebar(bool force_update) {
void Plater::priv::reset_window_layout()
{
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
// Loading a layout docks and hides every pane it does not list, and the default layout lists no
// dock panes: a floating dock pane is docked again, like the rest of the window.
for (const auto& [window, dock_pane] : m_dock_panes)
if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk())
pane.Show(dock_pane_visible(dock_pane, pane));
sidebar_layout.is_collapsed = false;
update_sidebar(true);
}
bool Plater::priv::dock_pane_visible(const DockPane& dock_pane, const wxAuiPaneInfo& pane) const
{
// A floating pane is a top-level window, so it does not hide with the Plater on other tabs.
return dock_pane.shown && (!pane.IsFloating() || sidebar_layout.show);
}
void Plater::priv::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close)
{
const wxString base_name = wxString::FromUTF8(name);
wxString unique_name = base_name;
for (int i = 2; m_aui_mgr.GetPane(unique_name).IsOk(); ++i)
unique_name = base_name + wxString::Format("#%d", i);
// A restored layout below already holds pixels.
const wxSize pixels = q->FromDIP(size);
wxAuiPaneInfo info;
info.Name(unique_name).Caption(caption).BestSize(pixels).FloatingSize(pixels).DestroyOnClose(true);
if (dock == "left")
info.Left();
else if (dock == "bottom")
info.Bottom();
else
info.Right();
if (dock == "float")
info.Float();
// Put the pane back where it was the last time the window layout was saved with it open.
const std::string saved = aui_pane_layout_entry(wxGetApp().app_config->get("window_layout"), unique_name.utf8_string());
if (!saved.empty()) {
m_aui_mgr.LoadPaneInfo(wxString::FromUTF8(saved), info);
info.Caption(caption).DestroyOnClose(true).Show();
}
// Floating is disabled on Wayland.
if ((m_aui_mgr.GetFlags() & wxAUI_MGR_ALLOW_FLOATING) == 0) {
info.Dock().Floatable(false);
if (info.dock_direction == wxAUI_DOCK_NONE)
info.Right();
}
const DockPane& dock_pane = m_dock_panes[window] = DockPane{std::move(on_close)};
info.Show(dock_pane_visible(dock_pane, info));
m_aui_mgr.AddPane(window, info);
// wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar
// does, for the saved layout.
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
}
evt.Skip();
});
m_aui_mgr.Update();
}
void Plater::priv::remove_dock_pane(wxWindow* window)
{
m_dock_panes.erase(window);
if (m_aui_mgr.DetachPane(window))
m_aui_mgr.Update();
window->Destroy();
}
void Plater::priv::show_dock_pane(wxWindow* window, bool show)
{
const auto it = m_dock_panes.find(window);
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (it == m_dock_panes.end() || !pane.IsOk())
return;
it->second.shown = show;
if (pane.IsShown() == dock_pane_visible(it->second, pane))
return;
pane.Show(!pane.IsShown());
m_aui_mgr.Update();
}
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
if (!sidebar_layout.is_enabled) {
return Sidebar::None;
@@ -17832,6 +17952,19 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
void Plater::reset_window_layout() { p->reset_window_layout(); }
void Plater::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close)
{
p->add_dock_pane(window, name, caption, dock, size, std::move(on_close));
}
void Plater::remove_dock_pane(wxWindow* window) { p->remove_dock_pane(window); }
void Plater::remove_dock_panes()
{
while (!p->m_dock_panes.empty())
p->remove_dock_pane(p->m_dock_panes.begin()->first);
}
void Plater::show_dock_pane(wxWindow* window, bool show) { p->show_dock_pane(window, show); }
//BBS
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
+11
View File
@@ -476,6 +476,17 @@ public:
void reset_window_layout();
// Dock panes sit alongside the sidebar; `window` must be a child of the Plater. `dock` is
// "left", "right", "bottom" or "float", and `size` is in DIPs. A pane closed from its own close
// button is destroyed after on_close runs; remove_dock_pane() destroys it without calling on_close.
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close);
void remove_dock_pane(wxWindow* window);
void show_dock_pane(wxWindow* window, bool show);
// Removes every dock pane without calling on_close, for MainFrame::shutdown() (app exit and a
// language switch), while the Plater and any floating frames still exist.
void remove_dock_panes();
// Called after the Preferences dialog is closed and the program settings are saved.
// Update the UI based on the current preferences.
void update_ui_from_settings();
@@ -1,80 +1,23 @@
#include "PluginWebDialog.hpp"
#include "WebDialog.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include "slic3r/GUI/Widgets/WebHosting.hpp"
#include <wx/event.h>
#include <wx/uri.h>
#include <utility>
namespace Slic3r { namespace GUI {
namespace {
// 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
// references window.wx lazily (at call time) so it never races the backend's
// deferred registration of the "wx" message handler. Guarded against
// double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
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";
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
// relative URLs resolve against the bundled web resources directory.
wxString web_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
// Whether a loaded document is the plugin HTML's own base URL. The web view reports the URL it
// parsed, so any fragment the page navigated to is ignored and the escaping it applies to what the
// resources path holds (a space, a non-ASCII character) is undone first.
bool is_content_url(const wxString& url)
{
return wxURI::Unescape(url.BeforeFirst('#')) == web_base_url();
}
} // namespace
PluginWebDialog::PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style)
WebDialog::WebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style)
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
, m_html(html)
, m_on_message(std::move(on_message))
@@ -84,7 +27,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
{
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
// is swapped in via SetPage once the bootstrap finishes loading.
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
// Paint the window/webview in the themed background so there is no white
// flash before the (transparent) bootstrap page and plugin HTML render.
@@ -96,22 +39,22 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
// create_webview() via add_user_scripts(); nothing to add here.
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_NAVIGATED, &PluginWebDialog::on_navigated, this, wv->GetId());
Bind(wxEVT_WEBVIEW_LOADED, &WebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &WebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_NAVIGATED, &WebDialog::on_navigated, this, wv->GetId());
}
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
Bind(wxEVT_CLOSE_WINDOW, &WebDialog::on_close_window, this);
}
void PluginWebDialog::add_user_scripts()
void WebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
wv->AddUserScript(wxString::FromUTF8(web_hosting::orca_bridge_script()));
}
}
PluginWebDialog::~PluginWebDialog()
WebDialog::~WebDialog()
{
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
// that event is sent from the base ~wxDialog(), after this subclass's members
@@ -121,19 +64,19 @@ PluginWebDialog::~PluginWebDialog()
m_on_destroyed();
}
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
void WebDialog::post_message(WebDialog* dialog, const nlohmann::json& data)
{
if (dialog != nullptr && dialog->is_open())
dialog->push_message(data);
}
void PluginWebDialog::request_close(PluginWebDialog* dialog)
void WebDialog::request_close(WebDialog* dialog)
{
if (dialog != nullptr)
dialog->Close();
}
void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
void WebDialog::destroy_silently(WebDialog* dialog)
{
if (dialog == nullptr)
return;
@@ -147,42 +90,42 @@ void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
dialog->Destroy();
}
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
void WebDialog::on_bootstrap_event(wxWebViewEvent& event)
{
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
// The first bootstrap load (or its error) triggers the swap to plugin HTML.
if (!m_content_loaded)
load_plugin_content();
load_page_html();
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a reload.
// A failed navigation is reported against the page that stayed but never commits. Edge ignores the
// base URL and restores SetPage content itself, so nothing matches there.
else if (is_content_url(event.GetURL())) {
else if (web_hosting::is_content_url(event.GetURL())) {
if (m_own_page_load)
m_own_page_load = false;
else if (loaded && m_content_navigated)
load_plugin_content();
load_page_html();
}
if (loaded)
m_content_navigated = false;
event.Skip();
}
void PluginWebDialog::on_navigated(wxWebViewEvent& event)
void WebDialog::on_navigated(wxWebViewEvent& event)
{
m_content_navigated = is_content_url(event.GetURL());
m_content_navigated = web_hosting::is_content_url(event.GetURL());
event.Skip();
}
void PluginWebDialog::load_plugin_content()
void WebDialog::load_page_html()
{
m_content_loaded = true;
if (wxWebView* wv = browser()) {
m_own_page_load = true;
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
wv->SetPage(wxString::FromUTF8(m_html), web_hosting::content_base_url());
}
}
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
void WebDialog::on_script_message(const nlohmann::json& payload)
{
if (payload.value("channel", std::string()) == "orca") {
const std::string kind = payload.value("kind", std::string());
@@ -202,7 +145,7 @@ void PluginWebDialog::on_script_message(const nlohmann::json& payload)
handle_common_script_command(payload);
}
void PluginWebDialog::push_message(const nlohmann::json& data)
void WebDialog::push_message(const nlohmann::json& data)
{
if (!m_open)
return;
@@ -211,7 +154,7 @@ void PluginWebDialog::push_message(const nlohmann::json& data)
call_web_handler(envelope, wxT("__orcaDispatch"));
}
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
void WebDialog::finish(bool submitted, const nlohmann::json& data)
{
if (!m_open)
return;
@@ -230,7 +173,7 @@ void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
Close();
}
void PluginWebDialog::on_close_window(wxCloseEvent&)
void WebDialog::on_close_window(wxCloseEvent&)
{
if (!m_open) {
// finish() already dispatched submit/close and requested the close.
@@ -250,7 +193,7 @@ void PluginWebDialog::on_close_window(wxCloseEvent&)
Destroy();
}
void PluginWebDialog::fire_submit(const nlohmann::json& data)
void WebDialog::fire_submit(const nlohmann::json& data)
{
if (m_on_submit) {
SubmitHandler cb = std::move(m_on_submit);
@@ -258,7 +201,7 @@ void PluginWebDialog::fire_submit(const nlohmann::json& data)
}
}
void PluginWebDialog::fire_close()
void WebDialog::fire_close()
{
if (m_close_fired)
return;
@@ -1,5 +1,5 @@
#ifndef slic3r_GUI_PluginWebDialog_hpp_
#define slic3r_GUI_PluginWebDialog_hpp_
#ifndef slic3r_GUI_WebDialog_hpp_
#define slic3r_GUI_WebDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
@@ -21,7 +21,7 @@ namespace Slic3r { namespace GUI {
// GIL held; the plugin layer wraps any Python callables in a GIL-safe holder.
//
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
class WebDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
using MessageHandler = std::function<void(const nlohmann::json& data)>;
@@ -32,20 +32,20 @@ public:
// user/JS-initiated close (while the window is alive). on_destroyed runs from
// the destructor on every path and must touch host-side state only (no Python
// / no derived members).
PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~PluginWebDialog() override;
WebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~WebDialog() override;
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
static void request_close(PluginWebDialog* dialog);
static void destroy_for_plugin(PluginWebDialog* dialog);
static void post_message(WebDialog* dialog, const nlohmann::json& data);
static void request_close(WebDialog* dialog);
static void destroy_silently(WebDialog* dialog);
// Push a payload to the page; delivered to handlers registered via
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
@@ -65,7 +65,7 @@ protected:
private:
void on_bootstrap_event(wxWebViewEvent& event);
void on_navigated(wxWebViewEvent& event);
void load_plugin_content();
void load_page_html();
void on_close_window(wxCloseEvent& event);
void fire_submit(const nlohmann::json& data);
void fire_close();
@@ -86,4 +86,4 @@ private:
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_PluginWebDialog_hpp_
#endif // slic3r_GUI_WebDialog_hpp_
+111
View File
@@ -0,0 +1,111 @@
#include "WebPanel.hpp"
#include "GUI_App.hpp"
#include "Widgets/WebHosting.hpp"
#include "Widgets/WebView.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <boost/log/trivial.hpp>
#include <wx/sizer.h>
namespace Slic3r { namespace GUI {
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
: wxPanel(parent, wxID_ANY)
{
SetBackgroundColour(wxGetApp().get_window_default_clr());
auto* sizer = new wxBoxSizer(wxVERTICAL);
SetSizer(sizer);
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
m_browser->SetBackgroundColour(GetBackgroundColour());
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(bridge_script));
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebPanel::on_load_event, this);
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebPanel::on_load_event, this);
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebPanel::on_navigated, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebPanel::on_script_message, this);
m_browser->Bind(EVT_WEBVIEW_RECREATED, &WebPanel::on_webview_recreated, this);
sizer->Add(m_browser, 1, wxEXPAND);
}
void WebPanel::on_load_event(wxWebViewEvent& event)
{
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
if (!m_content_loaded) {
// The first bootstrap load (or its error) triggers the swap to the plugin HTML.
m_content_loaded = true;
load_page_html();
} else if (!web_hosting::is_content_url(event.GetURL())) {
// Not our document (a linked page, a substituted error page), or any document on Edge, which ignores
// the base URL and restores SetPage content on a reload itself; either way it takes the app theme.
if (loaded)
apply_theme();
} else if (m_own_page_load) {
m_own_page_load = false;
// The document-start theme script is fixed at creation, so re-apply the app theme.
if (loaded)
apply_theme();
} else if (loaded && m_content_navigated) {
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a
// reload. A failed navigation is reported against the page that stayed but never commits.
load_page_html();
}
if (loaded)
m_content_navigated = false;
event.Skip();
}
void WebPanel::on_navigated(wxWebViewEvent& event)
{
m_content_navigated = web_hosting::is_content_url(event.GetURL());
event.Skip();
}
void WebPanel::load_page_html()
{
if (const std::optional<std::string> html = page_html()) {
m_own_page_load = true;
m_browser->SetPage(wxString::FromUTF8(*html), web_hosting::content_base_url());
}
}
void WebPanel::on_script_message(wxWebViewEvent& event)
{
const nlohmann::json payload = nlohmann::json::parse(event.GetString().utf8_string(), nullptr, false);
if (!payload.is_object() || payload.value("channel", std::string()) != "orca")
return;
const std::string kind = payload.value("kind", std::string());
if (!on_page_message(kind, payload.contains("data") ? payload["data"] : nlohmann::json()))
BOOST_LOG_TRIVIAL(warning) << "WebPanel ignored a window.orca '" << kind << "' call; this host does not support it";
}
void WebPanel::on_webview_recreated(wxCommandEvent&)
{
SetBackgroundColour(wxGetApp().get_window_default_clr());
m_browser->SetBackgroundColour(GetBackgroundColour());
Refresh();
// Handled without Skip(), so WebView::RecreateAll() does not reload the plugin page.
apply_theme();
}
void WebPanel::apply_theme()
{
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
}
void WebPanel::post_to_page(const std::string& json)
{
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(json)));
}
}} // namespace Slic3r::GUI
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <nlohmann/json.hpp>
#include <wx/panel.h>
#include <wx/webview.h>
#include <optional>
#include <string>
namespace Slic3r { namespace GUI {
// Host-supplied HTML in a web view panel, for any window that embeds or derives from it (today plugin
// Pages tabs and docked panels): bootstrap page and swap, theme and bridge scripts, live re-theming, and
// window.orca messages routed to on_page_message().
class WebPanel : public wxPanel
{
public:
WebPanel(wxWindow* parent, const char* bridge_script);
protected:
wxWebView* browser() const { return m_browser; }
// Delivers an already serialised JSON value to the page's window.orca.onMessage handlers,
// waiting briefly for the bridge while the page is still loading. Main thread only.
void post_to_page(const std::string& json);
// The plugin HTML to show once the bootstrap page has loaded, and again when WebKit reloads it;
// std::nullopt leaves it blank.
virtual std::optional<std::string> page_html() = 0;
// A window.orca message from the page; false for a kind this host does not handle (logged).
virtual bool on_page_message(const std::string& kind, const nlohmann::json& data) = 0;
private:
void on_load_event(wxWebViewEvent& event);
void on_navigated(wxWebViewEvent& event);
void on_script_message(wxWebViewEvent& event);
void on_webview_recreated(wxCommandEvent& event);
void apply_theme();
void load_page_html();
wxWebView* m_browser{nullptr};
bool m_content_loaded{false};
bool m_own_page_load{false}; // a SetPage of the plugin HTML is in flight
bool m_content_navigated{false}; // a navigation to the base URL has committed
};
}} // namespace Slic3r::GUI
+69
View File
@@ -0,0 +1,69 @@
#include "WebHosting.hpp"
#include "slic3r/GUI/GUI.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <wx/uri.h>
namespace Slic3r { namespace GUI { namespace web_hosting {
namespace {
// 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
// references window.wx lazily (at call time) so it never races the backend's
// deferred registration of the "wx" message handler. Guarded against
// double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
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
wxString bootstrap_url()
{
return wxString("file://") + from_u8((boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string());
}
wxString content_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
bool is_content_url(const wxString& url)
{
// The web view reports the URL it parsed, which escapes anything the resources path holds
// (a space, a non-ASCII character), while content_base_url() is the raw path.
return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url();
}
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
}}} // namespace Slic3r::GUI::web_hosting
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <wx/string.h>
namespace Slic3r { namespace GUI { namespace web_hosting {
// Shared by the hosts that show plugin HTML: WebDialog and WebPanel.
// The bundled blank page a plugin web view loads before the plugin HTML is swapped in.
constexpr const char* BOOTSTRAP_PAGE = "web/dialog/WebDialog/blank.html";
// The file:// URL of BOOTSTRAP_PAGE.
wxString bootstrap_url();
// The file:// base URL plugin HTML is loaded against, so relative URLs resolve to bundled resources.
wxString content_base_url();
// Whether `url` is the plugin HTML's base URL, ignoring any fragment. WebKit reports it for the
// injected page, a reload and a failed navigation alike, so a match alone is not a new document.
bool is_content_url(const wxString& url);
// The window.orca bridge of plugin windows and docked panels. Pages tabs ship their own.
const char* orca_bridge_script();
}}} // namespace Slic3r::GUI::web_hosting
+3 -1
View File
@@ -75,6 +75,8 @@ if(document.documentElement)
} // namespace
std::string WebViewHostDialog::theme_apply_script() { return host_theme_apply_js(); }
// 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().
@@ -87,7 +89,7 @@ std::string WebViewHostDialog::theme_user_script()
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
std::string WebViewHostDialog::plugin_defaults_user_script()
std::string WebViewHostDialog::element_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
+4 -2
View File
@@ -49,9 +49,11 @@ public:
const std::string& prelude = {},
const std::string& on_inject = {});
// Shared by modeless Pages tabs and PluginWebDialog.
// Shared by WebPanel hosts and WebDialog.
static std::string theme_user_script();
static std::string plugin_defaults_user_script();
static std::string element_defaults_user_script();
// Re-themes an already-loaded page in place, for web views hosted outside a dialog.
static std::string theme_apply_script();
protected:
wxWebView* browser() const { return m_browser; }
+132 -28
View File
@@ -7,8 +7,10 @@
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/MainFrame.hpp>
#include <slic3r/GUI/MsgDialog.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <slic3r/GUI/DockPanel.hpp>
#include <slic3r/GUI/PluginProgressDialog.hpp>
#include <slic3r/GUI/PluginWebDialog.hpp>
#include <slic3r/GUI/WebDialog.hpp>
#include <slic3r/GUI/NotificationManager.hpp>
#include <nlohmann/json.hpp>
@@ -20,6 +22,7 @@
#include <wx/defs.h>
#include <wx/window.h>
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <future>
@@ -73,7 +76,7 @@ CallablePtr make_holder(py::object obj)
// Adapt a Python callable to a GUI message handler that acquires the GIL and
// swallows/logs exceptions (a raising handler must not escape into wx events).
GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
GUI::WebDialog::MessageHandler make_message_adapter(py::object on_message)
{
CallablePtr holder = make_holder(std::move(on_message));
if (!holder)
@@ -91,7 +94,7 @@ GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
};
}
GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
GUI::WebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
{
CallablePtr holder = make_holder(std::move(on_submit));
if (!holder)
@@ -109,6 +112,25 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
};
}
// The plugin's on_close: fired only on a user/JS-initiated close (not forced teardown), while the
// window is alive. Empty if the plugin passed None.
std::function<void()> make_close_adapter(const CallablePtr& holder)
{
if (!holder)
return nullptr;
return [holder]() {
PythonGILState gil;
if (!gil)
return;
try {
holder->fn();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
PyErr_Clear();
}
};
}
// --------------------------------------------------------------------------
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
// owning plugin so all of a plugin's UI can be torn down on unload.
@@ -350,28 +372,13 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
if (!UiRegistry::instance().is_open(new_id))
return;
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
// teardown), while the dialog is alive. Empty if the plugin passed None.
GUI::PluginWebDialog::CloseHandler on_close;
if (close_holder) {
on_close = [close_holder]() {
PythonGILState gil;
if (!gil)
return;
try {
close_holder->fn();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
PyErr_Clear();
}
};
}
GUI::WebDialog::CloseHandler on_close = make_close_adapter(close_holder);
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* dlg = new GUI::PluginWebDialog(ui_parent(), wxString::FromUTF8(title), html,
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
UiRegistry::instance().bind(new_id, dlg, plugin_key);
if (modal) {
dlg->ShowModal();
@@ -387,14 +394,69 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
return py::cast(UiWindowHandle{new_id});
}
// --------------------------------------------------------------------------
// orca.host.ui.create_dock_panel + UiDockPanel handle
// --------------------------------------------------------------------------
constexpr const char* DOCK_POSITIONS[] = {"left", "right", "bottom", "float"};
struct UiDockPanelHandle
{
int id{0};
};
py::object ui_create_dock_panel(const std::string& html, const std::string& title, int width, int height,
py::object on_message, py::object on_close, const std::string& dock)
{
if (std::find(std::begin(DOCK_POSITIONS), std::end(DOCK_POSITIONS), dock) == std::end(DOCK_POSITIONS))
throw std::invalid_argument("orca.host.ui.create_dock_panel dock must be \"left\", \"right\", \"bottom\" or \"float\"");
auto msg_adapter = make_message_adapter(std::move(on_message));
CallablePtr close_holder = make_holder(std::move(on_close));
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
const int w = width > 0 ? width : 320;
const int h = height > 0 ? height : 480;
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
// Deferred and pre-bound for the same reasons as create_window().
const int new_id = UiRegistry::instance().reserve_id();
UiRegistry::instance().bind(new_id, nullptr, plugin_key);
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, dock, w, h,
msg_adapter = std::move(msg_adapter),
close_holder = std::move(close_holder)]() mutable {
if (!UiRegistry::instance().is_open(new_id))
return;
GUI::Plater* plater = GUI::wxGetApp().plater();
if (plater == nullptr || GUI::wxGetApp().is_closing()) {
UiRegistry::instance().remove(new_id);
return;
}
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
std::move(on_destroyed));
UiRegistry::instance().bind(new_id, panel, plugin_key);
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
wxSize(w, h), [panel]() { panel->fire_close(); });
});
return py::cast(UiDockPanelHandle{new_id});
}
void handle_post(int id, py::object data)
{
if (wxTheApp == nullptr)
return;
json j = py_to_json(data); // GIL held (binding body)
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
GUI::PluginWebDialog::post_message(d, j);
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
panel->push_message(j);
else
GUI::WebDialog::post_message(dynamic_cast<GUI::WebDialog*>(window), j);
});
}
@@ -403,8 +465,23 @@ void handle_close(int id)
if (wxTheApp == nullptr)
return;
GUI::wxGetApp().CallAfter([id]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
GUI::PluginWebDialog::request_close(d);
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
panel->request_close();
else
GUI::WebDialog::request_close(dynamic_cast<GUI::WebDialog*>(window));
});
}
void handle_show(int id, bool show)
{
if (wxTheApp == nullptr)
return;
GUI::wxGetApp().CallAfter([id, show]() {
auto* panel = UiRegistry::instance().get_as<GUI::DockPanel>(id);
GUI::Plater* plater = GUI::wxGetApp().plater();
if (panel != nullptr && plater != nullptr)
plater->show_dock_pane(panel, show);
});
}
@@ -563,6 +640,31 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
py::class_<UiDockPanelHandle>(ui, "UiDockPanel", "Handle to a dockable plugin HTML panel created by create_dock_panel().")
.def_property_readonly("id", [](const UiDockPanelHandle& h) { return h.id; })
.def(
"post", [](const UiDockPanelHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
.def(
"show", [](const UiDockPanelHandle& h) { handle_show(h.id, true); }, "Show the panel again after hide().")
.def(
"hide", [](const UiDockPanelHandle& h) { handle_show(h.id, false); }, "Hide the panel without closing it.")
.def(
"close", [](const UiDockPanelHandle& h) { handle_close(h.id); }, "Close the panel (fires on_close).")
.def(
"is_open", [](const UiDockPanelHandle& h) { return UiRegistry::instance().is_open(h.id); },
"Return True until the panel is closed; a hidden panel is still open.");
ui.def("create_dock_panel", &ui_create_dock_panel, py::arg("html"), py::arg("title") = "OrcaSlicer",
py::arg("width") = 320, py::arg("height") = 480, py::arg("on_message") = py::none(),
py::arg("on_close") = py::none(), py::arg("dock") = "right",
"Open an HTML panel docked beside the 3D view and return a UiDockPanel. dock is \"left\", \"right\", "
"\"bottom\" or \"float\", and width/height are in DIPs; the user can move and resize the panel, and a panel "
"opened again comes back where the window layout was last saved. The panel belongs to the Prepare and "
"Preview tabs. on_message(data) is called on the UI thread when the page posts; window.orca.close() "
"or the panel's close button closes it and calls on_close(). A post() made before the page has "
"loaded can be dropped, so have the page request its first data.");
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
@@ -630,8 +732,10 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
// 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)) {
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
GUI::PluginWebDialog::destroy_for_plugin(dialog);
if (auto* dialog = dynamic_cast<GUI::WebDialog*>(window))
GUI::WebDialog::destroy_silently(dialog);
else if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
panel->destroy_silently();
else if (window != nullptr)
window->Destroy();
}
+19 -79
View File
@@ -1,17 +1,12 @@
#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>
@@ -66,27 +61,11 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
} // namespace
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
, 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);
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
m_cap->set_message_sender([lifetime](const std::string& message) {
@@ -117,89 +96,54 @@ void PluginPage::detach_capability()
m_cap.reset();
}
wxString PluginPage::web_base_url() const
std::optional<std::string> PluginPage::page_html()
{
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + GUI::from_u8(path) + "/";
}
if (m_cap == nullptr)
return std::nullopt;
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());
return m_cap->get_ui();
} 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();
}
detach_capability();
return std::nullopt;
}
void PluginPage::on_new_window(wxWebViewEvent& event)
{
const wxString url = event.GetURL();
if (!url.empty() && m_browser != nullptr)
m_browser->LoadURL(url);
if (!url.empty())
browser()->LoadURL(url);
event.Veto();
}
void PluginPage::on_script_message(wxWebViewEvent& event)
bool PluginPage::on_page_message(const std::string& kind, const nlohmann::json& data)
{
if (kind != "message")
return false;
if (!m_cap)
return;
return true;
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));
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() << "'";
}
return true;
}
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)));
post_to_page(nlohmann::json::accept(message)
? message
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
}
PluginPages::~PluginPages()
@@ -268,10 +212,6 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
}
auto* page = new PluginPage(m_parent, std::move(capability));
if (!page->is_valid()) {
page->Destroy();
return false;
}
if (!icon.empty()) {
try {
+7 -11
View File
@@ -1,5 +1,6 @@
#pragma once
#include <slic3r/GUI/WebPanel.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
@@ -11,14 +12,13 @@
#include <vector>
#include <wx/bitmap.h>
#include <wx/panel.h>
#include <wx/webview.h>
class Notebook;
namespace Slic3r {
class PluginPage : public wxPanel
class PluginPage : public GUI::WebPanel
{
public:
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
@@ -26,24 +26,20 @@ public:
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; }
protected:
std::optional<std::string> page_html() override;
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
private:
void load_plugin_content();
wxString bootstrap_url() const;
wxString web_base_url() const;
void on_new_window(wxWebViewEvent& event);
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;
};
@@ -10,7 +10,31 @@
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <type_traits>
// IPrinterAgent reports failure through its return values and its callers do not catch, so nothing
// the plugin does may leave the trampoline as an exception: a Python raise, a missing override or a
// wrongly typed return is logged and answered with what NetworkAgent returns when no agent is set.
#define ORCA_PY_AGENT_CATCH(name) \
catch (const std::exception& ex) { this->log_failure(#name, ex.what()); } \
catch (...) { this->log_failure(#name, "unknown error"); }
#define ORCA_PY_AGENT_OVERRIDE(ret, name, ...) \
try { \
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, ret, PrinterAgentPluginCapability, name, ##__VA_ARGS__); \
} ORCA_PY_AGENT_CATCH(name) \
return printer_agent_failure<ret>()
namespace Slic3r {
// NetworkAgent's no-agent answer: -1 for a status code, the empty value (false, "", none) otherwise.
template<typename T> T printer_agent_failure()
{
if constexpr (std::is_same_v<T, int>)
return -1;
else if constexpr (!std::is_void_v<T>)
return T{};
}
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
{
public:
@@ -18,294 +42,223 @@ public:
AgentInfo get_agent_info() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
get_agent_info);
ORCA_PY_AGENT_OVERRIDE(AgentInfo, get_agent_info);
}
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
dev_ip, username, password, use_ssl);
ORCA_PY_AGENT_OVERRIDE(int, connect_printer, dev_id, dev_ip, username, password, use_ssl);
}
int disconnect_printer() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
ORCA_PY_AGENT_OVERRIDE(int, disconnect_printer);
}
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
json_str, qos, flag);
ORCA_PY_AGENT_OVERRIDE(int, send_message, dev_id, json_str, qos, flag);
}
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
dev_id, json_str, qos, flag);
ORCA_PY_AGENT_OVERRIDE(int, send_message_to_printer, dev_id, json_str, qos, flag);
}
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_ams_refresh_rfid,
dev_id, tray_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_ams_refresh_rfid, dev_id, tray_id, sequence_id, lan_mode);
}
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_ams_calibrate,
dev_id, ams_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_ams_calibrate, dev_id, ams_id, sequence_id, lan_mode);
}
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_ams_select_tray,
dev_id, tray_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_ams_select_tray, dev_id, tray_id, sequence_id, lan_mode);
}
int command_start_camera(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_start_camera, dev_id);
ORCA_PY_AGENT_OVERRIDE(int, command_start_camera, dev_id);
}
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_xyz_abs,
dev_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_xyz_abs, dev_id, sequence_id, lan_mode);
}
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_auto_leveling,
dev_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_auto_leveling, dev_id, sequence_id, lan_mode);
}
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing,
int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_go_home,
dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_go_home, dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode);
}
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl,
int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_set_bed,
dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_set_bed, dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode);
}
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_set_nozzle,
dev_id, temp, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_set_nozzle, dev_id, temp, sequence_id, lan_mode);
}
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val,
int speed, bool is_core_xy, bool supports_mqtt_axis_control,
int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_axis_control,
dev_id, axis, unit, input_val, speed, is_core_xy, supports_mqtt_axis_control,
sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE(int, command_axis_control, dev_id, axis, unit, input_val, speed,
is_core_xy, supports_mqtt_axis_control, sequence_id, lan_mode);
}
bool start_discovery(bool start, bool sending) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
sending);
ORCA_PY_AGENT_OVERRIDE(bool, start_discovery, start, sending);
}
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
sec_link, detect);
// Passed as a pointer: pybind11 copies a reference argument, so the plugin's writes would be lost.
ORCA_PY_AGENT_OVERRIDE(int, bind_detect, dev_ip, sec_link, &detect);
}
std::string get_user_selected_machine() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
get_user_selected_machine);
ORCA_PY_AGENT_OVERRIDE(std::string, get_user_selected_machine);
}
int set_user_selected_machine(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
set_user_selected_machine, dev_id);
ORCA_PY_AGENT_OVERRIDE(int, set_user_selected_machine, dev_id);
}
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
}
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
params, update_fn, cancel_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_local_print, params, update_fn, cancel_fn);
}
FilamentSyncMode get_filament_sync_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode);
ORCA_PY_AGENT_OVERRIDE(FilamentSyncMode, get_filament_sync_mode);
}
CameraStreamMode get_camera_stream_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, CameraStreamMode, PrinterAgentPluginCapability,
get_camera_stream_mode);
ORCA_PY_AGENT_OVERRIDE(CameraStreamMode, get_camera_stream_mode);
}
std::string get_camera_url() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, std::string, PrinterAgentPluginCapability, get_camera_url);
ORCA_PY_AGENT_OVERRIDE(std::string, get_camera_url);
}
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id, sync_mode);
ORCA_PY_AGENT_OVERRIDE(bool, fetch_filament_info, dev_id, sync_mode);
}
int check_cert() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, check_cert);
ORCA_PY_AGENT_OVERRIDE(int, check_cert);
}
void install_device_cert(std::string dev_id, bool lan_only) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only);
ORCA_PY_AGENT_OVERRIDE(void, install_device_cert, dev_id, lan_only);
}
int ping_bind(std::string ping_code) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
ORCA_PY_AGENT_OVERRIDE(int, ping_bind, ping_code);
}
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
dev_model, sec_link, timezone, improved, update_fn);
ORCA_PY_AGENT_OVERRIDE(int, bind, dev_ip, dev_id, dev_model, sec_link, timezone, improved, update_fn);
}
int unbind(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, unbind, dev_id);
ORCA_PY_AGENT_OVERRIDE(int, unbind, dev_id);
}
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
update_fn, cancel_fn, wait_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_print, params, update_fn, cancel_fn, wait_fn);
}
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
}
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
update_fn, cancel_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_sdcard_print, params, update_fn, cancel_fn);
}
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
file_name, callback);
ORCA_PY_AGENT_OVERRIDE(int, get_hms_snapshot, dev_id, file_name, callback);
}
int set_server_callback(OnServerErrFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_server_callback, fn);
}
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_ssdp_msg_fn, fn);
}
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_printer_connected_fn, fn);
}
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_subscribe_failure_fn, fn);
}
int set_on_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_message_fn, fn);
}
int set_on_user_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_user_message_fn, fn);
}
int set_on_local_connect_fn(OnLocalConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_local_connect_fn, fn);
}
int set_on_local_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_local_message_fn, fn);
}
int set_queue_on_main_fn(QueueOnMainFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_queue_on_main_fn, fn);
}
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
@@ -315,24 +268,33 @@ public:
// instead of failing, mirroring what PYBIND11_OVERRIDE does for the other optional methods.
int request_bind_ticket(std::string* ticket) override
{
ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
return PrinterAgentPluginCapability::request_bind_ticket(ticket);
try {
pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket)
*ticket = result[1].cast<std::string>();
return result[0].cast<int>();
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
return PrinterAgentPluginCapability::request_bind_ticket(ticket);
try {
pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket)
*ticket = result[1].cast<std::string>();
return result[0].cast<int>();
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
} ORCA_PY_AGENT_CATCH(request_bind_ticket)
return printer_agent_failure<int>();
}
private:
void log_failure(const char* operation, const char* error) const
{
BOOST_LOG_TRIVIAL(error) << "Printer agent plugin '" << this->audit_plugin_key() << "': " << operation << " failed: " << error;
}
};
} // namespace Slic3r