16
Jun
2008
 

Cocoa Tutorial: Custom Folder Icons

by Matt Long

This is just another one of those things that seems like it ought to be a simple little code snippet and you’re there, but in actuality it’s just not the case. I am building an exporter for my application that will export a movie project in iMovie HD format. I want to mimic the file format exactly and one of the things I noticed about the directories that are stored inside iMovie HD‘s custom format (select ‘Show Package Contents’ from the context menu in the Finder) is that they have custom icons assigned to them. So the problem to solve was how to do that programatically. Here is what I’ve found.

Stop The Presses After I published this yesterday, one of our kind readers pointed out in the comments that this problem has been solved much more elegantly using NSWorkspace starting in OS X Tiger (10.4). Yes. You read that right. Tiger! Heck if I could find this solution using Google, though. Oh well.. All you really need is – (BOOL)setIcon:(NSImage *)image forFile:(NSString *)fullPath options:(NSWorkspaceIconCreationOptions)options in NSWorkspace to do the same thing.

Feel free to read on as this will provide good legacy information. Just note that setting a custom icon for a folder is not as hard as this post would make it seem.

Poking A Stick At It

If you haven’t yet realized in your programming career that often the best and sometimes only way to figure something out is to ‘poke a stick at it’ then it’s probably time you started. I’m not sure where this reference comes from, but it seems apropo. Maybe it’s a cave man thing? You’re not sure what something is, so you poke a stick at it and see what happens.

Different from cave men, though, my proverbial/virtual stick is Terminal.app. If you look inside an iMovie HD project file, which contains folders with custom icons, using Terminal.app you’ll see a listing that looks something like this.

drwxr-xr-x@  6 mlong  staff   204 Mar 17 18:00 Cache
-rw-r--r--@  1 mlong  staff  1677 Mar  7 17:22 Makin Guacamole.iMovieProj
-rw-r--r--@  1 mlong  staff   895 Mar  7 17:19 Makin Guacamole~.iMovieProj
drwxr-xr-x@  4 mlong  staff   136 Mar 17 18:00 Media
drwxr-xr-x@  5 mlong  staff   170 Mar  7 17:22 Shared Movies

This is a list from a time lapse movie I made of myself making guacamole. You can watch it here if you’re so inclined. Notice that there are special attributes on each of the files and folders as indicated by the at (@) symbol. You can see what’s going on with a particular directory by using the mdls command. Try it on one of the folders, say Media for example. and you’ll get a listing like this.

bash: mlong$ mdls Media
kMDItemFSContentChangeDate = 2008-03-17 18:00:49 -0600
kMDItemFSCreationDate      = 2008-03-07 17:19:34 -0700
kMDItemFSCreatorCode       = ""
kMDItemFSFinderFlags       = 1024
kMDItemFSHasCustomIcon     = 1
kMDItemFSInvisible         = 0
kMDItemFSIsExtensionHidden = 0
kMDItemFSIsStationery      = 0
kMDItemFSLabel             = 0
kMDItemFSName              = "Media"
kMDItemFSNodeCount         = 2
kMDItemFSOwnerGroupID      = 20
kMDItemFSOwnerUserID       = 501
kMDItemFSSize              = (null)
kMDItemFSTypeCode          = ""

Notice that the attribute called kMDItemFSHasCustomIcon is set to 1. If you now cd into the Media directory and run ls -al. You’ll see something like the following.

bash: mlong$ ls -al
total 31480
drwxr-xr-x@ 4 mlong  staff       136 Mar 17 18:00 .
drwxr-xr-x@ 7 mlong  staff       238 Mar  7 17:22 ..
-rw-r--r--@ 1 mlong  staff         0 Mar 17 18:00 Icon?
-rw-r--r--@ 1 mlong  staff  16056849 Mar  7 17:20 Makin Guacamole 01.mov

See that little 0 byte file called Icon?. This is what is telling the Finder about the custom icon being used.

The next step now that I knew that there was actually a way to specify a custom icon was to do a Google search to see if anyone had provided code to use. It was pretty sparse, but I was able to find something useful which is what this post is really about. A company called Shiny Frog based in Italy posted some open-source code that demonstrates how to set custom folder icons among other things. You can download their entire open-source code base here. I’ve leveraged only the folder icon setting code for this blog post.

Coding It Up

Here are the two projects for this post:
Set Custom Icon Project (Procedural Code)
Set Custom Icon Project (OO Code)

