Archive for the ‘Development’ Category

Sample Task

Friday, June 16th, 2006

OK so this was supposed to be done yesterday, but shiny things distracted me as they often do. But here’s part two of the promised example code.

Mailer

Mailer task

This is one of those tasks I would’ve liked to include in the core suite included with Proxi but there are a few lingering questions about it. The MessageFramework, on which this is based, is sparsely documented and I’m not sure Apple’s commitment to it. It also requires that you’ve configured Mail correctly, i.e. you’re not using some other email application. So I was kind of on the fence about this one. Maybe it’ll become part of the core suite in a new release. Maybe not.

Anyway, this task is also pretty straightforward. Gather some info using 2 GValueTextField’s (these are new as of Proxi v1.1 and so they haven’t rippled completely through Proxi yet) and one GValueTextView. Those allow specially formatted text to appear as Value tokens.

The instructions for this task bundle are the same as for the trigger: Grab the source, compile with XCode, throw the bundle in /Library/Application Support/Proxi/PlugIns/ and restart Proxi. If you built it right and I didn’t screw something up, you should see Mailer in your task list.

For more information on creating your own triggers and task, check this article.

Source: mail.zip
Proxipedia: Mailer Task

Sample Trigger

Wednesday, June 14th, 2006

For some time I’ve been meaning to put together a sample Trigger and Task bundle to aid those wanting to create their own specialized components. And lately I’ve had a request for this as well, so I spent a little time today putting together part one of a this two part project.

Stock Quote

Stock Quote trigger

I wanted to do a little bit more than a Hello World, but nothing to complicated that it took away from demonstrating the basic principles. I had portions of this code laying about to grab a stock quote via a SOAP call, so I decided to roll that into a trigger.

This is a pretty basic trigger. It asks for a stock symbol (AAPL is always a good one to test with) and also has a slider to set the update interval.

So, if your curious, grab the source, compile with XCode, throw the bundle in /Library/Application Support/Proxi/PlugIns/ and restart Proxi. If you built it right and I didn’t screw something up, you should see Stock Quote in your trigger list.

For more information on creating your own triggers and task, check this article that I posted a while back. I’ll try and post a sample task tomorrow.

Source: quote.zip
Proxipedia: Stock Quote

Beta Feed

Monday, June 5th, 2006

For those interested in in staying on the cutting edge of Proxi releases, there is a blueprint that will let you switch Proxi to the beta software feed. You can get the blueprint here:

Feed Switcher

It works by triggering on a hotkey (control-option-B) which will execute a shell script that changes the location where Proxi looks for new updates. You may want to relaunch Proxi if it doesn’t seem to “notice” the change right away. A trigger is also available to switch back to the regular feed (control-option-R).

If you feel more comfortable executing these commands in the Terminal then you can forego the blueprints and executing the following in the Terminal:

Beta: defaults write com.griffintechnology.Proxi SUFeedURL http://proxi.griffintechnology.com/software/ProxiCastBeta.xml
Release: defaults write com.griffintechnology.Proxi SUFeedURL http://proxi.griffintechnology.com/software/ProxiCast.xml

If you switch to the beta feed, you’ll notice a new release of Proxi is available. Version 1.1 adds these items:

  • Added radioSHARK trigger (requires radioSHARK v2.0.1)
  • Added radioSHARK task (requires radioSHARK v2.0.1)
  • Added Battery monitor task
  • RSS Monitor errors now display in the RSS settings view
  • RSS Monitor now correctly handles more varieties of feeds
  • Fixed a problem where unnamed triggers did not appear to be selectable

Rolling your own Triggers and Tasks

Tuesday, February 21st, 2006

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.

Logitech G15 plugin

Saturday, January 21st, 2006

Don picked up a couple Logitech G15 keyboards on Friday and handed one to me thinking it would be pretty cool if iNotify could use the display. I thought so too and the result is the G15 Task plugin. In the unlikely event you have one of these keyboards lying about (or if your simply curious), you can install the plugin at:

/Library/Application Support/iNotify/PlugIns

Relaunch iNotify and you’ll be all set. The plugin works about the same as the Screen Message task. Just add whatever you like to the G15 Message field and when triggered, the result will appear on the G15’s display. It also appears on a tiny window that mirrors the G15’s display. You can access this window by clicking on the unlabeled round button in the top right corner of the G15 task. The results look like this

and this

The 5 blacks keys immediately below the display are used for the following functions (from left to right): clear display, scroll to beginning, page up, page down, scroll to end.