mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			949 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Objective-C
		
	
	
	
	
	
			
		
		
	
	
			949 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Objective-C
		
	
	
	
	
	
| /*
 | |
|  * Copyright (C) 2010-2016 Apple Inc. All rights reserved.
 | |
|  *
 | |
|  * Redistribution and use in source and binary forms, with or without
 | |
|  * modification, are permitted provided that the following conditions
 | |
|  * are met:
 | |
|  * 1. Redistributions of source code must retain the above copyright
 | |
|  *    notice, this list of conditions and the following disclaimer.
 | |
|  * 2. Redistributions in binary form must reproduce the above copyright
 | |
|  *    notice, this list of conditions and the following disclaimer in the
 | |
|  *    documentation and/or other materials provided with the distribution.
 | |
|  *
 | |
|  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 | |
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 | |
|  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 | |
|  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 | |
|  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 | |
|  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 | |
|  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 | |
|  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 | |
|  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 | |
|  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 | |
|  * THE POSSIBILITY OF SUCH DAMAGE.
 | |
|  */
 | |
| 
 | |
| #import "BrowserWindowController.h"
 | |
| 
 | |
| #import "AppDelegate.h"
 | |
| #import <SecurityInterface/SFCertificateTrustPanel.h>
 | |
| #import <WebKit/WKFrameInfo.h>
 | |
| #import <WebKit/WKNavigationActionPrivate.h>
 | |
| #import <WebKit/WKNavigationDelegatePrivate.h>
 | |
| #import <WebKit/WKPreferencesPrivate.h>
 | |
| #import <WebKit/WKUIDelegate.h>
 | |
| #import <WebKit/WKUIDelegatePrivate.h>
 | |
| #import <WebKit/WKWebViewConfigurationPrivate.h>
 | |
| #import <WebKit/WKWebViewPrivate.h>
 | |
| #import <WebKit/WKWebViewPrivateForTesting.h>
 | |
| #import <WebKit/WKWebsiteDataStorePrivate.h>
 | |
| #import <WebKit/WebNSURLExtras.h>
 | |
| #import <WebKit/_WKIconLoadingDelegate.h>
 | |
| #import <WebKit/_WKInspector.h>
 | |
| #import <WebKit/_WKLinkIconParameters.h>
 | |
| #import <WebKit/_WKUserInitiatedAction.h>
 | |
| 
 | |
| static void* keyValueObservingContext = &keyValueObservingContext;
 | |
| 
 | |
| @interface PlaywrightNSTextFinder : NSTextFinder
 | |
| 
 | |
| @property (nonatomic, copy) dispatch_block_t hideInterfaceCallback;
 | |
| 
 | |
| @end
 | |
| 
 | |
| @implementation PlaywrightNSTextFinder
 | |
| 
 | |
| - (void)performAction:(NSTextFinderAction)op
 | |
| {
 | |
|     [super performAction:op];
 | |
| 
 | |
|     if (op == NSTextFinderActionHideFindInterface && _hideInterfaceCallback)
 | |
|         _hideInterfaceCallback();
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| @interface BrowserWindowController () <NSTextFinderBarContainer, WKNavigationDelegate, WKUIDelegate, _WKIconLoadingDelegate, NSSharingServicePickerDelegate, NSSharingServiceDelegate>
 | |
| @end
 | |
| 
 | |
| @implementation BrowserWindowController {
 | |
|     IBOutlet NSProgressIndicator *progressIndicator;
 | |
|     IBOutlet NSButton *reloadButton;
 | |
|     IBOutlet NSButton *lockButton;
 | |
|     IBOutlet NSButton *backButton;
 | |
|     IBOutlet NSButton *forwardButton;
 | |
|     IBOutlet NSButton *share;
 | |
|     IBOutlet NSToolbar *toolbar;
 | |
|     IBOutlet NSTextField *urlText;
 | |
|     IBOutlet NSView *containerView;
 | |
|     IBOutlet NSButton *toggleUseShrinkToFitButton;
 | |
| 
 | |
|     WKWebViewConfiguration *_configuration;
 | |
|     WKWebView *_webView;
 | |
|     BOOL _zoomTextOnly;
 | |
|     BOOL _isPrivateBrowsingWindow;
 | |
|     NSAlert* _alert;
 | |
| 
 | |
|     BOOL _useShrinkToFit;
 | |
| 
 | |
|     PlaywrightNSTextFinder *_textFinder;
 | |
|     NSView *_textFindBarView;
 | |
|     BOOL _findBarVisible;
 | |
| }
 | |
| 
 | |
| - (id)initWithWindow:(NSWindow *)window
 | |
| {
 | |
|     self = [super initWithWindow:window];
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)windowDidLoad
 | |
| {
 | |
| #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 110000
 | |
|     // FIXME: We should probably adopt the default unified style, but we'd need
 | |
|     // somewhere to put the window/page title.
 | |
|     self.window.toolbarStyle = NSWindowToolbarStyleExpanded;
 | |
| 
 | |
|     reloadButton.image = [NSImage imageWithSystemSymbolName:@"arrow.clockwise" accessibilityDescription:@"Reload"];
 | |
|     // FIXME: Should these be localized?
 | |
|     backButton.image = [NSImage imageWithSystemSymbolName:@"chevron.left" accessibilityDescription:@"Go back"];
 | |
|     forwardButton.image = [NSImage imageWithSystemSymbolName:@"chevron.right" accessibilityDescription:@"Go forward"];
 | |
|     share.image = [NSImage imageWithSystemSymbolName:@"square.and.arrow.up" accessibilityDescription:@"Share"];
 | |
|     toggleUseShrinkToFitButton.image = [NSImage imageWithSystemSymbolName:@"arrow.up.left.and.arrow.down.right" accessibilityDescription:@"Use Shrink to fit"];
 | |
| #endif
 | |
|     [share sendActionOn:NSEventMaskLeftMouseDown];
 | |
|     [super windowDidLoad];
 | |
| }
 | |
| 
 | |
| - (IBAction)openLocation:(id)sender
 | |
| {
 | |
|     [[self window] makeFirstResponder:urlText];
 | |
| }
 | |
| 
 | |
| - (NSString *)addProtocolIfNecessary:(NSString *)address
 | |
| {
 | |
|     if ([address rangeOfString:@"://"].length > 0)
 | |
|         return address;
 | |
| 
 | |
|     if ([address hasPrefix:@"data:"])
 | |
|         return address;
 | |
| 
 | |
|     if ([address hasPrefix:@"about:"])
 | |
|         return address;
 | |
| 
 | |
|     return [@"http://" stringByAppendingString:address];
 | |
| }
 | |
| 
 | |
| - (IBAction)share:(id)sender
 | |
| {
 | |
|     NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:@[ self.currentURL ]];
 | |
|     picker.delegate = self;
 | |
|     [picker showRelativeToRect:NSZeroRect ofView:sender preferredEdge:NSRectEdgeMinY];
 | |
| }
 | |
