mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 07:18:19 +03:00
Compare commits
13 Commits
refactor/p
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd82d54920 | ||
|
|
100937cb0a | ||
|
|
dbb991bf07 | ||
|
|
66d3f3f9c3 | ||
|
|
133c6e77ef | ||
|
|
06520c91a4 | ||
|
|
1a978e9006 | ||
|
|
d6965973a6 | ||
|
|
780b4a14b4 | ||
|
|
40bca911d9 | ||
|
|
2e246341d1 | ||
|
|
9513300835 | ||
|
|
f7caf0db07 |
@@ -465,6 +465,57 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
y = r.ymin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In multi-line mode, clamp y to stay within the text vertical bounds.
|
||||
// This lets the click still land at a valid location if the mouse is slightly
|
||||
// above or below the text.
|
||||
StbTexteditRow r;
|
||||
int n = STB_TEXTEDIT_STRINGLEN(str);
|
||||
int i = 0;
|
||||
float base_y = 0, y_min, y_max;
|
||||
|
||||
// Get the first row to establish y_min and start the iteration
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
if (r.num_chars <= 0)
|
||||
{
|
||||
state->cursor = 0;
|
||||
state->select_start = state->cursor;
|
||||
state->select_end = state->cursor;
|
||||
state->has_preferred_x = 0;
|
||||
return;
|
||||
}
|
||||
y_min = r.ymin;
|
||||
y_max = base_y + r.ymax;
|
||||
i = r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
|
||||
// Walk the remaining rows to find the bottom of the last row
|
||||
while (i < n)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
|
||||
if (r.num_chars <= 0)
|
||||
break;
|
||||
y_max = base_y + r.ymax;
|
||||
i += r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
}
|
||||
|
||||
// If the text ends with a newline, account for the empty trailing line
|
||||
// so the cursor can be placed on it
|
||||
if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, n);
|
||||
y_max = base_y + r.ymax;
|
||||
}
|
||||
|
||||
// Subtract half the last line height to avoid rounding issues when the mouse
|
||||
// is just barely below the last line (keep cursor on the last line, not after the text)
|
||||
y_max -= (r.ymax - r.ymin) * 0.5f;
|
||||
|
||||
if (y < y_min) y = y_min;
|
||||
if (y > y_max) y = y_max;
|
||||
}
|
||||
|
||||
state->cursor = stb_text_locate_coord(str, x, y);
|
||||
state->select_start = state->cursor;
|
||||
@@ -485,6 +536,50 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
y = r.ymin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In multi-line mode, clamp y to stay within the text vertical bounds.
|
||||
// This lets the drag keep working if the mouse goes off the top or bottom of the text.
|
||||
StbTexteditRow r;
|
||||
int n = STB_TEXTEDIT_STRINGLEN(str);
|
||||
int i = 0;
|
||||
float base_y = 0, y_min, y_max;
|
||||
|
||||
// Get the first row to establish y_min and start the iteration
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, 0);
|
||||
if (r.num_chars <= 0)
|
||||
return;
|
||||
y_min = r.ymin;
|
||||
y_max = base_y + r.ymax;
|
||||
i = r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
|
||||
// Walk the remaining rows to find the bottom of the last row
|
||||
while (i < n)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
|
||||
if (r.num_chars <= 0)
|
||||
break;
|
||||
y_max = base_y + r.ymax;
|
||||
i += r.num_chars;
|
||||
base_y += r.baseline_y_delta;
|
||||
}
|
||||
|
||||
// If the text ends with a newline, account for the empty trailing line
|
||||
// so the cursor can be placed on it
|
||||
if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE)
|
||||
{
|
||||
STB_TEXTEDIT_LAYOUTROW(&r, str, n);
|
||||
y_max = base_y + r.ymax;
|
||||
}
|
||||
|
||||
// Subtract half the last line height to avoid rounding issues when the mouse
|
||||
// is just barely below the last line (keep cursor on the last line, not after the text)
|
||||
y_max -= (r.ymax - r.ymin) * 0.5f;
|
||||
|
||||
if (y < y_min) y = y_min;
|
||||
if (y > y_max) y = y_max;
|
||||
}
|
||||
|
||||
if (state->select_start == state->select_end)
|
||||
state->select_start = state->cursor;
|
||||
|
||||
@@ -913,7 +913,7 @@ if (UNIX AND NOT APPLE)
|
||||
target_link_libraries(libslic3r_gui ${X11_LIBRARIES} ${webkit2gtk_LIBRARIES})
|
||||
endif()
|
||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${webkit2gtk_INCLUDE_DIRS})
|
||||
target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES})
|
||||
target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES} ${webkit2gtk_LIBRARIES})
|
||||
|
||||
# Propagate GDK backend detection results as compile definitions so that
|
||||
# LinuxDisplayBackend.cpp can include the right GDK headers.
|
||||
|
||||
@@ -4756,7 +4756,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
deselect_all();
|
||||
}
|
||||
//BBS Select plate in this 3D canvas.
|
||||
else if (evt.LeftUp() && !m_mouse.dragging && m_picking_enabled && !m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !is_layers_editing_enabled())
|
||||
// The left up may come from an ImGui window (e.g. a drag started on the gizmo floating window and released over the bed),
|
||||
// in which case it must not be treated as a click on the plate, otherwise the gizmo would be closed (see deselect_all below).
|
||||
else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_picking_enabled && !m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !is_layers_editing_enabled())
|
||||
{
|
||||
int hover_idx = m_hover_plate_idxs.front();
|
||||
wxGetApp().plater()->select_plate_by_hover_id(hover_idx);
|
||||
|
||||
@@ -1119,6 +1119,10 @@ public:
|
||||
|
||||
void set_mouse_as_dragging() { m_mouse.dragging = true; }
|
||||
bool is_mouse_dragging() const { return m_mouse.dragging; }
|
||||
// True when the current left up event comes from an ImGui window and was not processed by it
|
||||
// (e.g. a drag that started on a gizmo floating window and was released over the 3D scene).
|
||||
// Such a release is the end of an ImGui interaction, not a click on the scene.
|
||||
bool is_mouse_left_up_ignored() const { return m_mouse.ignore_left_up; }
|
||||
|
||||
double get_size_proportional_to_max_bed_size(double factor) const;
|
||||
|
||||
|
||||
@@ -566,8 +566,11 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event)
|
||||
|
||||
void GLGizmoEmboss::on_mouse_change_selection(const wxMouseEvent &mouse_event)
|
||||
{
|
||||
static bool was_dragging = true;
|
||||
if ((mouse_event.LeftUp() || mouse_event.RightUp()) && !was_dragging) {
|
||||
static bool was_dragging = true;
|
||||
// The left up may be the end of a drag that started on the gizmo floating window (e.g. selecting
|
||||
// text in the input field). Such a release is not a click on the scene and must not close the gizmo.
|
||||
// (The flag is only set for left up events, so right up behavior is unchanged.)
|
||||
if ((mouse_event.LeftUp() || mouse_event.RightUp()) && !was_dragging && !m_parent.is_mouse_left_up_ignored()) {
|
||||
// is hovered volume closest hovered?
|
||||
int hovered_idx = m_parent.get_first_hover_volume_idx();
|
||||
if (hovered_idx < 0)
|
||||
|
||||
@@ -2,17 +2,124 @@
|
||||
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <wx/filename.h>
|
||||
|
||||
#ifdef __WIN32__
|
||||
#include <WebView2.h>
|
||||
#include <objbase.h>
|
||||
#ifdef __VISUALC__
|
||||
#include <wrl/event.h>
|
||||
using Microsoft::WRL::Callback;
|
||||
#else
|
||||
#include <wx/msw/private/comptr.h>
|
||||
#include <wx/msw/wrl/event.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
// Signal names for glib
|
||||
#define DOWNLOAD_STARTED_SIGNAL "download-started"
|
||||
#define DOWNLOAD_DESTINATION_SIGNAL "decide-destination"
|
||||
#define DOWNLOAD_FINISHED_SIGNAL "finished"
|
||||
#define DOWNLOAD_FAILED_SIGNAL "failed"
|
||||
#define DOWNLOAD_POLICY_SIGNAL "decide-policy"
|
||||
#endif
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
wxString sanitize_plugin_download_filename(const wxString& suggested)
|
||||
{
|
||||
wxString name = wxFileName(suggested).GetFullName();
|
||||
for (size_t i = 0; i < name.length(); ++i) {
|
||||
if (name[i] < 0x20 || name[i] == '/' || name[i] == '\\' || name[i] == ':' || name[i] == '<' ||
|
||||
name[i] == '>' || name[i] == '"' || name[i] == '|' || name[i] == '?' || name[i] == '*')
|
||||
name[i] = '_';
|
||||
}
|
||||
while (!name.empty() && (name.Last() == ' ' || name.Last() == '.'))
|
||||
name.RemoveLast();
|
||||
|
||||
#ifdef __WIN32__
|
||||
const wxString stem = wxFileName(name).GetName().Upper();
|
||||
if (stem == "CON" || stem == "PRN" || stem == "AUX" || stem == "NUL" ||
|
||||
(stem.length() == 4 && (stem.StartsWith("COM") || stem.StartsWith("LPT")) && stem[3] >= '1' && stem[3] <= '9'))
|
||||
name.Prepend('_');
|
||||
#endif
|
||||
|
||||
return name.empty() ? wxString("download") : name;
|
||||
}
|
||||
|
||||
boost::filesystem::path unique_plugin_download_path(const boost::filesystem::path& dir,
|
||||
const boost::filesystem::path& filename)
|
||||
{
|
||||
boost::filesystem::path candidate = dir / filename;
|
||||
#ifdef __WIN32__
|
||||
const std::wstring stem = filename.stem().wstring();
|
||||
const std::wstring ext = filename.extension().wstring();
|
||||
for (int n = 1; boost::filesystem::exists(candidate); ++n)
|
||||
candidate = dir / (stem + L" (" + std::to_wstring(n) + L")" + ext);
|
||||
#else
|
||||
const std::string stem = filename.stem().string();
|
||||
const std::string ext = filename.extension().string();
|
||||
for (int n = 1; boost::filesystem::exists(candidate); ++n)
|
||||
candidate = dir / (stem + " (" + std::to_string(n) + ")" + ext);
|
||||
#endif
|
||||
return candidate;
|
||||
}
|
||||
|
||||
void notify_plugin_download(PluginDownloadSession& session, nlohmann::json info)
|
||||
{
|
||||
if (session.notified)
|
||||
return;
|
||||
session.notified = true;
|
||||
if (session.callback) {
|
||||
try {
|
||||
session.callback(info);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "PluginWebDialog: download callback failed: " << e.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "PluginWebDialog: download callback failed.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void finish_plugin_download(PluginDownloadSession& session, long long size)
|
||||
{
|
||||
notify_plugin_download(session,
|
||||
{{"filename", std::string(session.resolved_filename.utf8_string())},
|
||||
{"path", std::string(session.resolved_path.utf8_string())},
|
||||
{"mimeType", std::string(session.mime_type.utf8_string())},
|
||||
{"size", size},
|
||||
{"success", true},
|
||||
{"error", ""}});
|
||||
}
|
||||
|
||||
void fail_plugin_download(PluginDownloadSession& session, const std::string& error)
|
||||
{
|
||||
notify_plugin_download(session,
|
||||
{{"filename", std::string(session.resolved_filename.utf8_string())},
|
||||
{"path", ""},
|
||||
{"mimeType", std::string(session.mime_type.utf8_string())},
|
||||
{"size", 0},
|
||||
{"success", false},
|
||||
{"error", error}});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
|
||||
@@ -66,10 +173,10 @@ constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
||||
} 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); }
|
||||
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;
|
||||
@@ -88,19 +195,314 @@ wxString web_base_url()
|
||||
return wxString("file://") + from_u8(dir) + "/";
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
constexpr const char* PLUGIN_DOWNLOAD_REDIRECT_DATA_KEY = "orca-plugin-download-redirect-config";
|
||||
constexpr const char* PLUGIN_DOWNLOAD_STARTED_KEY = "orca-plugin-download-started-connected";
|
||||
constexpr const char* PLUGIN_DOWNLOAD_POLICY_KEY = "orca-plugin-download-policy-connected";
|
||||
|
||||
gboolean on_plugin_download_decide_destination(WebKitDownload* download, const gchar* suggested_filename, gpointer user_data)
|
||||
{
|
||||
auto* session = static_cast<PluginDownloadSession*>(user_data);
|
||||
const wxString safe_name = sanitize_plugin_download_filename(wxString::FromUTF8(suggested_filename ? suggested_filename : ""));
|
||||
if (session->target_dir.empty()) {
|
||||
fail_plugin_download(*session, "Plugin storage directory is unavailable.");
|
||||
webkit_download_cancel(download);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path dir(session->target_dir.utf8_string());
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(dir, ec);
|
||||
if (ec || !fs::is_directory(dir, ec)) {
|
||||
fail_plugin_download(*session, "Could not create the plugin storage directory.");
|
||||
webkit_download_cancel(download);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const fs::path dest = unique_plugin_download_path(dir, fs::path(std::string(safe_name.utf8_string())));
|
||||
GError* uri_error = nullptr;
|
||||
gchar* uri = g_filename_to_uri(dest.string().c_str(), nullptr, &uri_error);
|
||||
if (!uri) {
|
||||
const std::string error = (uri_error && uri_error->message) ? uri_error->message :
|
||||
"Could not build a destination path for the download.";
|
||||
fail_plugin_download(*session, error);
|
||||
webkit_download_cancel(download);
|
||||
if (uri_error)
|
||||
g_error_free(uri_error);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
session->resolved_filename = safe_name;
|
||||
session->resolved_path = wxString::FromUTF8(dest.string());
|
||||
webkit_download_set_destination(download, uri);
|
||||
g_free(uri);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void on_plugin_download_finished(WebKitDownload* download, gpointer user_data)
|
||||
{
|
||||
auto* session = static_cast<PluginDownloadSession*>(user_data);
|
||||
WebKitURIResponse* response = webkit_download_get_response(download);
|
||||
const gchar* mime = response ? webkit_uri_response_get_mime_type(response) : nullptr;
|
||||
session->mime_type = wxString::FromUTF8(mime ? mime : "");
|
||||
finish_plugin_download(*session, webkit_download_get_received_data_length(download));
|
||||
delete session;
|
||||
}
|
||||
|
||||
void on_plugin_download_failed(WebKitDownload*, GError* error, gpointer user_data)
|
||||
{
|
||||
auto* session = static_cast<PluginDownloadSession*>(user_data);
|
||||
fail_plugin_download(*session, (error && error->message) ? error->message : "Download failed.");
|
||||
delete session;
|
||||
}
|
||||
|
||||
void on_plugin_download_started(WebKitWebContext*, WebKitDownload* download, gpointer)
|
||||
{
|
||||
WebKitWebView* source_view = webkit_download_get_web_view(download);
|
||||
if (!source_view)
|
||||
return;
|
||||
|
||||
auto* config = static_cast<PluginDownloadRedirectConfig*>(g_object_get_data(G_OBJECT(source_view), PLUGIN_DOWNLOAD_REDIRECT_DATA_KEY));
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
auto* session = new PluginDownloadSession{config->target_dir, config->callback};
|
||||
g_signal_connect(download, DOWNLOAD_DESTINATION_SIGNAL, G_CALLBACK(on_plugin_download_decide_destination), session);
|
||||
g_signal_connect(download, DOWNLOAD_FINISHED_SIGNAL, G_CALLBACK(on_plugin_download_finished), session);
|
||||
g_signal_connect(download, DOWNLOAD_FAILED_SIGNAL, G_CALLBACK(on_plugin_download_failed), session);
|
||||
}
|
||||
|
||||
gboolean on_plugin_decide_policy(WebKitWebView* web_view,
|
||||
WebKitPolicyDecision* decision,
|
||||
WebKitPolicyDecisionType decision_type,
|
||||
gpointer)
|
||||
{
|
||||
if (decision_type != WEBKIT_POLICY_DECISION_TYPE_RESPONSE ||
|
||||
!g_object_get_data(G_OBJECT(web_view), PLUGIN_DOWNLOAD_REDIRECT_DATA_KEY))
|
||||
return FALSE;
|
||||
|
||||
auto* response = WEBKIT_RESPONSE_POLICY_DECISION(decision);
|
||||
if (webkit_response_policy_decision_is_mime_type_supported(response))
|
||||
return FALSE;
|
||||
|
||||
// Binary responses without Content-Disposition can otherwise navigate the
|
||||
// page instead of becoming WebKitDownload objects.
|
||||
webkit_policy_decision_download(decision);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void enable_plugin_download_redirect(wxWebView* view, wxString target_dir, PluginDownloadCallback callback)
|
||||
{
|
||||
auto* native_view = static_cast<WebKitWebView*>(view->GetNativeBackend());
|
||||
if (!native_view)
|
||||
return;
|
||||
|
||||
auto* config = new PluginDownloadRedirectConfig{std::move(target_dir), std::move(callback)};
|
||||
g_object_set_data_full(G_OBJECT(native_view), PLUGIN_DOWNLOAD_REDIRECT_DATA_KEY, config,
|
||||
[](gpointer data) { delete static_cast<PluginDownloadRedirectConfig*>(data); });
|
||||
|
||||
WebKitWebContext* context = webkit_web_view_get_context(native_view);
|
||||
if (!g_object_get_data(G_OBJECT(context), PLUGIN_DOWNLOAD_STARTED_KEY)) {
|
||||
g_signal_connect(context, DOWNLOAD_STARTED_SIGNAL, G_CALLBACK(on_plugin_download_started), nullptr);
|
||||
g_object_set_data(G_OBJECT(context), PLUGIN_DOWNLOAD_STARTED_KEY, GUINT_TO_POINTER(1));
|
||||
}
|
||||
if (!g_object_get_data(G_OBJECT(native_view), PLUGIN_DOWNLOAD_POLICY_KEY)) {
|
||||
g_signal_connect(native_view, DOWNLOAD_POLICY_SIGNAL, G_CALLBACK(on_plugin_decide_policy), nullptr);
|
||||
g_object_set_data(G_OBJECT(native_view), PLUGIN_DOWNLOAD_POLICY_KEY, GUINT_TO_POINTER(1));
|
||||
}
|
||||
}
|
||||
#elif defined(__WIN32__)
|
||||
|
||||
HRESULT on_plugin_download_state_changed(const std::shared_ptr<PluginDownloadSession>& session,
|
||||
ICoreWebView2DownloadOperation* operation)
|
||||
{
|
||||
try {
|
||||
COREWEBVIEW2_DOWNLOAD_STATE state = COREWEBVIEW2_DOWNLOAD_STATE_IN_PROGRESS;
|
||||
if (!operation || FAILED(operation->get_State(&state)))
|
||||
return S_OK;
|
||||
if (state == COREWEBVIEW2_DOWNLOAD_STATE_IN_PROGRESS)
|
||||
return S_OK;
|
||||
|
||||
INT64 bytes_received = 0;
|
||||
operation->get_BytesReceived(&bytes_received);
|
||||
LPWSTR result_path = nullptr;
|
||||
operation->get_ResultFilePath(&result_path);
|
||||
if (result_path) {
|
||||
session->resolved_path = wxString(result_path);
|
||||
CoTaskMemFree(result_path);
|
||||
}
|
||||
|
||||
if (state == COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED) {
|
||||
finish_plugin_download(*session, bytes_received);
|
||||
} else {
|
||||
COREWEBVIEW2_DOWNLOAD_INTERRUPT_REASON reason =
|
||||
COREWEBVIEW2_DOWNLOAD_INTERRUPT_REASON_NONE;
|
||||
operation->get_InterruptReason(&reason);
|
||||
fail_plugin_download(*session, "Download interrupted (" + std::to_string(static_cast<int>(reason)) + ").");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail_plugin_download(*session, e.what());
|
||||
} catch (...) {
|
||||
fail_plugin_download(*session, "Download failed.");
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void cancel_plugin_download(ICoreWebView2DownloadStartingEventArgs* args, ICoreWebView2DownloadOperation* operation)
|
||||
{
|
||||
if (args)
|
||||
args->put_Cancel(TRUE);
|
||||
if (args)
|
||||
args->put_Handled(TRUE);
|
||||
if (operation)
|
||||
operation->Cancel();
|
||||
}
|
||||
|
||||
HRESULT on_plugin_download_starting(ICoreWebView2DownloadStartingEventArgs* args,
|
||||
const std::shared_ptr<PluginDownloadRedirectConfig>& config)
|
||||
{
|
||||
ICoreWebView2DownloadOperation* operation = nullptr;
|
||||
auto session = std::make_shared<PluginDownloadSession>();
|
||||
session->target_dir = config->target_dir;
|
||||
session->callback = config->callback;
|
||||
|
||||
try {
|
||||
if (!args || FAILED(args->get_DownloadOperation(&operation)))
|
||||
throw std::runtime_error("Could not initialize the download operation.");
|
||||
|
||||
LPWSTR suggested_path = nullptr;
|
||||
args->get_ResultFilePath(&suggested_path);
|
||||
const wxString filename = sanitize_plugin_download_filename(suggested_path ? wxString(suggested_path) : wxString());
|
||||
if (suggested_path)
|
||||
CoTaskMemFree(suggested_path);
|
||||
session->resolved_filename = filename;
|
||||
|
||||
LPWSTR mime_type = nullptr;
|
||||
if (SUCCEEDED(operation->get_MimeType(&mime_type)) && mime_type) {
|
||||
session->mime_type = wxString(mime_type);
|
||||
CoTaskMemFree(mime_type);
|
||||
}
|
||||
|
||||
if (session->target_dir.empty())
|
||||
throw std::runtime_error("Plugin storage directory is unavailable.");
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path dir(std::wstring(session->target_dir.wc_str()));
|
||||
fs::create_directories(dir);
|
||||
if (!fs::is_directory(dir))
|
||||
throw std::runtime_error("Could not create the plugin storage directory.");
|
||||
|
||||
const fs::path destination = unique_plugin_download_path(dir, fs::path(std::wstring(filename.wc_str())));
|
||||
session->resolved_path = wxString(destination.wstring());
|
||||
if (FAILED(args->put_ResultFilePath(destination.wstring().c_str())))
|
||||
throw std::runtime_error("Could not set the plugin download destination.");
|
||||
|
||||
auto state_handler = Callback<ICoreWebView2StateChangedEventHandler>(
|
||||
[session](ICoreWebView2DownloadOperation* download, IUnknown*) {
|
||||
return on_plugin_download_state_changed(session, download);
|
||||
});
|
||||
EventRegistrationToken token{};
|
||||
const HRESULT state_result = operation->add_StateChanged(state_handler.Get(), &token);
|
||||
if (FAILED(state_result))
|
||||
throw std::runtime_error("Could not monitor the plugin download.");
|
||||
} catch (const std::exception& e) {
|
||||
cancel_plugin_download(args, operation);
|
||||
fail_plugin_download(*session, e.what());
|
||||
} catch (...) {
|
||||
cancel_plugin_download(args, operation);
|
||||
fail_plugin_download(*session, "Could not redirect the plugin download.");
|
||||
}
|
||||
|
||||
if (operation)
|
||||
operation->Release();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void attach_plugin_download_redirect(wxWebView* view, const std::shared_ptr<PluginDownloadRedirectConfig>& config)
|
||||
{
|
||||
auto* web_view = static_cast<ICoreWebView2*>(view->GetNativeBackend());
|
||||
if (!web_view)
|
||||
throw std::runtime_error("WebView2 is not ready for download redirection.");
|
||||
|
||||
ICoreWebView2_4* web_view_4 = nullptr;
|
||||
const HRESULT query_result = web_view->QueryInterface(IID_ICoreWebView2_4,
|
||||
reinterpret_cast<void**>(&web_view_4));
|
||||
if (FAILED(query_result) || !web_view_4)
|
||||
throw std::runtime_error("WebView2 download interception is unavailable.");
|
||||
|
||||
auto handler = Callback<ICoreWebView2DownloadStartingEventHandler>(
|
||||
[config](ICoreWebView2*, ICoreWebView2DownloadStartingEventArgs* args) {
|
||||
try {
|
||||
return on_plugin_download_starting(args, config);
|
||||
} catch (...) {
|
||||
// COM event callbacks must not allow exceptions to escape into WebView2.
|
||||
if (args)
|
||||
args->put_Cancel(TRUE);
|
||||
return E_FAIL;
|
||||
}
|
||||
});
|
||||
EventRegistrationToken token{};
|
||||
const HRESULT add_result = web_view_4->add_DownloadStarting(handler.Get(), &token);
|
||||
web_view_4->Release();
|
||||
if (FAILED(add_result))
|
||||
throw std::runtime_error("Could not register WebView2 download interception.");
|
||||
}
|
||||
|
||||
void enable_plugin_download_redirect(wxWebView* view, wxString target_dir, PluginDownloadCallback callback)
|
||||
{
|
||||
if (!view)
|
||||
return;
|
||||
|
||||
auto config = std::make_shared<PluginDownloadRedirectConfig>(PluginDownloadRedirectConfig{std::move(target_dir), std::move(callback)});
|
||||
if (view->GetNativeBackend()) {
|
||||
attach_plugin_download_redirect(view, config);
|
||||
return;
|
||||
}
|
||||
|
||||
// WebView2 creates its native controller asynchronously. The created event is
|
||||
// queued by wxWidgets after GetNativeBackend() becomes usable.
|
||||
view->Bind(wxEVT_WEBVIEW_CREATED, [view, config](wxWebViewEvent&) {
|
||||
try {
|
||||
attach_plugin_download_redirect(view, config);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "PluginWebDialog: " << e.what();
|
||||
}
|
||||
});
|
||||
}
|
||||
#elif defined(__WXMAC__) || defined(__WXOSX__)
|
||||
|
||||
void enable_plugin_download_redirect(wxWebView* view, wxString target_dir, PluginDownloadCallback callback)
|
||||
{
|
||||
if (!view || !view->GetNativeBackend())
|
||||
return;
|
||||
|
||||
WKWebView_setDownloadRedirect(
|
||||
view->GetNativeBackend(),
|
||||
std::move(target_dir),
|
||||
std::move(callback));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
const wxString& title,
|
||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
const wxString& title,
|
||||
const std::string& plugin_key,
|
||||
const std::string& html,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style)
|
||||
const std::string& url,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
DownloadHandler on_download_complete,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style)
|
||||
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
||||
, m_html(html)
|
||||
, m_url(url)
|
||||
, m_plugin_key(plugin_key)
|
||||
, m_on_message(std::move(on_message))
|
||||
, m_on_submit(std::move(on_submit))
|
||||
, m_on_close(std::move(on_close))
|
||||
@@ -122,6 +524,19 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
// 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());
|
||||
|
||||
wxString storage_dir;
|
||||
try {
|
||||
storage_dir = wxString::FromUTF8(PluginManager::instance().get_storage_dir(m_plugin_key));
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "PluginWebDialog: get_storage_dir('" << m_plugin_key
|
||||
<< "') failed, blocking downloads for this dialog: " << e.what();
|
||||
}
|
||||
try {
|
||||
enable_plugin_download_redirect(wv, std::move(storage_dir), std::move(on_download_complete));
|
||||
} catch (const std::runtime_error& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "PluginWebDialog: " << e.what();
|
||||
}
|
||||
}
|
||||
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
|
||||
}
|
||||
@@ -183,14 +598,18 @@ void PluginWebDialog::load_plugin_content()
|
||||
if (m_content_loaded)
|
||||
return;
|
||||
m_content_loaded = true;
|
||||
if (wxWebView* wv = browser())
|
||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
||||
if (wxWebView* wv = browser()) {
|
||||
if (!m_url.empty())
|
||||
wv->LoadURL(wxString::FromUTF8(m_url));
|
||||
else
|
||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
||||
{
|
||||
if (payload.value("channel", std::string()) == "orca") {
|
||||
const std::string kind = payload.value("kind", std::string());
|
||||
const std::string kind = payload.value("kind", std::string());
|
||||
const nlohmann::json data = payload.contains("data") ? payload["data"] : nlohmann::json();
|
||||
if (kind == "message") {
|
||||
if (m_on_message)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -12,8 +14,35 @@
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// A host-owned webview window that renders plugin-supplied raw HTML and bridges
|
||||
// messages to/from the page through a small injected `window.orca` API.
|
||||
using PluginDownloadCallback = std::function<void(const nlohmann::json&)>;
|
||||
|
||||
struct PluginDownloadRedirectConfig
|
||||
{
|
||||
wxString target_dir;
|
||||
PluginDownloadCallback callback;
|
||||
};
|
||||
|
||||
struct PluginDownloadSession
|
||||
{
|
||||
wxString target_dir;
|
||||
PluginDownloadCallback callback;
|
||||
bool notified{false};
|
||||
wxString resolved_filename;
|
||||
wxString resolved_path;
|
||||
wxString mime_type;
|
||||
long long size{0};
|
||||
};
|
||||
|
||||
wxString sanitize_plugin_download_filename(const wxString& suggested);
|
||||
boost::filesystem::path unique_plugin_download_path(const boost::filesystem::path& dir,
|
||||
const boost::filesystem::path& filename);
|
||||
void notify_plugin_download(PluginDownloadSession& session, nlohmann::json info);
|
||||
void finish_plugin_download(PluginDownloadSession& session, long long size);
|
||||
void fail_plugin_download(PluginDownloadSession& session, const std::string& error);
|
||||
|
||||
// A host-owned webview window that renders plugin-supplied raw HTML or loads a
|
||||
// plugin-supplied URL and bridges messages to/from the page through a small
|
||||
// injected `window.orca` API.
|
||||
//
|
||||
// This class is deliberately Python-agnostic: it talks to the plugin layer only
|
||||
// through std::function hooks. Those hooks must NOT capture bare pybind11
|
||||
@@ -26,18 +55,23 @@ class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
|
||||
public:
|
||||
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||
using SubmitHandler = std::function<void(const nlohmann::json& data)>;
|
||||
using DownloadHandler = PluginDownloadCallback;
|
||||
using CloseHandler = std::function<void()>;
|
||||
|
||||
// on_submit fires once for window.orca.submit(). on_close fires only on a
|
||||
// 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).
|
||||
// on_submit fires once for window.orca.submit(). on_download_complete fires once
|
||||
// when a plugin download finishes or fails. on_close fires only on a 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& plugin_key,
|
||||
const std::string& html,
|
||||
const std::string& url,
|
||||
const wxSize& size,
|
||||
MessageHandler on_message,
|
||||
SubmitHandler on_submit,
|
||||
DownloadHandler on_download_complete,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||
@@ -71,6 +105,8 @@ private:
|
||||
void finish(bool submitted, const nlohmann::json& data);
|
||||
|
||||
std::string m_html;
|
||||
std::string m_url;
|
||||
std::string m_plugin_key;
|
||||
bool m_content_loaded{false};
|
||||
bool m_open{true};
|
||||
bool m_close_fired{false};
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
#include "slic3r/GUI/PluginWebDialog.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
@@ -12,6 +14,7 @@ extern double mac_max_scaling_factor();
|
||||
extern void set_miniaturizable(void * window);
|
||||
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
||||
void WKWebView_setTransparentBackground(void * web);
|
||||
void WKWebView_setDownloadRedirect(void* web, wxString target_dir, PluginDownloadCallback callback);
|
||||
void set_tag_when_enter_full_screen(bool isfullscreen);
|
||||
void set_title_colour_after_set_title(void * window);
|
||||
void initGestures(void * view, wxEvtHandler * handler);
|
||||
|
||||
@@ -12,9 +12,240 @@
|
||||
|
||||
#include <objc/runtime.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
@interface MacDarkMode : NSObject {}
|
||||
@end
|
||||
|
||||
using Slic3r::GUI::PluginDownloadCallback;
|
||||
using Slic3r::GUI::PluginDownloadRedirectConfig;
|
||||
using Slic3r::GUI::PluginDownloadSession;
|
||||
using Slic3r::GUI::fail_plugin_download;
|
||||
using Slic3r::GUI::finish_plugin_download;
|
||||
using Slic3r::GUI::sanitize_plugin_download_filename;
|
||||
using Slic3r::GUI::unique_plugin_download_path;
|
||||
|
||||
@interface OrcaWKDownloadContext : NSObject
|
||||
{
|
||||
@public
|
||||
std::shared_ptr<PluginDownloadRedirectConfig> m_config;
|
||||
NSMutableArray* m_download_delegates;
|
||||
}
|
||||
- (id)initWithConfig:(std::shared_ptr<PluginDownloadRedirectConfig>)config;
|
||||
- (void)removeDownloadDelegate:(id)delegate;
|
||||
@end
|
||||
|
||||
API_AVAILABLE(macos(11.3))
|
||||
@interface OrcaWKDownloadDelegate : NSObject <WKDownloadDelegate>
|
||||
{
|
||||
OrcaWKDownloadContext* m_context;
|
||||
std::shared_ptr<PluginDownloadSession> m_session;
|
||||
}
|
||||
- (id)initWithContext:(OrcaWKDownloadContext*)context;
|
||||
@end
|
||||
|
||||
static char g_orca_wk_download_context_key;
|
||||
|
||||
@implementation OrcaWKDownloadContext
|
||||
|
||||
- (id)initWithConfig:(std::shared_ptr<PluginDownloadRedirectConfig>)config
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
m_config = std::move(config);
|
||||
m_download_delegates = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)removeDownloadDelegate:(id)delegate
|
||||
{
|
||||
[m_download_delegates removeObjectIdenticalTo:delegate];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[m_download_delegates release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation OrcaWKDownloadDelegate
|
||||
|
||||
- (id)initWithContext:(OrcaWKDownloadContext*)context
|
||||
{
|
||||
if ((self = [super init]))
|
||||
m_context = context;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)download:(WKDownload*)download
|
||||
decideDestinationUsingResponse:(NSURLResponse*)response
|
||||
suggestedFilename:(NSString*)suggestedFilename
|
||||
completionHandler:(void (^)(NSURL* destinationURL))completionHandler
|
||||
{
|
||||
(void)download;
|
||||
m_session = std::make_shared<PluginDownloadSession>();
|
||||
m_session->resolved_filename = sanitize_plugin_download_filename(suggestedFilename ? wxCFStringRef::AsString(suggestedFilename) : wxString());
|
||||
m_session->mime_type = response.MIMEType ? wxCFStringRef::AsString(response.MIMEType) : wxString();
|
||||
m_session->size = response.expectedContentLength > 0 ? response.expectedContentLength : 0;
|
||||
m_session->callback = m_context->m_config->callback;
|
||||
|
||||
try {
|
||||
if (m_context->m_config->target_dir.empty())
|
||||
throw std::runtime_error("Plugin storage directory is unavailable.");
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path dir(std::string(m_context->m_config->target_dir.utf8_string()));
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(dir, ec);
|
||||
if (ec || !fs::is_directory(dir, ec))
|
||||
throw std::runtime_error("Could not create the plugin storage directory.");
|
||||
|
||||
const fs::path destination = unique_plugin_download_path(
|
||||
dir, fs::path(std::string(m_session->resolved_filename.utf8_string())));
|
||||
m_session->resolved_path = wxString::FromUTF8(destination.string());
|
||||
const wxString destination_url = wxString::FromUTF8(destination.string());
|
||||
if (completionHandler)
|
||||
completionHandler([NSURL fileURLWithPath:wxCFStringRef(destination_url).AsNSString()]);
|
||||
} catch (const std::exception& e) {
|
||||
fail_plugin_download(*m_session, e.what());
|
||||
if (completionHandler)
|
||||
completionHandler(nil);
|
||||
} catch (...) {
|
||||
fail_plugin_download(*m_session, "Could not redirect the plugin download.");
|
||||
if (completionHandler)
|
||||
completionHandler(nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)downloadDidFinish:(WKDownload*)download
|
||||
{
|
||||
(void)download;
|
||||
if (m_session)
|
||||
finish_plugin_download(*m_session, m_session->size);
|
||||
[m_context removeDownloadDelegate:self];
|
||||
}
|
||||
|
||||
- (void)download:(WKDownload*)download didFailWithError:(NSError*)error resumeData:(NSData*)resumeData
|
||||
{
|
||||
(void)download;
|
||||
(void)resumeData;
|
||||
if (m_session) {
|
||||
const wxString message = error ? wxCFStringRef::AsString(error.localizedDescription) : wxString("Download failed.");
|
||||
fail_plugin_download(*m_session, std::string(message.utf8_string()));
|
||||
}
|
||||
[m_context removeDownloadDelegate:self];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static OrcaWKDownloadContext* wk_download_context(WKWebView* webView)
|
||||
{
|
||||
return (OrcaWKDownloadContext*)objc_getAssociatedObject(webView, &g_orca_wk_download_context_key);
|
||||
}
|
||||
|
||||
static void wk_decide_navigation_response(WKWebView* webView,
|
||||
WKNavigationResponse* response,
|
||||
void (^decisionHandler)(WKNavigationResponsePolicy))
|
||||
{
|
||||
OrcaWKDownloadContext* context = wk_download_context(webView);
|
||||
if (!context) {
|
||||
decisionHandler(WKNavigationResponsePolicyAllow);
|
||||
return;
|
||||
}
|
||||
|
||||
bool should_download = !response.canShowMIMEType;
|
||||
if ([response.response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSString* disposition = [(NSHTTPURLResponse*)response.response valueForHTTPHeaderField:@"Content-Disposition"];
|
||||
should_download = should_download ||
|
||||
[disposition rangeOfString:@"attachment" options:NSCaseInsensitiveSearch].location != NSNotFound;
|
||||
}
|
||||
if (@available(macOS 11.3, *)) {
|
||||
decisionHandler(should_download ? WKNavigationResponsePolicyDownload : WKNavigationResponsePolicyAllow);
|
||||
} else {
|
||||
decisionHandler(WKNavigationResponsePolicyAllow);
|
||||
}
|
||||
}
|
||||
|
||||
static void wk_did_become_download(WKWebView* webView, WKNavigationResponse*, WKDownload* download) API_AVAILABLE(macos(11.3))
|
||||
{
|
||||
OrcaWKDownloadContext* context = wk_download_context(webView);
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
OrcaWKDownloadDelegate* delegate = [[OrcaWKDownloadDelegate alloc] initWithContext:context];
|
||||
[context->m_download_delegates addObject:delegate];
|
||||
[download setDelegate:delegate];
|
||||
[delegate release];
|
||||
}
|
||||
|
||||
static void wk_decide_navigation_response_imp(id,
|
||||
SEL,
|
||||
WKWebView* webView,
|
||||
WKNavigationResponse* response,
|
||||
void (^decisionHandler)(WKNavigationResponsePolicy))
|
||||
{
|
||||
wk_decide_navigation_response(webView, response, decisionHandler);
|
||||
}
|
||||
|
||||
static void wk_did_become_download_imp(id,
|
||||
SEL,
|
||||
WKWebView* webView,
|
||||
WKNavigationResponse* response,
|
||||
WKDownload* download) API_AVAILABLE(macos(11.3))
|
||||
{
|
||||
wk_did_become_download(webView, response, download);
|
||||
}
|
||||
|
||||
static void wk_did_become_action_download_imp(id,
|
||||
SEL,
|
||||
WKWebView* webView,
|
||||
WKNavigationAction* action,
|
||||
WKDownload* download) API_AVAILABLE(macos(11.3))
|
||||
{
|
||||
(void)action;
|
||||
OrcaWKDownloadContext* context = wk_download_context(webView);
|
||||
if (!context)
|
||||
return;
|
||||
OrcaWKDownloadDelegate* delegate = [[OrcaWKDownloadDelegate alloc] initWithContext:context];
|
||||
[context->m_download_delegates addObject:delegate];
|
||||
[download setDelegate:delegate];
|
||||
[delegate release];
|
||||
}
|
||||
|
||||
static void install_wk_download_delegate_methods(WKWebView* webView)
|
||||
{
|
||||
id navigationDelegate = webView.navigationDelegate;
|
||||
if (!navigationDelegate)
|
||||
return;
|
||||
|
||||
Class delegateClass = object_getClass(navigationDelegate);
|
||||
class_addMethod(delegateClass,
|
||||
@selector(webView:decidePolicyForNavigationResponse:decisionHandler:),
|
||||
(IMP)wk_decide_navigation_response_imp,
|
||||
"v@:@@@");
|
||||
if (@available(macOS 11.3, *)) {
|
||||
class_addMethod(delegateClass,
|
||||
@selector(webView:navigationResponse:didBecomeDownload:),
|
||||
(IMP)wk_did_become_download_imp,
|
||||
"v@:@@@");
|
||||
class_addMethod(delegateClass,
|
||||
@selector(webView:navigationAction:didBecomeDownload:),
|
||||
(IMP)wk_did_become_action_download_imp,
|
||||
"v@:@@@");
|
||||
}
|
||||
}
|
||||
|
||||
@implementation MacDarkMode
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -83,6 +314,19 @@ void set_title_colour_after_set_title(void * window)
|
||||
}
|
||||
}
|
||||
|
||||
void WKWebView_setDownloadRedirect(void* web, wxString target_dir, PluginDownloadCallback callback)
|
||||
{
|
||||
WKWebView* webView = (WKWebView*)web;
|
||||
if (!webView)
|
||||
return;
|
||||
|
||||
auto config = std::make_shared<PluginDownloadRedirectConfig>(PluginDownloadRedirectConfig{std::move(target_dir), std::move(callback)});
|
||||
OrcaWKDownloadContext* context = [[OrcaWKDownloadContext alloc] initWithConfig:std::move(config)];
|
||||
objc_setAssociatedObject(webView, &g_orca_wk_download_context_key, context, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
install_wk_download_delegate_methods(webView);
|
||||
[context release];
|
||||
}
|
||||
|
||||
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &))
|
||||
{
|
||||
[(WKWebView*)web evaluateJavaScript:wxCFStringRef(script).AsNSString() completionHandler: ^(id result, NSError *error) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -97,6 +98,13 @@ ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key
|
||||
PluginAuditManager::instance().set_current_capability(capability_name);
|
||||
PluginAuditManager::instance().set_audit_mode(mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots.clear();
|
||||
|
||||
// Test and diagnostic callers may use a synthetic plugin key that is not in the manager's
|
||||
// registry. Real plugin callbacks are always registered, so only add the storage root when
|
||||
// the key resolves; an unregistered context remains restricted by the normal audit policy.
|
||||
PluginDescriptor descriptor;
|
||||
if (PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
PluginAuditManager::instance().add_scoped_allowed_root(PluginManager::instance().get_storage_dir(plugin_key));
|
||||
}
|
||||
|
||||
ScopedPluginAuditContext::~ScopedPluginAuditContext()
|
||||
|
||||
@@ -631,7 +631,7 @@ void parse_metadata_rfc822(const std::string& content,
|
||||
bool is_ignored_plugin_directory(const boost::filesystem::path& path)
|
||||
{
|
||||
const std::string name = path.filename().string();
|
||||
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
|
||||
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR || name == PLUGIN_DATA_DIR;
|
||||
}
|
||||
|
||||
bool is_safe_relative_path(const boost::filesystem::path& path)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <vector>
|
||||
|
||||
#define PLUGIN_SUBSCRIBED_DIR "_subscribed"
|
||||
#define PLUGIN_DATA_DIR "plugin_data"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -522,6 +522,38 @@ bool PluginManager::try_get_plugin_descriptor_for_capability(const std::string&
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string PluginManager::get_storage_dir(const std::string& plugin_key) const
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
throw std::runtime_error("The current plugin is not registered");
|
||||
|
||||
const fs::path base_storage_dir = fs::path(get_orca_plugins_dir()) / PLUGIN_DATA_DIR;
|
||||
|
||||
if (!descriptor.is_cloud_plugin()) {
|
||||
const fs::path local_storage_dir = base_storage_dir / plugin_key;
|
||||
fs::create_directories(local_storage_dir);
|
||||
return local_storage_dir.string();
|
||||
}
|
||||
|
||||
auto agent = m_cloud_service.get_cloud_agent();
|
||||
if (!agent)
|
||||
throw std::runtime_error("Cloud plugin storage is unavailable before networking is initialized");
|
||||
|
||||
const std::string user_id = agent->get_user_id();
|
||||
if (user_id.empty())
|
||||
throw std::runtime_error("Cloud plugin storage is unavailable without a logged-in user");
|
||||
|
||||
if (!is_valid_plugin_id(plugin_key))
|
||||
throw std::runtime_error("The current cloud plugin key is not a valid folder name");
|
||||
|
||||
const fs::path cloud_storage_dir = base_storage_dir / PLUGIN_SUBSCRIBED_DIR / user_id / plugin_key;
|
||||
fs::create_directories(cloud_storage_dir);
|
||||
return cloud_storage_dir.string();
|
||||
}
|
||||
|
||||
// ── Capability instances ────────────────────────────────────────────────────────────────────
|
||||
|
||||
std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugin_capabilities(const std::string& plugin_key,
|
||||
|
||||
@@ -143,6 +143,10 @@ public:
|
||||
bool try_get_plugin_descriptor_for_capability(const std::string& capability_name,
|
||||
PluginCapabilityType type,
|
||||
PluginDescriptor& out) const;
|
||||
// Per-plugin storage directory under orca_plugins/plugin_data, created if missing. Throws
|
||||
// std::runtime_error if the plugin is unregistered, the key is invalid, or (cloud plugins)
|
||||
// no user is logged in yet.
|
||||
std::string get_storage_dir(const std::string& plugin_key) const;
|
||||
|
||||
std::vector<std::shared_ptr<PluginCapabilityInterface>> get_plugin_capabilities(
|
||||
const std::string& plugin_key = "", // "" => all plugins
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
#include "PluginHost.hpp"
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "PluginHostUi.hpp"
|
||||
#include <slic3r/plugin/PluginAuditManager.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace host_bindings {
|
||||
void register_plugin(pybind11::module_& host)
|
||||
{
|
||||
auto plugin_host = host.def_submodule("plugin", "Plugin host API");
|
||||
|
||||
plugin_host.def(
|
||||
"storage",
|
||||
[]() -> std::string {
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
if (plugin_key.empty())
|
||||
throw std::runtime_error("plugin.storage() must be called from a plugin callback");
|
||||
|
||||
return PluginManager::instance().get_storage_dir(plugin_key);
|
||||
},
|
||||
"Return the installed folder of the current plugin.");
|
||||
}
|
||||
} // namespace host_bindings
|
||||
|
||||
void PluginHost::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
auto host = module.def_submodule("host", "Host application API");
|
||||
@@ -15,6 +37,7 @@ void PluginHost::RegisterBindings(pybind11::module_& module)
|
||||
host_bindings::register_presets(host);
|
||||
host_bindings::register_model(host);
|
||||
host_bindings::register_app(host);
|
||||
host_bindings::register_plugin(host);
|
||||
|
||||
// UI: native dialogs and interactive HTML windows for plugins.
|
||||
PluginHostUi::RegisterBindings(host);
|
||||
|
||||
@@ -12,5 +12,5 @@ void register_presets(pybind11::module_& host); // PluginHostPresets.cpp
|
||||
void register_model(pybind11::module_& host); // PluginHostModel.cpp
|
||||
void register_app(pybind11::module_& host); // PluginHostApp.cpp
|
||||
void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp
|
||||
|
||||
void register_plugin(pybind11::module_& host); // PluginHost.cpp
|
||||
} // namespace Slic3r::host_bindings
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
#include <slic3r/GUI/MsgDialog.hpp>
|
||||
#include <slic3r/GUI/NotificationManager.hpp>
|
||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||
|
||||
@@ -14,6 +15,7 @@
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <wx/app.h>
|
||||
#include <wx/defs.h>
|
||||
@@ -103,6 +105,51 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
||||
};
|
||||
}
|
||||
|
||||
GUI::PluginWebDialog::DownloadHandler default_download_handler()
|
||||
{
|
||||
return [](const json& data) {
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().plater() == nullptr)
|
||||
return;
|
||||
|
||||
auto *notifications = GUI::wxGetApp().plater()->get_notification_manager();
|
||||
if (notifications == nullptr)
|
||||
return;
|
||||
|
||||
if (data.value("success", false)) {
|
||||
const boost::filesystem::path path(data.value("path", std::string()));
|
||||
if (!path.empty() && !path.parent_path().empty()) {
|
||||
notifications->push_import_finished_notification(
|
||||
path.string(), path.parent_path().string(), false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string error = data.value("error", std::string("Download failed."));
|
||||
notifications->push_notification(
|
||||
GUI::NotificationType::CustomNotification,
|
||||
GUI::NotificationManager::NotificationLevel::WarningNotificationLevel,
|
||||
std::string("Download failed: ") + error);
|
||||
};
|
||||
}
|
||||
|
||||
GUI::PluginWebDialog::DownloadHandler make_download_adapter(py::object on_download_complete)
|
||||
{
|
||||
CallablePtr holder = make_holder(std::move(on_download_complete));
|
||||
if (!holder)
|
||||
return default_download_handler();
|
||||
return [holder](const json& data) {
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return;
|
||||
try {
|
||||
holder->fn(json_to_py(data));
|
||||
} catch (py::error_already_set& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_download_complete 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.
|
||||
@@ -277,19 +324,24 @@ struct UiProgressHandle
|
||||
};
|
||||
|
||||
py::object ui_create_window(const std::string& html, const std::string& title, int width, int height,
|
||||
py::object on_message, py::object on_close, long style, py::object on_submit)
|
||||
py::object on_message, py::object on_close, long style, py::object on_submit,
|
||||
py::object on_download_complete, const std::string& url)
|
||||
{
|
||||
auto msg_adapter = make_message_adapter(std::move(on_message));
|
||||
auto submit_adapter = make_submit_adapter(std::move(on_submit));
|
||||
CallablePtr close_holder = make_holder(std::move(on_close));
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
const int w = width > 0 ? width : 820;
|
||||
const int h = height > 0 ? height : 600;
|
||||
auto msg_adapter = make_message_adapter(std::move(on_message));
|
||||
auto submit_adapter = make_submit_adapter(std::move(on_submit));
|
||||
auto download_adapter = make_download_adapter(std::move(on_download_complete));
|
||||
CallablePtr close_holder = make_holder(std::move(on_close));
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
const int w = width > 0 ? width : 820;
|
||||
const int h = height > 0 ? height : 600;
|
||||
|
||||
if ((style & ~WINDOW_MODAL) != 0)
|
||||
throw std::invalid_argument("unsupported orca.host.ui.create_window style flags");
|
||||
const bool modal = (style & WINDOW_MODAL) != 0;
|
||||
|
||||
if (html.empty() == url.empty())
|
||||
throw std::invalid_argument("orca.host.ui.create_window requires exactly one of html or url");
|
||||
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
@@ -313,9 +365,10 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
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, w, h,
|
||||
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, url, title, w, h,
|
||||
msg_adapter = std::move(msg_adapter),
|
||||
submit_adapter = std::move(submit_adapter),
|
||||
download_adapter = std::move(download_adapter),
|
||||
close_holder = std::move(close_holder), modal]() mutable {
|
||||
// Torn down (plugin unload / app shutdown) before the window materialized.
|
||||
if (!UiRegistry::instance().is_open(new_id))
|
||||
@@ -340,8 +393,9 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
// 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,
|
||||
auto* dlg = new GUI::PluginWebDialog(ui_parent(), wxString::FromUTF8(title), plugin_key, html, url,
|
||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||
std::move(download_adapter),
|
||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||
if (modal) {
|
||||
@@ -487,12 +541,15 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
||||
"is_open", [](const UiWindowHandle& h) { return UiRegistry::instance().is_open(h.id); },
|
||||
"Return True while the window is open.");
|
||||
|
||||
ui.def("create_window", &ui_create_window, py::arg("html"), py::arg("title") = "OrcaSlicer", py::arg("width") = 820,
|
||||
ui.def("create_window", &ui_create_window, py::arg("html") = "", py::arg("title") = "OrcaSlicer", py::arg("width") = 820,
|
||||
py::arg("height") = 600, py::arg("on_message") = py::none(), py::arg("on_close") = py::none(),
|
||||
py::arg("style") = WINDOW_MODELESS, py::arg("on_submit") = py::none(),
|
||||
"Open a persistent HTML window or modal dialog and return a UiWindow. style is WINDOW_MODELESS "
|
||||
"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::arg("on_download_complete") = py::none(),
|
||||
py::arg("url") = "",
|
||||
"Open a persistent HTML window or modal dialog and return a UiWindow. Provide exactly one of html or url. "
|
||||
"style is WINDOW_MODELESS 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; on_download_complete(info) is called on the UI thread when a "
|
||||
"download finishes or fails; offload heavy work to a thread and push results back with window.post().");
|
||||
|
||||
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,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
|
||||
@@ -180,7 +181,18 @@ TEST_CASE("cloud metadata refresh preserves a plugin's stored config", "[PluginC
|
||||
CHECK_FALSE(orphaned.has_error());
|
||||
CHECK(orphaned.get_update_status() == PluginUpdateStatus::Normal);
|
||||
|
||||
// Orphaned is informational only: the local package must remain loadable and usable.
|
||||
// Orphaned is informational only: the local package must remain loadable and usable. Loading
|
||||
// it exercises get_storage_dir(), which for a cloud plugin needs a real logged-in session
|
||||
// (PluginManager::get_storage_dir() throws otherwise) -- the same session a real user would
|
||||
// already hold for a plugin they'd previously subscribed to and downloaded. Give the manager
|
||||
// one via the real agent: OrcaCloudServiceAgent's constructor does no networking, and
|
||||
// set_user_session(..., persist=false) only sets in-memory session state, so this stays a
|
||||
// fast, offline unit test.
|
||||
auto cloud_agent = std::make_shared<OrcaCloudServiceAgent>(get_orca_plugins_dir());
|
||||
cloud_agent->set_user_session(/*token=*/"test-token", /*user_id=*/"test-user", /*username=*/"test-user",
|
||||
/*nickname=*/"", /*avatar=*/"", /*refresh_token=*/"", /*persist=*/false);
|
||||
manager.set_cloud_agent(cloud_agent);
|
||||
|
||||
std::string load_error;
|
||||
manager.load_plugin(uuid, /*skip_deps=*/true);
|
||||
REQUIRE(manager.wait_for_plugin_load(uuid, std::chrono::seconds(120), load_error));
|
||||
|
||||
Reference in New Issue
Block a user