#ifdef WITH_WEBVIEW #include #ifdef KINC_MACOS #import #import #include "webview.h" #include #include @interface LNXRNTWebViewDelegate : NSObject { @public int webviewId; @public bool isDetached; @public NSWindow* detachedWindow; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message; - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation; - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error; - (BOOL)windowShouldClose:(NSWindow *)sender; - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler; - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler; - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler; - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures; - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray *URLs))completionHandler; @end @implementation LNXRNTWebViewDelegate - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { NSString *msgBody = [[message body] isKindOfClass:[NSString class]] ? [message body] : [NSString stringWithFormat:@"%@", [message body]]; const char *cstr = [msgBody UTF8String]; WebViewManager::instance().dispatchMessage(webviewId, std::string(cstr)); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { WebViewManager::instance().dispatchLoad(webviewId); } - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { NSString *desc = [error localizedDescription]; WebViewManager::instance().dispatchError(webviewId, std::string([desc UTF8String])); } - (BOOL)windowShouldClose:(NSWindow *)sender { WebViewManager::instance().dispatchClose(webviewId); [sender orderOut:nil]; return NO; } - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:message]; [alert addButtonWithTitle:@"OK"]; [alert runModal]; completionHandler(); } - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:message]; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Cancel"]; NSModalResponse result = [alert runModal]; completionHandler(result == NSAlertFirstButtonReturn); } - (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:prompt]; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Cancel"]; NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 250, 24)]; [input setStringValue:defaultText ? defaultText : @""]; [alert setAccessoryView:input]; NSModalResponse result = [alert runModal]; if (result == NSAlertFirstButtonReturn) { completionHandler([input stringValue]); } else { completionHandler(nil); } } - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { if (!navigationAction.targetFrame.isMainFrame) { return nil; } NSRect frame = NSMakeRect(100, 100, 800, 600); NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; NSWindow *newWindow = [[NSWindow alloc] initWithContentRect:frame styleMask:styleMask backing:NSBackingStoreBuffered defer:YES]; WKWebView *newWebView = [[WKWebView alloc] initWithFrame:frame configuration:configuration]; newWebView.navigationDelegate = self; newWebView.UIDelegate = self; [newWindow setContentView:newWebView]; [newWindow makeKeyAndOrderFront:nil]; return newWebView; } - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray *URLs))completionHandler { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection; openPanel.canChooseDirectories = parameters.allowsDirectories; if ([openPanel runModal] == NSModalResponseOK) { completionHandler(openPanel.URLs); } else { completionHandler(nil); } } @end struct MacWebViewState { WKWebView* webView; LNXRNTWebViewDelegate* delegate; NSWindow* detachedWindow; bool isDetached; }; extern "C" NSWindow* kinc_get_mac_window_handle(int window_index); int platformCreate(WebViewInstance* wv) { auto* state = new MacWebViewState(); state->webView = nullptr; state->delegate = nullptr; state->detachedWindow = nullptr; state->isDetached = (wv->parentWindow < 0); wv->platformHandle = state; WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; WKUserContentController* contentController = [[WKUserContentController alloc] init]; state->delegate = [[LNXRNTWebViewDelegate alloc] init]; state->delegate->webviewId = wv->id; state->delegate->isDetached = state->isDetached; [contentController addScriptMessageHandler:state->delegate name:@"lnxrnt"]; config.userContentController = contentController; CGFloat scale = 1.0; if (!state->isDetached) { NSWindow* kincWindow = kinc_get_mac_window_handle(wv->parentWindow); if (kincWindow) { scale = [kincWindow backingScaleFactor]; } } CGFloat ptWidth = wv->width / scale; CGFloat ptHeight = wv->height / scale; NSRect frame = NSMakeRect(wv->x, wv->y, ptWidth, ptHeight); state->webView = [[WKWebView alloc] initWithFrame:frame configuration:config]; state->webView.navigationDelegate = state->delegate; state->webView.UIDelegate = state->delegate; if (wv->transparent) { [state->webView setValue:@NO forKey:@"opaque"]; [state->webView setValue:@NO forKey:@"drawsBackground"]; } if (state->isDetached) { NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; state->detachedWindow = [[NSWindow alloc] initWithContentRect:frame styleMask:styleMask backing:NSBackingStoreBuffered defer:YES]; [state->detachedWindow setTitle:[NSString stringWithUTF8String:wv->title.c_str()]]; [state->detachedWindow setContentView:state->webView]; [state->detachedWindow setDelegate:state->delegate]; state->delegate->detachedWindow = state->detachedWindow; } else { NSWindow* kincWindow = kinc_get_mac_window_handle(wv->parentWindow); if (!kincWindow) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to get Kinc NSWindow for webview window %d", wv->parentWindow); [state->webView release]; [contentController release]; [state->delegate release]; delete state; wv->platformHandle = nullptr; return -1; } NSView* contentView = [kincWindow contentView]; CGFloat flippedY = [contentView frame].size.height - wv->y - ptHeight; [state->webView setFrameOrigin:NSMakePoint(wv->x, flippedY)]; [contentView addSubview:state->webView positioned:NSWindowAbove relativeTo:nil]; } return wv->id; } void platformDestroy(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state) return; if (state->webView) { [state->webView removeFromSuperview]; [state->webView release]; } if (state->detachedWindow) { [state->detachedWindow close]; } if (state->delegate) { [state->delegate release]; } delete state; wv->platformHandle = nullptr; } void platformLoadHTML(WebViewInstance* wv, const std::string& html) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; NSString* nsHtml = [NSString stringWithUTF8String:html.c_str()]; [state->webView loadHTMLString:nsHtml baseURL:nil]; } void platformLoadURL(WebViewInstance* wv, const std::string& url) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; NSString* nsUrl = [NSString stringWithUTF8String:url.c_str()]; NSURL* nsURL = [NSURL URLWithString:nsUrl]; [state->webView loadRequest:[NSURLRequest requestWithURL:nsURL]]; } void platformEvalJS(WebViewInstance* wv, const std::string& js) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; NSString* nsJs = [NSString stringWithUTF8String:js.c_str()]; [state->webView evaluateJavaScript:nsJs completionHandler:nil]; } void platformEvalJSAsync(WebViewInstance* wv, const std::string& js) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; NSString* nsJs = [NSString stringWithUTF8String:js.c_str()]; int webviewId = wv->id; [state->webView evaluateJavaScript:nsJs completionHandler:^(id result, NSError* error) { std::string resultStr; if (error) { resultStr = "null"; } else if (result == nil) { resultStr = "null"; } else if ([result isKindOfClass:[NSString class]]) { resultStr = std::string([[result description] UTF8String]); } else if ([result isKindOfClass:[NSNumber class]]) { resultStr = std::string([[result description] UTF8String]); } else { NSError* jsonErr = nil; NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:0 error:&jsonErr]; if (jsonData && !jsonErr) { NSString* jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; resultStr = std::string([jsonStr UTF8String]); } else { resultStr = std::string([[result description] UTF8String]); } } WebViewManager::instance().dispatchEvalResult(webviewId, resultStr); }]; } void platformShow(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state) return; if (state->isDetached && state->detachedWindow) { [state->detachedWindow makeKeyAndOrderFront:nil]; } else if (state->webView) { [state->webView setHidden:NO]; } } void platformHide(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state) return; if (state->isDetached && state->detachedWindow) { [state->detachedWindow orderOut:nil]; } else if (state->webView) { [state->webView setHidden:YES]; } } void platformResize(WebViewInstance* wv, int width, int height) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; if (state->isDetached && state->detachedWindow) { NSRect frame = NSMakeRect(wv->x, wv->y, width, height); [state->detachedWindow setFrame:frame display:YES]; } else { NSView* superview = [state->webView superview]; CGFloat scale = [[superview window] backingScaleFactor]; if (scale == 0) scale = 1.0; CGFloat ptW = width / scale; CGFloat ptH = height / scale; if (superview) { CGFloat flippedY = [superview frame].size.height - wv->y - ptH; [state->webView setFrame:NSMakeRect(wv->x, flippedY, ptW, ptH)]; } else { [state->webView setFrameSize:NSMakeSize(ptW, ptH)]; } } } void platformMove(WebViewInstance* wv, int x, int y) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; if (state->isDetached && state->detachedWindow) { [state->detachedWindow setFrameOrigin:NSMakePoint(x, y)]; } else { NSView* superview = [state->webView superview]; if (superview) { CGFloat scale = [[superview window] backingScaleFactor]; if (scale == 0) scale = 1.0; CGFloat ptH = wv->height / scale; CGFloat flippedY = [superview frame].size.height - y - ptH; [state->webView setFrameOrigin:NSMakePoint(x, flippedY)]; } } } void platformSetTransparent(WebViewInstance* wv, bool transparent) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; [state->webView setValue:(transparent ? @NO : @YES) forKey:@"opaque"]; [state->webView setValue:(transparent ? @NO : @YES) forKey:@"drawsBackground"]; } void platformSetClickThrough(WebViewInstance* wv, bool enabled) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; if (enabled) { [state->webView setAcceptsTouchEvents:NO]; [state->webView evaluateJavaScript: @"document.body.style.pointerEvents = 'none';" completionHandler:nil]; } else { [state->webView setAcceptsTouchEvents:YES]; [state->webView evaluateJavaScript: @"document.body.style.pointerEvents = 'auto';" completionHandler:nil]; } } void platformSetTitle(WebViewInstance* wv, const std::string& title) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->isDetached || !state->detachedWindow) return; [state->detachedWindow setTitle:[NSString stringWithUTF8String:title.c_str()]]; } void platformSend(WebViewInstance* wv, const std::string& message) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; NSString* escaped = [[NSString stringWithUTF8String:message.c_str()] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; escaped = [escaped stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]; escaped = [escaped stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; escaped = [escaped stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; NSString* js = [NSString stringWithFormat: @"if(window.__lnxrntOnMessage)window.__lnxrntOnMessage('%@');", escaped]; [state->webView evaluateJavaScript:js completionHandler:nil]; } void platformGoBack(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; if ([state->webView canGoBack]) [state->webView goBack]; } void platformGoForward(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; if ([state->webView canGoForward]) [state->webView goForward]; } void platformReload(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; [state->webView reload]; } bool platformCanGoBack(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return false; return [state->webView canGoBack]; } bool platformCanGoForward(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return false; return [state->webView canGoForward]; } std::string platformGetURL(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return ""; NSString* url = [state->webView.URL absoluteString]; return url ? std::string([url UTF8String]) : ""; } std::string platformGetTitle(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return ""; NSString* title = [state->webView title]; return title ? std::string([title UTF8String]) : ""; } void platformMinimize(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->isDetached || !state->detachedWindow) return; [state->detachedWindow miniaturize:nil]; } void platformMaximize(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->isDetached || !state->detachedWindow) return; [state->detachedWindow zoom:nil]; } void platformRestore(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->isDetached || !state->detachedWindow) return; if ([state->detachedWindow isMiniaturized]) [state->detachedWindow deminiaturize:nil]; if ([state->detachedWindow isZoomed]) [state->detachedWindow zoom:nil]; } void platformSetFullscreen(WebViewInstance* wv, bool fullscreen) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->isDetached || !state->detachedWindow) return; if (fullscreen) { [state->detachedWindow toggleFullScreen:nil]; } else { if (([state->detachedWindow styleMask] & NSWindowStyleMaskFullScreen) != 0) { [state->detachedWindow toggleFullScreen:nil]; } } } bool platformIsFullscreen(WebViewInstance* wv) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->isDetached || !state->detachedWindow) return false; return ([state->detachedWindow styleMask] & NSWindowStyleMaskFullScreen) != 0; } void platformEnableDevTools(WebViewInstance* wv, bool enabled) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; [state->webView.configuration.preferences setValue:@(enabled) forKey:@"developerExtrasEnabled"]; } void platformSetContextMenuEnabled(WebViewInstance* wv, bool enabled) { auto* state = (MacWebViewState*)wv->platformHandle; if (!state || !state->webView) return; if (!enabled) { NSString* js = @"document.addEventListener('contextmenu', function(e){e.preventDefault();}, true);"; [state->webView evaluateJavaScript:js completionHandler:nil]; } } void platformTick() { // WKWebView callbacks are in NSRunLoop already integrated with Kinc loop. } #endif #endif