Random Wok

Aperture Export SDK: Missing Files

For anyone who is trying to make an export plug-in for Aperture: make sure you keep a copy of your Tiger development environment backed up somewhere. I installed Leopard and the dev tools and found that my Random Wok plug-in would not compile.

It failed at this line:

#import <PluginManager/PROAPIAccessing.h>

in ApertureExportPlugIn.h. The PROAPIAccessing.h file is missing because it and PROPlugInBundleRegistration.h are not included in either Leopard or the ApertureSDK 1.5.5.

To fix it , I copied across the Headers folder that contains those two from a Tiger back up. The full path is /Library/Frameworks/PluginManager.framework/Versions/B/Headers. There is already a soft link for Headers that is correct. It should look like this:
aperturesdk
|

Aperture Plugin: Integrating Localized Data Part 2

cocoasmall
Now the strings in my code and my image are localized into French as well and English, I can move on to the strings in the interface. So far the French nib is just a copy of The English nib, created when I made the French nib localization. I used nibtool to extract the strings before translation, and I use nibtool again to put the translated strings back.

To do this I fire up Terminal, cd to the French.lproj folder and use the following command line:

nibtool -w new.nib -d file.strings Random_Wok.nib

This creates a new nib file with the English strings replaced by French ones. I use the Finder to replace the old nib with the new one and I am done. Now my resources look like this:
rwok344
But if I run the plug-in in French, some of the strings no longer fit:
rwok345
This is unfortunately typical for English. With its huge vocabulary, English can take up as little as 50% of the space of other languages. So my nice tight interface needs adjusting.

And there is a problem with subversion. After checking all of this in I find that my repository does not contain the French files and the NoImage.tiff file is in both the localized and the main folder:
rwok346
To fix this I do two subversion things: svn delete the extra TIFF and svn add the folder and its contents. Svnx could do the delete, but not the add. After a fair amount of trying things that did not consistently work, I eventually went to the command line, did a svn add of the French.lproj folder, then quit and relaunched Xcode, then finally did a commit. That worked and now everything is synchronized.

I fix the layout with IB, but don't neaten it up yet. That's because I want to run it past my translator again to make sure nothing weird has happened that I won't spot. Once the translator has OKed it, I'll peek the pixels and straighten everything.

And then there is the "Images Selected" binding. I have this set up with two display patterns, one for the number of images and one for the pluralization string. For French I have to put the second string in twice since both words gain an "s" in the plural:
rwok347
When I come to add German, this will break. The German strings are "Bild ausgewählt" and "Bilder ausgewählt". There is an "er" added in the plural, not an "s". Japanese is easy: no plurals exist in the language. A better solution is to put both singular and plural strings in the strings file and do all of this in code.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Integrating Localized Data Part 1

cocoasmall
Now my translators have sent back localized versions of Localizable.strings, file.strings, and images that I sent to them, I can integrate them into my project. Here is how my project is organized right now:
rwok330
To localize the NoImage.tiff image, I select NoImage.tiff and get Info, then click on Make File Localizable:
rwok335
This changes the image into a group and shows the targets that it is associated with:
rwok336
The Resources have been rearranged like this:
rwok337
Clicking on the General tab shows the languages that the image is localized for:
rwok338
I'm going to add French, so I click Add Localization and select French. The French image created by Xcode is just a copy of the English image at this stage:
rwok339
To get my French image in, I change its name from PasDimage.tiff to NoImage.tiff so that the code will be able to access it with the same file name and replace the current image in the French.lprog folder via the Finder. Xcode has a handy contextual menu item called Reveal File In Finder to help with this.

To Localize the strings I do the same sequence, this time putting my English Localizable.strings into the English localization as well as the French Localizable.strings file into the French localization.

I localize the nib file too, creating the localization, but just leaving it as a duplicate of the English for now. I want to see how Random Wok works in French with what I have to far. Only a few things will be French at this stage: the progress message and the missing thumbnail image will show me that things are working correctly. But how to run in French?

I go to the International preference pane and move French to the top:
rwok340
Now when I run Aperture it will be French.

But when I do, I find that the NoImage image is not there. And when I run it in English it is not there either. The problem is this code:
rwok341
I get the TIFF file using a path that does not take into account the localization folders (English, French). So the initWithContentsOfFile method fails and returns nil. The fix is to use -pathForResource:ofType:inDirectory with a nil directory name:
rwok342
And now the image shows up correctly when I run in French or English:
rwok343
Next is fixing the interface strings.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Preparing For Localization

cocoasmall
Random Wok 1.0 is currently not localized: the only language it supports is English. Before I release the 1.1 version I am going to add translations for the languages that Aperture supports: French, German, and Japanese. This requires extra files in the plugin's bundle that provide the language information.

To prepare, I replace all the messages and strings that are generated by the program and are human-readable with macros that retrieve the localized version. Code like this:
rwok240
is replaced by code like this:
rwok241
I use the NSLocalizedStringWithDefaultValue macro because it allows me to provide a key (exporting-images in this case) that is not the same as the string Exporting Images.... It also supports use of a specific bundle. I need that because Random Wok is a plugin and otherwise Aperture's main bundle would be used.

Once I have replaced all the strings in my code with macros, I use the terminal to run the genstrings utility on all the source files in my project:
rwok321
I get this entry in the text file Localizable.strings created by genstrings for the exporting-images string above:
rwok242
To provide for other languages the Localizable.strings file is duplicated and the string on the right replaced by the translation. When the plugin is run, the correct language files in the bundle are accessed and the key used in the code (exporting-images) matched with the entry in the file. Since the file is encoded UTF-16, the right hand string can contain ASCII and any unicode characters.

