Sandvox Developers Guide

Sandvox has a full-featured plug-in API for third-party developers who wish to add functionality to the application.

Developers who wish to build Sandvox plug-ins should be familiar with Cocoa development, including bindings.

Why Develop a Plug-In?

Sandvox 2 comes with a number of pre-installed plugins that are set up for a number of useful objects to make it easy to build up your web pages. But of course there will always be more ideas of what to create — ideas that we ourselves might have had but haven't had time to make, or maybe that you have thought of but we haven't. (And there are plenty of ideas — Look at how many Google Gadgets there are!)

Many ideas can be implemented by putting in a raw HTML object, but it's not particularly useful if you have an idea that you would like other people to be easily able to configure. Writing your own plugin means that you do the "hard work" once, of figuring out what HTML actually needs to be created for inclusion on the web page, and all the user of Sandvox needs to do is fiddle with some settings in the inspector.

Resources

Creating a Plug-In Project

Fire up Xcode and make a new Cocoa Bundle project like so:

Cocoa_Bundle.png

  1. Find your plug-in in the Targets group and Get Info on it. Make sure you're editing All Configurations, not just Debug or Release
  2. Change Wrapper Extension [WRAPPER_EXTENSION] from "bundle" to "svxElement"
  3. Keep this panel open; you'll need it for the next section!

Linking With Sandvox

In order to compile and link correctly, your plug-in needs to set two build parameters for both Debug and Release builds. These are Header Search Paths [HEADER_SEARCH_PATHS] and Bundle Loader [BUNDLE_LOADER]. Both of these need to point to specific resources inside the Sandvox application. (This Guide assumes Sandvox is installed in /Applications.)

Header Search Paths must include /path/to/Sandvox.app/Contents/Headers

UHSP.png

Bundle Loader must be set to /path/to/Sandvox.app/Contents/MacOS/Sandvox

BundleLoader.png

As of version 2.8, Sandvox is a Universal app, supporting bother 32 and 64-bit. We recommend you build your plug-in to match. For older releases:

ValidArchs.png

Principal Class

Your plug-in needs a principal class descended from SVPlugIn, the base class Sandvox provides. Make such a class; its header should look something like this:

#import <Sandvox.h>
@interface MyPlugIn : SVPlugIn
{
}
@end

Note how you import from Sandvox (rather than Cocoa) to gain access to the SVPlugIn base class.

In the bundle's Info.plist, set NSPrincipalClass to be the name of your custom class. This is how Sandvox discovers how to load your plug-in for use.

Info.plist

Switching to the Properties tab in the Target inspector:

Properties.png

Installing the Plug-In

Sandvox detects plug-ins in the following locations:

It's easiest if you can make Xcode place your built plug-in in one of those locations automatically.

Bring up the Build settings for your plug-in target again. For all configurations, set the Build Products Path to be the absolute path of your Sandvox app support folder. For example:

Build_Products_Path.png

Then set the Per-configuration Build Products Path to $(BUILD_DIR):

Per-configuration Build Products Path.png

When distributing the plug-in to customers, you need only send out the built plug-in bundle. Customers can double-click the file and Sandvox will install it for them.

Debugging With Sandvox

The easiest way to debug your plug-in is to let Xcode automatically launch it with Sandvox as the executable.

  1. In Xcode, choose "Project → New Custom Executable…"
  2. Name the executable "Sandvox" and select the Sandvox application.
NewCustomExecutable.png

Now when you Build and Run or Build and Debug your project within Xcode, Sandvox will automatically be launched and your plug-in loaded for testing.

Icon

Add an icon for display in the Objects menu

HTML Template

Often the most convenient way for a plug-in to generate HTML is to use a template. Templates are written like snippets of regular HTML, but you can use square brackets for special processing of the file.

To use a template, create a file named Template.html and add it to the plug-in's Resources.

HTML_Template.png

Plug-In Properties

Most plug-ins will want to have some sort of settings that users can tweak. Sandvox will take care of managing these properties for persistence, undo & redo. All you need do is:

For example:

@interface MyPlugIn : SVPlugIn
{
  @private
    NSString    *myProperty;
}
@property(nonatomic, copy) NSString *myProperty;
@end
@implementation MyPlugIn
+ (NSArray *)plugInKeys;
{
    return [[super plugInKeys] arrayByAddingObject:@"myProperty"];
}
@synthesize myProperty;
- (void)dealloc;
{
    [myProperty release];
    [super dealloc];
}
@end

