Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added support for callbacks and menus #14

Closed
wants to merge 1 commit into from

6 participants

@hildjj

Tested callbacks on sound completion, then went on to support menus. Note: there are likely still some memory leaks due to the JavaScriptCore bridging fighting with ARC.

To use:
macgap.menu.getItem("File").submenu().addItem("Foo", "", function() { console.log("Foo!"); })

@asgeo1

Awesome! I really needed a JS bridge for the menu.

This commit no longer works with master though - I was able to just take out the bits I needed. It would probably be a good idea to split this into some smaller commits too.

@subtleGradient

Awesome. Any chance of getting menu support in any time soon? That'd be hip.

@micho

I'd love to see this in.

Speaking of menus, how hard would it be to add a status bar menu (like dropbox's) with this? It'd be great to have the option, for "always-on" apps.

@pferdefleisch

Bump :)

@jeff-h jeff-h referenced this pull request
Merged

Add Callbacks and Menus #95

@jeff-h
Owner

@hildjj — I have just committed a (modified) version of your code. Thanks so much for your work!

@jeff-h jeff-h closed this
@hildjj
@jeff-h
Owner

No worries—it was good to learn properly how it works. It's a great extension to MacGap's functionality too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 15, 2012
  1. @hildjj

    Added support for callbacks, tested first with sound (callback when d…

    hildjj authored
    …one playing) then expanded into menu support
