mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-22 10:41:41 +03:00
958 lines
32 KiB
Objective-C
958 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.buttonNumber == 1 &&
|
|
(navigationAction.modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagShift)) != 0) {
|
|
WKWindowFeatures* windowFeatures = [[[WKWindowFeatures alloc] init] autorelease];
|
|
WKWebView* newView = [self webView:webView createWebViewWithConfiguration:webView.configuration forNavigationAction:navigationAction windowFeatures:windowFeatures];
|
|
[newView loadRequest:navigationAction.request];
|
|
decisionHandler(WKNavigationActionPolicyCancel);
|
|
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
|