The property can then be referred to in the HTML Template. A trivial example that places the text into some HTML if available:

<div>[[if myProperty]][[=&myProperty]][[else]]Nothing entered[[endif]]</div>

For more information on plug-in properties/keys, see SVPlugIn.h.

Inspector

Plug-ins can provide a view for the Inspector so users can conveniently customise settings there. You'll generally provide this in a xib or nib file.

The main class responsible for managing the Inspector of your plug-in is SVInspectorViewController. It's a direct descendant of NSViewController — so we inherit some functionality like nib loading for free! Make sure you familiarize yourself with that class first.

In general, Sandvox will create an Inspector for your plug-in on-demand, and then hang onto it in memory. As different instances of the plug-in are selected in the Web View, Sandvox the same Inspector will be recycled to handle the new selection.

Creating an Inspector View

View_XIB.png

Name the file Inspector.xib or Inspector.nib and add to Resources.

Inspector_XIB.png

Setting up File's Owner can be tricky as Xcode may not have found the SVInspectorViewController class.

  1. File → Read Class Files…
  2. Select all the files from Sandvox.app → Contents → Headers folder. Directing IB to this folder can be tricky; I find the easiest way is to drag the Headers folder from the FInder onto the open panel. The files will then be available to select
  3. Interface Builder will now know about the SVInspectorViewController. Set it as File's Owner
  4. Connect its view outlet up to the main view
  5. Make the view 230px wide, and give it flexible width & height autoresizing mask.

In most cases, Cocoa bindings provide the simplest way to hook up your Inspector's controls to the model (your SVPlugIn subclass). SVInspectorViewController provides the inspectedObjectsController property pointing to an array controller. That array controller's selection is the selected instances of your plug-in.

So to bind a control to a model property, bind to File's Owner with the keypath:

inspectedObjectsController.selection.myProperty

Layout

I blogged a selection of rules for layout of Inspectors. They're all relevant to the Sandvox Inspector as a whole; you need only worry about those that affect the content your plug-in provides.

A few other tips:

Table Views

To use a tableview in your Inspector, we generally recommend using bindings again.

  1. Add an array controller to the nib
  2. Bind each column of the table to the array controller
  3. Bind the array controller to File's Owner
    • Use a keypath like: inspectedObjectsController.selection.myArray where myArray is a property of the plug-in
    • Make sure to use the "Handles Content as Compound Value" option

Inspector_array_controller_binding.png

Further Inspector Customization

If bindings alone aren't enough to implement your Inspector, you can create a custom controller.

  1. Subclass SVInspectorViewController. For Sandvox to automatically discover this class, name it correspond to your plug-in's classname:
    • If the plug-in classname contains "plugin", replace that with "Inspector". e.g. FooBarPlugIn becomes FooBarInspector
    • Otherwise just append "Inspector". e.g. MyGreatClass becomes MyGreatClassInspector
  2. Set File's Owner in the inspector nib to be this new class
  3. Add the additional outlets, actions, or functionality your custom class needs
  4. If you didn't follow the naming conventions above, or special setup is required, override +[SVPlugIn makeInspectorViewController] to create and return an instance of your custom class

It's common to perform additional setup of the Inspector's views upon loading. In the past Cocoa has generally used the -awakeFromNib method for this, but it's not entirely suited for Sandvox plug-ins. Instead:

- (void)loadView
{
  [super loadView];
  // Custom setup goes here
}

For further information, see SVInspectorViewController.h and SVPlugIn.h.

Customizing HTML Generation

When Sandvox needs some HTML from your plug-in, it calls this method:

- (void)writeHTML:(id <SVPlugInContext>)context;

The context is a powerful object which HTML gets written to, as well as providing information about what the HTML is being generated for. For full information, please give SVPlugInContext.h a thorough read.

The default implementation of -writeHTML: looks for an HTML Template and runs through that. (Therefore, if you override -writeHTML: but still wish the HTML Template to be used, be sure to call [super writeHTML:context] at some point in your implementation.) There are some plug-ins though where a template alone is not sufficient.

Some ideas:

Generate HTML Without a Template

Override -writeHTML: to instead write markup directly to the context.

Register Dependencies

Sandvox knows to reload a plug-in's HTML by maintaining a list of keypaths which that HTML depends upon. When using a template, that list is built automatically from the contents of the template. But if you need to register any extra dependencies that aren't in the template — or you're not using a template — do so with:

