Saturday, October 9, 2010

The Joy of Fonts

Question of the day:
"Hey dmac... is using NSHeight([myFont boundingRectForFont]) the right height to use when working with a font?"
I responded that [myFont ascender] - [myFont descender] should give you the same value, and may be faster. It turns out that [myFont ascender] - [myFont descender]for Helvetica 16 gave a value of 16 which seemed logical, however the height of [myFont boundingRectForFont] came up with 25.6406. Whoa... that's insane.


I grabbed a copy of NSFontAttributeExplorer from Apple's example code, and modified it to be 16 pt instead of 12 pt (my version here). Giving it a run showed (click on image for bigger picture):



What is occurring is that the bounding rect for font does measure the difference between the highest ascender and the lowest descender for a font, and that those values don't necessarily agree with the "documented" ascender for a font. You can see this playing with TextEdit. 
Basically the glyphs have collided due to them extending beyond their documented "bounds". So what is the "right" value to use? TextEdit seems to think that [myFont ascender] - [myFont descender]is proper even though you may run into colliding glyphs.

It's dead Jim.

Project post-mortems are always useful to record what you learned on the project. This is just a random brain dump of stuff... hopefully somebody finds it useful.

Dev Environment
Unless otherwise specified below this using Xcode 3.1.4 on Mac OS X 10.5.8.

Cocoa Bindings
After a lot of experience working with bindings, they are finally starting to fall into place for me. I still find that they either work right away (and are great), or I spend a long time trying to figure out why they are broken. Also, I find bindings to be extremely fragile due to their dependence on key value observing. There needs to be a better way than KVO in general, because a simple typo, or refactor can break a lot of things with no errors at compile time, and the exceptions that occur at runtime can be extremely hard to track down. Unfortunately since the most regular use of KVO is for UI development, it can be very difficult to unit test effectively in my experience.

PackageMaker 3.0.3
PackageMaker was the bane of my existence on this project. I spent more time trying to get PM to do what I wanted it to do than I did doing just about anything else. My project included a Scripting Addition (osax), a Preference Pane, and a faceless background app that was launched using a launch agent. I wanted the user to be able to install this as either a local install (relative to /) or a user install (relative to ${HOME}). Older versions of PM wouldn't allow you to do this by default, and required you to install into a tmp directory, and then copy components using scripts which always seemed rather clumsy. PM 3.0.3 gives you the option of having local and user installs. In my honest opinoion PM 3.0.3 itself is so flawed as to be almost useless.
  • PM supports relative paths, but there is no way that I could find to control these relative paths from an external build environment such as xcodebuilde. I would love to be able to create debug and release packages and have them use paths relative to my build directory. I got around this by having PM always use the same paths relative to itself, and having my debug/release builds copy the files into the correct place as a build step.
  • Each "entry in contents" has a "components" tab that controls if that component should allow relocation and/or allow downgrade. Anything that has an "app" extension is by default relocatable. You can turn this off, but PM will change it back to the default (on) at what appears to be random times. For my particular case where I wanted my background app always installed in /Library/Application Support/, I really wanted it to be non-relocatable. Even when I turned it off, and checked my packagemaker project into perforce (so it was in effect locked) I found that the packagemaker command line call would sometimes turn it back on when I called packagemaker .... --pmdoc myproject.packproj.
  • Each contents entry also has a "contents" tab with owner and group. This likes to reset itself at random times as far as I can tell as well.
  • Each contents entry also has the ability to run a preinstall and postinstall script located under the scripts tab. The documentation for this is extremely weak and basically point you here instead. Unfortunately neither place actually documents what happens and what the arguments are.
  • Several features that existed in PackageMaker 2 (such as localizing errors, some of the scripts etc) appear to be gone completely.
  • There is no documentation at all about what the "Scripts directory" is and/or how to use it. Turns out that all of your scripts are stored in the "Scripts directory", so any resources you want to access from your script can be found relative to $0. I don't know if the "Scripts directory" is unique per component, or shared across components.
  • If you have a script named "preinstall" or "postinstall" in your "Scripts directory" the UI fills in the preinstall or postinstall scripts occasionally. I haven't quite figured out how to trigger it. When I did trigger it, it didn't seem to execute the scripts when installing the final package.
  • Distributions allow you to put requirements on them, and the requirements can be a script. Instead of listing the bugs out individually here, I'll refer you to this post I found. I'm not sure how many of those bugs were fixed between 3.0.1 and 3.0.3 but that list scared me.
  • PackageMaker files show up in Xcode as folders (since they are a "packaged document") instead of a single file.
  • Dealing with packaged documents with Perforce is a pain.
  • PackageMaker files (xxblah-contents.xml) contain full paths to things, even if you make everything in them relative. This is only a pain when you'd prefer not to have your file hierarchy exposed to the world when you are working on an opensource project.
I'm sure I ran into other issues as well and forgot to scribble them down. I ended up just forcing my users to install at the local level. I haven't tested any of this on Snow Leopard, but considering the version there is 3.0.4 shipping with Xcode 3.2 (as opposed to 3.0.3 on Xcode 3.1.4) I doubt that they've fixed 
enough of the crucial bugs to make PackageMaker a viable option for any real packages. I would highly recommend reading through the installer-dev mailing list before making any decisions regarding using PackageMaker. Once I decided that PackageMaker as a dead end, I tried making my whole thing work as a drag install, but eventually gave up on it. I ended up falling back to using Stéphane Sudre's venerable Iceberg and will be looking at his new Packages for future work.