| 
 | |
| - (IBAction)showHideWebView:(id)sender
 | |
| {
 | |
|     self.mainContentView.hidden = !self.mainContentView.isHidden;
 | |
| }
 | |
| 
 | |
| - (CGFloat)pageScaleForMenuItemTag:(NSInteger)tag
 | |
| {
 | |
|     if (tag == 1)
 | |
|         return 1;
 | |
|     if (tag == 2)
 | |
|         return 1.25;
 | |
|     if (tag == 3)
 | |
|         return 1.5;
 | |
|     if (tag == 4)
 | |
|         return 2.0;
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| - (void)awakeFromNib
 | |
| {
 | |
|     _webView = [[WKWebView alloc] initWithFrame:[containerView bounds] configuration:_configuration];
 | |
|     _webView._windowOcclusionDetectionEnabled = NO;
 | |
| 
 | |
|     _webView.allowsMagnification = YES;
 | |
|     _webView.allowsBackForwardNavigationGestures = YES;
 | |
| 
 | |
|     [_webView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
 | |
|     [containerView addSubview:_webView];
 | |
| 
 | |
|     [progressIndicator bind:NSHiddenBinding toObject:_webView withKeyPath:@"loading" options:@{ NSValueTransformerNameBindingOption : NSNegateBooleanTransformerName }];
 | |
|     [progressIndicator bind:NSValueBinding toObject:_webView withKeyPath:@"estimatedProgress" options:nil];
 | |
| 
 | |
|     [_webView addObserver:self forKeyPath:@"title" options:0 context:keyValueObservingContext];
 | |
|     [_webView addObserver:self forKeyPath:@"URL" options:0 context:keyValueObservingContext];
 | |
|     [_webView addObserver:self forKeyPath:@"hasOnlySecureContent" options:0 context:keyValueObservingContext];
 | |
| 
 | |
|     _webView.navigationDelegate = self;
 | |
|     _webView.UIDelegate = self;
 | |
| 
 | |
|     _webView._observedRenderingProgressEvents = _WKRenderingProgressEventFirstLayout
 | |
|         | _WKRenderingProgressEventFirstVisuallyNonEmptyLayout
 | |
|         | _WKRenderingProgressEventFirstPaintWithSignificantArea
 | |
|         | _WKRenderingProgressEventFirstLayoutAfterSuppressedIncrementalRendering
 | |
|         | _WKRenderingProgressEventFirstPaintAfterSuppressedIncrementalRendering;
 | |
| 
 | |
|     _webView.customUserAgent = @"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15";
 | |
| 
 | |
|     _webView._usePlatformFindUI = NO;
 | |
| 
 | |
|     _textFinder = [[PlaywrightNSTextFinder alloc] init];
 | |
|     _textFinder.incrementalSearchingEnabled = YES;
 | |
|     _textFinder.incrementalSearchingShouldDimContentView = NO;
 | |
|     _textFinder.client = _webView;
 | |
|     _textFinder.findBarContainer = self;
 | |
|     _textFinder.hideInterfaceCallback = ^{
 | |
|         [_webView _hideFindUI];
 | |
|     };
 | |
| 
 | |
|     _zoomTextOnly = NO;
 | |
| }
 | |
| 
 | |
| - (instancetype)initWithConfiguration:(WKWebViewConfiguration *)configuration
 | |
| {
 | |
|     if (!(self = [super initWithWindowNibName:@"BrowserWindow"]))
 | |
|         return nil;
 | |
|     _configuration = [configuration copy];
 | |
|     _isPrivateBrowsingWindow = !_configuration.websiteDataStore.isPersistent;
 | |
|     self.window.styleMask &= ~NSWindowStyleMaskFullSizeContentView;
 | |
|     [self.window makeKeyAndOrderFront:nil];
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)dealloc
 | |
| {
 | |
|     [[NSNotificationCenter defaultCenter] removeObserver:self];
 | |
|     [_webView removeObserver:self forKeyPath:@"title"];
 | |
|     [_webView removeObserver:self forKeyPath:@"URL"];
 | |
|     [_webView removeObserver:self forKeyPath:@"hasOnlySecureContent"];
 | |
| 
 | |
|     [progressIndicator unbind:NSHiddenBinding];
 | |
|     [progressIndicator unbind:NSValueBinding];
 | |
| 
 | |
|     [_textFinder release];
 | |
|     [_webView release];
 | |
|     [_configuration release];
 | |
| 
 | |
|     [super dealloc];
 | |
| }
 | |
| 
 | |
| - (IBAction)fetch:(id)sender
 | |
| {
 | |
|     [urlText setStringValue:[self addProtocolIfNecessary:urlText.stringValue]];
 | |
|     NSURL *url = [NSURL _webkit_URLWithUserTypedString:urlText.stringValue];
 | |
|     [_webView loadRequest:[NSURLRequest requestWithURL:url]];
 | |
| }
 | |
| 
 | |
| - (IBAction)setPageScale:(id)sender
 | |
| {
 | |
|     CGFloat scale = [self pageScaleForMenuItemTag:[sender tag]];
 | |
|     [_webView _setPageScale:scale withOrigin:CGPointZero];
 | |
| }
 | |
| 
 | |
| - (CGFloat)viewScaleForMenuItemTag:(NSInteger)tag
 | |
| {
 | |
|     if (tag == 1)
 | |
|         return 1;
 | |
|     if (tag == 2)
 | |
|         return 0.75;
 | |
|     if (tag == 3)
 | |
|         return 0.5;
 | |
|     if (tag == 4)
 | |
|         return 0.25;
 | |
| 
 | |
|     return 1;
 | |
| }
 | |
| 
 | |
| - (IBAction)setViewScale:(id)sender
 | |
| {
 | |
|     CGFloat scale = [self viewScaleForMenuItemTag:[sender tag]];
 | |
|     CGFloat oldScale = [_webView _viewScale];
 | |
| 
 | |
|     if (scale == oldScale)
 | |
|         return;
 | |
| 
 | |
|     [_webView _setLayoutMode:_WKLayoutModeDynamicSizeComputedFromViewScale];
 | |
| 
 | |
|     NSRect oldFrame = self.window.frame;
 | |
|     NSSize newFrameSize = NSMakeSize(oldFrame.size.width * (scale / oldScale), oldFrame.size.height * (scale / oldScale));
 | |
|     [self.window setFrame:NSMakeRect(oldFrame.origin.x, oldFrame.origin.y - (newFrameSize.height - oldFrame.size.height), newFrameSize.width, newFrameSize.height) display:NO animate:NO];
 | |
| 
 | |
|     [_webView _setViewScale:scale];
 | |
| }
 | |
| 
 | |
| static BOOL areEssentiallyEqual(double a, double b)
 | |
| {
 | |
|     double tolerance = 0.001;
 | |
|     return (fabs(a - b) <= tolerance);
 | |
| }
 | |
| 
 | |
| #pragma GCC diagnostic push
 | |
| #pragma GCC diagnostic ignored "-Wdeprecated-implementations"
 | |
| - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
 | |
| #pragma GCC diagnostic pop
 | |
| {
 | |
|     SEL action = menuItem.action;
 | |
| 
 | |
|     if (action == @selector(saveAsPDF:))
 | |
|         return YES;
 | |
|     if (action == @selector(saveAsWebArchive:))
 | |
|         return YES;
 | |
| 
 | |
|     if (action == @selector(zoomIn:))
 | |
|         return [self canZoomIn];
 | |
|     if (action == @selector(zoomOut:))
 | |
|         return [self canZoomOut];
 | |
|     if (action == @selector(resetZoom:))
 | |
|         return [self canResetZoom];
 | |
| 
 | |
|     if (action == @selector(toggleFullWindowWebView:))
 | |
|         [menuItem setTitle:[self webViewFillsWindow] ? @"Inset Web View" : @"Fit Web View to Window"];
 | |
|     else if (action == @selector(toggleZoomMode:))
 | |
|         [menuItem setState:_zoomTextOnly ? NSControlStateValueOn : NSControlStateValueOff];
 | |
|     else if (action == @selector(showHideWebInspector:))
 | |
|         [menuItem setTitle:_webView._inspector.isVisible ? @"Close Web Inspector" : @"Show Web Inspector"];
 | |
| 
 | |
|     if (action == @selector(setPageScale:))
 | |
|         [menuItem setState:areEssentiallyEqual([_webView _pageScale], [self pageScaleForMenuItemTag:[menuItem tag]])];
 | |
| 
 | |
|     if (action == @selector(setViewScale:))
 | |
|         [menuItem setState:areEssentiallyEqual([_webView _viewScale], [self viewScaleForMenuItemTag:[menuItem tag]])];
 | |
| 
 | |
|     return YES;
 | |
| }
 | |
| 
 | |
| - (IBAction)reload:(id)sender
 | |
| {
 | |
|     [_webView reload];
 | |
| }
 | |
| 
 | |
| - (IBAction)showCertificate:(id)sender
 | |
| {
 | |
|     if (_webView.serverTrust)
 | |
|         [[SFCertificateTrustPanel sharedCertificateTrustPanel] beginSheetForWindow:self.window modalDelegate:nil didEndSelector:nil contextInfo:NULL trust:_webView.serverTrust message:@"TLS Certificate Details"];
 | |
| }
 | |
| 
 | |
| - (IBAction)goBack:(id)sender
 | |
| {
 | |
|     [_webView goBack];
 | |
| }
 | |
| 
 | |
| - (IBAction)goForward:(id)sender
 | |
| {
 | |
|     [_webView goForward];
 | |
| }
 | |
| 
 | |
| - (IBAction)toggleFullWindowWebView:(id)sender
 | |
| {
 | |
|     BOOL newFillWindow = ![self webViewFillsWindow];
 | |
|     [self setWebViewFillsWindow:newFillWindow];
 | |
| }
 | |
| 
 | |
| - (BOOL)webViewFillsWindow
 | |
| {
 | |
|     return NSEqualRects(containerView.bounds, self.mainContentView.frame);
 | |
| }
 | |
| 
 | |
| - (void)setWebViewFillsWindow:(BOOL)fillWindow
 | |
| {
 | |
|     if (fillWindow)
 | |
|         [self.mainContentView setFrame:containerView.bounds];
 | |
|     else {
 | |
|         const CGFloat viewInset = 100.0f;
 | |
|         NSRect viewRect = NSInsetRect(containerView.bounds, viewInset, viewInset);
 | |
|         // Make it not vertically centered, to reveal y-flipping bugs.
 | |
|         viewRect = NSOffsetRect(viewRect, 0, -25);
 | |
|         [self.mainContentView setFrame:viewRect];
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (IBAction)toggleZoomMode:(id)sender
 | |
| {
 | |
|     if (_zoomTextOnly) {
 | |
|         _zoomTextOnly = NO;
 | |
|         double currentTextZoom = _webView._textZoomFactor;
 | |
|         _webView._textZoomFactor = 1;
 | |
|         _webView.pageZoom = currentTextZoom;
 | |
|     } else {
 | |
|         _zoomTextOnly = YES;
 | |
|         double currentPageZoom = _webView._pageZoomFactor;
 | |
|         _webView._textZoomFactor = currentPageZoom;
 | |
|         _webView.pageZoom = 1;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (IBAction)resetZoom:(id)sender
 | |
| {
 | |
|     if (![self canResetZoom])
 | |
|         return;
 | |
| 
 | |
|     if (_zoomTextOnly)
 | |
|         _webView._textZoomFactor = 1;
 | |
|     else
 | |
|         _webView.pageZoom = 1;
 | |
| }
 | |
| 
 | |
| - (BOOL)canResetZoom
 | |
| {
 | |
|     return _zoomTextOnly ? (_webView._textZoomFactor != 1) : (_webView.pageZoom != 1);
 | |
| }
 | |
| 
 | |
| - (IBAction)toggleShrinkToFit:(id)sender
 | |
| {
 | |
|     _useShrinkToFit = !_useShrinkToFit;
 | |
|     toggleUseShrinkToFitButton.image = _useShrinkToFit ? [NSImage imageNamed:@"NSExitFullScreenTemplate"] : [NSImage imageNamed:@"NSEnterFullScreenTemplate"];
 | |
|     [_webView _setLayoutMode:_useShrinkToFit ? _WKLayoutModeDynamicSizeComputedFromMinimumDocumentSize : _WKLayoutModeViewSize];
 | |
| }
 | |
| 
 | |
| - (IBAction)showHideWebInspector:(id)sender
 | |
| {
 | |
|     _WKInspector *inspector = _webView._inspector;
 | |
|     if (inspector.isVisible)
 | |
|         [inspector hide];
 | |
|     else
 | |
|         [inspector show];
 | |
| }
 | |
| 
 | |
| - (NSURL *)currentURL
 | |
| {
 | |
|     return _webView.URL;
 | |
| }
 | |
| 
 | |
| - (NSView *)mainContentView
 | |
| {
 | |
|     return _webView;
 | |
| }
 | |
| 
 | |
| - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item
 | |
| {
 | |
|     SEL action = item.action;
 | |
| 
 | |
|     if (action == @selector(goBack:) || action == @selector(goForward:))
 | |
|         return [_webView validateUserInterfaceItem:item];
 | |
| 
 | |
|     return YES;
 | |
| }
 | |
| 
 | |
| - (void)validateToolbar
 | |
| {
 | |
|     [toolbar validateVisibleItems];
 | |
| }
 | |
| 
 | |
| - (BOOL)windowShouldClose:(id)sender
 | |
| {
 | |
|     return YES;
 | |
| }
 | |
| 
 | |
| - (void)windowWillClose:(NSNotification *)notification
 | |
| {
 | |
|     [_webView removeFromSuperview];
 | |
|     _textFinder.hideInterfaceCallback = nil;
 | |
|     [self release];
 | |
| 
 | |
|     // Post two events (don't ask me why!) to spin event loop and drain
 | |
|     // automatically created autorelease pools that will release our window.
 | |
|     // See https://www.mikeash.com/pyblog/more-fun-with-autorelease.html
 | |
|     // for some discussion.
 | |
|     NSEvent* event1 = [NSEvent
 | |
|       otherEventWithType:NSEventTypeApplicationDefined
 | |
|                 location:NSMakePoint(0, 0)
 | |
|            modifierFlags:0
 | |
|                timestamp:[[NSDate date] timeIntervalSince1970]
 | |
|             windowNumber:0
 | |
|                  context:nil
 | |
|                  subtype:0
 | |
|                    data1:0
 | |
|                    data2:0];
 | |
|     [NSApp postEvent:event1 atStart:YES];
 | |
|     NSEvent* event2 = [NSEvent
 | |
|       otherEventWithType:NSEventTypeApplicationDefined
 | |
|                 location:NSMakePoint(0, 0)
 | |
|            modifierFlags:0
 | |
|                timestamp:[[NSDate date] timeIntervalSince1970]
 | |
|             windowNumber:0
 | |
|                  context:nil
 | |
|                  subtype:0
 | |
|                    data1:0
 | |
|                    data2:0];
 | |
|     [NSApp postEvent:event2 atStart:NO];
 | |
| }
 | |
| 
 | |
| - (void)webViewDidClose:(WKWebView *)webView {
 | |
|     [self webView:webView handleJavaScriptDialog:false value:nil];
 | |
|     [self.window close];
 | |
| }
 | |
| 
 | |
| - (void)_webView:(WKWebView *)webView getWindowFrameWithCompletionHandler:(void (^)(CGRect))completionHandler
 | |
| {
 | |
|     completionHandler([self.window frame]);
 | |
| }
 | |
| 
 | |
| #define DefaultMinimumZoomFactor (.5)
 | |
| #define DefaultMaximumZoomFactor (3.0)
 | |
| #define DefaultZoomFactorRatio (1.2)
 | |
| 
 | |
| - (CGFloat)currentZoomFactor
 | |
| {
 | |
|     return _zoomTextOnly ? _webView._textZoomFactor : _webView.pageZoom;
 | |
| }
 | |
| 
 | |
| - (void)setCurrentZoomFactor:(CGFloat)factor
 | |
| {
 | |
|     if (_zoomTextOnly)
 | |
|         _webView._textZoomFactor = factor;
 | |
|     else
 | |
|         _webView.pageZoom = factor;
 | |
| }
 | |
| 
 | |
| - (BOOL)canZoomIn
 | |
| {
 | |
|     return self.currentZoomFactor * DefaultZoomFactorRatio < DefaultMaximumZoomFactor;
 | |
| }
 | |
| 
 | |
| - (void)zoomIn:(id)sender
 | |
| {
 | |
|     if (!self.canZoomIn)
 | |
|         return;
 | |
| 
 | |
|     self.currentZoomFactor *= DefaultZoomFactorRatio;
 | |
| }
 | |
| 
 | |
| - (BOOL)canZoomOut
 | |
| {
 | |
|     return self.currentZoomFactor / DefaultZoomFactorRatio > DefaultMinimumZoomFactor;
 | |
| }
 | |
| 
 | |
| - (void)zoomOut:(id)sender
 | |
| {
 | |
|     if (!self.canZoomIn)
 | |
|         return;
 | |
| 
 | |
|     self.currentZoomFactor /= DefaultZoomFactorRatio;
 | |
| }
 | |
| 
 | |
| - (void)updateTitle:(NSString *)title
 | |
| {
 | |
|     if (!title) {
 | |
|         NSURL *url = _webView.URL;
 | |
|         title = url.lastPathComponent ?: url._web_userVisibleString;
 | |
|     }
 | |
| 
 | |
|     self.window.title = [NSString stringWithFormat:@"🎭 Playwright: %@", title];
 | |
| }
 | |
| 
 | |
| - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
 | |
| {
 | |
|     if (context != keyValueObservingContext || object != _webView)
 | |
|         return;
 | |
| 
 | |
|     if ([keyPath isEqualToString:@"title"])
 | |
|         [self updateTitle:_webView.title];
 | |
|     else if ([keyPath isEqualToString:@"URL"])
 | |
|         [self updateTextFieldFromURL:_webView.URL];
 | |
|     else if ([keyPath isEqualToString:@"hasOnlySecureContent"])
 | |
|         [self updateLockButtonIcon:_webView.hasOnlySecureContent];
 | |
| }
 | |
| 
 | |
| - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
 | |
| {
 | |
|     // WebView lifecycle will control the BrowserWindowController life times.
 | |
|     BrowserWindowController *controller = [[BrowserWindowController alloc] initWithConfiguration:configuration];
 | |
|     return controller->_webView;
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
 | |
| {
 | |
|     NSAlert* alert = [[NSAlert alloc] init];
 | |
| 
 | |
|     [alert setMessageText:[NSString stringWithFormat:@"JavaScript alert dialog from %@.", [frame.request.URL absoluteString]]];
 | |
|     [alert setInformativeText:message];
 | |
|     [alert addButtonWithTitle:@"OK"];
 | |
| 
 | |
|     _alert = alert;
 | |
|     [alert beginSheetModalForWindow:self.window completionHandler:^void (NSModalResponse response) {
 | |
|         completionHandler();
 | |
|         [alert release];
 | |
|         _alert = nil;
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
 | |
| {
 | |
|     NSAlert* alert = [[NSAlert alloc] init];
 | |
| 
 | |
|     [alert setMessageText:[NSString stringWithFormat:@"JavaScript confirm dialog from %@.", [frame.request.URL  absoluteString]]];
 | |
|     [alert setInformativeText:message];
 | |
| 
 | |
|     [alert addButtonWithTitle:@"OK"];
 | |
|     [alert addButtonWithTitle:@"Cancel"];
 | |
| 
 | |
|     _alert = alert;
 | |
|     [alert beginSheetModalForWindow:self.window completionHandler:^void (NSModalResponse response) {
 | |
|         completionHandler(response == NSAlertFirstButtonReturn);
 | |
|         [alert release];
 | |
|         _alert = nil;
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler
 | |
| {
 | |
|     NSAlert* alert = [[NSAlert alloc] init];
 | |
| 
 | |
|     [alert setMessageText:[NSString stringWithFormat:@"JavaScript prompt dialog from %@.", [frame.request.URL absoluteString]]];
 | |
|     [alert setInformativeText:prompt];
 | |
| 
 | |
|     [alert addButtonWithTitle:@"OK"];
 | |
|     [alert addButtonWithTitle:@"Cancel"];
 | |
| 
 | |
|     NSTextField* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
 | |
|     [input setStringValue:defaultText];
 | |
|     [alert setAccessoryView:input];
 | |
| 
 | |
|     _alert = alert;
 | |
|     [alert beginSheetModalForWindow:self.window completionHandler:^void (NSModalResponse response) {
 | |
|         [input validateEditing];
 | |
|         completionHandler(response == NSAlertFirstButtonReturn ? [input stringValue] : nil);
 | |
|         [alert release];
 | |
|         _alert = nil;
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView handleJavaScriptDialog:(BOOL)accept value:(NSString *)value
 | |
| {
 | |
|     if (!_alert)
 | |
|         return;
 | |
|     NSTextField* input = (NSTextField*)_alert.accessoryView;
 | |
|     if (accept && input && value)
 | |
|         [input setStringValue:value];
 | |
|     [self.window endSheet:_alert.window returnCode: accept ? NSAlertFirstButtonReturn : NSModalResponseCancel];
 | |
| }
 | |
| 
 | |
| #if __has_feature(objc_generics)
 | |
| - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray<NSURL *> * URLs))completionHandler
 | |
| #else
 | |
| - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray *URLs))completionHandler
 | |
| #endif
 | |
| {
 | |
|     NSOpenPanel *openPanel = [NSOpenPanel openPanel];
 | |
| 
 | |
|     openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection;
 | |
| 
 | |
|     [openPanel beginSheetModalForWindow:webView.window completionHandler:^(NSInteger result) {
 | |
|         if (result == NSModalResponseOK)
 | |
|             completionHandler(openPanel.URLs);
 | |
|         else
 | |
|             completionHandler(nil);
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (void)_webView:(WebView *)sender runBeforeUnloadConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
 | |
| {
 | |
|     NSAlert *alert = [[NSAlert alloc] init];
 | |
| 
 | |
|     alert.messageText = [NSString stringWithFormat:@"JavaScript before unload dialog from %@.", [frame.request.URL absoluteString]];
 | |
|     alert.informativeText = message;
 | |
| 
 | |
|     [alert addButtonWithTitle:@"Leave Page"];
 | |
|     [alert addButtonWithTitle:@"Stay On Page"];
 | |
| 
 | |
|     _alert = alert;
 | |
|     [alert beginSheetModalForWindow:self.window completionHandler:^void (NSModalResponse response) {
 | |
|         completionHandler(response == NSAlertFirstButtonReturn);
 | |
|         [alert release];
 | |
|         _alert = nil;
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (WKDragDestinationAction)_webView:(WKWebView *)webView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo
 | |
| {
 | |
|     return WKDragDestinationActionAny;
 | |
| }
 | |
| 
 | |
| - (void)updateTextFieldFromURL:(NSURL *)URL
 | |
| {
 | |
|     if (!URL)
 | |
|         return;
 | |
| 
 | |
|     if (!URL.absoluteString.length)
 | |
|         return;
 | |
| 
 | |
|     urlText.stringValue = [URL _web_userVisibleString];
 | |
| }
 | |
| 
 | |
| - (void)updateLockButtonIcon:(BOOL)hasOnlySecureContent
 | |
| {
 | |
|     if (hasOnlySecureContent)
 | |
|         [lockButton setImage:[NSImage imageNamed:NSImageNameLockLockedTemplate]];
 | |
|     else
 | |
|         [lockButton setImage:[NSImage imageNamed:NSImageNameLockUnlockedTemplate]];
 | |
| }
 | |
| 
 | |
| - (void)loadURLString:(NSString *)urlString
 | |
| {
 | |
|     // FIXME: We shouldn't have to set the url text here.
 | |
|     [urlText setStringValue:urlString];
 | |
|     [self fetch:nil];
 | |
| }
 | |
| 
 | |
| - (void)loadHTMLString:(NSString *)HTMLString
 | |
| {
 | |
|     [_webView loadHTMLString:HTMLString baseURL:nil];
 | |
| }
 | |
| 
 | |
| static NSSet *dataTypes()
 | |
| {
 | |
|     return [WKWebsiteDataStore allWebsiteDataTypes];
 | |
| }
 | |
| 
 | |
| - (IBAction)fetchWebsiteData:(id)sender
 | |
| {
 | |
|     [_configuration.websiteDataStore _fetchDataRecordsOfTypes:dataTypes() withOptions:_WKWebsiteDataStoreFetchOptionComputeSizes completionHandler:^(NSArray *websiteDataRecords) {
 | |
|         NSLog(@"did fetch website data %@.", websiteDataRecords);
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (IBAction)fetchAndClearWebsiteData:(id)sender
 | |
| {
 | |
|     [_configuration.websiteDataStore fetchDataRecordsOfTypes:dataTypes() completionHandler:^(NSArray *websiteDataRecords) {
 | |
|         [_configuration.websiteDataStore removeDataOfTypes:dataTypes() forDataRecords:websiteDataRecords completionHandler:^{
 | |
|             [_configuration.websiteDataStore fetchDataRecordsOfTypes:dataTypes() completionHandler:^(NSArray *websiteDataRecords) {
 | |
|                 NSLog(@"did clear website data, after clearing data is %@.", websiteDataRecords);
 | |
|             }];
 | |
|         }];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (IBAction)clearWebsiteData:(id)sender
 | |
| {
 | |
|     [_configuration.websiteDataStore removeDataOfTypes:dataTypes() modifiedSince:[NSDate distantPast] completionHandler:^{
 | |
|         NSLog(@"Did clear website data.");
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (IBAction)printWebView:(id)sender
 | |
| {
 | |
|     [[_webView printOperationWithPrintInfo:[NSPrintInfo sharedPrintInfo]] runOperationModalForWindow:self.window delegate:nil didRunSelector:nil contextInfo:nil];
 | |
| }
 | |
| 
 | |
| #pragma mark WKNavigationDelegate
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
 | |
| {
 | |
|     LOG(@"decidePolicyForNavigationAction");
 | |
| 
 | |
|     if (navigationAction.shouldPerformDownload) {
 | |
|         decisionHandler(WKNavigationActionPolicyDownload);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     if (navigationAction._canHandleRequest) {
 | |
|         decisionHandler(WKNavigationActionPolicyAllow);
 | |
|         return;
 | |
|     }
 | |
|     decisionHandler(WKNavigationActionPolicyCancel);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
 | |
| {
 | |
|     if (![navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
 | |
|       decisionHandler(WKNavigationResponsePolicyAllow);
 | |
|       return;
 | |
|     }
 | |
|     NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response;
 | |
| 
 | |
|     NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"];
 | |
|     if (disposition && [disposition hasPrefix:@"attachment"]) {
 | |
|         decisionHandler(WKNavigationResponsePolicyDownload);
 | |
|         return;
 | |
|     }
 | |
|     decisionHandler(WKNavigationResponsePolicyAllow);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
 | |
| {
 | |
|     LOG(@"didStartProvisionalNavigation: %@", navigation);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation
 | |
| {
 | |
|     LOG(@"didReceiveServerRedirectForProvisionalNavigation: %@", navigation);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
 | |
| {
 | |
|     LOG(@"didFailProvisionalNavigation: %@navigation, error: %@", navigation, error);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation
 | |
| {
 | |
|     LOG(@"didCommitNavigation: %@", navigation);
 | |
|     [self updateTitle:nil];
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
 | |
| {
 | |
|     LOG(@"didFinishNavigation: %@", navigation);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler
 | |
| {
 | |
|     LOG(@"didReceiveAuthenticationChallenge: %@", challenge);
 | |
|     if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) {
 | |
|         NSAlert *alert = [[NSAlert alloc] init];
 | |
|         NSView *container = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 200, 48)] autorelease];
 | |
|         NSTextField *userInput = [[[NSTextField alloc] initWithFrame:NSMakeRect(0, 24, 200, 24)] autorelease];
 | |
|         NSTextField *passwordInput = [[[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)] autorelease];
 | |
| 
 | |
|         [alert setMessageText:[NSString stringWithFormat:@"Log in to %@:%lu.", challenge.protectionSpace.host, challenge.protectionSpace.port]];
 | |
|         [alert addButtonWithTitle:@"Log in"];
 | |
|         [alert addButtonWithTitle:@"Cancel"];
 | |
|         [container addSubview:userInput];
 | |
|         [container addSubview:passwordInput];
 | |
|         [alert setAccessoryView:container];
 | |
|         [userInput setNextKeyView:passwordInput];
 | |
|         [alert.window setInitialFirstResponder:userInput];
 | |
| 
 | |
|         [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse response) {
 | |
|             [userInput validateEditing];
 | |
|             if (response == NSAlertFirstButtonReturn)
 | |
|                 completionHandler(NSURLSessionAuthChallengeUseCredential, [[[NSURLCredential alloc] initWithUser:[userInput stringValue] password:[passwordInput stringValue] persistence:NSURLCredentialPersistenceForSession] autorelease]);
 | |
|             else
 | |
|                 completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
 | |
|             [alert release];
 | |
|         }];
 | |
|         return;
 | |
|     }
 | |
|     completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
 | |
| {
 | |
|     LOG(@"didFailNavigation: %@, error %@", navigation, error);
 | |
| }
 | |
| 
 | |
| - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView
 | |
| {
 | |
|     NSLog(@"WebContent process crashed; reloading");
 | |
|     [self reload:nil];
 | |
| }
 | |
| 
 | |
| - (void)_webView:(WKWebView *)webView renderingProgressDidChange:(_WKRenderingProgressEvents)progressEvents
 | |
| {
 | |
|     if (progressEvents & _WKRenderingProgressEventFirstLayout)
 | |
|         LOG(@"renderingProgressDidChange: %@", @"first layout");
 | |
| 
 | |
|     if (progressEvents & _WKRenderingProgressEventFirstVisuallyNonEmptyLayout)
 | |
|         LOG(@"renderingProgressDidChange: %@", @"first visually non-empty layout");
 | |
| 
 | |
|     if (progressEvents & _WKRenderingProgressEventFirstPaintWithSignificantArea)
 | |
|         LOG(@"renderingProgressDidChange: %@", @"first paint with significant area");
 | |
| 
 | |
|     if (progressEvents & _WKRenderingProgressEventFirstLayoutAfterSuppressedIncrementalRendering)
 | |
|         LOG(@"renderingProgressDidChange: %@", @"first layout after suppressed incremental rendering");
 | |
| 
 | |
|     if (progressEvents & _WKRenderingProgressEventFirstPaintAfterSuppressedIncrementalRendering)
 | |
|         LOG(@"renderingProgressDidChange: %@", @"first paint after suppressed incremental rendering");
 | |
| }
 | |
| 
 | |
| - (void)webView:(WKWebView *)webView shouldLoadIconWithParameters:(_WKLinkIconParameters *)parameters completionHandler:(void (^)(void (^)(NSData*)))completionHandler
 | |
| {
 | |
|     completionHandler(^void (NSData *data) {
 | |
|         LOG(@"Icon URL %@ received icon data of length %u", parameters.url, (unsigned)data.length);
 | |
|     });
 | |
| }
 | |
| 
 | |
| #pragma mark Find in Page
 | |
| 
 | |
| - (IBAction)performTextFinderAction:(id)sender
 | |
| {
 | |
|     [_textFinder performAction:[sender tag]];
 | |
| }
 | |
| 
 | |
| - (NSView *)findBarView
 | |
| {
 | |
|     return _textFindBarView;
 | |
| }
 | |
| 
 | |
| - (void)setFindBarView:(NSView *)findBarView
 | |
| {
 | |
|     _textFindBarView = findBarView;
 | |
|     _findBarVisible = YES;
 | |
|     [_textFindBarView setFrame:NSMakeRect(0, 0, containerView.bounds.size.width, _textFindBarView.frame.size.height)];
 | |
| }
 | |
| 
 | |
| - (BOOL)isFindBarVisible
 | |
| {
 | |
|     return _findBarVisible;
 | |
| }
 | |
| 
 | |
| - (void)setFindBarVisible:(BOOL)findBarVisible
 | |
| {
 | |
|     _findBarVisible = findBarVisible;
 | |
|     if (findBarVisible)
 | |
|         [containerView addSubview:_textFindBarView];
 | |
|     else
 | |
|         [_textFindBarView removeFromSuperview];
 | |
| }
 | |
| 
 | |
| - (NSView *)contentView
 | |
| {
 | |
|     return _webView;
 | |
| }
 | |
| 
 | |
| - (void)findBarViewDidChangeHeight
 | |
| {
 | |
| }
 | |
| 
 | |
| - (void)_webView:(WKWebView *)webView requestMediaCaptureAuthorization: (_WKCaptureDevices)devices decisionHandler:(void (^)(BOOL authorized))decisionHandler
 | |
| {
 | |
|     decisionHandler(true);
 | |
| }
 | |
| 
 | |
| - (void)_webView:(WKWebView *)webView includeSensitiveMediaDeviceDetails:(void (^)(BOOL includeSensitiveDetails))decisionHandler
 | |
| {
 | |
|     decisionHandler(false);
 | |
| }
 | |
| 
 | |
| - (IBAction)saveAsPDF:(id)sender
 | |
| {
 | |
|     NSSavePanel *panel = [NSSavePanel savePanel];
 | |
| #pragma clang diagnostic push
 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations"
 | |
|     panel.allowedFileTypes = @[ @"pdf" ];
 | |
| #pragma clang diagnostic pop
 | |
|     [panel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
 | |
|         if (result == NSModalResponseOK) {
 | |
|             [_webView createPDFWithConfiguration:nil completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
 | |
|                 [pdfSnapshotData writeToURL:[panel URL] options:0 error:nil];
 | |
|             }];
 | |
|         }
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (IBAction)saveAsWebArchive:(id)sender
 | |
| {
 | |
|     NSSavePanel *panel = [NSSavePanel savePanel];
 | |
| #pragma clang diagnostic push
 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations"
 | |
|     panel.allowedFileTypes = @[ @"webarchive" ];
 | |
| #pragma clang diagnostic pop
 | |
|     [panel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
 | |
|         if (result == NSModalResponseOK) {
 | |
|             [_webView createWebArchiveDataWithCompletionHandler:^(NSData *archiveData, NSError *error) {
 | |
|                 [archiveData writeToURL:[panel URL] options:0 error:nil];
 | |
|             }];
 | |
|         }
 | |
|     }];
 | |
| }
 | |
| 
 | |
| - (WKWebView *)webView
 | |
| {
 | |
|     return _webView;
 | |
| }
 | |
| 
 | |
| @end
 | 
