I’ve been asked about this a couple times. Yes, Proxi was designed from the beginning to support the addition of third party plugins, and it’s pretty straightforward to create your own. Here’s how it works:
At launch, Proxi loads every bundle in /Library/Application Support/Proxi/Plugins and checks each class in the bundle for conformity to the GTrigger or GTask protocols. Those classes that support one of these protocols are added to the component window and the appropriate menus.
The protocols are pretty straightforward
@protocol GTrigger
+ (GComponentDescription *) componentDescription;
+ (NSArray *) valueInfoArray;
- (NSView *) settingsView;
- (NSImage *) image;
- (void) setImage: (NSImage *) inImage;
@end
@protocol GTask
+ (GComponentDescription *) componentDescription;
- (NSView *) settingsView;
- (NSImage *) image;
- (void) setImage: (NSImage *) inImage;
- (BOOL) processNotification: (NSDictionary *) inValues;
@end
Notice both types of classes return a GComponentDescription object. This object is used to provide the user information about your component and also define the name of the class. Here’s an example of the speechTrigger’s componentDescription method:
+ (GComponentDescription *) componentDescription
+ (GComponentDescription *) componentDescription
{
static GComponentDescription *sComponentDesc = nil;
if (sComponentDesc == nil) {
NSBundle *ourBundle = [NSBundle bundleForClass: [speechTrigger class]];
NSString *imagePath;
sComponentDesc = [[GComponentDescription alloc] init];
[sComponentDesc setComponentClass: @"speechTrigger"];
[sComponentDesc setCopyright: @"Griffin Technology Inc., 2006"];
[sComponentDesc setName: @"Speech Recognition"];
[sComponentDesc setOwner: @"Griffin Technology"];
[sComponentDesc setSummary: @"Use speech recognition to trigger on a given phrase."];
if (imagePath = [ourBundle pathForResource: @"speak" ofType: @"tiff"]) {
[sComponentDesc setImage: [[NSImage alloc] initWithContentsOfFile: imagePath]];
}
}
return sComponentDesc;
}
Pretty basic, so next let’s look at the other methods required by the GTrigger protocol:
GTrigger protocol
+ (NSArray *) valueInfoArray
Each trigger has to provide an NSArray of the values it will provide when it executes and also provide a description of each of those types in the form of an NSDictionary. The dictionary requires values for the GTriggerValueName key as well as the GTriggerValueType key. So you might define your array like this and return it from valueInfoArray:
sValueArray = [[NSArray arrayWithObjects:
[NSDictionary dictionaryWithObjectsAndKeys: @"Name", GTriggerValueName,
NSStringFromClass([NSString class]), GTriggerValueType, nil],
[NSDictionary dictionaryWithObjectsAndKeys: @"Count", GTriggerValueName,
NSStringFromClass([NSNumber class]), GTriggerValueType, nil],
[NSDictionary dictionaryWithObjectsAndKeys: @"URL", GTriggerValueName,
NSStringFromClass([NSURL class]), GTriggerValueType, nil],
[NSDictionary dictionaryWithObjectsAndKeys: @"Date", GTriggerValueName,
NSStringFromClass([NSDate class]), GTriggerValueType, nil],
[NSDictionary dictionaryWithObjectsAndKeys: @"Image", GTriggerValueName,
NSStringFromClass([NSImage class]), GTriggerValueType, nil], nil] retain];
- (NSView *) settingsView
You’ll most likely need to allow your users some means of configuring your trigger. They will do so through the view you return from this method. In reality you’ll probably only need to allocate a single view and return a pointer to it here. For example in most of the components I’ve written, I load my class’ bundle in the load method and store a pointer to a settings view and, as I use cocoa bindings in most cases, a pointer to a controller object for later use. For example, in speechTrigger’s load:
sTriggerNib = [[NSNib alloc] initWithNibNamed: @"speechTrigger" bundle: ourBundle];
if ([sTriggerNib instantiateNibWithOwner: NSApp topLevelObjects: &nibObjects]) {
NSEnumerator *objEnumerator = [nibObjects objectEnumerator];
id anObj;
[nibObjects retain]; // around for the life of the app
while (anObj = [objEnumerator nextObject]) {
if ([anObj isKindOfClass: [NSView class]])
sView = anObj;
if ([anObj isKindOfClass: [NSObjectController class]])
sController = anObj;
}
}
and later sView is returned from settingsView.
- (void) encodeWithCoder: (NSCoder *) inCoder / - (id) initWithCoder: (NSCoder *) inCoder
GTriggers should support standard keyed object encoding and decoding. Apple has plenty of information available.
- (NSImage *) image
Here again, you’ll probably only need the image from your trigger’s GComponentDescription, but you may want to alter it based on your GTrigger’s configuration. For example, the AirClick trigger adds a badge to it’s image based on the primary button. The NSImage returned from this method should have a height and width of 32 pixels.
- (void) setImage: (NSImage *) inImage
I’m not sure why this is here really
Actually, just as a reminder that image needs to be KVO compliant. ie if you change your image, do so through [self setValue: image forKey: @"image"] or bracket the change with [self willChangeValueForKey: @"image"] and [self didChangeValueForKey: @"image"]
GTrigger informal protocol and notifications
GNotificationName
That’s basically it for a GTrigger… except you may want to, oh I dunno, generate a trigger. That’s pretty straightforward, post a notification named GNotificationName to the default NSNotificationCenter. You must supply a userinfo NSDictionary a key/value pair for every value you defined in valueInfoArray. For the valueInfoArray demonstrated above the notification would look like:
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter postNotificationName: GNotificationName object: self
userInfo: [NSDictionary dictionaryWithObjectsAndKeys:
[self myName], @"Name",
[self myCount], @"Count",
[self myURL], @"URL",
[self myDate], @"Date",
[self myImage], @"Image",
nil]];
- (void) willBeHidden
Your GTrigger receives this message just prior to it’s settingsView being displayed. If you’re using cocoa bindings, this is a really good place to make sure your controller is bound to the correct model.
- (void) willBeShown
Your GTrigger receives this message just prior to having it’s settingsView removed from the view hierarchy. If your using cocoa bindings, this is a really good place to make sure you unbind your controller from your model.
GTask protocol
- (void) encodeWithCoder: (NSCoder *) inCoder / - (id) initWithCoder: (NSCoder *) inCoder
These serve the same purpose as they do for GTriggers.
- (NSView *) settingsView, - (NSImage *) image, - (void) setImage: (NSImage *) inImage
Same for these.
- (BOOL) processNotification: (NSDictionary *) inValues
Your GTask receives this message in response to a trigger. The keys in this dictionary correspond to the values provided by the controlling GTrigger’s notification userinfo dictionary concatenated with any Extra Values that the user may have defined for this trigger.
GTask informal protocol
- (void) willBeHidden / - (void) willBeShown
These serve the same purpose as they do for GTriggers.
- (void) setValueDescriptions: (NSArray *) inValueDescriptions
Implement setValueDescriptions if you’d like to keep tabs on the values your GTask can expect to receive from the GTrigger controlling it. Useful if you need to update some aspect of your UI or if you’re simply nosy.
Useful ProxiLib utilities and classes
GValueTextView
Make your NSTextView a GValueTextView in InterfaceBuilder if you want to embed trigger values in your text. Handles the displaying, copying, dragging and so forth of embedded trigger value tokens. Kind of like a NSTokenField but different.
+ (NSAttributedString *) substituteTokensInString: (NSAttributedString *) inString usingValues: (NSDictionary *) inValues
Use substituteTokensInString: usingValues: to substitute the tokens in an NSAttributedString with the values passed to processNotification. This method goes hand in hand with any GTask using a GValueTextView to accept user text.
The short version:
Create a Cocoa Bundle in XCode and link to the ProxiLib.framework. Create an object/s that conforms to GTrigger or GTask protocol. Compile, and place in /Library/Application Support/Proxi/Plugins. Restart Proxi.