Files
quark-shell-mac/quark-shell/QSHWebViewDelegate.m
2015-12-28 00:11:43 -08:00

626 lines
20 KiB
Objective-C

//
// QSHWebViewDelegate.m
// quark-shell
//
// Created by Xhacker Liu on 3/31/14.
// Copyright (c) 2014 Xhacker. All rights reserved.
//
#import "QSHWebViewDelegate.h"
#import "QSHPreferencesViewController.h"
#import "QSHWebScriptObjectConverter.h"
#import "QSHWebViewWindowController.h"
#import <MASShortcut+Monitoring.h>
#import <RHPreferences.h>
#import <Sparkle/Sparkle.h>
#import <ISO8601DateFormatter.h>
#import <StartAtLoginController.h>
static NSString * const kWebScriptNamespace = @"quark";
static const NSInteger kPreferencesDefaultHeight = 192;
@interface QSHWebViewDelegate () <NSUserNotificationCenterDelegate, WebPolicyDelegate> {
NSString *appVersion;
NSString *appBundleVersion;
NSString *platform;
BOOL debug;
}
@property (nonatomic) NSWindowController *preferencesWindowController;
@property (nonatomic) QSHWebViewWindowController *webViewWindowController;
@property (nonatomic) NSMutableDictionary *messageSubscribers;
@end
@implementation QSHWebViewDelegate
+ (void)initialize
{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"WebKitDeveloperExtras"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (instancetype)init
{
self = [super init];
if (self) {
_messageSubscribers = [NSMutableDictionary dictionary];
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
appVersion = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
appBundleVersion = [infoDictionary objectForKey:@"CFBundleVersion"];
platform = @"mac";
}
return self;
}
- (void)webView:(WebView *)webView didClearWindowObject:(WebScriptObject *)windowScriptObject forFrame:(WebFrame *)frame
{
[windowScriptObject setValue:self forKey:kWebScriptNamespace];
}
#pragma mark WebScripting Protocol
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)selector
{
if (selector == @selector(openPopup) ||
selector == @selector(closePopup) ||
selector == @selector(togglePopup) ||
selector == @selector(resizePopup:) ||
selector == @selector(quit) ||
selector == @selector(openURL:) ||
selector == @selector(changeIcon:) ||
selector == @selector(changeHighlightedIcon:) ||
selector == @selector(changeClickAction:) ||
selector == @selector(changeSecondaryClickAction:) ||
selector == @selector(changeLabel:) ||
selector == @selector(resetMenubarIcon) ||
selector == @selector(setLaunchAtLogin:) ||
selector == @selector(notify:) ||
selector == @selector(removeAllScheduledNotifications) ||
selector == @selector(removeAllDeliveredNotifications) ||
selector == @selector(addKeyboardShortcut:) ||
selector == @selector(clearKeyboardShortcut) ||
selector == @selector(setupPreferenes:) ||
selector == @selector(openPreferences) ||
selector == @selector(closePreferences) ||
selector == @selector(newWindow:) ||
selector == @selector(closeWindow) ||
selector == @selector(pin) ||
selector == @selector(unpin) ||
selector == @selector(checkUpdate:) ||
selector == @selector(checkUpdateInBackground:) ||
selector == @selector(emitMessage:withPayload:) ||
selector == @selector(subscribeMessage:withCallback:) ||
selector == @selector(showMenu:)) {
return NO;
}
return YES;
}
+ (NSString *)webScriptNameForSelector:(SEL)selector
{
id result = nil;
if (selector == @selector(resizePopup:)) {
result = @"resizePopup";
}
else if (selector == @selector(notify:)) {
result = @"notify";
}
else if (selector == @selector(changeIcon:)) {
result = @"setMenubarIcon";
}
else if (selector == @selector(changeHighlightedIcon:)) {
result = @"setMenubarHighlightedIcon";
}
else if (selector == @selector(changeClickAction:)) {
result = @"setClickAction";
}
else if (selector == @selector(changeSecondaryClickAction:)) {
result = @"setSecondaryClickAction";
}
else if (selector == @selector(changeLabel:)) {
result = @"setLabel";
}
else if (selector == @selector(openURL:)) {
result = @"openURL";
}
else if (selector == @selector(addKeyboardShortcut:)) {
result = @"addKeyboardShortcut";
}
else if (selector == @selector(setupPreferenes:)) {
result = @"setupPreferences";
}
else if (selector == @selector(newWindow:)) {
result = @"newWindow";
}
else if (selector == @selector(checkUpdate:)) {
result = @"checkUpdate";
}
else if (selector == @selector(checkUpdateInBackground:)) {
result = @"checkUpdateInBackground";
}
else if (selector == @selector(emitMessage:withPayload:)) {
result = @"emit";
}
else if (selector == @selector(subscribeMessage:withCallback:)) {
result = @"on";
}
else if (selector == @selector(showMenu:)) {
result = @"showMenu";
}
else if (selector == @selector(setLaunchAtLogin:)) {
result = @"setLaunchAtLogin";
}
return result;
}
+ (BOOL)isKeyExcludedFromWebScript:(const char *)name
{
if (strncmp(name, "appVersion", 10) == 0 ||
strncmp(name, "appBundleVersion", 16) == 0 ||
strncmp(name, "platform", 8) == 0 ||
strncmp(name, "debug", 5) == 0) {
return NO;
}
return YES;
}
#pragma mark - Methods for JavaScript
- (void)openPopup
{
[self.appDelegate showWindow];
}
- (void)closePopup
{
[self.appDelegate hideWindow];
}
- (void)togglePopup
{
[self.appDelegate toggleWindow];
}
- (void)resizePopup:(WebScriptObject *)obj
{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
NSDictionary *options = [converter dictionaryFromWebScriptObject:obj];
CGFloat width = [options[@"width"] doubleValue];
CGFloat height = [options[@"height"] doubleValue];
if (options[@"width"] && options[@"height"]) {
[self.appDelegate resizeWindow:CGSizeMake(width, height)];
}
}
- (void)quit
{
[NSApp terminate:nil];
}
- (void)openURL:(NSString *)url
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
}
- (void)changeIcon:(NSString *)base64
{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:base64]];
NSImage *icon = [[NSImage alloc] initWithData:data];
icon.size = NSMakeSize(20, 20);
if (IS_PRIOR_TO_10_9) {
self.statusItemView.icon = icon;
}
else {
[icon setTemplate:YES];
self.statusItem.button.image = icon;
}
}
- (void)changeHighlightedIcon:(NSString *)base64
{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:base64]];
NSImage *icon = [[NSImage alloc] initWithData:data];
self.statusItemView.highlightedIcon = icon;
}
- (void)changeClickAction:(WebScriptObject *)callback
{
self.appDelegate.clickCallback = ^{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
[converter callFunction:callback];
};
}
- (void)changeSecondaryClickAction:(WebScriptObject *)callback
{
self.appDelegate.secondaryClickCallback = ^{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
[converter callFunction:callback];
};
}
- (void)resetMenubarIcon
{
if (IS_PRIOR_TO_10_9) {
self.statusItemView.icon = [NSImage imageNamed:@"StatusIcon"];
self.statusItemView.highlightedIcon = [NSImage imageNamed:@"StatusIconWhite"];
}
else {
NSImage *icon = [NSImage imageNamed:@"StatusIcon"];
[icon setTemplate:YES];
self.statusItem.button.image = icon;
}
}
- (void)changeLabel:(NSString *)label
{
NSDictionary *barTextAttributes;
if (IS_PRIOR_TO_10_9) {
self.statusItemView.label = label;
barTextAttributes = @{NSFontAttributeName: MENUBAR_FONT};
}
else {
self.statusItem.title = label;
if (!IS_PRIOR_TO_10_10) {
self.statusItem.button.font = MENUBAR_FONT_10_11;
}
barTextAttributes = @{NSFontAttributeName: self.statusItem.button.font};
}
// 20 is image width, 10 is extra margin
self.statusItem.length = 20 + [label sizeWithAttributes:barTextAttributes].width + 10;
}
- (void)setLaunchAtLogin:(BOOL)launchAtLogin
{
StartAtLoginController *loginController = [[StartAtLoginController alloc] initWithIdentifier:@"com.hackplan.quark-shell-helper"];
loginController.startAtLogin = launchAtLogin;
}
- (void)notify:(WebScriptObject *)obj
{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
NSDictionary *message = [converter dictionaryFromWebScriptObject:obj];
NSUserNotification *notification = [[NSUserNotification alloc] init];
notification.title = message[@"title"];
notification.informativeText = message[@"content"];
notification.deliveryDate = [NSDate date];
notification.soundName = NSUserNotificationDefaultSoundName;
notification.userInfo = @{@"popupOnClick": message[@"popupOnClick"]};
if (message[@"time"]) {
static ISO8601DateFormatter *formatter;
if (!formatter) {
formatter = [[ISO8601DateFormatter alloc] init];
}
notification.deliveryDate = [formatter dateFromString:message[@"time"]];
}
NSUserNotificationCenter *notificationCenter = [NSUserNotificationCenter defaultUserNotificationCenter];
notificationCenter.delegate = self;
[notificationCenter scheduleNotification:notification];
}
- (void)removeAllScheduledNotifications
{
NSUserNotificationCenter *notificationCenter = [NSUserNotificationCenter defaultUserNotificationCenter];
notificationCenter.scheduledNotifications = @[];
}
- (void)removeAllDeliveredNotifications
{
NSUserNotificationCenter *notificationCenter = [NSUserNotificationCenter defaultUserNotificationCenter];
[notificationCenter removeAllDeliveredNotifications];
}
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
{
return YES;
}
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{
if (notification.userInfo[@"popupOnClick"]) {
[self.appDelegate showWindow];
}
[[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:notification];
}
- (void)addKeyboardShortcut:(WebScriptObject *)shortcutObj
{
NSUInteger keycode = [[shortcutObj valueForKey:@"keycode"] integerValue];
NSUInteger flags = [[shortcutObj valueForKey:@"modifierFlags"] integerValue];
if (keycode == 0 && flags == 0) {
// the shortcut recorder returns 0 0 for no shortcut
// however, 0 0 is a single 'a', in this case, shouldn't be fired
return;
}
WebScriptObject *callback = [shortcutObj valueForKey:@"callback"];
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:keycode modifierFlags:flags];
[MASShortcut removeGlobalHotkeyMonitor:shortcut.description];
[MASShortcut addGlobalHotkeyMonitorWithShortcut:shortcut handler:^{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
[converter callFunction:callback];
}];
}
- (void)clearKeyboardShortcut
{
[MASShortcut clearGlobalHotkeyMonitor];
}
- (void)setupPreferenes:(WebScriptObject *)scriptObj
{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
NSArray *preferencesArray = [converter arrayFromWebScriptObject:scriptObj];
NSMutableArray *viewControllers = [NSMutableArray array];
for (NSDictionary *preferences in preferencesArray) {
NSInteger height = preferences[@"height"] ? [preferences[@"height"] integerValue]: kPreferencesDefaultHeight;
QSHPreferencesViewController *vc = [[QSHPreferencesViewController alloc]
initWithIdentifier:preferences[@"identifier"]
toolbarImage:[NSImage imageNamed:preferences[@"icon"]]
toolbarLabel:preferences[@"label"]
height:height
delegate:self];
for (NSDictionary *component in preferences[@"nativeComponents"]) {
[vc addNativeComponent:component];
}
[viewControllers addObject:vc];
}
NSString *title = @"Preferences";
self.preferencesWindowController = [[RHPreferencesWindowController alloc] initWithViewControllers:viewControllers andTitle:title];
}
- (void)openPreferences
{
[self.preferencesWindowController showWindow:nil];
}
- (void)closePreferences
{
[self.preferencesWindowController close];
}
- (void)newWindow:(WebScriptObject *)scriptObj
{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
NSDictionary *options = [converter dictionaryFromWebScriptObject:scriptObj];
NSString *urlString = options[@"url"];
CGFloat width = [options[@"width"] doubleValue];
CGFloat height = [options[@"height"] doubleValue];
self.webViewWindowController = [[QSHWebViewWindowController alloc] initWithURLString:urlString width:width height:height];
if (options[@"x"] && options[@"y"]) {
CGFloat screenWidth = [[NSScreen mainScreen] frame].size.width;
CGFloat screenHeight = [[NSScreen mainScreen] frame].size.height;
CGFloat x = [options[@"x"] doubleValue];
if ([options[@"x"] isEqual:@"center"]) {
x = (screenWidth - width) / 2;
}
CGFloat yFlipped = screenHeight - [options[@"y"] doubleValue] - height;
if ([options[@"y"] isEqual:@"center"]) {
yFlipped = (screenHeight - height) / 2;
}
[self.webViewWindowController.window setFrameOrigin:NSMakePoint(x, yFlipped)];
}
if (options[@"border"] && [options[@"border"] boolValue] == NO) {
self.webViewWindowController.window.styleMask = NSBorderlessWindowMask;
}
if (options[@"shadow"] && [options[@"shadow"] boolValue] == NO) {
self.webViewWindowController.window.hasShadow = NO;
}
if ([options[@"alwaysOnTop"] boolValue]) {
self.webViewWindowController.window.level = NSScreenSaverWindowLevel;
}
if (options[@"alpha"]) {
self.webViewWindowController.window.alphaValue = [options[@"alpha"] doubleValue];
}
self.webViewWindowController.webView.frameLoadDelegate = self;
self.webViewWindowController.webView.UIDelegate = self;
self.webViewWindowController.webView.policyDelegate = self;
[self.webViewWindowController showWindow:nil];
}
- (void)closeWindow
{
[self.webViewWindowController close];
}
- (void)pin
{
self.appDelegate.pinned = YES;
}
- (void)unpin
{
self.appDelegate.pinned = NO;
}
- (void)checkUpdate:(NSString *)url
{
SUUpdater *updater = [[SUUpdater alloc] init];
updater.feedURL = [NSURL URLWithString:url];
[updater checkForUpdates:nil];
}
- (void)checkUpdateInBackground:(NSString *)url
{
SUUpdater *updater = [[SUUpdater alloc] init];
updater.feedURL = [NSURL URLWithString:url];
[updater checkForUpdatesInBackground];
}
- (void)emitMessage:(NSString *)name withPayload:(WebScriptObject *)payload
{
for (WebScriptObject *callback in self.messageSubscribers[name]) {
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
[converter callFunction:callback withArgs:@[payload]];
}
}
- (void)subscribeMessage:(NSString *)name withCallback:(WebScriptObject *)callback
{
if (self.messageSubscribers[name]) {
[self.messageSubscribers[name] addObject:callback];
}
else {
self.messageSubscribers[name] = [NSMutableArray arrayWithObject:callback];
}
}
- (void)showMenu:(WebScriptObject *)scriptObj
{
QSHWebScriptObjectConverter *converter = [[QSHWebScriptObjectConverter alloc] initWithWebView:self.webView];
NSDictionary *options = [converter dictionaryFromWebScriptObject:scriptObj];
NSMenu *menu = [[NSMenu alloc] init];
menu.autoenablesItems = NO;
for (NSDictionary *item in options[@"items"]) {
if ([item[@"type"] isEqualToString:@"separator"]) {
[menu addItem:[NSMenuItem separatorItem]];
}
else {
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:item[@"label"] action:@selector(menuItemClicked:) keyEquivalent:@""];
menuItem.target = self;
menuItem.representedObject = ^{
[converter callFunction:item[@"click"]];
};
[menuItem setEnabled:YES];
[menu addItem:menuItem];
}
}
CGFloat x = [options[@"x"] doubleValue];
CGFloat yFlipped = self.webView.frame.size.height - [options[@"y"] doubleValue];
[menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(x, yFlipped) inView:self.webView];
}
- (void)menuItemClicked:(NSMenuItem *)sender
{
void(^clickHandler)(void) = sender.representedObject;
clickHandler();
}
#pragma mark - Delegate methods
- (void)webView:(WebView *)webView addMessageToConsole:(NSDictionary *)message
{
if (![message isKindOfClass:[NSDictionary class]]) {
return;
}
NSLog(@"JavaScript console: %@:%@: %@",
[message[@"sourceURL"] lastPathComponent],
message[@"lineNumber"],
message[@"message"]);
}
- (void)webView:(WebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame
{
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:@"OK"];
alert.messageText = message;
alert.alertStyle = NSWarningAlertStyle;
[alert runModal];
}
- (BOOL)webView:(WebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame
{
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:@"Yes"];
[alert addButtonWithTitle:@"No"];
alert.messageText = message;
alert.alertStyle = NSWarningAlertStyle;
if ([alert runModal] == NSAlertFirstButtonReturn) {
return YES;
}
else {
return NO;
}
}
// Enable <input type="file">
- (void)webView:(WebView *)sender runOpenPanelForFileButtonWithResultListener:(id < WebOpenPanelResultListener >)resultListener
{
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
openPanel.canChooseFiles = YES;
openPanel.canChooseDirectories = NO;
[openPanel beginWithCompletionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
NSURL *fileURL = openPanel.URL;
[resultListener chooseFilename:fileURL.relativePath];
}
}];
}
// Enable WebSQL: http://stackoverflow.com/questions/353808/implementing-a-webview-database-quota-delegate
- (void)webView:(WebView *)sender frame:(WebFrame *)frame exceededDatabaseQuotaForSecurityOrigin:(id)origin database:(NSString *)databaseIdentifier
{
static const unsigned long long defaultQuota = 5 * 1024 * 1024;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([origin respondsToSelector:@selector(setQuota:)]) {
[origin performSelector:@selector(setQuota:) withObject:@(defaultQuota)];
}
#pragma clang diagnostic pop
else {
NSLog(@"Could not increase quota for %lld", defaultQuota);
}
}
- (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame
{
if (sender == self.webViewWindowController.webView) {
self.webViewWindowController.window.title = title;
}
}
#pragma mark WebPolicyDelegate
- (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
{
NSString *scheme = request.URL.scheme;
if ([scheme isEqualToString:@"file"]) {
[listener use];
}
else {
[listener ignore];
[[NSWorkspace sharedWorkspace] openURL:request.URL];
}
}
#pragma mark - WebUIDelegate
- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
{
if (debug) {
return defaultMenuItems;
}
return nil;
}
@end