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
- This page and any articles it links to serve as the main documentation
- This is a live document that we'll be updating frequently from your feedback
- There is no separate dedicated SDK as such; instead Sandvox version 2.0 and above already includes everything you need to build a plug-in against it
- Download the latest version of Sandvox and you can get cracking!
- Control-click the app bundle in the Finder and "Show Package Contents". All public classes, neatly commented, can be found in
Contents/Headers
Creating a Plug-In Project
Fire up Xcode and make a new Cocoa Bundle project like so:
- 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
- Change Wrapper Extension [WRAPPER_EXTENSION] from "bundle" to "svxElement"
- 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
Bundle Loader must be set to /path/to/Sandvox.app/Contents/MacOS/Sandvox
Sandvox (version 2.8 or higher) is a Universal app, supporting bother 32 and 64-bit. We recommend you build your plug-in to match. For older releases:
- Sandvox 2.0 was a 32-bit app, running on both Intel and PowerPC Macs
- Sandvox 2.5 dropped PowerPC (and Leopard) support, becoming solely a 32-bit Intel app
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:
- Set Identifier to be unique to your organization and plug-in. e.g.
com.mycompany.MyPlugin
- Leave Type set to BNDL
- Set Creator to Svox
- Set Principal Class (NSPrincipalClass) to be that of your SVPlugIn subclass. e.g.
MyPlugIn
- Any other keys should be set via the plist editor in Xcode or with Property List Editor
Installing the Plug-In
Sandvox detects plug-ins in the following locations:
- ~/Library/Application Support/Sandvox/PlugIns/
- ~/Library/Application Support/Sandvox/
- Library/Application Support/Sandvox/PlugIns/
- Library/Application Support/Sandvox/
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:
Then set the Per-configuration Build Products Path to $(BUILD_DIR)
:
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.
- In Xcode, choose "Project → New Custom Executable…"
- Name the executable "Sandvox" and select the Sandvox application.
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.
Note: It seems that Xcode will always launch Sandvox in the mode best suited for the Mac you're running on. i.e. If you've instructed the Finder to "Open in 32-bit mode" for Sandvox, that won't be respected by Xcode. 32-bit testing has to be performed manually instead.
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.
- The full template specification for more technical details
To use a template, create a file named Template.html and add it to the plug-in's Resources.
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:
- Register the properties with the system by overriding
+plugInKeys
- Implement setter and getter methods
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
Name the file Inspector.xib or Inspector.nib and add to Resources.
Setting up File's Owner can be tricky as Xcode may not have found the SVInspectorViewController
class.
- File → Read Class Files…
- 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
- Interface Builder will now know about the
SVInspectorViewController
. Set it as File's Owner - Connect its view outlet up to the main view
- 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:
- When using a text field + stepper combo:
- Separate them by 3 pixels, as according to Interface Builder
- Right-align the contents of the field
- Use a formatter to include the units as part of the field rather than a separate label
- For pixel measurements, we prefer "px" as the units
Table Views
To use a tableview in your Inspector, we generally recommend using bindings again.
- Add an array controller to the nib
- Bind each column of the table to the array controller
- 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
- Use a keypath like:
Further Inspector Customization
If bindings alone aren't enough to implement your Inspector, you can create a custom controller.
- 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
becomesFooBarInspector
- Otherwise just append "Inspector". e.g.
MyGreatClass
becomesMyGreatClassInspector
- If the plug-in classname contains "plugin", replace that with "Inspector". e.g.
- Set File's Owner in the inspector nib to be this new class
- Add the additional outlets, actions, or functionality your custom class needs
- 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
+alloc
-init
-awakeFromInsert
(not available yet)-awakeFromNew
-pageDidChange:
Fetch (like Core Data)
e.g. when opening a document containing a saved instance of your plug-in
+alloc
-init
- 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; }
-awakeFromFetch
Deserialization
e.g. when duplicating a page containing an existing instance of your plug-in.
+alloc
-init
-awakeFromInsert
(not available yet)- For each property in
+plugInKeys
:-setSerializedValue:forKey:
-pageDidChange:
Pasteboard
e.g. Dragging URL into Site Outline or page.
- Determine if supported:
+readableTypesForPasteboard:
- Determine best choice:
+priorityForPasteboardItem:
- Handle multiple dropped items in a single batch?
+supportsMultiplePasteboardItems
+alloc
-init
-awakeFromInsert
-awakeFromPasteboardItems:
-pageDidChange:
But can also support dropping onto an existing plug-in in the editor:
- Determine if supported:
+readableTypesForPasteboard:
-awakeFromPasteboardItems:
Change of Design
-pageDidChange:
is called for every page the plug-in appears on
Significant page layout setting changes, such as showing the sidebar
-pageDidChange:
is called for the page(s) that changed
Added to sidebar of a page
-pageDidChange:
is called with the new page