In the end, this post is not so much a tutorial as it is a showcase of the code. The code that it takes to change a folder icon is fairly lengthy. I have broken it out into two projects here. In the first, I broke out the code from the class that it was in when I downloaded it from Shiny Frog and put it into a procedural format. The second uses the classes as they’re found in the Shiny Frog code. Here is the code for the procedural way in its entirety. I’ve left all of the original comments from Shiny Frog in the code.

Note: If you want to simply use the code as classes, stick around for the next section in this post as I explain how you can just use what Shiny Frog provided.

- (void)setIcon:(NSString*)iconFilepath forDirectory:(NSString*)directoryPath;
{
    
    FSRef ref;
    OSStatus result;
    IconFamilyHandle hIconFamily;

    CFURLRef urlRef;
    Boolean gotFSRef;
    
    // Create a CFURL with the specified POSIX path.
    urlRef = CFURLCreateWithFileSystemPath( kCFAllocatorDefault,
                                           (CFStringRef) iconFilepath,
                                           kCFURLPOSIXPathStyle,
                                           FALSE /* isDirectory */ );
    if (urlRef == NULL)
    {
        NSLog(@"** Couldn't make a CFURLRef for the file." );
        return;
    }
    
    // Try to create an FSRef from the URL.  (If the specified file doesn't exist, this
    // function will return false, but if we've reached this code we've already insured
    // that the file exists.)
    gotFSRef = CFURLGetFSRef( urlRef, &ref );
    CFRelease( urlRef );
    
    if (!gotFSRef)
    {
        NSLog(@"** Couldn't get an FSRef for the file.\n" );
        return;
    }
    result = ReadIconFromFSRef( &ref, &hIconFamily );
    
    [self setAsCustomIconForDirectory:directoryPath familyHandle:hIconFamily];
    
}

