/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12 #define WKWEBVIEW_OPENPANEL_SUPPORTED 1 #endif static NSURL* appendParametersToFileURL (const URL& url, NSURL* fileUrl) { if (@available (macOS 10.10, *)) { const auto parameterNames = url.getParameterNames(); const auto parameterValues = url.getParameterValues(); jassert (parameterNames.size() == parameterValues.size()); if (parameterNames.isEmpty()) return fileUrl; NSUniquePtr components ([[NSURLComponents alloc] initWithURL: fileUrl resolvingAgainstBaseURL: NO]); NSUniquePtr queryItems ([[NSMutableArray alloc] init]); for (int i = 0; i < parameterNames.size(); ++i) [queryItems.get() addObject: [NSURLQueryItem queryItemWithName: juceStringToNS (parameterNames[i]) value: juceStringToNS (parameterValues[i])]]; [components.get() setQueryItems: queryItems.get()]; return [components.get() URL]; } const auto queryString = url.getQueryString(); if (queryString.isNotEmpty()) if (NSString* fileUrlString = [fileUrl absoluteString]) return [NSURL URLWithString: [fileUrlString stringByAppendingString: juceStringToNS (queryString)]]; return fileUrl; } static NSMutableURLRequest* getRequestForURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { NSString* urlString = juceStringToNS (url); if (@available (macOS 10.9, *)) { urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]]; } else { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") urlString = [urlString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; JUCE_END_IGNORE_WARNINGS_GCC_LIKE } if (NSURL* nsURL = [NSURL URLWithString: urlString]) { NSMutableURLRequest* r = [NSMutableURLRequest requestWithURL: nsURL cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 30.0]; if (postData != nullptr && postData->getSize() > 0) { [r setHTTPMethod: nsStringLiteral ("POST")]; [r setHTTPBody: [NSData dataWithBytes: postData->getData() length: postData->getSize()]]; } if (headers != nullptr) { for (int i = 0; i < headers->size(); ++i) { auto headerName = (*headers)[i].upToFirstOccurrenceOf (":", false, false).trim(); auto headerValue = (*headers)[i].fromFirstOccurrenceOf (":", false, false).trim(); [r setValue: juceStringToNS (headerValue) forHTTPHeaderField: juceStringToNS (headerName)]; } } return r; } return nullptr; } #if JUCE_MAC template struct WebViewKeyEquivalentResponder : public ObjCClass { WebViewKeyEquivalentResponder() : ObjCClass ("WebViewKeyEquivalentResponder_") { ObjCClass::addMethod (@selector (performKeyEquivalent:), performKeyEquivalent); ObjCClass::registerClass(); } private: static BOOL performKeyEquivalent (id self, SEL selector, NSEvent* event) { const auto isCommandDown = [event] { const auto modifierFlags = [event modifierFlags]; #if defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12 if (@available (macOS 10.12, *)) return (modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagCommand; #endif JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") return (modifierFlags & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask; JUCE_END_IGNORE_WARNINGS_GCC_LIKE }(); if (isCommandDown) { auto sendAction = [&] (SEL actionSelector) -> BOOL { return [NSApp sendAction: actionSelector to: [[self window] firstResponder] from: self]; }; if ([[event charactersIgnoringModifiers] isEqualToString: @"x"]) return sendAction (@selector (cut:)); if ([[event charactersIgnoringModifiers] isEqualToString: @"c"]) return sendAction (@selector (copy:)); if ([[event charactersIgnoringModifiers] isEqualToString: @"v"]) return sendAction (@selector (paste:)); if ([[event charactersIgnoringModifiers] isEqualToString: @"a"]) return sendAction (@selector (selectAll:)); } return ObjCClass::template sendSuperclassMessage (self, selector, event); } }; JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") struct DownloadClickDetectorClass : public ObjCClass { DownloadClickDetectorClass() : ObjCClass ("JUCEWebClickDetector_") { addIvar ("owner"); addMethod (@selector (webView:decidePolicyForNavigationAction:request:frame:decisionListener:), decidePolicyForNavigationAction); addMethod (@selector (webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener:), decidePolicyForNewWindowAction); addMethod (@selector (webView:didFinishLoadForFrame:), didFinishLoadForFrame); addMethod (@selector (webView:didFailLoadWithError:forFrame:), didFailLoadWithError); addMethod (@selector (webView:didFailProvisionalLoadWithError:forFrame:), didFailLoadWithError); addMethod (@selector (webView:willCloseFrame:), willCloseFrame); addMethod (@selector (webView:runOpenPanelForFileButtonWithResultListener:allowMultipleFiles:), runOpenPanel); registerClass(); } static void setOwner (id self, WebBrowserComponent* owner) { object_setInstanceVariable (self, "owner", owner); } static WebBrowserComponent* getOwner (id self) { return getIvar (self, "owner"); } private: static String getOriginalURL (NSDictionary* actionInformation) { if (NSURL* url = [actionInformation valueForKey: nsStringLiteral ("WebActionOriginalURLKey")]) return nsStringToJuce ([url absoluteString]); return {}; } static void decidePolicyForNavigationAction (id self, SEL, WebView*, NSDictionary* actionInformation, NSURLRequest*, WebFrame*, id listener) { if (getOwner (self)->pageAboutToLoad (getOriginalURL (actionInformation))) [listener use]; else [listener ignore]; } static void decidePolicyForNewWindowAction (id self, SEL, WebView*, NSDictionary* actionInformation, NSURLRequest*, NSString*, id listener) { getOwner (self)->newWindowAttemptingToLoad (getOriginalURL (actionInformation)); [listener ignore]; } static void didFinishLoadForFrame (id self, SEL, WebView* sender, WebFrame* frame) { if ([frame isEqual: [sender mainFrame]]) { NSURL* url = [[[frame dataSource] request] URL]; getOwner (self)->pageFinishedLoading (nsStringToJuce ([url absoluteString])); } } static void didFailLoadWithError (id self, SEL, WebView* sender, NSError* error, WebFrame* frame) { if ([frame isEqual: [sender mainFrame]] && error != nullptr && [error code] != NSURLErrorCancelled) { auto errorString = nsStringToJuce ([error localizedDescription]); bool proceedToErrorPage = getOwner (self)->pageLoadHadNetworkError (errorString); // WebKit doesn't have an internal error page, so make a really simple one ourselves if (proceedToErrorPage) getOwner (self)->goToURL ("data:text/plain;charset=UTF-8," + errorString); } } static void willCloseFrame (id self, SEL, WebView*, WebFrame*) { getOwner (self)->windowCloseRequest(); } static void runOpenPanel (id, SEL, WebView*, id resultListener, BOOL allowMultipleFiles) { struct DeletedFileChooserWrapper : private DeletedAtShutdown { DeletedFileChooserWrapper (std::unique_ptr fc, id rl) : chooser (std::move (fc)), listener (rl) { [listener.get() retain]; } std::unique_ptr chooser; ObjCObjectHandle> listener; }; auto chooser = std::make_unique (TRANS("Select the file you want to upload..."), File::getSpecialLocation (File::userHomeDirectory), "*"); auto* wrapper = new DeletedFileChooserWrapper (std::move (chooser), resultListener); auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles | (allowMultipleFiles ? FileBrowserComponent::canSelectMultipleItems : 0); wrapper->chooser->launchAsync (flags, [wrapper] (const FileChooser&) { for (auto& f : wrapper->chooser->getResults()) [wrapper->listener.get() chooseFilename: juceStringToNS (f.getFullPathName())]; delete wrapper; }); } }; JUCE_END_IGNORE_WARNINGS_GCC_LIKE #endif struct API_AVAILABLE (macos (10.10)) WebViewDelegateClass : public ObjCClass { WebViewDelegateClass() : ObjCClass ("JUCEWebViewDelegate_") { addIvar ("owner"); addMethod (@selector (webView:decidePolicyForNavigationAction:decisionHandler:), decidePolicyForNavigationAction); addMethod (@selector (webView:didFinishNavigation:), didFinishNavigation); addMethod (@selector (webView:didFailNavigation:withError:), didFailNavigation); addMethod (@selector (webView:didFailProvisionalNavigation:withError:), didFailProvisionalNavigation); addMethod (@selector (webViewDidClose:), webViewDidClose); addMethod (@selector (webView:createWebViewWithConfiguration:forNavigationAction: windowFeatures:), createWebView); #if WKWEBVIEW_OPENPANEL_SUPPORTED if (@available (macOS 10.12, *)) addMethod (@selector (webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), runOpenPanel); #endif registerClass(); } static void setOwner (id self, WebBrowserComponent* owner) { object_setInstanceVariable (self, "owner", owner); } static WebBrowserComponent* getOwner (id self) { return getIvar (self, "owner"); } private: static void decidePolicyForNavigationAction (id self, SEL, WKWebView*, WKNavigationAction* navigationAction, void (^decisionHandler)(WKNavigationActionPolicy)) { if (getOwner (self)->pageAboutToLoad (nsStringToJuce ([[[navigationAction request] URL] absoluteString]))) decisionHandler (WKNavigationActionPolicyAllow); else decisionHandler (WKNavigationActionPolicyCancel); } static void didFinishNavigation (id self, SEL, WKWebView* webview, WKNavigation*) { getOwner (self)->pageFinishedLoading (nsStringToJuce ([[webview URL] absoluteString])); } static void displayError (WebBrowserComponent* owner, NSError* error) { if ([error code] != NSURLErrorCancelled) { auto errorString = nsStringToJuce ([error localizedDescription]); bool proceedToErrorPage = owner->pageLoadHadNetworkError (errorString); // WKWebView doesn't have an internal error page, so make a really simple one ourselves if (proceedToErrorPage) owner->goToURL ("data:text/plain;charset=UTF-8," + errorString); } } static void didFailNavigation (id self, SEL, WKWebView*, WKNavigation*, NSError* error) { displayError (getOwner (self), error); } static void didFailProvisionalNavigation (id self, SEL, WKWebView*, WKNavigation*, NSError* error) { displayError (getOwner (self), error); } static void webViewDidClose (id self, SEL, WKWebView*) { getOwner (self)->windowCloseRequest(); } static WKWebView* createWebView (id self, SEL, WKWebView*, WKWebViewConfiguration*, WKNavigationAction* navigationAction, WKWindowFeatures*) { getOwner (self)->newWindowAttemptingToLoad (nsStringToJuce ([[[navigationAction request] URL] absoluteString])); return nil; } #if WKWEBVIEW_OPENPANEL_SUPPORTED API_AVAILABLE (macos (10.12)) static void runOpenPanel (id, SEL, WKWebView*, WKOpenPanelParameters* parameters, WKFrameInfo*, void (^completionHandler)(NSArray*)) { using CompletionHandlerType = decltype (completionHandler); class DeletedFileChooserWrapper : private DeletedAtShutdown { public: DeletedFileChooserWrapper (std::unique_ptr fc, CompletionHandlerType h) : chooser (std::move (fc)), handler (h) { [handler.get() retain]; } ~DeletedFileChooserWrapper() { callHandler (nullptr); } void callHandler (NSArray* urls) { if (handlerCalled) return; handler.get() (urls); handlerCalled = true; } std::unique_ptr chooser; private: ObjCObjectHandle handler; bool handlerCalled = false; }; auto chooser = std::make_unique (TRANS("Select the file you want to upload..."), File::getSpecialLocation (File::userHomeDirectory), "*"); auto* wrapper = new DeletedFileChooserWrapper (std::move (chooser), completionHandler); auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles | ([parameters allowsMultipleSelection] ? FileBrowserComponent::canSelectMultipleItems : 0); #if (defined (MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14) if (@available (macOS 10.14, *)) { if ([parameters allowsDirectories]) flags |= FileBrowserComponent::canSelectDirectories; } #endif wrapper->chooser->launchAsync (flags, [wrapper] (const FileChooser&) { auto results = wrapper->chooser->getResults(); auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) results.size()]; for (auto& f : results) [urls addObject: [NSURL fileURLWithPath: juceStringToNS (f.getFullPathName())]]; wrapper->callHandler (urls); delete wrapper; }); } #endif }; //============================================================================== struct WebViewBase { virtual ~WebViewBase() = default; virtual void goToURL (const String&, const StringArray*, const MemoryBlock*) = 0; virtual void goBack() = 0; virtual void goForward() = 0; virtual void stop() = 0; virtual void refresh() = 0; virtual id getWebView() = 0; }; #if JUCE_MAC JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") class WebViewImpl : public WebViewBase { public: WebViewImpl (WebBrowserComponent* owner) { static WebViewKeyEquivalentResponder webviewClass; webView.reset ([webviewClass.createInstance() initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f) frameName: nsEmptyString() groupName: nsEmptyString()]); static DownloadClickDetectorClass cls; clickListener.reset ([cls.createInstance() init]); DownloadClickDetectorClass::setOwner (clickListener.get(), owner); [webView.get() setPolicyDelegate: clickListener.get()]; [webView.get() setFrameLoadDelegate: clickListener.get()]; [webView.get() setUIDelegate: clickListener.get()]; } ~WebViewImpl() override { [webView.get() setPolicyDelegate: nil]; [webView.get() setFrameLoadDelegate: nil]; [webView.get() setUIDelegate: nil]; } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) override { if (url.trimStart().startsWithIgnoreCase ("javascript:")) { [webView.get() stringByEvaluatingJavaScriptFromString: juceStringToNS (url.fromFirstOccurrenceOf (":", false, false))]; return; } stop(); auto getRequest = [&]() -> NSMutableURLRequest* { if (url.trimStart().startsWithIgnoreCase ("file:")) { auto file = URL (url).getLocalFile(); if (NSURL* nsUrl = [NSURL fileURLWithPath: juceStringToNS (file.getFullPathName())]) return [NSMutableURLRequest requestWithURL: appendParametersToFileURL (url, nsUrl) cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 30.0]; return nullptr; } return getRequestForURL (url, headers, postData); }; if (NSMutableURLRequest* request = getRequest()) [[webView.get() mainFrame] loadRequest: request]; } void goBack() override { [webView.get() goBack]; } void goForward() override { [webView.get() goForward]; } void stop() override { [webView.get() stopLoading: nil]; } void refresh() override { [webView.get() reload: nil]; } id getWebView() override { return webView.get(); } void mouseMove (const MouseEvent&) { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") // WebKit doesn't capture mouse-moves itself, so it seems the only way to make // them work is to push them via this non-public method.. if ([webView.get() respondsToSelector: @selector (_updateMouseoverWithFakeEvent)]) [webView.get() performSelector: @selector (_updateMouseoverWithFakeEvent)]; JUCE_END_IGNORE_WARNINGS_GCC_LIKE } private: ObjCObjectHandle webView; ObjCObjectHandle clickListener; }; JUCE_END_IGNORE_WARNINGS_GCC_LIKE #endif class API_AVAILABLE (macos (10.11)) WKWebViewImpl : public WebViewBase { public: WKWebViewImpl (WebBrowserComponent* owner) { #if JUCE_MAC static WebViewKeyEquivalentResponder webviewClass; webView.reset ([webviewClass.createInstance() initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f)]); #else webView.reset ([[WKWebView alloc] initWithFrame: CGRectMake (0, 0, 100.0f, 100.0f)]); #endif static WebViewDelegateClass cls; webViewDelegate.reset ([cls.createInstance() init]); WebViewDelegateClass::setOwner (webViewDelegate.get(), owner); [webView.get() setNavigationDelegate: webViewDelegate.get()]; [webView.get() setUIDelegate: webViewDelegate.get()]; } ~WKWebViewImpl() override { [webView.get() setNavigationDelegate: nil]; [webView.get() setUIDelegate: nil]; } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) override { auto trimmed = url.trimStart(); if (trimmed.startsWithIgnoreCase ("javascript:")) { [webView.get() evaluateJavaScript: juceStringToNS (url.fromFirstOccurrenceOf (":", false, false)) completionHandler: nil]; return; } stop(); if (trimmed.startsWithIgnoreCase ("file:")) { auto file = URL (url).getLocalFile(); if (NSURL* nsUrl = [NSURL fileURLWithPath: juceStringToNS (file.getFullPathName())]) [webView.get() loadFileURL: appendParametersToFileURL (url, nsUrl) allowingReadAccessToURL: nsUrl]; } else if (NSMutableURLRequest* request = getRequestForURL (url, headers, postData)) { [webView.get() loadRequest: request]; } } void goBack() override { [webView.get() goBack]; } void goForward() override { [webView.get() goForward]; } void stop() override { [webView.get() stopLoading]; } void refresh() override { [webView.get() reload]; } id getWebView() override { return webView.get(); } private: ObjCObjectHandle webView; ObjCObjectHandle webViewDelegate; }; //============================================================================== class WebBrowserComponent::Pimpl #if JUCE_MAC : public NSViewComponent #else : public UIViewComponent #endif { public: Pimpl (WebBrowserComponent* owner) { if (@available (macOS 10.11, *)) webView = std::make_unique (owner); #if JUCE_MAC else webView = std::make_unique (owner); #endif setView (webView->getWebView()); } ~Pimpl() { webView = nullptr; setView (nil); } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { webView->goToURL (url, headers, postData); } void goBack() { webView->goBack(); } void goForward() { webView->goForward(); } void stop() { webView->stop(); } void refresh() { webView->refresh(); } private: std::unique_ptr webView; }; //============================================================================== WebBrowserComponent::WebBrowserComponent (bool unloadWhenHidden) : unloadPageWhenHidden (unloadWhenHidden) { setOpaque (true); browser.reset (new Pimpl (this)); addAndMakeVisible (browser.get()); } WebBrowserComponent::~WebBrowserComponent() = default; //============================================================================== void WebBrowserComponent::goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { lastURL = url; if (headers != nullptr) lastHeaders = *headers; else lastHeaders.clear(); if (postData != nullptr) lastPostData = *postData; else lastPostData.reset(); blankPageShown = false; browser->goToURL (url, headers, postData); } void WebBrowserComponent::stop() { browser->stop(); } void WebBrowserComponent::goBack() { lastURL.clear(); blankPageShown = false; browser->goBack(); } void WebBrowserComponent::goForward() { lastURL.clear(); browser->goForward(); } void WebBrowserComponent::refresh() { browser->refresh(); } //============================================================================== void WebBrowserComponent::paint (Graphics&) { } void WebBrowserComponent::checkWindowAssociation() { if (isShowing()) { reloadLastURL(); if (blankPageShown) goBack(); } else { if (unloadPageWhenHidden && ! blankPageShown) { // when the component becomes invisible, some stuff like flash // carries on playing audio, so we need to force it onto a blank // page to avoid this, (and send it back when it's made visible again). blankPageShown = true; browser->goToURL ("about:blank", nullptr, nullptr); } } } void WebBrowserComponent::reloadLastURL() { if (lastURL.isNotEmpty()) { goToURL (lastURL, &lastHeaders, &lastPostData); lastURL.clear(); } } void WebBrowserComponent::parentHierarchyChanged() { checkWindowAssociation(); } void WebBrowserComponent::resized() { browser->setSize (getWidth(), getHeight()); } void WebBrowserComponent::visibilityChanged() { checkWindowAssociation(); } void WebBrowserComponent::focusGained (FocusChangeType) { } void WebBrowserComponent::clearCookies() { NSHTTPCookieStorage* storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; if (NSArray* cookies = [storage cookies]) { const NSUInteger n = [cookies count]; for (NSUInteger i = 0; i < n; ++i) [storage deleteCookie: [cookies objectAtIndex: i]]; } [[NSUserDefaults standardUserDefaults] synchronize]; } } // namespace juce