#include "webview.h"
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <limits.h>
struct webview_priv {
id pool;
id window;
id webview;
id windowDelegate;
int should_exit;
};
struct cocoa_webview {
const char *url;
const char *title;
int width;
int height;
int resizable;
int debug;
int frameless;
webview_external_invoke_cb_t external_invoke_cb;
struct webview_priv priv;
void *userdata;
};
WEBVIEW_API void webview_free(webview_t w) {
free(w);
}
WEBVIEW_API void* webview_get_user_data(webview_t w) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
return wv->userdata;
}
WEBVIEW_API webview_t webview_new(
const char* title, const char* url,
int width, int height, int resizable, int debug, int frameless,
webview_external_invoke_cb_t external_invoke_cb, void* userdata) {
struct cocoa_webview* wv = (struct cocoa_webview*)calloc(1, sizeof(*wv));
wv->width = width;
wv->height = height;
wv->title = title;
wv->url = url;
wv->resizable = resizable;
wv->debug = debug;
wv->frameless = frameless;
wv->external_invoke_cb = external_invoke_cb;
wv->userdata = userdata;
if (webview_init(wv) != 0) {
webview_free(wv);
return NULL;
}
return wv;
}
#define NSAlertStyleWarning 0
#define NSAlertStyleCritical 2
#define NSWindowStyleMaskResizable 8
#define NSWindowStyleMaskMiniaturizable 4
#define NSWindowStyleMaskTitled 1
#define NSWindowStyleMaskClosable 2
#define NSWindowStyleMaskFullScreen (1 << 14)
#define NSViewWidthSizable 2
#define NSViewHeightSizable 16
#define NSBackingStoreBuffered 2
#define NSEventMaskAny ULONG_MAX
#define NSEventModifierFlagCommand (1 << 20)
#define NSEventModifierFlagOption (1 << 19)
#define NSAlertStyleInformational 1
#define NSAlertFirstButtonReturn 1000
#define WKNavigationActionPolicyDownload 2
#define NSModalResponseOK 1
#define WKNavigationActionPolicyDownload 2
#define WKNavigationResponsePolicyAllow 1
#define WKUserScriptInjectionTimeAtDocumentStart 0
#define NSApplicationActivationPolicyRegular 0
#define NSApplicationDefinedEvent 15
#define NSWindowStyleMaskBorderless 0
static id get_nsstring(const char *c_str) {
return objc_msgSend((id)objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"), c_str);
}
static id create_menu_item(id title, const char *action, const char *key) {
id item =
objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc"));
objc_msgSend(item, sel_registerName("initWithTitle:action:keyEquivalent:"),
title, sel_registerName(action), get_nsstring(key));
objc_msgSend(item, sel_registerName("autorelease"));
return item;
}
static void webview_window_will_close(id self, SEL cmd, id notification) {
struct cocoa_webview *wv =
(struct cocoa_webview *)objc_getAssociatedObject(self, "webview");
wv->priv.should_exit = 1;
id event = objc_msgSend((id)objc_getClass("NSEvent"),
sel_registerName("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:"),
NSApplicationDefinedEvent,
(id)objc_getClass("NSZeroPoint"),
0, 0.0, 0, NULL, 0, 0, 0);
id app = objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication"));
objc_msgSend(app, sel_registerName("postEvent:atStart:"), event,
objc_msgSend((id)objc_getClass("NSDate"),
sel_registerName("distantPast")));
}
static void webview_external_invoke(id self, SEL cmd, id contentController,
id message) {
struct cocoa_webview *wv =
(struct cocoa_webview *)objc_getAssociatedObject(contentController, "webview");
if (wv == NULL || wv->external_invoke_cb == NULL) {
return;
}
wv->external_invoke_cb(wv, (const char *)objc_msgSend(
objc_msgSend(message, sel_registerName("body")),
sel_registerName("UTF8String")));
}
static void run_open_panel(id self, SEL cmd, id webView, id parameters,
id frame, void (^completionHandler)(id)) {
id openPanel = objc_msgSend((id)objc_getClass("NSOpenPanel"),
sel_registerName("openPanel"));
objc_msgSend(
openPanel, sel_registerName("setAllowsMultipleSelection:"),
objc_msgSend(parameters, sel_registerName("allowsMultipleSelection")));
objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 1);
objc_msgSend(
openPanel, sel_registerName("beginWithCompletionHandler:"), ^(id result) {
if (result == (id)NSModalResponseOK) {
completionHandler(objc_msgSend(openPanel, sel_registerName("URLs")));
} else {
completionHandler(nil);
}
});
}
static void run_save_panel(id self, SEL cmd, id download, id filename,
void (^completionHandler)(int allowOverwrite,
id destination)) {
id savePanel = objc_msgSend((id)objc_getClass("NSSavePanel"),
sel_registerName("savePanel"));
objc_msgSend(savePanel, sel_registerName("setCanCreateDirectories:"), 1);
objc_msgSend(savePanel, sel_registerName("setNameFieldStringValue:"),
filename);
objc_msgSend(savePanel, sel_registerName("beginWithCompletionHandler:"),
^(id result) {
if (result == (id)NSModalResponseOK) {
id url = objc_msgSend(savePanel, sel_registerName("URL"));
id path = objc_msgSend(url, sel_registerName("path"));
completionHandler(1, path);
} else {
completionHandler(NO, nil);
}
});
}
static void run_confirmation_panel(id self, SEL cmd, id webView, id message,
id frame, void (^completionHandler)(bool)) {
id alert =
objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new"));
objc_msgSend(alert, sel_registerName("setIcon:"),
objc_msgSend((id)objc_getClass("NSImage"),
sel_registerName("imageNamed:"),
get_nsstring("NSCaution")));
objc_msgSend(alert, sel_registerName("setShowsHelp:"), 0);
objc_msgSend(alert, sel_registerName("setInformativeText:"), message);
objc_msgSend(alert, sel_registerName("addButtonWithTitle:"),
get_nsstring("OK"));
objc_msgSend(alert, sel_registerName("addButtonWithTitle:"),
get_nsstring("Cancel"));
if (objc_msgSend(alert, sel_registerName("runModal")) ==
(id)NSAlertFirstButtonReturn) {
completionHandler(true);
} else {
completionHandler(false);
}
objc_msgSend(alert, sel_registerName("release"));
}
static void run_alert_panel(id self, SEL cmd, id webView, id message, id frame,
void (^completionHandler)(void)) {
id alert =
objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new"));
objc_msgSend(alert, sel_registerName("setIcon:"),
objc_msgSend((id)objc_getClass("NSImage"),
sel_registerName("imageNamed:"),
get_nsstring("NSCaution")));
objc_msgSend(alert, sel_registerName("setShowsHelp:"), 0);
objc_msgSend(alert, sel_registerName("setInformativeText:"), message);
objc_msgSend(alert, sel_registerName("addButtonWithTitle:"),
get_nsstring("OK"));
objc_msgSend(alert, sel_registerName("runModal"));
objc_msgSend(alert, sel_registerName("release"));
completionHandler();
}
static void download_failed(id self, SEL cmd, id download, id error) {
printf("%s",
(const char *)objc_msgSend(
objc_msgSend(error, sel_registerName("localizedDescription")),
sel_registerName("UTF8String")));
}
static void make_nav_policy_decision(id self, SEL cmd, id webView, id response,
void (^decisionHandler)(int)) {
if (objc_msgSend(response, sel_registerName("canShowMIMEType")) == 0) {
decisionHandler(WKNavigationActionPolicyDownload);
} else {
decisionHandler(WKNavigationResponsePolicyAllow);
}
}
WEBVIEW_API int webview_init(webview_t w) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
wv->priv.pool = objc_msgSend((id)objc_getClass("NSAutoreleasePool"),
sel_registerName("new"));
objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication"));
static Class __WKScriptMessageHandler;
if(__WKScriptMessageHandler == NULL) {
__WKScriptMessageHandler = objc_allocateClassPair(
objc_getClass("NSObject"), "__WKScriptMessageHandler", 0);
class_addProtocol(__WKScriptMessageHandler, objc_getProtocol("WKScriptMessageHandler"));
class_addMethod(
__WKScriptMessageHandler,
sel_registerName("userContentController:didReceiveScriptMessage:"),
(IMP)webview_external_invoke, "v@:@@");
objc_registerClassPair(__WKScriptMessageHandler);
}
id scriptMessageHandler =
objc_msgSend((id)__WKScriptMessageHandler, sel_registerName("new"));
static Class __WKDownloadDelegate;
if(__WKDownloadDelegate == NULL) {
__WKDownloadDelegate = objc_allocateClassPair(
objc_getClass("NSObject"), "__WKDownloadDelegate", 0);
class_addProtocol(__WKDownloadDelegate, objc_getProtocol("WKDownloadDelegate"));
class_addMethod(
__WKDownloadDelegate,
sel_registerName("_download:decideDestinationWithSuggestedFilename:"
"completionHandler:"),
(IMP)run_save_panel, "v@:@@?");
class_addMethod(__WKDownloadDelegate,
sel_registerName("_download:didFailWithError:"),
(IMP)download_failed, "v@:@@");
objc_registerClassPair(__WKDownloadDelegate);
}
id downloadDelegate =
objc_msgSend((id)__WKDownloadDelegate, sel_registerName("new"));
static Class __WKPreferences;
if(__WKPreferences == NULL) {
__WKPreferences = objc_allocateClassPair(objc_getClass("WKPreferences"),
"__WKPreferences", 0);
objc_property_attribute_t type = {"T", "c"};
objc_property_attribute_t ownership = {"N", ""};
objc_property_attribute_t attrs[] = {type, ownership};
class_replaceProperty(__WKPreferences, "developerExtrasEnabled", attrs, 2);
objc_registerClassPair(__WKPreferences);
}
id wkPref = objc_msgSend((id)__WKPreferences, sel_registerName("new"));
objc_msgSend(wkPref, sel_registerName("setValue:forKey:"),
objc_msgSend((id)objc_getClass("NSNumber"),
sel_registerName("numberWithBool:"), !!wv->debug),
objc_msgSend((id)objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"),
"developerExtrasEnabled"));
id userController = objc_msgSend((id)objc_getClass("WKUserContentController"),
sel_registerName("new"));
objc_setAssociatedObject(userController, "webview", (id)(w),
OBJC_ASSOCIATION_ASSIGN);
objc_msgSend(
userController, sel_registerName("addScriptMessageHandler:name:"),
scriptMessageHandler,
objc_msgSend((id)objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"), "invoke"));
id windowExternalOverrideScript = objc_msgSend(
(id)objc_getClass("WKUserScript"), sel_registerName("alloc"));
objc_msgSend(
windowExternalOverrideScript,
sel_registerName("initWithSource:injectionTime:forMainFrameOnly:"),
get_nsstring("window.external = this; invoke = function(arg){ "
"webkit.messageHandlers.invoke.postMessage(arg); };"),
WKUserScriptInjectionTimeAtDocumentStart, 0);
objc_msgSend(userController, sel_registerName("addUserScript:"),
windowExternalOverrideScript);
id config = objc_msgSend((id)objc_getClass("WKWebViewConfiguration"),
sel_registerName("new"));
id processPool = objc_msgSend(config, sel_registerName("processPool"));
objc_msgSend(processPool, sel_registerName("_setDownloadDelegate:"),
downloadDelegate);
objc_msgSend(config, sel_registerName("setProcessPool:"), processPool);
objc_msgSend(config, sel_registerName("setUserContentController:"),
userController);
objc_msgSend(config, sel_registerName("setPreferences:"), wkPref);
static Class __NSWindowDelegate;
if(__NSWindowDelegate == NULL) {
__NSWindowDelegate = objc_allocateClassPair(objc_getClass("NSObject"),
"__NSWindowDelegate", 0);
class_addProtocol(__NSWindowDelegate, objc_getProtocol("NSWindowDelegate"));
class_replaceMethod(__NSWindowDelegate, sel_registerName("windowWillClose:"),
(IMP)webview_window_will_close, "v@:@");
objc_registerClassPair(__NSWindowDelegate);
}
wv->priv.windowDelegate =
objc_msgSend((id)__NSWindowDelegate, sel_registerName("new"));
objc_setAssociatedObject(wv->priv.windowDelegate, "webview", (id)(w),
OBJC_ASSOCIATION_ASSIGN);
id nsTitle =
objc_msgSend((id)objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"), wv->title);
CGRect r = CGRectMake(0, 0, wv->width, wv->height);
unsigned int style;
if (wv->frameless) {
style = NSWindowStyleMaskBorderless;
} else {
style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable;
}
if (wv->resizable) {
style = style | NSWindowStyleMaskResizable;
}
wv->priv.window =
objc_msgSend((id)objc_getClass("NSWindow"), sel_registerName("alloc"));
objc_msgSend(wv->priv.window,
sel_registerName("initWithContentRect:styleMask:backing:defer:"),
r, style, NSBackingStoreBuffered, 0);
objc_msgSend(wv->priv.window, sel_registerName("autorelease"));
objc_msgSend(wv->priv.window, sel_registerName("setTitle:"), nsTitle);
objc_msgSend(wv->priv.window, sel_registerName("setDelegate:"),
wv->priv.windowDelegate);
objc_msgSend(wv->priv.window, sel_registerName("center"));
static Class __WKUIDelegate;
if(__WKUIDelegate == NULL) {
__WKUIDelegate = objc_allocateClassPair(objc_getClass("NSObject"), "__WKUIDelegate", 0);
class_addProtocol(__WKUIDelegate, objc_getProtocol("WKUIDelegate"));
class_addMethod(__WKUIDelegate,
sel_registerName("webView:runOpenPanelWithParameters:"
"initiatedByFrame:completionHandler:"),
(IMP)run_open_panel, "v@:@@@?");
class_addMethod(__WKUIDelegate,
sel_registerName("webView:runJavaScriptAlertPanelWithMessage:"
"initiatedByFrame:completionHandler:"),
(IMP)run_alert_panel, "v@:@@@?");
class_addMethod(
__WKUIDelegate,
sel_registerName("webView:runJavaScriptConfirmPanelWithMessage:"
"initiatedByFrame:completionHandler:"),
(IMP)run_confirmation_panel, "v@:@@@?");
objc_registerClassPair(__WKUIDelegate);
}
id uiDel = objc_msgSend((id)__WKUIDelegate, sel_registerName("new"));
static Class __WKNavigationDelegate;
if(__WKNavigationDelegate == NULL) {
__WKNavigationDelegate = objc_allocateClassPair(
objc_getClass("NSObject"), "__WKNavigationDelegate", 0);
class_addProtocol(__WKNavigationDelegate,
objc_getProtocol("WKNavigationDelegate"));
class_addMethod(
__WKNavigationDelegate,
sel_registerName(
"webView:decidePolicyForNavigationResponse:decisionHandler:"),
(IMP)make_nav_policy_decision, "v@:@@?");
objc_registerClassPair(__WKNavigationDelegate);
}
id navDel = objc_msgSend((id)__WKNavigationDelegate, sel_registerName("new"));
wv->priv.webview =
objc_msgSend((id)objc_getClass("WKWebView"), sel_registerName("alloc"));
objc_msgSend(wv->priv.webview,
sel_registerName("initWithFrame:configuration:"), r, config);
objc_msgSend(wv->priv.webview, sel_registerName("setUIDelegate:"), uiDel);
objc_msgSend(wv->priv.webview, sel_registerName("setNavigationDelegate:"),
navDel);
id nsURL = objc_msgSend((id)objc_getClass("NSURL"),
sel_registerName("URLWithString:"),
get_nsstring(wv->url == NULL ? "" : wv->url));
objc_msgSend(wv->priv.webview, sel_registerName("loadRequest:"),
objc_msgSend((id)objc_getClass("NSURLRequest"),
sel_registerName("requestWithURL:"), nsURL));
objc_msgSend(wv->priv.webview, sel_registerName("setAutoresizesSubviews:"), 1);
objc_msgSend(wv->priv.webview, sel_registerName("setAutoresizingMask:"),
(NSViewWidthSizable | NSViewHeightSizable));
objc_msgSend(objc_msgSend(wv->priv.window, sel_registerName("contentView")),
sel_registerName("addSubview:"), wv->priv.webview);
objc_msgSend(wv->priv.window, sel_registerName("orderFrontRegardless"));
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("setActivationPolicy:"),
NSApplicationActivationPolicyRegular);
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("finishLaunching"));
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("activateIgnoringOtherApps:"), 1);
id menubar =
objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc"));
objc_msgSend(menubar, sel_registerName("initWithTitle:"), get_nsstring(""));
objc_msgSend(menubar, sel_registerName("autorelease"));
id appName = objc_msgSend(objc_msgSend((id)objc_getClass("NSProcessInfo"),
sel_registerName("processInfo")),
sel_registerName("processName"));
id appMenuItem =
objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc"));
objc_msgSend(appMenuItem,
sel_registerName("initWithTitle:action:keyEquivalent:"), appName,
NULL, get_nsstring(""));
id appMenu =
objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc"));
objc_msgSend(appMenu, sel_registerName("initWithTitle:"), appName);
objc_msgSend(appMenu, sel_registerName("autorelease"));
objc_msgSend(appMenuItem, sel_registerName("setSubmenu:"), appMenu);
objc_msgSend(menubar, sel_registerName("addItem:"), appMenuItem);
id title =
objc_msgSend(get_nsstring("Hide "),
sel_registerName("stringByAppendingString:"), appName);
id item = create_menu_item(title, "hide:", "h");
objc_msgSend(appMenu, sel_registerName("addItem:"), item);
item = create_menu_item(get_nsstring("Hide Others"),
"hideOtherApplications:", "h");
objc_msgSend(item, sel_registerName("setKeyEquivalentModifierMask:"),
(NSEventModifierFlagOption | NSEventModifierFlagCommand));
objc_msgSend(appMenu, sel_registerName("addItem:"), item);
item =
create_menu_item(get_nsstring("Show All"), "unhideAllApplications:", "");
objc_msgSend(appMenu, sel_registerName("addItem:"), item);
objc_msgSend(appMenu, sel_registerName("addItem:"),
objc_msgSend((id)objc_getClass("NSMenuItem"),
sel_registerName("separatorItem")));
title = objc_msgSend(get_nsstring("Quit "),
sel_registerName("stringByAppendingString:"), appName);
item = create_menu_item(title, "terminate:", "q");
objc_msgSend(appMenu, sel_registerName("addItem:"), item);
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("setMainMenu:"), menubar);
wv->priv.should_exit = 0;
return 0;
}
WEBVIEW_API int webview_loop(webview_t w, int blocking) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
id until = (blocking ? objc_msgSend((id)objc_getClass("NSDate"),
sel_registerName("distantFuture"))
: objc_msgSend((id)objc_getClass("NSDate"),
sel_registerName("distantPast")));
id app = objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication"));
id event = objc_msgSend(
app,
sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"),
ULONG_MAX, until,
objc_msgSend((id)objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"),
"kCFRunLoopDefaultMode"),
true);
if (event) {
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("sendEvent:"), event);
}
return wv->priv.should_exit;
}
WEBVIEW_API int webview_eval(webview_t w, const char *js) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
objc_msgSend(wv->priv.webview,
sel_registerName("evaluateJavaScript:completionHandler:"),
get_nsstring(js), NULL);
return 0;
}
WEBVIEW_API void webview_set_title(webview_t w, const char *title) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
objc_msgSend(wv->priv.window, sel_registerName("setTitle:"),
get_nsstring(title));
}
WEBVIEW_API void webview_set_fullscreen(webview_t w, int fullscreen) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
unsigned long windowStyleMask = (unsigned long)objc_msgSend(
wv->priv.window, sel_registerName("styleMask"));
int b = (((windowStyleMask & NSWindowStyleMaskFullScreen) ==
NSWindowStyleMaskFullScreen)
? 1
: 0);
if (b != fullscreen) {
objc_msgSend(wv->priv.window, sel_registerName("toggleFullScreen:"), NULL);
}
}
WEBVIEW_API void webview_set_color(webview_t w, uint8_t r, uint8_t g,
uint8_t b, uint8_t a) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
id color = objc_msgSend((id)objc_getClass("NSColor"),
sel_registerName("colorWithRed:green:blue:alpha:"),
(float)r / 255.0, (float)g / 255.0, (float)b / 255.0,
(float)a / 255.0);
objc_msgSend(wv->priv.window, sel_registerName("setBackgroundColor:"), color);
if (0.5 >= ((r / 255.0 * 299.0) + (g / 255.0 * 587.0) + (b / 255.0 * 114.0)) /
1000.0) {
objc_msgSend(wv->priv.window, sel_registerName("setAppearance:"),
objc_msgSend((id)objc_getClass("NSAppearance"),
sel_registerName("appearanceNamed:"),
get_nsstring("NSAppearanceNameVibrantDark")));
} else {
objc_msgSend(wv->priv.window, sel_registerName("setAppearance:"),
objc_msgSend((id)objc_getClass("NSAppearance"),
sel_registerName("appearanceNamed:"),
get_nsstring("NSAppearanceNameVibrantLight")));
}
objc_msgSend(wv->priv.window, sel_registerName("setOpaque:"), 0);
objc_msgSend(wv->priv.window,
sel_registerName("setTitlebarAppearsTransparent:"), 1);
}
static void webview_dispatch_cb(void *arg) {
struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)arg;
(context->fn)(context->w, context->arg);
free(context);
}
WEBVIEW_API void webview_dispatch(webview_t w, webview_dispatch_fn fn,
void *arg) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)malloc(
sizeof(struct webview_dispatch_arg));
context->w = w;
context->arg = arg;
context->fn = fn;
dispatch_async_f(dispatch_get_main_queue(), context, webview_dispatch_cb);
}
WEBVIEW_API void webview_exit(webview_t w) {
struct cocoa_webview* wv = (struct cocoa_webview*)w;
wv->external_invoke_cb = NULL;
objc_msgSend(wv->priv.window, sel_registerName("close"));
}
WEBVIEW_API void webview_print_log(const char *s) { printf("%s\n", s); }