Sunday, October 2, 2011

MacOSX standard artwork in LiveCode

When you develop an application for Mac, users have certain expectations about how your app will look and behave. One of the often forgotten parts of your user interface is the icon artwork - it is very tempting to simply buy an icon collection and then use it everywhere in your cross-platform application. However, to help developers achieve a consistent user interface, across multiple versions of the operating system, Apple has been adding 'template' images to its Cocoa NSImage class.

Here are a few examples which I recently used in a project:


For a complete list of the available template images, look here.

Great stuff, but how are we going to get at it from LiveCode? Not directly, I'm afraid. But all is not lost: a while back, Apple's Java engineers added to their Java implementation on MacOS X 10.5 Leopard (and later) the ability to fetch these images using a simple call:
Toolkit.getDefaultToolkit().getImage("NSImage://NSColorPanel")

You can see where I'm heading with this: writing a helper Java class that you can execute with a simple 'shell' function call. So let's start by writing up the Java class code. We'll set it up with two parameters: the first parameter accepts either "*" (to say you want a complete set of images) or a comma-separated list of image names; the second parameter is the output directory where the available images will be saved in PNG format.

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.EnumSet;
import javax.imageio.ImageIO;

public class NSImageFetcher {

public enum NSImageName {
// Image Template Constants
NSQuickLookTemplate,
NSBluetoothTemplate,
NSIChatTheaterTemplate,
NSSlideshowTemplate,
NSActionTemplate,
NSSmartBadgeTemplate,
NSPathTemplate,
NSInvalidDataFreestandingTemplate,
NSLockLockedTemplate,
NSLockUnlockedTemplate,
NSGoRightTemplate,
NSGoLeftTemplate,
NSRightFacingTriangleTemplate,
NSLeftFacingTriangleTemplate,
NSAddTemplate,
NSRemoveTemplate,
NSRevealFreestandingTemplate,
NSFollowLinkFreestandingTemplate,
NSEnterFullScreenTemplate,
NSExitFullScreenTemplate,
NSStopProgressTemplate,
NSStopProgressFreestandingTemplate,
NSRefreshTemplate,
NSRefreshFreestandingTemplate,
NSFolder,
NSTrashEmpty,
NSTrashFull,
NSHomeTemplate,
NSBookmarksTemplate,
NSCaution,
NSStatusAvailable,
NSStatusPartiallyAvailable,
NSStatusUnavailable,
NSStatusNone,
NSApplicationIcon,
NSMenuOnStateTemplate,
NSMenuMixedStateTemplate,
NSUserGuest,
NSMobileMe,
// Multiple Documents Drag Image
NSMultipleDocuments,
// Sharing Permissions Names Images
NSUser,
NSUserGroup,
NSEveryone,
// System Entity Images
NSBonjour,
NSDotMac,
NSComputer,
NSFolderBurnable,
NSFolderSmart,
NSNetwork,
// Toolbar Named Images
NSUserAccounts,
NSPreferencesGeneral,
NSAdvanced,
NSInfo,
NSFontPanel,
NSColorPanel,
// View Type Template Images
NSIconViewTemplate,
NSListViewTemplate,
NSColumnViewTemplate,
NSFlowViewTemplate;
}


public static Image getNSImage(final NSImageName nsImageName) {
return getNSImage(nsImageName.name());
}

public static Image getNSImage(final String nsImageName) {
return Toolkit.getDefaultToolkit().getImage("NSImage://" + nsImageName);
}

public static void saveNSImage(final NSImageName nsImageName, final File outputFile) throws IOException {
saveNSImage(nsImageName.name(), outputFile);
}

public static void saveNSImage(final String nsImageName, final File outputFile) throws IOException {
final Image nsImage = getNSImage(nsImageName);
if (nsImage != null) {
BufferedImage bufferedImage = null;
if (nsImage instanceof BufferedImage) {
bufferedImage = (BufferedImage) nsImage;
} else {
bufferedImage = new BufferedImage(nsImage.getWidth(null), nsImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);
final Graphics bufImgGraphics = bufferedImage.getGraphics();
bufImgGraphics.drawImage(nsImage, 0, 0, null);
}
if (!outputFile.exists()) {
outputFile.createNewFile();
}
ImageIO.write(bufferedImage, "png", outputFile);
}
}

public static void main(String[] args) throws IOException {
// We can't run in headless mode or we get empty icons
// System.setProperty("java.awt.headless", "true");

if (args.length != 2) {
System.out.println("Usage: java NSImageFetcher <list of NSImageNames>,<output directory>");
return;
}
final File outputDirectory = new File(args[1]);
if ("*".equals(args[0])) {
for (final NSImageName nsImageName : EnumSet.allOf(NSImageName.class)) {
final File outputFile = new File(outputDirectory.getAbsolutePath() + File.separator + nsImageName.name() + ".png");
saveNSImage(nsImageName, outputFile);
}
} else {
for (final String nsImageName : args[0].split(",")) {
try {
final File outputFile = new File(outputDirectory.getAbsolutePath() + File.separator + nsImageName + ".png");
saveNSImage(nsImageName, outputFile);
} catch (IllegalArgumentException e) {
// ignore
}
}
}
}
}

And here's the LiveCode button script to put all the images into a subdirectory 'resources' in the directory where the stack is saved:

on mouseUp
-- save settings
local tOldDefaultFolder, tOldHideConsoleWindows
put the defaultFolder into tOldDefaultFolder
set the defaultFolder to StackFolderPath()
put the hideConsoleWindows into tOldHideConsoleWindows
set the hideConsoleWindows to true
-- call the command line via shell
local tShellCommand, tShellResult
put "java NSImageFetcher" && \
quote & "*" & quote && \
quote & StackFolderPath("resources") & quote into tShellCommand
answer tShellCommand & return & shell(tShellCommand)
-- restore settings
set the defaultFolder to tOldDefaultFolder
set the hideConsoleWindows to tOldHideConsoleWindows
-- go ahead and link
end mouseUp

function StackFolderPath pRelativePath
local tPath
put the effective filename of this stack into tPath
set the itemDelimiter to slash
if pRelativePath is empty then
delete item -1 of tPath
else
put pRelativePath into item -1 of tPath
end if
return tPath
end StackFolderPath

Okay, MacOSX comes with a preinstalled Java runtime so the above should work on MacOSX 10.5 Leopard, MacOSX 10.6 Snow Leopard and even on MacOSX 10.7 Lion (since there's still a Java 6 'out of the box'). Naturally, the artwork may change between operating system versions. So my approach would be to:
- check if there is a '<MyCompanyName>/<MyAppName>/resources' directory in the user's preferences directory (by means of the 'specialFolderPath' function);
- if the directory is absent, create it and use the 'shell' function as above to fetch the images I need, and save the 'systemVersion' in a text file in the same directory
- if the directory is present, compare the current value of the 'systemVersion' with the one saved in said directory, and once again fetch the images if there's a difference

It's up to you to either use references to these .PNG images by setting the 'filename' property of an 'image' control; or to then import the .PNG files into a stack.

Note that this artwork is copyrighted by Apple, Inc. You shouldn't extract it and then use it in your apps on any platform other than MacOSX. Oh, and please head the warning "You should always use named images according to their intended purpose, and not according to how the image appears when loaded. The appearance of images can change between releases. If you use an image for its intended purpose (and not because of it looks), your code should look correct from release to release."