/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. 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 (defined (MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_11) \ || (defined (__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0) #define JUCE_USE_WKWEBVIEW 1 #if (defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12) #define WKWEBVIEW_OPENPANEL_SUPPORTED 1 #endif #endif NSMutableURLRequest* getRequestForURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { NSString* urlString = juceStringToNS (url); #if (JUCE_MAC && (defined (MAC_OS_X_VERSION_10_9) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_9)) \ || (JUCE_IOS && (defined (__IPHONE_7_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0)) urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; #else urlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; #endif 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_USE_WKWEBVIEW struct WebViewDelegateClass : public ObjCClass { WebViewDelegateClass() : ObjCClass ("JUCEWebViewDelegate_") { addIvar ("owner"); addMethod (@selector (webView:decidePolicyForNavigationAction:decisionHandler:), decidePolicyForNavigationAction, "v@:@@@"); addMethod (@selector (webView:didFinishNavigation:), didFinishNavigation, "v@:@@"); addMethod (@selector (webView:didFailNavigation:withError:), didFailNavigation, "v@:@@@"); addMethod (@selector (webView:didFailProvisionalNavigation:withError:), didFailProvisionalNavigation, "v@:@@@"); addMethod (@selector (webView:webViewDidClose:), webViewDidClose, "v@:@"); addMethod (@selector (webView:createWebViewWithConfiguration:forNavigationAction: windowFeatures:), createWebView, "@@:@@@@"); #if WKWEBVIEW_OPENPANEL_SUPPORTED addMethod (@selector (webView:runOpenPanelWithParameters: initiatedByFrame:completionHandler:), runOpenPanel, "v@:@@@@"); #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 static void runOpenPanel (id, SEL, WKWebView*, WKOpenPanelParameters* parameters, WKFrameInfo*, void (^completionHandler)(NSArray*)) { #if JUCE_MODAL_LOOPS_PERMITTED FileChooser chooser (TRANS("Select the file you want to upload..."), File::getSpecialLocation (File::userHomeDirectory), "*"); auto flags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles | ([parameters allowsMultipleSelection] ? FileBrowserComponent::canSelectMultipleItems : 0); #if (defined (MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_14) if ([parameters allowsDirectories]) flags |= FileBrowserComponent::canSelectDirectories; #endif if (chooser.showDialog (flags, nullptr)) { auto results = chooser.getResults(); auto urls = [NSMutableArray arrayWithCapacity: (NSUInteger) results.size()]; for (auto& f : results) [urls addObject: [NSURL fileURLWithPath: juceStringToNS (f.getFullPathName())]]; completionHandler (urls); } else { completionHandler (nil); } #else ignoreUnused (parameters, completionHandler); jassertfalse; // Can't use this without modal loops being enabled! #endif } #endif }; //============================================================================== class WebBrowserComponent::Pimpl #if JUCE_MAC : public NSViewComponent #else : public UIViewComponent #endif { public: Pimpl (WebBrowserComponent* owner) { ignoreUnused (owner); WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; #if JUCE_MAC auto frame = NSMakeRect (0, 0, 100.0f, 100.0f); #else auto frame = CGRectMake (0, 0, 100.0f, 100.0f); #endif webView = [[WKWebView alloc] initWithFrame: frame configuration: config]; static WebViewDelegateClass cls; webViewDelegate = [cls.createInstance() init]; WebViewDelegateClass::setOwner (webViewDelegate, owner); [webView setNavigationDelegate: webViewDelegate]; [webView setUIDelegate: webViewDelegate]; setView (webView); } ~Pimpl() { [webView setNavigationDelegate: nil]; [webView setUIDelegate: nil]; [webViewDelegate release]; setView (nil); } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { stop(); if (url.trimStart().startsWithIgnoreCase ("javascript:")) { [webView evaluateJavaScript: juceStringToNS (url.fromFirstOccurrenceOf (":", false, false)) completionHandler: nil]; } else if (NSMutableURLRequest* request = getRequestForURL (url, headers, postData)) { [webView loadRequest: request]; } } void goBack() { [webView goBack]; } void goForward() { [webView goForward]; } void stop() { [webView stopLoading]; } void refresh() { [webView reload]; } private: WKWebView* webView = nil; id webViewDelegate; }; #else #if JUCE_MAC struct WebViewKeyEquivalentResponder : public ObjCClass { WebViewKeyEquivalentResponder() : ObjCClass ("WebViewKeyEquivalentResponder_") { addMethod (@selector (performKeyEquivalent:), performKeyEquivalent, @encode (BOOL), "@:@"); registerClass(); } private: static BOOL performKeyEquivalent (id self, SEL selector, NSEvent* event) { NSResponder* first = [[self window] firstResponder]; if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask) { if ([[event charactersIgnoringModifiers] isEqualToString:@"x"]) return [NSApp sendAction:@selector(cut:) to:first from:self]; if ([[event charactersIgnoringModifiers] isEqualToString:@"c"]) return [NSApp sendAction:@selector(copy:) to:first from:self]; if ([[event charactersIgnoringModifiers] isEqualToString:@"v"]) return [NSApp sendAction:@selector(paste:) to:first from:self]; if ([[event charactersIgnoringModifiers] isEqualToString:@"a"]) return [NSApp sendAction:@selector(selectAll:) to:first from:self]; } objc_super s = { self, [WebView class] }; return ObjCMsgSendSuper (&s, selector, event); } }; struct DownloadClickDetectorClass : public ObjCClass { DownloadClickDetectorClass() : ObjCClass ("JUCEWebClickDetector_") { addIvar ("owner"); addMethod (@selector (webView:decidePolicyForNavigationAction:request:frame:decisionListener:), decidePolicyForNavigationAction, "v@:@@@@@"); addMethod (@selector (webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener:), decidePolicyForNewWindowAction, "v@:@@@@@"); addMethod (@selector (webView:didFinishLoadForFrame:), didFinishLoadForFrame, "v@:@@"); addMethod (@selector (webView:didFailLoadWithError:forFrame:), didFailLoadWithError, "v@:@@@"); addMethod (@selector (webView:didFailProvisionalLoadWithError:forFrame:), didFailLoadWithError, "v@:@@@"); addMethod (@selector (webView:willCloseFrame:), willCloseFrame, "v@:@@"); addMethod (@selector (webView:runOpenPanelForFileButtonWithResultListener:allowMultipleFiles:), runOpenPanel, "v@:@@", @encode (BOOL)); 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) { #if JUCE_MODAL_LOOPS_PERMITTED FileChooser chooser (TRANS("Select the file you want to upload..."), File::getSpecialLocation (File::userHomeDirectory), "*"); if (allowMultipleFiles ? chooser.browseForMultipleFilesToOpen() : chooser.browseForFileToOpen()) { for (auto& f : chooser.getResults()) [resultListener chooseFilename: juceStringToNS (f.getFullPathName())]; } #else ignoreUnused (resultListener, allowMultipleFiles); jassertfalse; // Can't use this without modal loops being enabled! #endif } }; #else struct WebViewDelegateClass : public ObjCClass { WebViewDelegateClass() : ObjCClass ("JUCEWebViewDelegate_") { addIvar ("owner"); addMethod (@selector (gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:), shouldRecognizeSimultaneouslyWithGestureRecognizer, "c@:@@"); addMethod (@selector (webView:shouldStartLoadWithRequest:navigationType:), shouldStartLoadWithRequest, "c@:@@@"); addMethod (@selector (webViewDidFinishLoad:), webViewDidFinishLoad, "v@:@"); registerClass(); } static void setOwner (id self, WebBrowserComponent* owner) { object_setInstanceVariable (self, "owner", owner); } static WebBrowserComponent* getOwner (id self) { return getIvar (self, "owner"); } private: static BOOL shouldRecognizeSimultaneouslyWithGestureRecognizer (id, SEL, UIGestureRecognizer*, UIGestureRecognizer*) { return YES; } static BOOL shouldStartLoadWithRequest (id self, SEL, UIWebView*, NSURLRequest* request, UIWebViewNavigationType) { return getOwner (self)->pageAboutToLoad (nsStringToJuce ([[request URL] absoluteString])); } static void webViewDidFinishLoad (id self, SEL, UIWebView* webView) { getOwner (self)->pageFinishedLoading (nsStringToJuce ([[[webView request] URL] absoluteString])); } }; #endif //============================================================================== class WebBrowserComponent::Pimpl #if JUCE_MAC : public NSViewComponent #else : public UIViewComponent #endif { public: Pimpl (WebBrowserComponent* owner) { #if JUCE_MAC static WebViewKeyEquivalentResponder webviewClass; webView = (WebView*) webviewClass.createInstance(); webView = [webView initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f) frameName: nsEmptyString() groupName: nsEmptyString()]; static DownloadClickDetectorClass cls; clickListener = [cls.createInstance() init]; DownloadClickDetectorClass::setOwner (clickListener, owner); [webView setPolicyDelegate: clickListener]; [webView setFrameLoadDelegate: clickListener]; [webView setUIDelegate: clickListener]; #else webView = [[UIWebView alloc] initWithFrame: CGRectMake (0, 0, 1.0f, 1.0f)]; static WebViewDelegateClass cls; webViewDelegate = [cls.createInstance() init]; WebViewDelegateClass::setOwner (webViewDelegate, owner); [webView setDelegate: webViewDelegate]; #endif setView (webView); } ~Pimpl() { #if JUCE_MAC [webView setPolicyDelegate: nil]; [webView setFrameLoadDelegate: nil]; [webView setUIDelegate: nil]; [clickListener release]; #else [webView setDelegate: nil]; [webViewDelegate release]; #endif setView (nil); } void goToURL (const String& url, const StringArray* headers, const MemoryBlock* postData) { stop(); if (url.trimStart().startsWithIgnoreCase ("javascript:")) { [webView stringByEvaluatingJavaScriptFromString: juceStringToNS (url.fromFirstOccurrenceOf (":", false, false))]; } else if (NSMutableURLRequest* request = getRequestForURL (url, headers, postData)) { #if JUCE_MAC [[webView mainFrame] loadRequest: request]; #else [webView loadRequest: request]; #endif #if JUCE_IOS [webView setScalesPageToFit: YES]; #endif } } void goBack() { [webView goBack]; } void goForward() { [webView goForward]; } #if JUCE_MAC void stop() { [webView stopLoading: nil]; } void refresh() { [webView reload: nil]; } #else void stop() { [webView stopLoading]; } void refresh() { [webView reload]; } #endif void mouseMove (const MouseEvent&) { // 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 respondsToSelector: @selector (_updateMouseoverWithFakeEvent)]) [webView performSelector: @selector (_updateMouseoverWithFakeEvent)]; } private: #if JUCE_MAC WebView* webView = nil; id clickListener; #else UIWebView* webView = nil; id webViewDelegate; #endif }; #endif //============================================================================== WebBrowserComponent::WebBrowserComponent (bool unloadWhenHidden) : unloadPageWhenBrowserIsHidden (unloadWhenHidden) { setOpaque (true); browser.reset (new Pimpl (this)); addAndMakeVisible (browser.get()); } WebBrowserComponent::~WebBrowserComponent() { } //============================================================================== 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 (unloadPageWhenBrowserIsHidden && ! 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