Installer
When the installer runs, the "line numbers" for any errors in the scripts are wrong.
    IOBluetooth Interfaces
    I will get into specifics of the two frameworks below, but they do have some things in common. These may seem nitpicky, but they got annoying quickly.
    • The tabbing in the header files is screwy.
    • Whomever named the methods didn't take Cocoa naming conventions into consideration (-getClassOfDevice, -getServiceClassMajor etc.).
    • Although the return values from many of the methods are documented as IOReturns they appear to be a mix of actual IOReturn values, OSStatus values or BluetoothHCIStatus values.
    • The documentation is very sporadic.
    IOBluetooth.framework
    The IOBluetooth.framework contains both the standard C and Objective C interfaces for using Bluetooth on the Mac. Issues I ran into:
    • There appears to be use of both NSNotificationCenter (kIOBluetoothDeviceNotificationNameConnected) and IOBluetoothUserNotifications which overlap and are confusing.
    • You shouldn't call any IOBluetooth methods from within a callback from the framework. This doesn't appear to be documented anywhere, but it will cause issues.
    • There is no documented way of checking to see if Bluetooth is on or not without displaying a user interface. If you call the SPIs
      int IOBluetoothPreferenceGetControllerPowerState(void);
      void IOBluetoothPreferenceSetControllerPowerState(int);
      you can control the state of bluetooth, but it may lie to you in that if you call IOBluetoothPreferenceSetControllerPowerState(1) immediately followed by IOBluetoothPreferenceGetControllerPowerState(), the get will return 1 even though if you attempt to set up a connection without waiting "a little while" it will fail. The little while was ~250 ms on my notebook, and closer to ~400 ms on my desktop.
    • If you do call IOBluetoothValidateHardware which is undocumented, but exposed in the API if you search hard enough (you have to find IOBluetoothUIUserLib.h in the IOBluetoothUI.framework), it still requires a delay after you call it before you can actually use the Bluetooth host adaptor.
    IOBluetoothUI.framework
    • On 10.5, the framework is missing IOBluetoothDevicePair.h file. It does exist in 10.6, and using the 10.6 header appears to work fine with the 10.5 implementation.
    • There doesn't appear to be anyway to attach any of the UI dialogs as sheets.
    • IOBluetoothPairingController throws exceptions (such as *** Assertion failure in -[NSTextFieldCell _objectValue:forString:errorDescription:], /SourceCache/AppKit/AppKit-949.54/AppKit.subproj/NSCell.m:1338) and then basically hangs the UI on you on 10.5.8.
    • IOBluetoothUIUserLib.h is not referenced by the master IOBluetoothUI.h/IOBluetoothUI.h header file, so finding that it even exists can be difficult.
    Scripting Additions
    • If you want to do anything interesting in a scripting addition it is very difficult to support both 10.6 and < 10.6 (see here for details). You can do it by basically writing your Scripting Dictionary such that you have a 10.5 and a 10.6 version of each call. This is ugly and unfortunate.
    LaunchD
    • If you are attempting to run launchctl from within a script being called by a package, you must be sure to run in the users context as opposed to running it as root. For example:
      # Must use su here (as opposed to sudo) because we need launchctl to run
      # fully in the users context, otherwise we will get a
      # launch_msg(): Socket is not connected error.
      su -l ${USER} -c "/bin/launchctl remove \"com.me.myagent\""
    Android on Mac
    Writing software for Android works pretty nicely on the Mac. Eclipse has the usual quirks of a Java app running on the Mac, but other than that it's certainly doable. One key point is that on 10.5.8 you need to be using JDK 1.6. You can choose JDK 1.6 by using the /Applications/Utilities/Java Preferences.app. Apple has their usual screwy naming conventions where Java 5 <-> JDK 1.5 and Java 6 <-> JDK 1.6. If you turn off JDK 1.5, when you turn on JDK 1.6 you will run into problems in Mail.app where it won't be able to show any previews and you will get a  "plugin missing" message. Turning 1.5 back on (and ordering it after 1.6 in the list) will fix this.

    Miscellanea
    • Make sure there are 64 bit versions of everything you are going to be depending on before you decide to depend on them.
    • You can actually do a pretty decent job of writing AppleHelp in VoodooPad. You can also do it in Omni Outliner.
    • To make a nice .DS_Store file for a disk image, create a disk image and set it up the way you want it. Make sure that the image file that you plan to use as a background is actually located on the disk image itself. Once you have it set up "eject" the disk image to force the Finder to write out the .DS_Store file, and then reopen it. Take a look at the .DS_Store file using hexdump to make sure it got everything right.
    • You never want to use shared frameworks or shared code in any sort of plugin (osax, prefpane etc) without being careful to change the names of the classes (and/or category methods) due to the lack of namespaces in Objective C. This applies strongly to everything in Google Toolbox for Mac.
    • Dealing with objects such as Bluetooth channels and connections that need to be cleaned up in a deterministic fashion can be tough when running under Garbage Collection. You cannot depend on using close routines in the finalize method because the order in which things are finalized is unspecified.