- (BOOL) setAsCustomIconForDirectory:(NSString*)path familyHandle:(IconFamilyHandle)hIconFam;
{
    NSFileManager *fm = [NSFileManager defaultManager];
    BOOL isDir;
    BOOL exists;
    NSString *iconrPath;
    FSRef targetFolderFSRef, iconrFSRef;
    SInt16 file;
    OSErr result;
    struct HFSUniStr255 filename;
    struct FSCatalogInfo catInfo;
    Handle hExistingCustomIcon;
    IconFamilyHandle hIconFamily;
    Handle hIconFamilyCopy;
    
    hIconFamily = hIconFam;
    
    // Confirm that "path" exists and specifies a directory.
    exists = [fm fileExistsAtPath:path isDirectory:&isDir];
    if( !isDir || !exists )
        return NO;
    
    // Get an FSRef for the folder.
    
    CFURLRef urlRef;
    Boolean gotFSRef;

    
    // Create a CFURL with the specified POSIX path.
    urlRef = CFURLCreateWithFileSystemPath( kCFAllocatorDefault,
                                           (CFStringRef) path,
                                           kCFURLPOSIXPathStyle,
                                           FALSE /* isDirectory */ );
    if (urlRef == NULL)
    {
        NSLog(@"** Couldn't make a CFURLRef for the file." );
        return NO;
    }
    
    // Try to create an FSRef from the URL.  (If the specified file doesn't exist, this
    // function will return false, but if we've reached this code we've already insured
    // that the file exists.)
    gotFSRef = CFURLGetFSRef( urlRef, &targetFolderFSRef );
    CFRelease( urlRef );
    
    if (!gotFSRef)
    {
        NSLog(@"** Couldn't get an FSRef for the file." );
        return NO;
    }
    
        
    
    // Remove and re-create any existing "Icon\r" file in the directory, and get an FSRef for it.
    iconrPath = [path stringByAppendingPathComponent:@"Icon\r"];
    if( [fm fileExistsAtPath:iconrPath] )
    {
        if( ![fm removeFileAtPath:iconrPath handler:nil] )
            return NO;
    }
    
    if (![@"" writeToFile:iconrPath atomically:YES])
    {
        return NO;
    }
    
    
    
    
    
    // Create a CFURL with the specified POSIX path.
    urlRef = CFURLCreateWithFileSystemPath( kCFAllocatorDefault,
                                           (CFStringRef) iconrPath,
                                           kCFURLPOSIXPathStyle,
                                           FALSE /* isDirectory */ );
    if (urlRef == NULL)
    {
        NSLog(@"** Couldn't make a CFURLRef for the file." );
        return NO;
    }
    
    // Try to create an FSRef from the URL.  (If the specified file doesn't exist, this
    // function will return false, but if we've reached this code we've already insured
    // that the file exists.)
    gotFSRef = CFURLGetFSRef( urlRef, &iconrFSRef );
    CFRelease( urlRef );
    
    if (!gotFSRef)
    {
        NSLog(@"** Couldn't get an FSRef for the file.\n" );
        return NO;
    }
    

    
    
    
    // Get type and creator information for the Icon file.
    result = FSGetCatalogInfo(
                              &iconrFSRef,
                              kFSCatInfoFinderInfo,
                              &catInfo,
                              /*outName*/ NULL,
                              /*fsSpec*/ NULL,
                              /*parentRef*/ NULL );
    if( result == fnfErr ) {
        // The file doesn't exist. Prepare to create it.
        
        struct FileInfo *finderInfo = (struct FileInfo *)catInfo.finderInfo;
        
        // These are the file type and creator given to Icon files created by
        // the Finder.
        finderInfo->fileType = 'icon';
        finderInfo->fileCreator = 'MACS';
        
        // Icon files should be invisible.
        finderInfo->finderFlags = kIsInvisible;
        
        // Because the inited flag is not set in finderFlags above, the Finder
        // will ignore the location, unless it's in the 'magic rectangle' of
        // { -24,000, -24,000, -16,000, -16,000 } (technote TB42).
        // So we need to make sure to set this to zero anyway, so that the
        // Finder will position it automatically. If the user makes the Icon
        // file visible for any reason, we don't want it to be positioned in an
        // exotic corner of the window.
        finderInfo->location.h = finderInfo->location.v = 0;
        
        // Standard reserved-field practice.
        finderInfo->reservedField = 0;
        
        // Modified by Matteo Rattotti on: Wed Dec  5 14:56:52 2007
        // the catalog must be set
        FSSetCatalogInfo (&iconrFSRef, kFSCatInfoFinderInfo, &catInfo);
        
    } else {
		if( result != noErr ){
			return NO;
		}
		else{ // Modified by Matteo Rattotti on: Wed Dec  5 14:56:52 2007
			// File already exist, so we only need to set the flag we want (invisib, etc...)
			struct FileInfo *finderInfo = (struct FileInfo *)catInfo.finderInfo;
			
			// File type
			finderInfo->fileType = 'icon';
			finderInfo->fileCreator = 'MACS';
            
			// Icon files should be invisible.
			finderInfo->finderFlags = kIsInvisible;
			finderInfo->location.h = finderInfo->location.v = 0;
            
			// Standard reserved-field practice.
			finderInfo->reservedField = 0;
            
			FSSetCatalogInfo (&iconrFSRef, kFSCatInfoFinderInfo, &catInfo);
		}
	}
    
    // Get the filename, to be applied to the Icon file.
    filename.length = [@"Icon\r" length];
    [@"Icon\r" getCharacters:filename.unicode];
    
    // Make sure the file has a resource fork that we can open.  (Although
    // this sounds like it would clobber an existing resource fork, the Carbon
    // Resource Manager docs for this function say that's not the case.)
    FSCreateResFile(
                    &targetFolderFSRef,
                    filename.length,
                    filename.unicode,
                    kFSCatInfoFinderInfo,
                    &catInfo,
                    &iconrFSRef,
                    /*newSpec*/ NULL);
    result = ResError();
    if (!(result == noErr || result == dupFNErr))
        return NO;
    
    // Open the file's resource fork.
    file = FSOpenResFile( &iconrFSRef, fsRdWrPerm );
    if (file == -1)
        return NO;
    
    // Make a copy of the icon family data to pass to AddResource().
    // (AddResource() takes ownership of the handle we pass in; after the
    // CloseResFile() call its master pointer will be set to 0xffffffff.
    // We want to keep the icon family data, so we make a copy.)
    // HandToHand() returns the handle of the copy in hIconFamily.
    hIconFamilyCopy = (Handle) hIconFamily;
    result = HandToHand( &hIconFamilyCopy );
    if (result != noErr) {
        CloseResFile( file );
        return NO;
    }
    
    // Remove the file's existing kCustomIconResource of type kIconFamilyType
    // (if any).
    hExistingCustomIcon = GetResource( kIconFamilyType, kCustomIconResource );
    if( hExistingCustomIcon )
        RemoveResource( hExistingCustomIcon );
    
    // Now add our icon family as the file's new custom icon.
    AddResource( (Handle)hIconFamilyCopy, kIconFamilyType,
                kCustomIconResource, "\p");
    
    if (ResError() != noErr) {
        CloseResFile( file );
        return NO;
    }
    
    // Close the file's resource fork, flushing the resource map and new icon
    // data out to disk.
    CloseResFile( file );
    if (ResError() != noErr)
        return NO;
    
    result = FSGetCatalogInfo( &targetFolderFSRef,
                              kFSCatInfoFinderInfo,
                              &catInfo,
                              /*outName*/ NULL,
                              /*fsSpec*/ NULL,
                              /*parentRef*/ NULL);
    if( result != noErr )
        return NO;
    
    // Tell the Finder that the folder now has a custom icon.
    ((struct FolderInfo *)catInfo.finderInfo)->finderFlags = ( ((struct FolderInfo *)catInfo.finderInfo)->finderFlags | kHasCustomIcon ) & ~kHasBeenInited;
    
    result = FSSetCatalogInfo( &targetFolderFSRef,
                              kFSCatInfoFinderInfo,
                              &catInfo);
    if( result != noErr )
        return NO;
    
    // Notify the system that the target directory has changed, to give Finder
    // the chance to find out about its new custom icon.
    result = FNNotify( &targetFolderFSRef, kFNDirectoryModifiedMessage, kNilOptions );
    if (result != noErr)
        return NO;
	
    return YES;
}

