5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 22:31:06 +08:00
wails/v2/internal/frontend/desktop/darwin/WailsContext.m
stffabi 3d24a9b4c4
[v2, darwin] Assign a unique requestId to every WKWebView request (#1681)
This makes sure we always use the correct WKURLSchemeTask during
processURLResponse. Only using the URL is not unique enough and
might result in pending requests if two requests with the same URL
are getting processed.
2022-08-01 18:55:25 +10:00

727 lines
23 KiB
Objective-C

//
// WailsContext.m
// test
//
// Created by Lea Anthony on 10/10/21.
//
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
#import "WailsContext.h"
#import "WailsAlert.h"
#import "WailsMenu.h"
#import "WindowDelegate.h"
#import "message.h"
#import "Role.h"
@implementation WailsWindow
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (void) applyWindowConstraints {
[self setMinSize:self.userMinSize];
[self setMaxSize:self.userMaxSize];
}
- (void) disableWindowConstraints {
[self setMinSize:NSMakeSize(0, 0)];
[self setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
}
@end
@implementation WailsContext
- (void) SetSize:(int)width :(int)height {
if (self.shuttingDown) return;
NSRect frame = [self.mainWindow frame];
frame.origin.y += frame.size.height - height;
frame.size.width = width;
frame.size.height = height;
[self.mainWindow setFrame:frame display:TRUE animate:FALSE];
}
- (void) SetPosition:(int)x :(int)y {
if (self.shuttingDown) return;
NSScreen* screen = [self getCurrentScreen];
NSRect windowFrame = [self.mainWindow frame];
NSRect screenFrame = [screen frame];
windowFrame.origin.x = screenFrame.origin.x + (float)x;
windowFrame.origin.y = (screenFrame.origin.y + screenFrame.size.height) - windowFrame.size.height - (float)y;
[self.mainWindow setFrame:windowFrame display:TRUE animate:FALSE];
}
- (void) SetMinSize:(int)minWidth :(int)minHeight {
if (self.shuttingDown) return;
NSSize size = { minWidth, minHeight };
self.mainWindow.userMinSize = size;
[self.mainWindow setMinSize:size];
[self adjustWindowSize];
}
- (void) SetMaxSize:(int)maxWidth :(int)maxHeight {
if (self.shuttingDown) return;
NSSize size = { FLT_MAX, FLT_MAX };
size.width = maxWidth > 0 ? maxWidth : FLT_MAX;
size.height = maxHeight > 0 ? maxHeight : FLT_MAX;
self.mainWindow.userMaxSize = size;
[self.mainWindow setMaxSize:size];
[self adjustWindowSize];
}
- (void) adjustWindowSize {
if (self.shuttingDown) return;
NSRect currentFrame = [self.mainWindow frame];
if ( currentFrame.size.width > self.mainWindow.userMaxSize.width ) currentFrame.size.width = self.mainWindow.userMaxSize.width;
if ( currentFrame.size.width < self.mainWindow.userMinSize.width ) currentFrame.size.width = self.mainWindow.userMinSize.width;
if ( currentFrame.size.height > self.mainWindow.userMaxSize.height ) currentFrame.size.height = self.mainWindow.userMaxSize.height;
if ( currentFrame.size.height < self.mainWindow.userMinSize.height ) currentFrame.size.height = self.mainWindow.userMinSize.height;
[self.mainWindow setFrame:currentFrame display:YES animate:FALSE];
}
- (void) dealloc {
[self.appdelegate release];
[self.mainWindow release];
[self.mouseEvent release];
[self.userContentController release];
[self.urlRequests release];
[self.urlRequestsLock release];
[self.applicationMenu release];
[super dealloc];
}
- (NSScreen*) getCurrentScreen {
NSScreen* screen = [self.mainWindow screen];
if( screen == NULL ) {
screen = [NSScreen mainScreen];
}
return screen;
}
- (void) SetTitle:(NSString*)title {
[self.mainWindow setTitle:title];
}
- (void) Center {
[self.mainWindow center];
}
- (BOOL) isFullscreen {
NSWindowStyleMask masks = [self.mainWindow styleMask];
if ( masks & NSWindowStyleMaskFullScreen ) {
return YES;
}
return NO;
}
- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight {
self.urlRequestsId = 0;
self.urlRequestsLock = [NSLock new];
self.urlRequests = [NSMutableDictionary new];
NSWindowStyleMask styleMask = 0;
if( !frameless ) {
if (!hideTitleBar) {
styleMask |= NSWindowStyleMaskTitled;
}
styleMask |= NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
}
if( fullSizeContent || frameless || titlebarAppearsTransparent ) {
styleMask |= NSWindowStyleMaskFullSizeContentView;
}
if (resizable) {
styleMask |= NSWindowStyleMaskResizable;
}
self.mainWindow = [[WailsWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height)
styleMask:styleMask backing:NSBackingStoreBuffered defer:NO];
if (!frameless && useToolbar) {
id toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"];
[toolbar autorelease];
[toolbar setShowsBaselineSeparator:!hideToolbarSeparator];
[self.mainWindow setToolbar:toolbar];
}
[self.mainWindow setTitleVisibility:hideTitle];
[self.mainWindow setTitlebarAppearsTransparent:titlebarAppearsTransparent];
// [self.mainWindow canBecomeKeyWindow];
id contentView = [self.mainWindow contentView];
if (windowIsTranslucent) {
NSVisualEffectView *effectView = [NSVisualEffectView alloc];
NSRect bounds = [contentView bounds];
[effectView initWithFrame:bounds];
[effectView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[effectView setState:NSVisualEffectStateActive];
[contentView addSubview:effectView positioned:NSWindowBelow relativeTo:nil];
}
if (appearance != nil) {
NSAppearance *nsAppearance = [NSAppearance appearanceNamed:appearance];
[self.mainWindow setAppearance:nsAppearance];
}
NSSize minSize = { minWidth, minHeight };
NSSize maxSize = { maxWidth, maxHeight };
if (maxSize.width == 0) {
maxSize.width = FLT_MAX;
}
if (maxSize.height == 0) {
maxSize.height = FLT_MAX;
}
self.mainWindow.userMaxSize = maxSize;
self.mainWindow.userMinSize = minSize;
if( !fullscreen ) {
[self.mainWindow applyWindowConstraints];
}
WindowDelegate *windowDelegate = [WindowDelegate new];
windowDelegate.hideOnClose = hideWindowOnClose;
windowDelegate.ctx = self;
[self.mainWindow setDelegate:windowDelegate];
// Webview stuff here!
WKWebViewConfiguration *config = [WKWebViewConfiguration new];
config.suppressesIncrementalRendering = true;
config.applicationNameForUserAgent = @"wails.io";
[config setURLSchemeHandler:self forURLScheme:@"wails"];
// [config.preferences setValue:[NSNumber numberWithBool:true] forKey:@"developerExtrasEnabled"];
WKUserContentController* userContentController = [WKUserContentController new];
[userContentController addScriptMessageHandler:self name:@"external"];
config.userContentController = userContentController;
self.userContentController = userContentController;
if (self.debug) {
[config.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
} else {
// Disable default context menus
WKUserScript *initScript = [WKUserScript new];
[initScript initWithSource:@"window.wails.flags.disableWailsDefaultContextMenu = true;"
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly:false];
[userContentController addUserScript:initScript];
}
self.webview = [WKWebView alloc];
CGRect init = { 0,0,0,0 };
[self.webview initWithFrame:init configuration:config];
[contentView addSubview:self.webview];
[self.webview setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable];
CGRect contentViewBounds = [contentView bounds];
[self.webview setFrame:contentViewBounds];
if (webviewIsTransparent) {
[self.webview setValue:[NSNumber numberWithBool:!webviewIsTransparent] forKey:@"drawsBackground"];
}
[self.webview setNavigationDelegate:self];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:FALSE forKey:@"NSAutomaticQuoteSubstitutionEnabled"];
// Mouse monitors
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
id window = [event window];
if (window == self.mainWindow) {
self.mouseEvent = event;
}
return event;
}];
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
id window = [event window];
if (window == self.mainWindow) {
self.mouseEvent = nil;
[self ShowMouse];
}
return event;
}];
self.applicationMenu = [NSMenu new];
}
- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags {
NSMenuItem *result = [[[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:key] autorelease];
if( flags != 0 ) {
[result setKeyEquivalentModifierMask:flags];
}
return result;
}
- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key {
return [self newMenuItem :title :selector :key :0];
}
- (NSMenu*) newMenu :(NSString*)title {
WailsMenu *result = [[WailsMenu new] initWithTitle:title];
[result setAutoenablesItems:NO];
return result;
}
- (void) Quit {
processMessage("Q");
}
- (void) loadRequest :(NSString*)url {
NSURL *wkUrl = [NSURL URLWithString:url];
NSURLRequest *wkRequest = [NSURLRequest requestWithURL:wkUrl];
[self.webview loadRequest:wkRequest];
}
- (void) SetBackgroundColour:(int)r :(int)g :(int)b :(int)a {
float red = r/255.0;
float green = g/255.0;
float blue = b/255.0;
float alpha = a/255.0;
id colour = [NSColor colorWithCalibratedRed:red green:green blue:blue alpha:alpha ];
[self.mainWindow setBackgroundColor:colour];
}
- (void) HideMouse {
[NSCursor hide];
}
- (void) ShowMouse {
[NSCursor unhide];
}
- (bool) isFullScreen {
long mask = [self.mainWindow styleMask];
return (mask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen;
}
// Fullscreen sets the main window to be fullscreen
- (void) Fullscreen {
if( ! [self isFullScreen] ) {
[self.mainWindow disableWindowConstraints];
[self.mainWindow toggleFullScreen:nil];
}
}
// UnFullscreen resets the main window after a fullscreen
- (void) UnFullscreen {
if( [self isFullScreen] ) {
[self.mainWindow applyWindowConstraints];
[self.mainWindow toggleFullScreen:nil];
}
}
- (void) Minimise {
[self.mainWindow miniaturize:nil];
}
- (void) UnMinimise {
[self.mainWindow deminiaturize:nil];
}
- (void) Hide {
[self.mainWindow orderOut:nil];
}
- (void) Show {
[self.mainWindow makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
}
- (void) HideApplication {
[[NSApplication sharedApplication] hide:self];
}
- (void) ShowApplication {
[[NSApplication sharedApplication] unhide:self];
[[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE];
}
- (void) Maximise {
if (![self.mainWindow isZoomed]) {
[self.mainWindow zoom:nil];
}
}
- (void) ToggleMaximise {
[self.mainWindow zoom:nil];
}
- (void) UnMaximise {
if ([self.mainWindow isZoomed]) {
[self.mainWindow zoom:nil];
}
}
- (void) SetAlwaysOnTop:(int)onTop {
if (onTop) {
[self.mainWindow setLevel:NSStatusWindowLevel];
} else {
[self.mainWindow setLevel:NSNormalWindowLevel];
}
}
- (void) ExecJS:(NSString*)script {
[self.webview evaluateJavaScript:script completionHandler:nil];
}
- (void) processURLResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON :(NSData *)data {
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
[self.urlRequestsLock lock];
id<WKURLSchemeTask> urlSchemeTask = self.urlRequests[key];
[self.urlRequestsLock unlock];
@try {
if (urlSchemeTask == nil) {
return;
}
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData: headersJSON options: NSJSONReadingMutableContainers error: nil];
NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease];
@try {
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
} @catch (NSException *exception) {
// This is very bad to detect a stopped schemeTask this should be implemented in a better way
// See todo in stopURLSchemeTask...
if (![exception.reason isEqualToString: @"This task has already been stopped"]) {
@throw exception;
}
}
} @finally {
[self.urlRequestsLock lock];
[self.urlRequests removeObjectForKey:key]; // This will release the urlSchemeTask which was retained from the dictionary
[self.urlRequestsLock unlock];
}
}
- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
// This callback is run with an autorelease pool
const char *url = [urlSchemeTask.request.URL.absoluteString UTF8String];
const char *method = [urlSchemeTask.request.HTTPMethod UTF8String];
const char *headerJSON = "";
const void *body = nil;
int bodyLen = 0;
NSData *headers = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
if (headers) {
NSString* headerString = [[[NSString alloc] initWithData:headers encoding:NSUTF8StringEncoding] autorelease];
headerJSON = [headerString UTF8String];
}
if (urlSchemeTask.request.HTTPBody) {
body = urlSchemeTask.request.HTTPBody.bytes;
bodyLen = urlSchemeTask.request.HTTPBody.length;
} else {
// TODO handle HTTPBodyStream
}
[self.urlRequestsLock lock];
self.urlRequestsId++;
unsigned long long requestId = self.urlRequestsId;
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
self.urlRequests[key] = urlSchemeTask;
[self.urlRequestsLock unlock];
processURLRequest(self, requestId, url, method, headerJSON, body, bodyLen);
}
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
// TODO implement the stopping process here in a better way...
// As soon as we introduce response body streaming we need to rewrite this nevertheless.
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
processMessage("DomReady");
}
- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
NSString *m = message.body;
// Check for drag
if ( [m isEqualToString:@"drag"] ) {
if( [self isFullScreen] ) {
return;
}
if( self.mouseEvent != nil ) {
[self.mainWindow performWindowDragWithEvent:self.mouseEvent];
}
return;
}
const char *_m = [m UTF8String];
processMessage(_m);
}
/***** Dialogs ******/
-(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength {
WailsAlert *alert = [WailsAlert new];
int style = NSAlertStyleInformational;
if (dialogType != nil ) {
if( [dialogType isEqualToString:@"warning"] ) {
style = NSAlertStyleWarning;
}
if( [dialogType isEqualToString:@"error"] ) {
style = NSAlertStyleCritical;
}
}
[alert setAlertStyle:style];
if( title != nil ) {
[alert setMessageText:title];
}
if( message != nil ) {
[alert setInformativeText:message];
}
[alert addButton:button1 :defaultButton :cancelButton];
[alert addButton:button2 :defaultButton :cancelButton];
[alert addButton:button3 :defaultButton :cancelButton];
[alert addButton:button4 :defaultButton :cancelButton];
NSImage *icon = nil;
if (iconData != nil) {
NSData *imageData = [NSData dataWithBytes:iconData length:iconDataLength];
icon = [[NSImage alloc] initWithData:imageData];
}
if( icon != nil) {
[alert setIcon:icon];
}
[alert.window setLevel:NSFloatingWindowLevel];
long response = [alert runModal];
int result;
if( response == NSAlertFirstButtonReturn ) {
result = 0;
}
else if( response == NSAlertSecondButtonReturn ) {
result = 1;
}
else if( response == NSAlertThirdButtonReturn ) {
result = 2;
} else {
result = 3;
}
processMessageDialogResponse(result);
}
-(void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters {
// Create the dialog
NSOpenPanel *dialog = [NSOpenPanel openPanel];
// Valid but appears to do nothing.... :/
if( title != nil ) {
[dialog setTitle:title];
}
// Filters - semicolon delimited list of file extensions
if( allowFiles ) {
if( filters != nil && [filters length] > 0) {
filters = [filters stringByReplacingOccurrencesOfString:@"*." withString:@""];
filters = [filters stringByReplacingOccurrencesOfString:@" " withString:@""];
NSArray *filterList = [filters componentsSeparatedByString:@";"];
#ifdef USE_NEW_FILTERS
NSMutableArray *contentTypes = [[NSMutableArray new] autorelease];
for (NSString *filter in filterList) {
if (@available(macOS 11.0, *)) {
UTType *t = [UTType typeWithFilenameExtension:filter];
[contentTypes addObject:t];
}
}
if (@available(macOS 11.0, *)) {
[dialog setAllowedContentTypes:contentTypes];
}
#else
[dialog setAllowedFileTypes:filterList];
#endif
} else {
[dialog setAllowsOtherFileTypes:true];
}
// Default Filename
if( defaultFilename != nil ) {
[dialog setNameFieldStringValue:defaultFilename];
}
[dialog setAllowsMultipleSelection: allowMultipleSelection];
[dialog setShowsHiddenFiles: showHiddenFiles];
}
// Default Directory
if( defaultDirectory != nil ) {
NSURL *url = [NSURL fileURLWithPath:defaultDirectory];
[dialog setDirectoryURL:url];
}
// Setup Options
[dialog setCanChooseFiles: allowFiles];
[dialog setCanChooseDirectories: allowDirectories];
[dialog setCanCreateDirectories: canCreateDirectories];
[dialog setResolvesAliases: resolveAliases];
[dialog setTreatsFilePackagesAsDirectories: treatPackagesAsDirectories];
// Setup callback handler
[dialog beginSheetModalForWindow:self.mainWindow completionHandler:^(NSModalResponse returnCode) {
if ( returnCode != NSModalResponseOK) {
processOpenFileDialogResponse("[]");
return;
}
NSMutableArray *arr = [NSMutableArray new];
for (NSURL *url in [dialog URLs]) {
[arr addObject:[url path]];
}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:arr options:0 error:nil];
NSString *nsjson = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
processOpenFileDialogResponse([nsjson UTF8String]);
[nsjson release];
[arr release];
}];
}
-(void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; {
// Create the dialog
NSSavePanel *dialog = [NSSavePanel savePanel];
// Do not hide extension
[dialog setExtensionHidden:false];
// Valid but appears to do nothing.... :/
if( title != nil ) {
[dialog setTitle:title];
}
// Filters - semicolon delimited list of file extensions
if( filters != nil && [filters length] > 0) {
filters = [filters stringByReplacingOccurrencesOfString:@"*." withString:@""];
filters = [filters stringByReplacingOccurrencesOfString:@" " withString:@""];
NSArray *filterList = [filters componentsSeparatedByString:@";"];
#ifdef USE_NEW_FILTERS
NSMutableArray *contentTypes = [[NSMutableArray new] autorelease];
for (NSString *filter in filterList) {
if (@available(macOS 11.0, *)) {
UTType *t = [UTType typeWithFilenameExtension:filter];
[contentTypes addObject:t];
}
}
if( contentTypes.count == 0) {
[dialog setAllowsOtherFileTypes:true];
} else {
if (@available(macOS 11.0, *)) {
[dialog setAllowedContentTypes:contentTypes];
}
}
#else
[dialog setAllowedFileTypes:filterList];
#endif
} else {
[dialog setAllowsOtherFileTypes:true];
}
// Default Filename
if( defaultFilename != nil ) {
[dialog setNameFieldStringValue:defaultFilename];
}
// Default Directory
if( defaultDirectory != nil ) {
NSURL *url = [NSURL fileURLWithPath:defaultDirectory];
[dialog setDirectoryURL:url];
}
// Setup Options
[dialog setCanSelectHiddenExtension:true];
// dialog.isExtensionHidden = false;
[dialog setCanCreateDirectories: canCreateDirectories];
[dialog setTreatsFilePackagesAsDirectories: treatPackagesAsDirectories];
[dialog setShowsHiddenFiles: showHiddenFiles];
// Setup callback handler
[dialog beginSheetModalForWindow:self.mainWindow completionHandler:^(NSModalResponse returnCode) {
if ( returnCode == NSModalResponseOK ) {
NSURL *url = [dialog URL];
if ( url != nil ) {
processSaveFileDialogResponse([url.path UTF8String]);
return;
}
}
processSaveFileDialogResponse("");
}];
}
- (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen {
self.aboutTitle = title;
self.aboutDescription = description;
NSData *imageData = [NSData dataWithBytes:imagedata length:datalen];
self.aboutImage = [[NSImage alloc] initWithData:imageData];
}
-(void) About {
WailsAlert *alert = [WailsAlert new];
[alert setAlertStyle:NSAlertStyleInformational];
if( self.aboutTitle != nil ) {
[alert setMessageText:self.aboutTitle];
}
if( self.aboutDescription != nil ) {
[alert setInformativeText:self.aboutDescription];
}
[alert.window setLevel:NSFloatingWindowLevel];
if ( self.aboutImage != nil) {
[alert setIcon:self.aboutImage];
}
[alert runModal];
}
@end