/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #if JUCE_MAC namespace juce { 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 (defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12) if (([event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagCommand) #else if (([event modifierFlags] & NSDeviceIndependentModifierFlagsMask) == NSCommandKeyMask) #endif { 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 //============================================================================== @interface WebViewTapDetector : NSObject { } - (BOOL) gestureRecognizer: (UIGestureRecognizer*) gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer*) otherGestureRecognizer; @end @implementation WebViewTapDetector - (BOOL) gestureRecognizer: (UIGestureRecognizer*) gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer*) otherGestureRecognizer { juce::ignoreUnused (gestureRecognizer, otherGestureRecognizer); return YES; } @end //============================================================================== @interface WebViewURLChangeDetector : NSObject { juce::WebBrowserComponent* ownerComponent; } - (WebViewURLChangeDetector*) initWithWebBrowserOwner: (juce::WebBrowserComponent*) ownerComponent; - (BOOL) webView: (UIWebView*) webView shouldStartLoadWithRequest: (NSURLRequest*) request navigationType: (UIWebViewNavigationType) navigationType; - (void) webViewDidFinishLoad: (UIWebView*) webView; @end @implementation WebViewURLChangeDetector - (WebViewURLChangeDetector*) initWithWebBrowserOwner: (juce::WebBrowserComponent*) ownerComp { [super init]; ownerComponent = ownerComp; return self; } - (BOOL) webView: (UIWebView*) webView shouldStartLoadWithRequest: (NSURLRequest*) request navigationType: (UIWebViewNavigationType) navigationType { juce::ignoreUnused (webView, navigationType); return ownerComponent->pageAboutToLoad (juce::nsStringToJuce (request.URL.absoluteString)); } - (void) webViewDidFinishLoad: (UIWebView*) webView { ownerComponent->pageFinishedLoading (juce::nsStringToJuce (webView.request.URL.absoluteString)); } @end namespace juce { #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()]; setView (webView); 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)]; setView (webView); tapDetector = [[WebViewTapDetector alloc] init]; urlDetector = [[WebViewURLChangeDetector alloc] initWithWebBrowserOwner: owner]; gestureRecogniser = nil; webView.delegate = urlDetector; #endif } ~Pimpl() { #if JUCE_MAC [webView setPolicyDelegate: nil]; [webView setFrameLoadDelegate: nil]; [webView setUIDelegate: nil]; [clickListener release]; #else webView.delegate = nil; [webView removeGestureRecognizer: gestureRecogniser]; [gestureRecogniser release]; [tapDetector release]; [urlDetector 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 { NSString* urlString = juceStringToNS (url); #if (JUCE_MAC && (defined (__MAC_OS_X_VERSION_MIN_REQUIRED) && defined (__MAC_10_9) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9)) || (JUCE_IOS && (defined (__IPHONE_OS_VERSION_MIN_REQUIRED) && 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)]; } } #if JUCE_MAC [[webView mainFrame] loadRequest: r]; #else [webView loadRequest: r]; #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; id clickListener; #else UIWebView* webView; WebViewTapDetector* tapDetector; WebViewURLChangeDetector* urlDetector; UITapGestureRecognizer* gestureRecogniser; #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