Nearly 280 Lines of code later and here we are. I’m sure there is a group of people out there who don’t care about the internals of how this works and would like to just use the code. In fact I would wager that that’s probably the majority. Here’s all you need if that’s you. Just drop in the files NSString+CarbonFSRefCreation.m, NSString+CarbonFSRefCreation.h, IconFamily.m and IconFamily.h from the Shiny Frog project into your project and you can then change a folder icon with these few lines of code:

IconFamily *fam = [IconFamily iconFamilyWithContentsOfFile:[icnsFilepath stringValue]];
[fam setAsCustomIconForDirectory:[dirPath stringValue]];

That’s a lot easier to read and use obviously. I recommend the object oriented approach as it keeps things much cleaner. And as Shiny Frog has released this code under a standard MIT license which allows you to do pretty much whatever you like as long as you maintain the copyright information, you could take the two classes here and create your own framework that you can then just link in whenever you need it.

Conclusion

When I searched for code online to figure out how to achieve setting a custom icon for folders it looked pretty slim. Had I needed to track down all of the steps myself it would have, obviously, taken much longer to figure out, but thanks the Shiny Frog, I was able to find the code to do exactly what I wanted. I’m hopeful that this blog post will hit the search engines and make it easier for folks to find as this seems like a fairly important thing to know how to do. Until next time.

Comments

enc says:

as i look at it, i think that 280 lines of code is a bit too much for this (even if we remove comments and whitespaces). i think there should be an easier way to code this up.

other than that – great piece of code. thanks!

Apple did finally take care of this mess in 10.4 by providing -[NSWorkspace setIcon:forFile:options:]. It lets you just pass in an NSImage you want to set for the icon and automagically takes care of the dirty work for you.

Matt Long says:

@Brian Webster

Hmmm. Now that’s handy. (… he says sheepishly). ;-)

Well this really draws into question my ability to track down a problem. Google!! You’ve failed me! :-D

Thanks for the pointer.

-Matt

The source of “poking things with a stick” was an Alan Kay interview. Alan Kay, for those who don’t know, was the inventor of Smalltalk and a luminary at Xerox PARC. He later was an Apple Fellow, and currently works on the Squeak project.

The context of Kay’s “poking things with a stick” is the design of the GUI interface we all are familiar with.

@Matt

This post is still very useful, just so we can show the damn kids these days how easy they have it.

“This is how we used to set a custom icon, with 300 lines of code! Uphill both ways through the snow! And that’s the way we liked it!”

:-)

girishkolari says:

I expected to have a badging icon to be displayed over a folder icon, when I tried it is displaying only the badging icon for the folder selected folder,

Basically I am trying to badge a file and folder in a particular location, is there any other solution then icon services?