There are two other sources of strings that I need to worry about for localization in my plugin. First, the nib file contains all the strings used in the interface and it is currently English-only. To fix that I will need a new nib file for each language and will probably have to adjust the placement of some interface elements due to the size of the new strings. Second, I have an image that is displayed when there is no thumbnail available that says "No Image". That will have to be replaced with a new image for each language.

Apple provides a tool for helping with the translation of nibs called nibtool. Nibtool used with the -L option extracts all the strings from a nib file and sends them to stdout. The Random Wok nib file generates entries like these:
rwok325
As before, a translation replaces the right hand string. Nibtool is used again to replace the strings in copies of the nib file with the translated versions.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Problems With Arrays and Key Presses

cocoasmall
A problem that I encountered along the way as I implemented my cache was that any change to the data always caused the array controller to load all the elements. This was exactly what I was trying to avoid with a cache, yet it was happening.

After much hair-pulling (and posting to Apple's Cocoa mailing list) I figured that the array controller believed that my array (implemented by methods in my Random_Wok class) was immutable, and therefore any observed change must mean that the entire array had changed and so need a reload. The fix was to make the array controller believe that the array was mutable. To do this I added three more methods:
rwok320
These are the mutable array methods. I didn't even have to write any code for them because they are never called. They are just there so that the array controller knows that my array is mutable and so will allow updates to individual elements.

Another problem I found that was while the Page Up and Page Down keys worked on the NSTableView, the Home and End keys did not. A little odd. To fix this I subclassed NSTableView and overrode -keyDown:.

I created a custom class called BTKeydownTableView and told Interface Builder to use it instead of NSTableView. Here is the interface:
rwok256
The implementation is very simple. I read the first character from the event queue and act on it:
rwok257
To scroll the window to the right place I tell the view to scroll to the first or last rows, as required.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Caching Table Data

cocoasmall
Retrieving the thumbnail image and generating the text for the table is time-consuming and called very often by the array controller that is controlling the table of images. This results in very slow scrolling of my table. To solve this problem, I added caching; the idea being that repeated requests for the same data come out of memory and do so quickly.

Adding caching was relatively simple, at least to implement simply. Here is the code:
rwok314
The cache needs a key to cache on. For this I turn the index number into a string and use that as the key. The cache itself is a mutable dictionary that holds dictionaries:
rwok315
This stores the data I get from Aperture fine and the interface is fast again. But now I have another problem. When the random file name parameters are changed, the cached data becomes stale. So each time a change occurs, I must update the cached text:
rwok316
And the cached images:
rwok317
This code enumerates through the cached data replacing the cached text and thumbnail objects. Bindings take care of the table updates. -updateCachedFilenames is called whenever anything changes that could affect the file name, such as a change of alpha case:
rwok318
-updateCachedImages is called when the type of image changes:
rwok319
This all works well, and is how Random Wok 1.0 was released. But it still has a problem. The cache grows forever, eating memory as it goes. And as the cache grows, the time taken to update the cached data grows. What is really needed is a cache that throws away the oldest entry once it reaches a certain limit and more data is added. So that is what I implemented next.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Translations Needed

rwok21
My next release of Random Wok will be localized. Since Aperture supports English, French, German, and Japanese my plugin cannot successfully support any more languages than those.

Are there any readers interested in volunteering their language skills to this task? There are probably less than 20 translations needed in all, maybe 150 words total. I will provide plain text files for translation: there is no need to have XCode or any development tools.
|

Aperture Plugin: Improving Image Table Performance

cocoasmall
My first attempt at providing table thumbnails was to load an NSMutableArray with all the images that the user has selected when the plugin starts. Unfortunately there can be a significant delay while they are retrieved from Aperture: above a few hundred the delay becomes irritating. Once the data was in my array, access was fast and the table scrolled up and down smoothly.

To improve start-up performance I replaced my array ivar imageTable with two methods: -countOfImageTableData and -objectInImageTableDataAtIndex:. Now, through the magic of KVC the array controller instantiated in the nib will access these two methods instead of the array. In fact it was trying to do that all along, failing, and falling back to accessing the ivar directly. The idea is that by generating the data on demand, the start-up performance problem will go away and the time taken distributed across all the images as they are viewed in the table.

Here is the code for the first method:
rwok311
The check for the API version exists because my code simply does not support it. There used to be a less efficient way to retrieve thumbnails that I have no intention of coding. The result of this is that the image table is blank if an old API is detected. I also log a message to that effect on initialization, so it is not a complete mystery to the user.

The second method is very simple:
rwok312
The code for -textForImageAtIndex: and -thumbnailForImageAtIndex: is not simple, however. But it is straight forward. The text code gets the properties of the image from Aperture and formats it appropriately. The thumbnail code is much easier to follow:
rwok313
Master images (and potentially any image) don't have thumbnails, so I have to substitute. I do that with a small TIFF image that I put into the plugin bundle that says "No Image".

How did I do? Start-up was fast, even for thousands of images, so that problem was fixed. But there were other problems. Scrolling was dog slow. Logging showed that -countOfImageTableData and -objectInImageTableDataAtIndex: were being called for practically every pixel of scrolling I was doing, retrieving the image and generating the text each time. Horrible.

So the next step was to implement caching: retaining the images and text in memory and delivering those instead of getting them from Aperture each time they are needed.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Implementing A Table Of Images

cocoasmall
At this point in the project I decided two things: that I was going to release the plugin as 1.0 in a month, and that I needed to change the interface. I had already switched from a vertical arrangement with the prefix on top, to a horizontal arrangement with the prefix on the left. The trigger for this next change was the unsatisfactory display of the example random filename and the need to distinguish between what happens to the random naming when the freeze feature is used and is not.

The final version I decided to go with features a table that shows a thumbnail, plus the image version name, the image caption, and the new random name. This is the clearest way to show what is going to happen: show it happening.
rw1.0mm
By using the propertiesWithoutThumbnailForImageAtIndex: and thumbnailForImageAtIndex: methods I can get the data I need from Aperture for each table entry. So my plan was to bind the table to an array controller and bind the array controller to an array in the Random_Wok object that contained all the thumbnails and text. Updating the thumbnails and text would cause changes to propagate through the controller to the view as needed.

So I added an array controller to the nib and called it ImageTable:
rwok305
I then bound it to the model like this:
rwok306
and set up like this:
rwok307
The model key is imageTableData and I use the keys thumb and text to access the data for the columns. So to support the needs of the array controller I implement an array called imageTableData as an ivar in Random_Wok and fill it with dictionaries with keys thumb and text, corresponding to NSImage and NSString objects I want to display.

I set up the table view like this:
rwok300
Notice that I actually used a custom class for this. More on that later. The first (image) column is set up like this:
rwok301
To display the image, I drag in an image cell. The properties of the image cell is accessed via the small triangle top right:
rwok302
The inspector shows it as an NSImageCell, set up this way:
rwok303
Now onto the bindings. The first column is bound to the array controller and uses the thumb key to get data from the model:
rwok304
The second column is set up this way:
rwok308
And bound to the text key through the array controller like this:
rwok309

As I discovered while attempting to bind each table column to a separate array, setting up table column bindings also automatically sets up the the table bindings. This makes it impossible to bind separate columns to separate array objects: they have to all go to one object and then use the key path to get data from separate places. This pretty much means that you need an array of dictionaries to drive a table.

To provide data for the model, I loaded all the thumbnails and text into the array during the plugin initialization. Through the bindings I was able to change the dictionary contents and have the table update automatically. This all worked fine for a small selection of images.

However if I selected 500 images, the array would take a long time to fill. Worse, this was happening before anything was displayed (since the array controller was set to prepare content), making it look like the plugin has frozen. And changes to the text caused by changing the parameters for the random file names were also very slow because they too would be performed 500 times on all the items in the array.

So another approach was needed.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Better Logging

cocoasmall
The logging I have been doing is very primitive. I call NSLog() to see what I want to see and have to manually insert and remove these calls (or mess with comments) to control what is going on. It's time for something better. Looking around, I found a handy logging class at Borkware that does much of what I want called MLog.

It implements a logging system that includes the line number and source file name with each message. This is very useful for understanding what is happening when reading the logs. The change I made to that code was to add control over the logging level.

Here are the definitions for my version of MLog.h. I implement seven levels and add the ability to revert to standard NSLog() calls, or to remove all the logging code completely:
rwok250
Macros take care of inserting the correct code and extracting the file name and line number from the preprocessor. The class implements two class methods: one for actually logging messages, and one for setting the current log level. The idea of the level control is that the minimum log level can be set either by an environment variable or by code and only messages logged at that level or above will be shown.

The Implementation includes a static variable that holds the current log level:
rwok251
Initialization is done in the class initializer:
rwok252
It reads the environment variable MLogMinLevel and uses that to set the initial level. The logging code compares the logging level passed to the method with the current level and ignores those below the minimum:
rwok253
The level setter code is very simple:
rwok254
To use the logging, I add lines like this to my code:
rwok260
and it provides messages that look like this that include the level, the file name and the line number:
rwok261
Since I provide logging control with an environment variable, I can quite easily create build configurations that behave differently. If I go to the Project menu and select Edit Project Settings I can duplicate the current Debug and Release configurations and make two new ones:
rwok130
Clicking on on the Build tab lets me set these up:
rwok131
I edit the preprocessor macros to include the symbols I need to control the logging system. The Release No Logging configuration sets __BTREMOVE-LOGGING, for instance. Once set up I can change the current configuration I want to build and run from the main XCode window just by selecting it:
rwok137
For this to fully work I edit the custom scripts I added to copy the executable and run Aperture, since the build names have changed and there are more of them.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Creating A Universal Binary

cocoasmall
Random Wok is only being compiled for Intel right now. I need a universal binary so that it will run on the PowerPC architecture as well. This is easy to change. I select the Random Wok target:
rwok230
Then click on the Info button, and under the Architectures tab, select what I need:
rwok231
That's all there is to it. Everything happens behind the scenes.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Automating Builds And Using The Debugger

cocoasmall
So far in this project all my debugging has been done with NSLog() calls since the code is pretty simple. To run my plugin each time I have been dragging the binary from the Build folder to Aperture's export plugins folder, launching Aperture, and then selecting Random Wok from the File > Export menu.

So how about debugging with the debugger? If I do a debug build, go through the same steps, and the run Aperture, my breakpoints are never hit. What is going on?

This is happening because the application, Aperture, is not being run by the debugger, and so my plugin is not being run by the debugger. To make XCode run Aperture I modified the instructions I found in a technical Q & A on Apple's developer site that shows how to handle this situation with a Web Kit plugin. In my case I create a new custom executable in the Projects folder on the left side of the XCode window and set it up this way:
rwok220
Then I make sure that my build options for the debug build are set correctly: no optimization, generate all symbols, don't strip:
rwok221
Now I can set breakpoints and have them hit:
rwok222
I still have to copy the executable and run Aperture manually. But there is a way to fix that. I add a new run script :
rwok224
And set it up like this:
rwok223
The debug version is set up with a symbolic link and the release version with a copy. Here is the full text:
# clean up any previous products/symbolic links in the target folder
if [ -a "${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}" ]; then
rm -Rf "${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
fi

# Depending on the build configuration, either copy or link to the most recent product
if [ "${CONFIGURATION}" == "Debug" ]; then
# if we're debugging, add a symbolic link to the plug-in
ln -sf "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" \
"${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
elif [ "${CONFIGURATION}" == "Release" ]; then
# if we're compiling for release, just copy the plugin to the Internet Plug-ins folder
cp -Rfv "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" \
"${USER_LIBRARY_DIR}/Application Support/Aperture/Plug-Ins/Export/${FULL_PRODUCT_NAME}"
fi

Here is how the debug version looks in the Export folder:
rwok225
Now when I compile and run or compile and debug, the script is run and Aperture is launched. Any breakpoints I have set work.

The other parts of this series can be found via the Cocoa page.
|

Subversion Part 4: Creating A Release

subversion_logo
Now I have my repository set up, my code imported, and a working copy checked out that I am doing development on, I am going to release a snapshot of the current code as a beta, since I want some people to test it. In order to be able to keep working on the main code (in trunk) while I wait for feedback from testers I have to fork my code. If the testers find a problem and I fix it, I want to send out just the fix, not the current development version with the fix. Later I can merge the fix back into my main code.

Using subversion I do this by copying my code base in the trunk releases virtual folder into a new folder. After making sure that my project is checked in, I open svnX and make sure the correct revision is selected in the top pane. By selecting trunk:
svn13
and clicking on the svn copy button, I am given an opportunity to name and locate my new virtual folder:
svn14
I select branches as the location and fill in a name for the target of the copy. I add a message and hit commit:
svn15
And there is the new release in the wrong place. I meant to put it in releases. No matter. I just use the svn move button to move the beta1.0 folder to the right place:
svn16
Done. Now to work on the beta I check it out, or I can continue working on the trunk. If I check it out I have to create a set up a new build folder for it of course. Here is the current folder tree:
svn6
The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Displaying The Image Count With Bindings

cocoasmall
To give some feedback to the user I want to include a display of the number of images that will be exported. I can do that by using bindings: by binding text on the window to an ivar in my Random_Wok object.

I create an ivar called imagesToProcess and an accessor to set it:
rwok207
In -willBeActivated, I add some code to set it up before it is used:
rwok209
And since changes in the image type (master or version) can change the number of images, I have to set it each time the export type changes:
rwok210
That is all the code except for this method:
rwok208
It returns "s" if imagesToProcess is more than one, otherwise an empty string. I need that in order to implement correct pluralization of my display string.

To display the image count on the window I add an NSTextField in the corner like this:
rwok211
Its value is unimportant because I will be constructing it dynamically with bindings, but it helps to have a descriptive string there. I set up its bindings like this:
rwok212
The Display Pattern string is what does the magic. Value1 and Value2 are bound to different key paths. The first to the value given by imagesToProcess, and the second to the plural string given by pluralImagesToProcess:
rwok213
When these are substituted into the Display Pattern string, the result is what the user needs to see how many images are selected:
rwok214

Because I implemented the accessors and I use them to change the value, bindings take care of doing this display updates automatically. If I select images in Aperture that include some with multiple versions of one master, the image count displayed changes when I click Master and then Version, just as it should.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Making A Customized Button

cocoasmall
The next feature I want to add is a Bagelturf badge that can be clicked to launch a browser to my home page. For that I need a graphic and some way of making a click open a URL. Since the badge won't obviously be a button, or have a button action, I want the cursor to change to a pointing hand to show that it is clickable.
rwok230
After a lot of messing about with tracking rectangles I discovered that the correct way to implement the pointing hand was by using -resetCursorRects. I have to subclass NSButton and then override -resetCursorRects to define the cursor shape that I want when the cursor is inside my button.

First I drag a button in Interface Builder onto my window and select the Rounded bevel Button type. Then I create a new file and declare it as a subclass of NSButton using this code:
rwok200
Then I add the implementation code:
rwok201
To use the custom class in my nib file, I save those files and drag the .h file onto my nib file. This makes the nib file aware of the custom class and selects the class:
rwok202
Now I can select my button and set its custom class to BTPointingHandButton:
rwok203
By making a small image in Photoshop that has transparent background and saving it as a TIFF, I can have the image lay on the window background. To add the image to my XCode project I drag it onto the Resources folder and opt to copy it in.
rwok231
Clicking on the image and using the inspector shows me what is there:
rwok204
By dragging the image onto the button, the image is automatically set:
rwok205
And finally I can add an action to File's owner via the attributes pane called bagelturfAction and hook it up to the button with control-drag. To make the action open a URL I add the action code to Random_Wok.m and use NSWorkspace's openURL method:
rwok206
Done. Now if I hover over the button the cursor changes and a click launches Safari and goes to my home page.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Saving The Default Folder

cocoasmall
As currently written the plugin defaults to the same folder each time I run it. This is not convenient. There is a very good chance that I will want to go back to the same folder each time, so I added the exported file path to the defaults that are written to a read from the prefs file.

As part of doing that I changed -defaultDirectory to this code:
rwok199
Here I check to see if the value read from the defaults (_defaultExportPath) is a valid folder using NSFileManager. If it is, then I use it, otherwise I use the built-in default of ~/Documents. This deals with the user moving or renaming the folder between runs.

Another change I have made is to make the Generate button do something. I decided that using the current date and time would make for a good salt value:
rwok197
And here is the latest interface:
rwok198
Note that since I am posting this after the plugin was released, this interface design is no longer current.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Storing The Settings Between Runs

cocoasmall
[Note: my blogging this project is some way behind the development. Random Wok 1.0 has already been released.]

An inconvenience with the current version is that it always presents blank text fields to the user. I would like to store the prefix, postfix, salt, use salt, random format, random length, and alpha case selection somewhere so that their settings are retained from one run to the next.

Implementing this turned out to be harder than I thought it would be. After reading up on NSUserDefaults and looking at several examples, it all looked straight forward enough. But something very odd happened: the values I were successfully saving and restoring between runs was being stored somewhere, but not in the com.bagelturf.Aperture.Export.Random_Wok file as I had been expecting. Not only were they not in the expected file, the expected file did not exist. It was never created.

The values I was storing were actually getting put into Aperture's own preferences file in ~/Library/Preferences. While under some circumstances this would have been the desired behavior (such as writing my own plugins for my own application), in mine it was not. I could not use most of the NSUserDefaults methods because of this side-effect.

So I created two methods: -getDefaults and -setDefaults and inside them used -persistentDomainForName: and -setPersistentDomain:forName to read and write the complete plist file as a dictionary.

Setting the defaults (that is writing the file) is done like this:
rwok190
It first accesses the shared instance of user defaults and then uses the bundle identifier to retrieve the settings into a mutable dictionary. That dictionary is then updated with the latest values from the ivars, suitably encoded, and written back to the file. This allows older versions of the plugin to work with newer versions of the plist file: anything not used is simply left alone.

Reading the defaults is much more involved. The extra work comes from the need to set up the file if it does not initially exist and to manage plugin version changes. The first part is much like before, with the addition of reading the version from the plugin bundle:
rwok191
Then I deal with the first run. This is indicated by an empty dictionary:
rwok192
This results in a file on the disk, and a mutable variable defaultsDict with the same information. Now I am ready to deal with a change of version number:
rwok193
I have nothing to do: there is only one version so far. If the version has changed, then the new version number is written to the dictionary and out to the file. Finally I am ready to set up the ivars from the dictionary values:
rwok194
I call -getDefaults from the -initWithAPIManager method so the ivars are ready to go when the window is created. Then in -willBeActivated I set up the various elements of the view:
rwok195
Finally in -willBeDeactivated I call -setDefaults to write the current state of the interface to the plist file.

Here is what the final com.bagelturf.Aperture.Export.Random_Wok.plist file looks like:
rwok196
The other parts of this series can be found via the Cocoa page.
|

Subversion Part 3: Checking Out And Using A Project

subversion_logo
To use my Random Wok project I have to check it out. While the files are in the database they are inaccessible to normal file operations. I will do the check out in svnX.

I'm going to check out all the files in the trunk virtual folder to a new folder called Random Wok in my Documents/Progging/Working folder. First I create that new folder. Then in svnX I select the trunk virtual folder in the browser at the bottom click on the svn checkout button and select Working/Random Wok as the folder to check out to. svnX creates the folder for me and fills it with the files:
svn5
svnX has a browser built in for looking at working copies. Opening that shows me the working version I just checked out and lets me give it a more descriptive name:
svn12
Double-clikcing on the line shows me a browser that I can use to see the file status:
svn13
Now I can get back into XCode. I double click to open the XCode project that was checked out and set it up to use subversion. From the SCM menu I select Configure SCM... and set it up like this:
svn14
I change the build location and the location for intermediate files, and enable and select the SCM. I also found it necessary to clicked on Rebuild Code Sense Index for XCode to have the data it needs about my source files. To fully set up the build destination I select the target and change the Build Products Path for all configurations to be that Random Wok Folder:
svn1
Now the SCM tab for my project looks like this, showing the initial check-in:
svn15
Now I can enable the SCM status display by selecting the Random Wok project icon at the top of the XCode project list and control-clicking on the column header:
svn16
Now if I edit a file and save it I get a status change, M, showing modified:
svn17
Also shows up in the SCM smart folder on left:
svn18
Another thing I need to do is to tell snv that the pbxuser files are binary. This will prevent svn trying to merge them. To do that I go back into the terminal:
svn11
How does svn know where the repository is? There is a hidden folder called .svn in the checked out folder. In fact there are hidden .svn folders in all the folders.

Lastly I commit my updated version to the repository. I select all the files to update and from the menu use CMS > Commit Changes, and fill in the commit message:
svn12
With the files checked out I can compile and use my project just as I did before I used subversion. Each time I get to a good stopping point, I check in the files.

Eventually I will have a version that I want to release. And that means using svnX again.

The other parts of this series can be found via the Cocoa page.
|

Random Wok 1.0 Released

rwicon
Random Wok 1.0 has now been released and is available on the Downloads page. It's a Universal binary for Aperture 1.5 or later and Mac OS X 10.4.8 or later. Random Wok exports images from Aperture and gives them random names. For more information on Random Wok see its product page.
|

Aperture Plugin: Dealing With Duplicate Random Names

cocoasmall
Now that I am finally exporting with random file names I feel like I am on the home straight. But there is plenty more still to do: looking for problems, for instance.

There are two things that can go wrong with the export. First there could be an existing file in the folder I am exporting to. This is actually quite likely since repeated exports will create the same names for the same files unless the salt is changed. To solve that I add some code to display an alert if the rename fails. I modify the call to movePath by adding a handler. If there is an error with the rename, the handler method will be called:
rwok137
To support the handler, I added two methods:
rwok138
I had to incorporate a workaround for a bug: the return value of fileManager:shouldProceedAfterError is actually ignored by movePath:toPath:. NO always comes back. So I had to create an ivar to pass that value.

Although this code handles errors just fine, there is a better way of dealing with the situation of existing files with the same name as new files. I can create all the random file names and make sure that none of them exist in the folder before I even start the export. That will ensure that errors are truly exceptional (cause by hardware or other programs maybe).

Another problem I may run into is that there may be too many files for the randomness. Exporting 1000 files with three random decimal digits in the name is guaranteed to run into trouble.

So my code needs to ensure that all of the random names are unique with respect to themselves and to the destination folder. The solution is to create dictionary of existing filenames with NSNull objects. I use a dictionary for finding duplicates because they are very efficient for large numbers of images:
rwok140
To that I add the files being renamed, the difference being that the files being renamed are stored with the new random name. The case-insensitive nature of the filing system means that I need another another dictionary to track file names coerced to lower case. Collisions in that dictionary mean that I have a problem.
rwok139
My renaming code ignores the existing files by looking for the NSNulls and renames the rest:
rwok140
The other parts of this series can be found via the Cocoa page.
|

Random Wok: Heading Toward Release

Behind the scenes my Aperture plugin Random Wok has been undergoing development beyond the blog entries I have been posting. I plan to release on March 22nd.

The interface has transformed somewhat, and it has gained the ability to display thumbnails:
rw2007-03-17
I have been learning a great deal about bindings, KVC, NSTableView, and other aspects of Cocoa programming. Over time I will post more blog articles about its development to show how this was implemented and what I learned. There will be a 1.1 release as well since there are some non-essential features and optimizations that I want to add that will not make it into the first release.

Read more about Random Wok on the Products page or see blog articles about its development on the Cocoa pages.
|

Subversion Part 2: Setting Up A Repository And Importing A Project

subversion_logo
The repository called Random Wok Repository I set up in Documents/Progging/Repositories needs some internal structure. That's because I am going to have it contain virtual folders called trunk, releases, and branches (I prefer releases over tags).

In the original Random Wok project folder I create the new structure: trunk, branches, releases. And then copy the current contents into trunk and delete the Build folder.
svn9
Now I am ready to do the import. I need to do this from the command line. First I change my working directory to the changing to the original project folder (yellow in my diagrams) and then import using svn import. I have to give a check-in message with -m or else svn complains with an error (no editor set up). The dot in the svn import line below references the current directory. The repository is defined with a URL that starts with file:///:
svn8
Now the structure looks like this:
svn3
Everything below Random Wok/ in red is in the repository and so the folders shown are virtual folders. They can be referenced with URLs passed to svn that start with file:/// by including the path to the repository and then the path inside the repository.

Now revision 1 is in the repository I can use a utility called svnX to inspect it. I no longer need the Terminal window open. svnX will not recognise an empty repository, so up until now I could not use it. I download snvX from Apple and install it. Svnx is a GUI front end to svn.

The prefs are already set up for me:
svn7
In the Repositories panel I add a new repository pressing +, giving the repository a name, and dragging the Random Wok Repository folder onto the Path field. Now svnX knows where the repository is:
svn10
I double-click on the repository to open it in svnX and look at the contents:
svn11
The lower part is a folder browser that shows the structure of the revision select in the pane above. I can see the import message and a list of revisions with just one revision in it. The svn mkdir button is used to make more virtual folders in the repository and svn copy and svn move button copy and move virtual folder trees in the repository.

I no longer need the original project file, so I can archive it to a ZIP file to get it out of the way:
svn4
In its current state I cannot use my project. It is inside the repository and inaccessible to XCode and other tools. So the next step is to check out a copy.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Generating The Random Name and Renaming Images

cocoasmall
Now I am able to generate the random string I can rename the images after they have been exported. So for each image file I do this:
rwok135
To get the image UUID, depending on the API version I call either a method that gets the image properties with or without the thumbnail. I don't need the thumbnail, so I prefer to use the one that uses least memory. To determine which API to use, I add this code to the -initiWithAPIManager method:
rwok134
To generate the random string I either use the salt string or not, depending on whether the checkbox is set. Then the random string is combined with the other parts of the name and used for the rename (achieved with the movePath:toPath:handler method). I add hyphens to the name in the code above for debug purposes. In real life the format string will drop the hyphens.

This code is not very memory-friendly. Each time around the loop will allocate more memory, so I need to refactor it with better memory use.

There. Finally making random file names!

The other parts of this series can be found via the Cocoa page.
|

Subversion Part 1: Downloading and Set Up

subversion_logo
For a long time I have wanted to implement a version control system. The attraction is being able to gain control over my code without simply making duplicates of the project folders. In particular I want to be able to fork and merge different code bases and roll back to working versions. As soon as I involve anyone else in the development process (even testers), it is no longer just me and all this becomes a necessity.

So I am going to install subversion and move my active projects over to it. I have been reading up on subversion for a while on and off, and think that I understand it well enough to implement it and blog it. For the record I'm using XCode 2.4.1, subversion 1.4.3, Mac OS X 10.4.8. Since the interaction among these changes, bugs get fixed, and features get added, not all of this article will stand the test of time.

I'm going to concern myself with just a local repository. I don't need remote access yet, so that will do. This decision means that I don't have to deal with Apache, WebDAV, and a host of other issues that make the set up complex.

A significant cause of confusion is the number of folder hierarchies that have to be understood and managed in the setting up and running of subversion. So to make what I am doing crystal clear, I have some diagrams to illustrate and am using color to differentiate the parts.

I have to work with three different folder hierarchies, so I better get organized.

The first hierarchy is my current project, Random Wok. Its project folder is called Random Wok and its path is Documents/Progging/Cocoa/Aperture/. Inside Random Wok is the XCode project file, the Build folder, and all the source files and resources. Once I have put the project into my repository I will mothball the Random Wok folder. Currently I create new versions by duplicating the Random Wok folder and numbering them. These files and folders are yellow:
svn1
The second hierarchy is a new one. To use subversion I will put my code into a folder called trunk that lives inside a virtual folder hierarchy inside subversion repository. That repository (implemented inside a folder) will itself be one of a number, all organized into a real folder hierarchy on my hard drive. As per the subversion documentation, I will also have a branches folder and a releases folder at the same level as the trunk folder. So my first step is to create a folder Documents/Progging/Repositories on my hard drive. That is the real folder that will hold all my repositories. The first one will be called Random Wok Repository and will hold one project. These files and folders will be red.

The third hierarchy I have to concern myself with is that of the checked-out code. I will be checking my code out of the repository to this new location, building and debugging it, and finally checking it back in again when I am happy with the result. If I want I can then delete the code, because it's all in the repository. I create a folder called Working in Documents/Progging and another called Builds. Working is where I will check the source out to and will be green. Builds is where I will have all the Build folders live. I want to be able to keep the builds separate from the code. Inside Builds I create a folder called Random Wok. Inside that is where the builds and temporary files will be kept. The Build files and folders will be blue.

Now I need a copy of subversion. Subversion is actually a collection of utilities, svn and svnadmin being the two I will use the most. I download a prebuilt binary from Coding Monkeys and run the installer. That puts the binaries into /usr/local/bin:
svn2
To make using these simpler I set the PATH variable in the current tcsh shell invocation to /usr/local/bin with setenv by typing setenv PATH /usr/local/bin.

Next I need to run svnadmin to create my first repository:
svn3
And there it is:
svn4
Now I run svn once so it will create a folder called .subversion in my home directory:
svn5
Much more information follows that. In the .subversion folder is a config file. I edit this using TextWrangler with File > Open Hidden... and selecting Enable: All Files. In the [miscellany] section I modify the list of global-ignores so that it looks like this (it's actually all on one line):
svn6
The modifications make subversion ignore some extra XCode files that I don't care about. I don't include the *.pbxuser files as some people do. That is because those per-user values include the custom executable settings that I need to launch Aperture. Here is the hierarchy so far:
svn2
Next I need to set up the repository and import a project.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Generating A Random String From A UUID

cocoasmall

The final part to implementing the randomness is actually making the real file names. To do that I write a method for generating a string from an NSData object using a character set and a length:
rwok131
This first uses eight bytes to create a long long (64 bit) value and then uses repeated division to extract character via an index into the character set string.

Then I use that to help create the random string from the UUID and the salt:
rwok132
First it makes a single string from the UUIDF and salt, then converts that to an NSData object and creates the MD5 digest of that. That is then used to generate the final string that will be used in the file name.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Creating The Example File Name

cocoasmall
There are several parameters that depend on the settings for the random string format, the alpha case, and the length. I concentrate all the decisions for these into one big ugly switch statement that starts off like this:
rwok127
This sets a character set for generating the random string, and also an example string that I will use on the display. Displaying the example random string now consists of this:

rwok128
I truncate the example random part by the selected string length. Anywhere that the random string parameters change, I add a call to recalculate the parameters, such as in the action code for selecting the length:
rwok129
Now the dialog looks like this:
rwok130
The example file name now follows the settings.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Adding Randomness

cocoasmall
To get my randomness I will use the MD5 message digest. That delivers a 128 bit number based on an input of any number of bytes. Since Mac OS X ships with MD5 functions in a dynamic library, all I have to do is to call that appropriately.

Appropriately in this case means using code that has already been written. Andreas Mayer submitted some code to the Cocoa mailing list that creates a category on NSData for MD5 and SHA1 digests. A Cocoa category is a collection of methods that extend those of an existing class. They cannot add ivars. The interface to the category AMDigest looks like this:
rwok125
It declares two methods, each returning an NSData object with the digest inside. Since it is a category on NSData, the methods are used just as any other NSData methods are. The implementation code wraps some C function calls and returns a new autoreleased NSData object:
rwok126
To add the methods, I create a new Objective C class called AMDigest in my XCode project and paste in the code. Left as is, this does not work, because I have not told XCode to link to the library and the function calls go unresolved. It took some work to figure out that the libcrypto.dylib file that is provided with OS X is a stub and should not be added to the project in the Frameworks and Libraries folder. All that is actually needed is to select the Random_Wok target and add this item to the build configuration:
rwok123
To use the new method I also have to #import the header file that contains the interface in my Random_Wok.m file.

The plan is to concatenate the salt and the UUID of the Aperture image together and create an MD5 digest of that. I can then use 64 bits of the digest to create a random string based on the settings of the length, format, and alpha case that the user has selected.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Ending Editing

cocoasmall
I discovered a problem with the Use Salt checkbox. If the checkbox is clicked then it disables the salt string text field. When it is clicked again, the field is re-enabled, but the string it contained is gone. Looking into this further I found that if I clicked away from the text field before disabling the field, the string would stick.

What is happening is that the field editor attached to the text field is not being told to end editing. Normally that is done when focus moves to another field or control, but not in the case of disabling the control. The cure was to change the -useSalt: method like this:
rwok124
That call to -endEditingFor: tells the window manager to end editing for the field and all is well.

I also fixed the name "Random_Wok" in the menu and the window title. I edited the info.plist file by changing the display name string:
rwok116
The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Adding a Progress Bar

cocoasmall
So far the progress bar supplied by Aperture as my images export does nothing. I'd like it to progress according to the fraction of images that have been exported out of the total number. Since progress for Random Wok includes a final renaming step that is very fast, I can display a progress bar that goes up to 90% for the actual export and then label the last 10% "Randomizing".

To implement the progress bar I have to set up the export progress structure and then update the current values that it contains as the situation progresses. The progress structure looks like this:
rwok101
To protect it against simultaneous update by multiple threads an NSLock is used. That lock has to be claimed and relinquished by any thread that wants access, so protecting against partial changes caused by context switching. The SDK comes with two methods already included: -lockProgress and -unlockProgress that lock using an NSLock ivar called _progressLock.

I set up the progress bar just before I tell Aperture to start the export. The code I add to -exportManagerShouldBeginExport looks like this:
rwok110
It does some math to make the progress bar go to about 90% of its full travel when all the images have been exported, then loads up the structure before starting the export. As the export proceeds, -exportmanagerShouldWriteImageData:toRelativePath:forImageAtIndex: will be called for each image. So it is that method that hosts the following code:
rwok111
In -exportManagerDidFinishExport I have an opportunity to change the progress message and handle the randomizing rename. So I add this code to the beginning of that method to switch the message and use an indeterminate style of bar:
rwok112
Notice that I do some memory management here with autorelease and retain. The example code does this, and I believe that I need to do it with the way I have handled the strings too. If not, using it in my code is harmless.

And here is the resulting progress bar:
rwok113
The other parts of this series can be found via the Cocoa page.
|

Random Wok: Who Wants To Test It?

Who wants to test Random Wok when it is ready? Send me an email if you are interested. Update: I have several people with Core Duo machines now. Anyone with Aperture on a G5 want to try?
|

Aperture Plugin: Preventing Illegal Path Characters

cocoasmall
Since the prefix and postscript strings will be going into a path, I also want to prevent the user from entering characters such as / and : that could cause unintended actions. There is no delegate method I can add to a text field to do this. Instead, a different class is used: a text formatter.

A text formatter can be attached to a text cell. Once attached, it can intervene in the formatting of the text to modify what the user sees. A phone number formatter would automatically add brackets and spaces as a number were entered, for instance. A formatter can also change the color and style of the text, and add and remove characters.

Since none of the supplied formatters do what I want, I will have to write my own. And to do that I have to create a new formatter class as a subclass of NSFormatter, the abstract base class that all formatters descend from.

The new formatter will be called BTValidPathElementFormatter and will prevent forward slash and colon characters from appearing, and optionally disallow leading periods and hyphens. Leading periods in file names make the file invisible in the Finder and that would almost certainly be unintended. Leading hyphens are not valid in a file name.

To create my new class in XCode I use File > New File and select Objective C class. Then I give it a name and make sure it is added to my project:
rwok83
Then in the header file, I add the prototypes for the necessary methods, and add my own methods and ivars for handling the leading periods and hyphens:
rwok91
The -isPartialStringValid: method allows me to figure out what was just added to the string, and then act accordingly to adjust the string and the current selection.

I need -init and -dealloc methods for my class. I use the -init method to initialize the superclass and up the default state of my ivars:
rwok92
And I have to provide methods for changing the period and hyphen options:
rwok93
These three methods are filled in with what is basically template code:
rwok94
And finally here are the guts of the class: the code that disallows the characters. The first part sets up a character set that will be used to compare against the string in the text field. Then two easy cases are dealt with quickly:
rwok95
The code uses NSRange structures to keep track of the current selection. Next, periods and hyphens and disallowed:
rwok96
I'm not certain if the unichar cast is needed, but I did notice that the type returned by characterAtIndex: is of type unichar, so put it in to be on the safe side. Finally I look for the illegal characters in the text that was inserted. I can figure out what was added by comparing the previous and current selections:
rwok97
And that is it for the new class. I have to make some changes in Random_Wok so that the new formatter is attached to the fields and set up correctly. I add this code to -willBeActivated:
rwok98
It creates an instance of the formatter for each of the two fields that need formatting. Additionally the prefix formatter is set to reject hyphens and periods. Finally the formatters are attached to the NSCells associated with the NSTextFields using -setFormatter.

A delegate method to NSControl -control:didFailToValidatePartialString:errorDescription is called whenever the formatter returns NO. Since Random_Wok is already a delegate of the three text fields, this will be called. By coding it to beep, I can give the user audible feedback that the key press is not permitted:
rwok99
And the code works. The prefix field will not accept leading periods or colons and both the prefix and postfix fields reject forward slashes and colons.

The other parts of this series can be found via the Cocoa page.
|

Aperture Plugin: Continuous Example File Name Updates

cocoasmall
There is a problem with the current interface: the user has to type in the prefix and postfix strings and then tab away from the field to end the editing. Until that happens, the example file name does not get updated. What I want is for the text fields to update the example file name continuously with each key press.

To do this I have to set up my Random_Wok object as the delegate to the text fields. If my Random_Wok class provides a -controlTextDidChange: method, then as a delegate, the NSTextField instances will call it for each change of the text.

I set up Random_Wok as the delegate in -willBeActivated: for each of the text fields:
rwok80
And provide a way to distinguish between the fields by giving them tags:
rwok81
I give them numbers 0, 1, and 2. Then I code the delegate to update the appropriate string and remake and display the example string on each call:
rwok82
And that works. Typing, deleting, pasting, any change to the text fields is reflected in an immediate change in the example filename.

The other parts of this series can be found via the Cocoa page.
|

Random Wok Aperture Export Plugin Product Page

rwicon
Since I am planning on releasing Random Wok in a month or less, it now has its own product page.
|

Aperture Plugin: Using Bindings To Populate The Length Pop-Up

cocoasmall
To populate the length pop-up I'm going to use bindings. Bindings allow changes in one object to automatically control another object. In this case I wa