This page is out of date. Refresh to see the latest.
View
28 MacGap.xcodeproj/project.pbxproj
@@ -7,6 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
+ BCAF5EBF14C327560007F7CE /* Command.m in Sources */ = {isa = PBXBuildFile; fileRef = BCAF5EBE14C327560007F7CE /* Command.m */; };
+ BCAF5EC414C327670007F7CE /* MenuItemProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = BCAF5EC114C327670007F7CE /* MenuItemProxy.m */; };
+ BCAF5EC514C327670007F7CE /* MenuProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = BCAF5EC314C327670007F7CE /* MenuProxy.m */; };
+ BCAF5EC814C327900007F7CE /* CallbackDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BCAF5EC714C327900007F7CE /* CallbackDelegate.m */; };
+ BCAF5ECA14C327B40007F7CE /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCAF5EC914C327B40007F7CE /* JavaScriptCore.framework */; };
FA32509D14BA813600BF0781 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA32509C14BA813600BF0781 /* WebKit.framework */; };
FA3250C314BA85E700BF0781 /* ContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = FA3250BC14BA85E700BF0781 /* ContentView.m */; };
FA3250C514BA85E700BF0781 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = FA3250BE14BA85E700BF0781 /* Utils.m */; };
@@ -42,6 +47,15 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ BCAF5EBD14C327560007F7CE /* Command.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Command.h; path = Classes/Commands/Command.h; sourceTree = "<group>"; };
+ BCAF5EBE14C327560007F7CE /* Command.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Command.m; path = Classes/Commands/Command.m; sourceTree = "<group>"; };
+ BCAF5EC014C327670007F7CE /* MenuItemProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MenuItemProxy.h; path = Classes/Commands/MenuItemProxy.h; sourceTree = "<group>"; };
+ BCAF5EC114C327670007F7CE /* MenuItemProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MenuItemProxy.m; path = Classes/Commands/MenuItemProxy.m; sourceTree = "<group>"; };
+ BCAF5EC214C327670007F7CE /* MenuProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MenuProxy.h; path = Classes/Commands/MenuProxy.h; sourceTree = "<group>"; };
+ BCAF5EC314C327670007F7CE /* MenuProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MenuProxy.m; path = Classes/Commands/MenuProxy.m; sourceTree = "<group>"; };
+ BCAF5EC614C327900007F7CE /* CallbackDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CallbackDelegate.h; path = Classes/CallbackDelegate.h; sourceTree = "<group>"; };
+ BCAF5EC714C327900007F7CE /* CallbackDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CallbackDelegate.m; path = Classes/CallbackDelegate.m; sourceTree = "<group>"; };
+ BCAF5EC914C327B40007F7CE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
FA32509C14BA813600BF0781 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
FA32509E14BA816E00BF0781 /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = MacGap/Growl.framework; sourceTree = "<group>"; };
FA3250BA14BA85E700BF0781 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Constants.h; path = Classes/Constants.h; sourceTree = "<group>"; };
@@ -84,6 +98,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ BCAF5ECA14C327B40007F7CE /* JavaScriptCore.framework in Frameworks */,
FA3250DC14BA875600BF0781 /* Growl.framework in Frameworks */,
FA32509D14BA813600BF0781 /* WebKit.framework in Frameworks */,
FAE451BF14BA79C600190544 /* Cocoa.framework in Frameworks */,
@@ -98,6 +113,8 @@
children = (
FA3250E114BA87DD00BF0781 /* Commands */,
FA3250BA14BA85E700BF0781 /* Constants.h */,
+ BCAF5EC614C327900007F7CE /* CallbackDelegate.h */,
+ BCAF5EC714C327900007F7CE /* CallbackDelegate.m */,
FA3250BB14BA85E700BF0781 /* ContentView.h */,
FA3250BC14BA85E700BF0781 /* ContentView.m */,
FA3250BF14BA85E700BF0781 /* WebViewDelegate.h */,
@@ -109,12 +126,18 @@
FA3250E114BA87DD00BF0781 /* Commands */ = {
isa = PBXGroup;
children = (
+ BCAF5EBD14C327560007F7CE /* Command.h */,
+ BCAF5EBE14C327560007F7CE /* Command.m */,
FA3250CA14BA860800BF0781 /* Dock.h */,
FA3250CB14BA860800BF0781 /* Dock.m */,
FA3250BD14BA85E700BF0781 /* Utils.h */,
FA3250BE14BA85E700BF0781 /* Utils.m */,
FA3250CC14BA860800BF0781 /* Growl.h */,
FA3250CD14BA860800BF0781 /* Growl.m */,
+ BCAF5EC014C327670007F7CE /* MenuItemProxy.h */,
+ BCAF5EC114C327670007F7CE /* MenuItemProxy.m */,
+ BCAF5EC214C327670007F7CE /* MenuProxy.h */,
+ BCAF5EC314C327670007F7CE /* MenuProxy.m */,
FA3250CE14BA860800BF0781 /* Path.h */,
FA3250CF14BA860800BF0781 /* Path.m */,
FA3250D014BA860800BF0781 /* Sound.h */,
@@ -146,6 +169,7 @@
FAE451BD14BA79C600190544 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ BCAF5EC914C327B40007F7CE /* JavaScriptCore.framework */,
FA3250DE14BA878D00BF0781 /* Growl.framework */,
FA32509E14BA816E00BF0781 /* Growl.framework */,
FA32509C14BA813600BF0781 /* WebKit.framework */,
@@ -267,6 +291,10 @@
FA3250C714BA85E700BF0781 /* WebViewDelegate.m in Sources */,
FAE451CB14BA79C600190544 /* main.m in Sources */,
FAE451D214BA79C600190544 /* AppDelegate.m in Sources */,
+ BCAF5EBF14C327560007F7CE /* Command.m in Sources */,
+ BCAF5EC414C327670007F7CE /* MenuItemProxy.m in Sources */,
+ BCAF5EC514C327670007F7CE /* MenuProxy.m in Sources */,
+ BCAF5EC814C327900007F7CE /* CallbackDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
20 MacGap/Classes/CallbackDelegate.h
@@ -0,0 +1,20 @@
+//
+// CallbackDelegate.h
+// MacGap
+//
+// Created by Joe Hildebrand on 1/10/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import "Command.h"
+
+@interface CallbackDelegate : Command {
+}
+
+@property JSObjectRef callback;
+
+- (id) initWithContext:(JSContextRef)aContext forCallback:(WebScriptObject*)aCallback;
+- (id) call;
+- (id) callWithParams:(id)firstOrNil, ... NS_REQUIRES_NIL_TERMINATION;
+
+@end
View
168 MacGap/Classes/CallbackDelegate.m
@@ -0,0 +1,168 @@
+//
+// CallbackDelegate.m
+// MacGap
+//
+// Created by Joe Hildebrand on 1/10/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import "CallbackDelegate.h"
+#import <JavaScriptCore/JavaScript.h>
+
+@implementation CallbackDelegate
+
+@synthesize callback;
+
+- (id) initWithContext:(JSContextRef)aContext forCallback:(WebScriptObject*)aCallback
+{
+ if (!aCallback)
+ return nil;
+ if ([aCallback isKindOfClass:[WebUndefined class]])
+ return nil;
+
+ self = [super initWithContext:aContext];
+ if (!self)
+ return nil;
+
+ callback = [aCallback JSObject];
+ JSValueProtect(context, callback);
+ return self;
+}
+
+- (void) dealloc
+{
+ if (callback)
+ {
+ JSValueUnprotect(context, callback);
+ callback = nil;
+ }
+}
+
+- (id) objectFromValue:(JSValueRef)val
+{
+ JSStringRef jstr;
+ NSString *rets;
+
+ switch(JSValueGetType(context, val))
+ {
+ case kJSTypeUndefined:
+ case kJSTypeNull:
+ return nil;
+ case kJSTypeBoolean:
+ return [NSNumber numberWithBool:JSValueToBoolean(context, val)];
+ case kJSTypeNumber:
+ return [NSNumber numberWithDouble:JSValueToNumber(context, val, NULL)];
+ case kJSTypeString:
+ jstr = JSValueToStringCopy(context, val, NULL);
+ size_t sz = JSStringGetMaximumUTF8CStringSize(jstr);
+ char *buf = (char*)malloc(sz);
+ JSStringGetUTF8CString(jstr, buf, sz);
+ rets = [NSString stringWithUTF8String:buf];
+ free(buf);
+ return rets;
+ case kJSTypeObject:
+ // TODO: dictionary or something
+ return nil;
+ default:
+ NSAssert(false, @"Invalid JavaScript type");
+ return nil;
+ }
+}
+
+- (JSValueRef) valueFromObject:(id)obj
+{
+ JSValueRef val = nil;
+ if (!obj)
+ {
+ val = JSValueMakeNull(context);
+ }
+ else if ([obj isKindOfClass:[NSString class]])
+ {
+ JSStringRef jstr = JSStringCreateWithUTF8CString([obj UTF8String]);
+ val = JSValueMakeString(context, jstr);
+ JSStringRelease(jstr);
+ }
+ else if ([obj isKindOfClass:[NSNumber class]])
+ {
+ val = JSValueMakeNumber(context, [obj doubleValue]);
+ }
+ else if ([obj isKindOfClass:[NSDictionary class]])
+ {
+ JSObjectRef o = JSObjectMake(context, NULL, NULL);
+ for (NSString *key in obj)
+ {
+ JSStringRef kstr = JSStringCreateWithUTF8CString([key UTF8String]);
+ JSValueRef v = [self valueFromObject:[obj objectForKey:key]];
+
+ JSObjectSetProperty(context, o, kstr, v, kJSPropertyAttributeNone, NULL);
+ JSStringRelease(kstr);
+ }
+ val = o;
+ }
+ else if ([obj isKindOfClass:[NSArray class]])
+ {
+ NSUInteger pcount = [obj count];
+ JSValueRef jsArgs[pcount];
+ NSUInteger i=0;
+ for (id v in obj)
+ {
+ jsArgs[i++] = [self valueFromObject:v];
+ }
+ val = JSObjectMakeArray(context, pcount, jsArgs, NULL);
+ }
+ else if ([obj isKindOfClass:[NSDate class]])
+ {
+ NSTimeInterval secs = [obj timeIntervalSince1970];
+ JSValueRef jsArgs[1];
+ // call the Date(milliseconds) constructor in JS
+ jsArgs[0] = JSValueMakeNumber(context, secs * 1000.0);
+ val = JSObjectMakeDate(context, 1, jsArgs, NULL);
+ }
+ else
+ {
+ NSLog(@"Warning: unknown object type for: %@", obj);
+ val = JSValueMakeUndefined(context);
+ }
+ return val;
+}
+
+- (id) call
+{
+ NSAssert(callback, @"Callback required");
+ if (!JSObjectIsFunction(context, callback))
+ return nil;
+
+ JSValueRef jsArgs[0];
+ JSValueRef ret = JSObjectCallAsFunction(context, callback, NULL, 0, jsArgs, NULL);
+ return [self objectFromValue:ret];
+}
+
+- (id) callWithParams:(id)firstOrNil, ...
+{
+ NSAssert(callback, @"Callback required");
+ if (!JSObjectIsFunction(context, callback))
+ return nil;
+ NSUInteger pcount = 0;
+ id p;
+ va_list args;
+ va_start(args, firstOrNil);
+ for (p=firstOrNil; p; p=va_arg(args, id))
+ {
+ pcount++;
+ }
+ va_end(args);
+
+ JSValueRef jsArgs[pcount];
+ NSUInteger j = 0;
+ va_start(args, firstOrNil);
+ for (p=firstOrNil; p; p=va_arg(args, id))
+ {
+ jsArgs[j++] = [self valueFromObject:p];
+ }
+ va_end(args);
+
+ JSValueRef ret = JSObjectCallAsFunction(context, callback, NULL, j, jsArgs, NULL);
+ return [self objectFromValue:ret];
+}
+
+@end
View
18 MacGap/Classes/Commands/Command.h
@@ -0,0 +1,18 @@
+//
+// Command.h
+// MacGap
+//
+// Created by Joe Hildebrand on 1/10/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <Webkit/WebScriptObject.h>
+
+@interface Command : NSObject {
+ JSContextRef context;
+}
+
+- (id) initWithContext:(JSContextRef)aContext;
+
+@end
View
28 MacGap/Classes/Commands/Command.m
@@ -0,0 +1,28 @@
+//
+// Command.m
+// MacGap
+//
+// Created by Joe Hildebrand on 1/10/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import "Command.h"
+#import <JavaScriptCore/JSContextRef.h>
+
+@implementation Command
+
+- (id) initWithContext:(JSContextRef)aContext {
+ self = [super init];
+ if (!self)
+ return nil;
+ context = aContext;
+ JSGlobalContextRetain((JSGlobalContextRef)context);
+ return self;
+}
+
+- (void)dealloc
+{
+ if (context)
+ JSGlobalContextRelease((JSGlobalContextRef)context);
+}
+@end
View
29 MacGap/Classes/Commands/MenuItemProxy.h
@@ -0,0 +1,29 @@
+//
+// MenuItemProxy.h
+// MacGap
+//
+// Created by Joe Hildebrand on 1/15/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import "Command.h"
+#import "CallbackDelegate.h"
+
+@class MenuProxy;
+
+@interface MenuItemProxy : Command {
+ NSMenuItem *item;
+ CallbackDelegate *callback;
+}
+
++ (MenuItemProxy*) proxyWithContext:(JSContextRef)aContext andMenuItem:(NSMenuItem*)anItem;
+
+- (MenuProxy*)addSubmenu;
+
+- (void) remove;
+- (void)setCallback:(WebScriptObject*)aCallback;
+- (void)setKey:(NSString*)aString;
+- (void) setTitle:(NSString*)title;
+- (MenuProxy*)submenu;
+
+@end
View
130 MacGap/Classes/Commands/MenuItemProxy.m
@@ -0,0 +1,130 @@
+//
+// MenuItemProxy.m
+// MacGap
+//
+// Created by Joe Hildebrand on 1/15/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import "MenuItemProxy.h"
+#import "MenuProxy.h"
+
+@implementation MenuItemProxy
+
+- (id) initWithContext:(JSContextRef)aContext andMenuItem:(NSMenuItem*)anItem
+{
+ NSAssert(anItem, @"anItem required");
+ self = [super initWithContext:aContext];
+ if (!self)
+ return nil;
+ item = anItem;
+ item.representedObject = self;
+
+ return self;
+}
+
++ (MenuItemProxy*) proxyWithContext:(JSContextRef)aContext andMenuItem:(NSMenuItem*)anItem
+{
+ MenuItemProxy *proxy = [anItem representedObject];
+ if (proxy)
+ {
+ NSLog(@"MIP Cache hit");
+ NSAssert([proxy class] == [MenuItemProxy class], @"Bad proxy");
+ return proxy;
+ }
+ return [[MenuItemProxy alloc] initWithContext:aContext andMenuItem:anItem];
+}
+
+- (NSString*) description
+{
+ return [item description];
+}
+
+- (MenuProxy*)addSubmenu
+{
+ NSMenu *s = [item submenu];
+ if (!s)
+ {
+ s = [[NSMenu alloc] initWithTitle:@"FFFFFFOOOOO"];
+ [item setSubmenu:s];
+ }
+ return [MenuProxy proxyWithContext:context andMenu:s];
+}
+
+- (void) remove
+{
+ NSMenu *menu = [item menu];
+ [menu removeItem:item];
+}
+
+- (void)callCallback:(id)sender
+{
+ [callback callWithParams:[sender title], nil];
+}
+
+- (void) setCallback:(WebScriptObject*)aCallback
+{
+ NSAssert(item, @"item required");
+ callback = [[CallbackDelegate alloc] initWithContext:context forCallback:aCallback];
+ [item setAction:@selector(callCallback:)];
+ [item setTarget:self];
+}
+
+- (void)setKey:(NSString*)aString
+{
+ [item setKeyEquivalent:aString];
+}
+
+- (void) setTitle:(NSString*)title
+{
+ [item setTitle:title];
+}
+
+- (MenuProxy*)submenu;
+{
+ // TODO: make this work as a property
+ NSMenu *s = [item submenu];
+ if (!s)
+ return nil;
+ return [MenuProxy proxyWithContext:context andMenu:s];
+}
+
+#pragma mark WebScripting protocol
+
++ (BOOL) isSelectorExcludedFromWebScript:(SEL)selector
+{
+ return [self webScriptNameForSelector:selector] == nil;
+}
+
++ (BOOL) isKeyExcludedFromWebScript:(const char*)name
+{
+ return YES;
+}
+
++ (NSString*) webScriptNameForSelector:(SEL)selector
+{
+ id result = nil;
+
+ if (selector == @selector(addSubmenu)) {
+ result = @"addSubmenu";
+ }
+ else if (selector == @selector(remove)) {
+ result = @"remove";
+ }
+ else if (selector == @selector(setCallback:)) {
+ result = @"setCallback";
+ }
+ else if (selector == @selector(setKey:)) {
+ result = @"setKey";
+ }
+ else if (selector == @selector(setTitle:)) {
+ result = @"setTitle";
+ }
+ else if (selector == @selector(submenu)) {
+ result = @"submenu";
+ }
+
+ return result;
+}
+
+@end
View
25 MacGap/Classes/Commands/MenuProxy.h
@@ -0,0 +1,25 @@
+//
+// MenuProxy.h
+// MacGap
+//
+// Created by Joe Hildebrand on 1/14/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import "Command.h"
+
+@class MenuItemProxy;
+
+@interface MenuProxy : Command {
+ NSMenu *menu;
+}
+
++ (MenuProxy*)proxyWithContext:(JSContextRef)aContext andMenu:(NSMenu*)aMenu;
+
+- (MenuItemProxy*)addItemWithTitle:(NSString*)title
+ keyEquivalent:(NSString*)aKey
+ callback:(WebScriptObject*)aCallback;
+- (MenuItemProxy*)addSeparator;
+- (MenuItemProxy*)itemForKey:(id)key;
+
+@end
View
188 MacGap/Classes/Commands/MenuProxy.m
@@ -0,0 +1,188 @@
+//
+// MenuProxy.m
+// MacGap
+//
+// Created by Joe Hildebrand on 1/14/12.
+// Copyright (c) 2012 Twitter. All rights reserved.
+//
+
+#import <objc/runtime.h>
+#import <JavaScriptCore/JavaScript.h>
+
+#import "MenuProxy.h"
+#import "MenuItemProxy.h"
+
+static char REPRESENTED_OBJECT;
+
+@interface NSMenu (represented)
+@property (strong) id representedObject;
+@end
+
+@implementation NSMenu (represented)
+
+- (id) representedObject
+{
+ return objc_getAssociatedObject(self, &REPRESENTED_OBJECT);
+}
+
+- (void) setRepresentedObject:(id)representedObject
+{
+ objc_setAssociatedObject(self,
+ &REPRESENTED_OBJECT,
+ representedObject,
+ OBJC_ASSOCIATION_RETAIN);
+}
+
+@end
+
+@implementation MenuProxy
+
+- (id) initWithContext:(JSContextRef)aContext andMenu:(NSMenu*)aMenu
+{
+ self = [super initWithContext:aContext];
+ if (!self)
+ return nil;
+ menu = aMenu;
+ menu.representedObject = self;
+ return self;
+}
+
++ (MenuProxy*)proxyWithContext:(JSContextRef)aContext andMenu:(NSMenu*)aMenu
+{
+ // singleton-ish.
+ MenuProxy *ret = [aMenu representedObject];
+ if (ret)
+ {
+ NSLog(@"MP cache hit");
+ return ret;
+ }
+ return [[MenuProxy alloc] initWithContext:aContext andMenu:aMenu];
+}
+
+- (void) dealloc
+{
+ menu.representedObject = nil;
+}
+
+- (NSString*) description
+{
+ return [menu description];
+}
+
+static BOOL isNullish(id o)
+{
+ if (!o)
+ return YES;
+ if ([o isKindOfClass:[WebUndefined class]])
+ return YES;
+ return NO;
+}
+
+- (MenuItemProxy*)addItemWithTitle:(NSString*)title
+ keyEquivalent:(NSString*)aKey
+ callback:(WebScriptObject*)aCallback
+{
+ if (isNullish(title))
+ title = @"";
+ if (isNullish(aKey))
+ aKey = @"";
+ NSMenuItem *item = [menu addItemWithTitle:title action:nil keyEquivalent:aKey];
+ MenuItemProxy *mip = [MenuItemProxy proxyWithContext:context andMenuItem:item];
+ if (!isNullish(aCallback))
+ [mip setCallback:aCallback];
+ return mip;
+}
+
+- (MenuItemProxy*)addSeparator
+{
+ NSMenuItem *sep = [NSMenuItem separatorItem];
+ [menu addItem:sep];
+ return [MenuItemProxy proxyWithContext:context andMenuItem:sep];
+}
+
+- (MenuItemProxy*)itemForKey:(id)key
+{
+ if (isNullish(key))
+ return nil;
+ NSMenuItem *item = nil;
+ if ([key isKindOfClass:[NSNumber class]])
+ {
+ item = [menu itemAtIndex:[key intValue]];
+ }
+ else if ([key isKindOfClass:[NSString class]])
+ {
+ item = [menu itemWithTitle:key];
+ if (!item)
+ {
+ // Try again, with ... appended. e.g. "Save..."
+ item = [menu itemWithTitle:
+ [key stringByAppendingString:@"\u2026"]];
+ }
+ }
+ if (!item)
+ return nil;
+
+ return [MenuItemProxy proxyWithContext:context andMenuItem:item];
+}
+
+/*
+- (id) valueForUndefinedKey:(NSString *)key
+{
+ NSLog(@"valueForUndefinedKey: %@", key);
+ NSScanner *scan = [NSScanner scannerWithString:key];
+ NSInteger index = 0;
+ NSMenuItem *item;
+
+ if ([scan scanInteger:&index])
+ item = [menu itemAtIndex:index];
+ else
+ item = [menu itemWithTitle:key];
+ if (!item)
+ return nil;
+
+ return [MenuItemProxy proxyWithContext:context andMenuItem:item];
+}
+
+- (id)invokeUndefinedMethodFromWebScript:(NSString *)name withArguments:(NSArray *)args
+{
+ NSLog(@"invokeUndefinedMethodFromWebScript: %@", name);
+ // There is something magical about this method. It must be used by
+ // JavaScriptCore to detect if you're doing dynamic processing. It must be here,
+ // but is never called for undefined properties.
+ return nil;
+}
+
++ (BOOL)isKeyExcludedFromWebScript:(const char *)name
+{
+ return NO;
+}
+*/
+
++ (BOOL) isSelectorExcludedFromWebScript:(SEL)selector
+{
+ return [self webScriptNameForSelector:selector] == nil;
+}
+
++ (BOOL) isKeyExcludedFromWebScript:(const char*)name
+{
+ return YES;
+}
+
++ (NSString*) webScriptNameForSelector:(SEL)selector
+{
+ id result = nil;
+
+ if (selector == @selector(addItemWithTitle:keyEquivalent:callback:)) {
+ result = @"addItem";
+ }
+ else if (selector == @selector(addSeparator)) {
+ result = @"addSeparator";
+ }
+ else if (selector == @selector(itemForKey:)) {
+ result = @"getItem";
+ }
+
+ return result;
+}
+
+@end
View
14 MacGap/Classes/Commands/Sound.h
@@ -1,11 +1,15 @@
#import <Cocoa/Cocoa.h>
+#import "Command.h"
+#import "CallbackDelegate.h"
-
-@interface Sound : NSObject {
-
+@interface Sound : Command {
}
-- (void) play:(NSString*)file;
-- (void) playSystem:(NSString*)name;
+// pending callbacks for sounds being played, to keep
+// ARC from freeing them too early
+@property (nonatomic, strong) NSMutableSet *pending;
+
+- (void) play:(NSString*)file onComplete:(WebScriptObject*)callback;
+- (void) playSystem:(NSString*)name onComplete:(WebScriptObject*)callback;
@end
View
75 MacGap/Classes/Commands/Sound.m
@@ -1,29 +1,80 @@
#import "Sound.h"
+@interface PlayDelegate : CallbackDelegate <NSSoundDelegate> {
+}
+@property (nonatomic, weak) Sound *sound;
+- (id) initWithContext:(JSContextRef)aContext
+ forCallback:(WebScriptObject*)aCallback
+ withSound:(Sound*)aSound;
+@end
+
+@implementation PlayDelegate
+@synthesize sound;
+- (id) initWithContext:(JSContextRef)aContext
+ forCallback:(WebScriptObject*)aCallback
+ withSound:(Sound*)aSound
+{
+ self = [super initWithContext:aContext forCallback:aCallback];
+ if (!self)
+ return nil;
+ sound = aSound;
+ return self;
+}
+
+- (void)sound:(NSSound *)aSound didFinishPlaying:(BOOL)finishedPlaying
+{
+ [self callWithParams:[aSound name], nil];
+ [sound.pending removeObject:self];
+}
+@end
+
@implementation Sound
+@synthesize pending;
-- (void) play:(NSString*)file
+- (id) initWithContext:(JSContextRef)aContext
+{
+ self = [super initWithContext:aContext];
+ if (!self)
+ return nil;
+ pending = [NSMutableSet new];
+ return self;
+}
+
+- (void)playSound:(NSSound*)sound onComplete:(WebScriptObject*)callback
+{
+ if (callback)
+ {
+ PlayDelegate *d = [[PlayDelegate alloc] initWithContext:context
+ forCallback:callback
+ withSound:self];
+ [pending addObject:d];
+ [sound setDelegate:d];
+ }
+ [sound play];
+}
+
+- (void) play:(NSString*)file onComplete:(WebScriptObject*)callback
{
NSURL* fileUrl = [NSURL fileURLWithPath:[[Utils sharedInstance] pathForResource:file]];
DebugNSLog(@"Sound file:%@", [fileUrl description]);
NSSound* sound = [[NSSound alloc] initWithContentsOfURL:fileUrl byReference:YES];
- [sound play];
+ [self playSound:sound onComplete:callback];
}
-- (void) playSystem:(NSString*) name
+- (void) playSystem:(NSString*)name onComplete:(WebScriptObject*)callback
{
NSSound *systemSound = [NSSound soundNamed:name];
- [systemSound play];
+ [self playSound:systemSound onComplete:callback];
}
#pragma mark WebScripting Protocol
+ (BOOL) isSelectorExcludedFromWebScript:(SEL)selector
{
- return NO;
+ return [self webScriptNameForSelector:selector] == nil;
}
+ (BOOL) isKeyExcludedFromWebScript:(const char*)name
@@ -31,4 +82,18 @@ + (BOOL) isKeyExcludedFromWebScript:(const char*)name
return YES;
}
++ (NSString*) webScriptNameForSelector:(SEL)selector
+{
+ id result = nil;
+
+ if (selector == @selector(play:onComplete:)) {
+ result = @"play";
+ }
+ else if (selector == @selector(playSystem:onComplete:)) {
+ result = @"playSystem";
+ }
+
+ return result;
+}
+
@end
View
1  MacGap/Classes/ContentView.h
@@ -10,5 +10,6 @@
@property (retain) WebView* webView;
@property (retain) WebViewDelegate* delegate;
+@property (strong) IBOutlet NSMenu *mainMenu;
@end
View
4 MacGap/Classes/ContentView.m
@@ -8,7 +8,7 @@ - (void)_setLocalStorageDatabasePath:(NSString *)path;
@implementation ContentView
-@synthesize webView, delegate;
+@synthesize webView, delegate, mainMenu;
- (void) awakeFromNib
{
@@ -21,7 +21,7 @@ - (void) awakeFromNib
[self.webView setPreferences:webPrefs];
- self.delegate = [[WebViewDelegate alloc] init];
+ self.delegate = [[WebViewDelegate alloc] initWithMenu:mainMenu];
[self.webView setFrameLoadDelegate:self.delegate];
[self.webView setUIDelegate:self.delegate];
[self.webView setResourceLoadDelegate:self.delegate];
View
9 MacGap/Classes/WebViewDelegate.h
@@ -6,13 +6,10 @@
@class Growl;
@class Path;
@class App;
+@class MenuProxy;
@interface WebViewDelegate : NSObject {
- Sound* sound;
- Dock* dock;
- Growl* growl;
- Path* path;
- App* app;
+ NSMenu *mainMenu;
}
@property (nonatomic, retain) Sound* sound;
@@ -20,5 +17,7 @@
@property (nonatomic, retain) Growl* growl;
@property (nonatomic, retain) Path* path;
@property (nonatomic, retain) App* app;
+@property (nonatomic, retain) MenuProxy* menu;
+- (id) initWithMenu:(NSMenu*)menu;
@end
View
16 MacGap/Classes/WebViewDelegate.m
@@ -4,6 +4,7 @@
#import "Growl.h"
#import "Path.h"
#import "App.h"
+#import "MenuProxy.h"
@implementation WebViewDelegate
@@ -12,14 +13,27 @@ @implementation WebViewDelegate
@synthesize growl;
@synthesize path;
@synthesize app;
+@synthesize menu;
+
+- (id) initWithMenu:(NSMenu*)aMenu
+{
+ self = [super init];
+ if (!self)
+ return nil;
+
+ mainMenu = aMenu;
+ return self;
+}
- (void) webView:(WebView*)webView didClearWindowObject:(WebScriptObject*)windowScriptObject forFrame:(WebFrame *)frame
{
- if (self.sound == nil) { self.sound = [Sound new]; }
+ JSContextRef context = [frame globalContext];
+ if (self.sound == nil) { self.sound = [[Sound alloc] initWithContext:context]; }
if (self.dock == nil) { self.dock = [Dock new]; }
if (self.growl == nil) { self.growl = [Growl new]; }
if (self.path == nil) { self.path = [Path new]; }
if (self.app == nil) { self.app = [App new]; }
+ if (self.menu == nil) { self.menu = [MenuProxy proxyWithContext:context andMenu:mainMenu]; }
[windowScriptObject setValue:self forKey:kWebScriptNamespace];
}
View
33 MacGap/en.lproj/MainMenu.xib
@@ -3,11 +3,11 @@
<data>
<int key="IBDocument.SystemTarget">1070</int>
<string key="IBDocument.SystemVersion">11C74</string>
- <string key="IBDocument.InterfaceBuilderVersion">1923</string>
+ <string key="IBDocument.InterfaceBuilderVersion">1938</string>
<string key="IBDocument.AppKitVersion">1138.23</string>
<string key="IBDocument.HIToolboxVersion">567.00</string>
<dictionary class="NSMutableDictionary" key="IBDocument.PluginVersions">
- <string key="com.apple.InterfaceBuilder.CocoaPlugin">1923</string>
+ <string key="com.apple.InterfaceBuilder.CocoaPlugin">1938</string>
<string key="com.apple.WebKitIBPlugin">822</string>
</dictionary>
<array key="IBDocument.IntegratedClassDependencies">
@@ -2023,6 +2023,14 @@
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
+ <string key="label">mainMenu</string>
+ <reference key="source" ref="439893737"/>
+ <reference key="destination" ref="649796088"/>
+ </object>
+ <int key="connectionID">545</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
<string key="label">window</string>
<reference key="source" ref="976324537"/>
<reference key="destination" ref="972006081"/>
@@ -3202,7 +3210,7 @@
<nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/>
- <int key="maxID">544</int>
+ <int key="maxID">545</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<array class="NSMutableArray" key="referencedPartialClassDescriptions">
@@ -3231,17 +3239,20 @@
<object class="IBPartialClassDescription">
<string key="className">ContentView</string>
<string key="superclassName">NSView</string>
- <object class="NSMutableDictionary" key="outlets">
- <string key="NS.key.0">webView</string>
- <string key="NS.object.0">WebView</string>
- </object>
- <object class="NSMutableDictionary" key="toOneOutletInfosByName">
- <string key="NS.key.0">webView</string>
- <object class="IBToOneOutletInfo" key="NS.object.0">
+ <dictionary class="NSMutableDictionary" key="outlets">
+ <string key="mainMenu">NSMenu</string>
+ <string key="webView">WebView</string>
+ </dictionary>
+ <dictionary class="NSMutableDictionary" key="toOneOutletInfosByName">
+ <object class="IBToOneOutletInfo" key="mainMenu">
+ <string key="name">mainMenu</string>
+ <string key="candidateClassName">NSMenu</string>
+ </object>
+ <object class="IBToOneOutletInfo" key="webView">
<string key="name">webView</string>
<string key="candidateClassName">WebView</string>
</object>
- </object>
+ </dictionary>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/ContentView.h</string>
Something went wrong with that request. Please try again.