- (void)addDependencyForKeyPath:(NSString *)keyPath ofObject:(NSObject *)object;

If all dependencies aren't registered, the HTML seen on screen can end up out-of-date, so make sure you've got everything covered!

Call Through to Plug-In Methods from the Template

You can happily mix template HTML and Objective-C code by calling through to a method on your plug-in.

Tailor HTML to the Circumstances

SVPlugInContext provides lots of information about the HTML it's generating. For example, if live data feeds are disabled, you could generate a placeholder <DIV> so that users still have something to work with onscreen.

@property(nonatomic, copy, readonly) NSURL *baseURL;
- (BOOL)isXHTML;
- (id <SVPage>)page;
- (BOOL)isForEditing;
- (BOOL)isForQuickLookPreview;
- (BOOL)isForPublishing;
- (BOOL)liveDataFeeds;

Much More

Seriously, give SVPlugInContext.h and SVPlugIn.h a read. There's a wealth of functionality in there!

URLs

The Sandvox Plug-In API makes heavy use of URLs. By a strange co-incidence, so do web pages.

However there is a slight impedance mismatch. When a URL appears in a webpage, it is often as a string relative to the page. e.g.

<img src="foo/bar.png" />

Our API on the other hand uses NSURL objects which represent an entire URL. e.g. http://example.com/foo/bar.png. To convert, simply use the SVPlugInContext method:

- (NSString *)relativeStringFromURL:(NSURL *)URL;

Resources

Plug-ins can request that a file be added to the _Resources directory of the published site. This is done by adding the resource to the context; behind the scenes Sandvox will take care of uploading the file. For example, if you there's a resource in your plug-in bundle:

// Locate resource
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"foo" ofType:@"png"]
NSURL *localURL = [NSURL fileURLWithPath:path]
// Add to context
id <SVPlugInContext> context = [self currentContext];
NSURL *url = [context addResourceWithURL:localURL]

You get back a URL for the resource that's appropriate to the current context.

To then refer to this image in your HTML for example:

NSString *src = [context relativeStringFromURL:url];
[context startElement:@"img" attributes:[NSDictionary dictionaryWithObject:src forKey:@"src"]];
[context endElement];

Plug-In Initialization

There are several routes Sandvox can take to initialize your plug-in. Make sure you've got them all covered! Here are the sequence of events each one takes:

Insert/Objects menu

  1. +alloc
  2. -init
  3. -awakeFromInsert (not available yet)
  4. -awakeFromNew
  5. -pageDidChange:

Fetch (like Core Data)

e.g. when opening a document containing a saved instance of your plug-in

  1. +alloc
  2. -init
  3. For each property in +plugInProperties: -setSerializedValue:forKey:

Note that properties, i.e., ivars, will be unarchived as non-mutable. This means, for example, in the case of an NSArray, you need to essentially replace the array each time you add an object for all of the right KVO notifications to be sent and for your code not to stomp on an a non-mutable instance variable.

@property (nonatomic, retain) NSArray *linkList;
- (void)addLink:(NSDictionary *)link
{
    NSMutableArray *links = [NSMutableArray arrayWithArray:self.linkList];
    [links addObject:link];
    self.linkList = links;
}
  1. -awakeFromFetch

Deserialization

e.g. when duplicating a page containing an existing instance of your plug-in.

  1. +alloc
  2. -init
  3. -awakeFromInsert (not available yet)
  4. For each property in +plugInKeys: -setSerializedValue:forKey:
  5. -pageDidChange:

Pasteboard

e.g. Dragging URL into Site Outline or page.

  1. Determine if supported: +readableTypesForPasteboard:
  2. Determine best choice: +priorityForPasteboardItem:
  3. Handle multiple dropped items in a single batch? +supportsMultiplePasteboardItems
  4. +alloc
  5. -init
  6. -awakeFromInsert
  7. -awakeFromPasteboardItems:
  8. -pageDidChange:

But can also support dropping onto an existing plug-in in the editor:

  1. Determine if supported: +readableTypesForPasteboard:
  2. -awakeFromPasteboardItems:

Change of Design

  1. -pageDidChange: is called for every page the plug-in appears on

Significant page layout setting changes, such as showing the sidebar

  1. -pageDidChange: is called for the page(s) that changed

Added to sidebar of a page

  1. -pageDidChange: is called with the new page

More

How can we improve this page? Let us know.