feat(WIP): macos impl

This commit is contained in:
Ian Chua
2026-08-03 14:55:54 +08:00
parent 06520c91a4
commit 133c6e77ef
4 changed files with 375 additions and 176 deletions

View File

@@ -2,6 +2,7 @@
#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"
@@ -10,6 +11,7 @@
#include <boost/log/trivial.hpp>
#include <wx/event.h>
#include <wx/filename.h>
#ifdef __WIN32__
#include <WebView2.h>
@@ -41,6 +43,83 @@ using Microsoft::WRL::Callback;
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
@@ -116,71 +195,19 @@ wxString web_base_url()
return wxString("file://") + from_u8(dir) + "/";
}
using PluginDownloadCallback = std::function<void(const nlohmann::json&)>;
#if defined(__linux__)
std::string sanitize_plugin_download_filename(const gchar* suggested)
{
std::string name = suggested ? suggested : "";
name = boost::filesystem::path(name).filename().string();
if (name.empty() || name == "." || name == "..")
name = "download";
return name;
}
boost::filesystem::path unique_plugin_download_path(const boost::filesystem::path& dir, const std::string& filename)
{
const boost::filesystem::path base = filename;
const std::string stem = base.stem().string();
const std::string ext = base.extension().string();
boost::filesystem::path candidate = dir / filename;
for (int n = 1; boost::filesystem::exists(candidate); ++n)
candidate = dir / (stem + " (" + std::to_string(n) + ")" + ext);
return candidate;
}
struct PluginDownloadRedirectConfig
{
wxString target_dir;
PluginDownloadCallback callback;
};
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";
struct PluginDownloadSession
{
wxString target_dir;
PluginDownloadCallback callback;
bool notified{false};
wxString resolved_filename;
wxString resolved_path;
};
void notify_plugin_download(PluginDownloadSession* session, nlohmann::json info)
{
if (session->notified)
return;
session->notified = true;
if (session->callback)
session->callback(info);
}
void fail_plugin_download(PluginDownloadSession* session, WebKitDownload* download, const std::string& filename, const std::string& error)
{
notify_plugin_download(session,
{{"filename", filename}, {"path", ""}, {"mimeType", ""}, {"size", 0}, {"success", false}, {"error", error}});
webkit_download_cancel(download);
}
gboolean on_plugin_download_decide_destination(WebKitDownload* download, const gchar* suggested_filename, gpointer user_data)
{
auto* session = static_cast<PluginDownloadSession*>(user_data);
const std::string safe_name = sanitize_plugin_download_filename(suggested_filename);
const wxString safe_name = sanitize_plugin_download_filename(wxString::FromUTF8(suggested_filename ? suggested_filename : ""));
if (session->target_dir.empty()) {
fail_plugin_download(session, download, safe_name, "Plugin storage directory is unavailable.");
fail_plugin_download(*session, "Plugin storage directory is unavailable.");
webkit_download_cancel(download);
return TRUE;
}
@@ -189,23 +216,25 @@ gboolean on_plugin_download_decide_destination(WebKitDownload* download, const g
boost::system::error_code ec;
fs::create_directories(dir, ec);
if (ec || !fs::is_directory(dir, ec)) {
fail_plugin_download(session, download, safe_name, "Could not create the plugin storage directory.");
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, safe_name);
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, download, safe_name, error);
fail_plugin_download(*session, error);
webkit_download_cancel(download);
if (uri_error)
g_error_free(uri_error);
return TRUE;
}
session->resolved_filename = wxString::FromUTF8(dest.filename().string());
session->resolved_filename = safe_name;
session->resolved_path = wxString::FromUTF8(dest.string());
webkit_download_set_destination(download, uri);
g_free(uri);
@@ -217,24 +246,15 @@ 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;
notify_plugin_download(session, {{"filename", std::string(session->resolved_filename.utf8_string())},
{"path", std::string(session->resolved_path.utf8_string())},
{"mimeType", mime ? mime : ""},
{"size", webkit_download_get_received_data_length(download)},
{"success", true},
{"error", ""}});
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);
notify_plugin_download(session, {{"filename", std::string(session->resolved_filename.utf8_string())},
{"path", std::string(session->resolved_path.utf8_string())},
{"mimeType", ""},
{"size", 0},
{"success", false},
{"error", (error && error->message) ? error->message : "Download failed."}});
fail_plugin_download(*session, (error && error->message) ? error->message : "Download failed.");
delete session;
}
@@ -295,98 +315,6 @@ void enable_plugin_download_redirect(wxWebView* view, wxString target_dir, Plugi
}
#elif defined(__WIN32__)
std::wstring sanitize_plugin_download_filename(const wchar_t* suggested)
{
std::wstring name;
try {
name = boost::filesystem::path(suggested ? suggested : L"").filename().wstring();
} catch (...) {
name.clear();
}
for (wchar_t& c : name) {
if (c < 0x20 || c == L'<' || c == L'>' || c == L':' || c == L'"' || c == L'/' || c == L'\\' ||
c == L'|' || c == L'?' || c == L'*')
c = L'_';
}
while (!name.empty() && (name.back() == L' ' || name.back() == L'.'))
name.pop_back();
if (name.empty() || name == L"." || name == L"..")
name = L"download";
// Windows reserves these device names even when an extension is present.
const std::wstring stem = boost::filesystem::path(name).stem().wstring();
const std::wstring upper_stem = [&stem] {
std::wstring value = stem;
for (wchar_t& c : value) {
if (c >= L'a' && c <= L'z')
c -= L'a' - L'A';
}
return value;
}();
if (upper_stem == L"CON" || upper_stem == L"PRN" || upper_stem == L"AUX" || upper_stem == L"NUL" ||
(upper_stem.size() == 4 && (upper_stem.substr(0, 3) == L"COM" || upper_stem.substr(0, 3) == L"LPT") &&
upper_stem[3] >= L'1' && upper_stem[3] <= L'9'))
name.insert(0, L"_");
return name;
}
boost::filesystem::path unique_plugin_download_path(const boost::filesystem::path& dir, const std::wstring& filename)
{
const boost::filesystem::path base(filename);
const std::wstring stem = base.stem().wstring();
const std::wstring ext = base.extension().wstring();
boost::filesystem::path candidate = dir / filename;
for (int n = 1; boost::filesystem::exists(candidate); ++n)
candidate = dir / (stem + L" (" + std::to_wstring(n) + L")" + ext);
return candidate;
}
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;
};
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 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}});
}
HRESULT on_plugin_download_state_changed(const std::shared_ptr<PluginDownloadSession>& session,
ICoreWebView2DownloadOperation* operation)
{
@@ -407,13 +335,7 @@ HRESULT on_plugin_download_state_changed(const std::shared_ptr<PluginDownloadSes
}
if (state == COREWEBVIEW2_DOWNLOAD_STATE_COMPLETED) {
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", bytes_received},
{"success", true},
{"error", ""}});
finish_plugin_download(*session, bytes_received);
} else {
COREWEBVIEW2_DOWNLOAD_INTERRUPT_REASON reason =
COREWEBVIEW2_DOWNLOAD_INTERRUPT_REASON_NONE;
@@ -452,10 +374,10 @@ HRESULT on_plugin_download_starting(ICoreWebView2DownloadStartingEventArgs* args
LPWSTR suggested_path = nullptr;
args->get_ResultFilePath(&suggested_path);
const std::wstring filename = sanitize_plugin_download_filename(suggested_path);
const wxString filename = sanitize_plugin_download_filename(suggested_path ? wxString(suggested_path) : wxString());
if (suggested_path)
CoTaskMemFree(suggested_path);
session->resolved_filename = wxString(filename);
session->resolved_filename = filename;
LPWSTR mime_type = nullptr;
if (SUCCEEDED(operation->get_MimeType(&mime_type)) && mime_type) {
@@ -472,7 +394,7 @@ HRESULT on_plugin_download_starting(ICoreWebView2DownloadStartingEventArgs* args
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, filename);
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.");
@@ -550,8 +472,17 @@ void enable_plugin_download_redirect(wxWebView* view, wxString target_dir, Plugi
});
}
#elif defined(__WXMAC__) || defined(__WXOSX__)
void enable_plugin_download_redirect(wxWebView*, wxString, PluginDownloadCallback)
{ throw std::runtime_error("Plugin webview download redirection is not implemented on macOS."); }
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

View File

@@ -3,6 +3,8 @@
#include "Widgets/WebViewHostDialog.hpp"
#include <boost/filesystem/path.hpp>
#include <functional>
#include <optional>
#include <string>
@@ -12,6 +14,32 @@
namespace Slic3r { namespace GUI {
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.
@@ -27,7 +55,7 @@ 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 = std::function<void(const nlohmann::json& data)>;
using DownloadHandler = PluginDownloadCallback;
using CloseHandler = std::function<void()>;
// on_submit fires once for window.orca.submit(). on_download_complete fires once

View File

@@ -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);

View File

@@ -12,9 +12,233 @@
#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
@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;
}
decisionHandler(should_download ? WKNavigationResponsePolicyDownload : WKNavigationResponsePolicyAllow);
}
static void wk_did_become_download(WKWebView* webView, WKNavigationResponse*, WKDownload* download)
{
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)
{
wk_did_become_download(webView, response, download);
}
static void wk_did_become_action_download_imp(id,
SEL,
WKWebView* webView,
WKNavigationAction* action,
WKDownload* download)
{
(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@:@@@");
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 +307,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) {