Archive for the ‘Cocoa’ category

The Technical Story of the Adium Fixes in Version 0.32

December 31, 2008

Here are the problems I had updating my Adium plugin for version 1.3 of Adium, in the order they were discovered.

First, Adium plugins now must declare a minimum version of Adium. If you don’t say “I require at least version x.x of Adium” then Adium assumes the plugin is too outdated to work and won’t load it. (Turns out that was the right assumption for my plugin!) You declare a minimum supported version by adding a value for AIMinimumAdiumVersionRequirement in your info.plist.

Second, AppleScript support for getting the current status message is broken. So I had to add a way for IMLocation to ask the plugin for the current status message. (It also means that I’m stuck requiring a plugin for any integration with Adium, which is a shame.) For what it’s worth, I prefer the new AppleScript interface for Adium (modulo the “not working right now” bit).

Third, the way plugins are written for Adium has changed. From a software-engineering perspective the changes are for the best, and will make a better Adium. But of course I’m complaining because I had to do more work :-).

Plugins used to inherit from AIPlugin, which had a field named adium that was analogous NSApp. Now plugins conform to the AIPlugin and AIPluginInfo protocols, and can inherit from anything. (Hint: #import <Adium/AISharedAdium.h> will give you the adium back, but now as a proper global.)

Fourth, the way I was updating the status message for Adium wasn’t working so well. And the more I looked into it, the more it looks like very little (if anything) was changed in Adium. It just had a different way of dealing with statuses that wasn’t working out all that well for me.

Solving this issue took me longer then solving all other problems combined. But in the end it was definitely worth it. I have better integration with Adium now. And I was able to get rid of some update-throtteling code of mine that prevented too many changes to the status being made too quickly (Adium is now smart enough to do that internally!).

Fifth, testing, testing, testing. All code that’s written must be tested. And because so many changes were made to Adium as well as my plugin’s code, I waited a few days before officially pushing my changes, to allow more time for bugs to be discovered.

NSWindow setResizable:

December 18, 2007

This article has been updated moved to a new location.

NSWorkspace Bug in – openFile: ?

December 7, 2007

EDITED TO ADD: I could reproduce this bug in a large project, but not isolate it in a smaller one. It is much likely for my code to have a bug then NSWorkspace. I’m still not 100% certain that this issue wasn’t my fault in some way I don’t understand. But no matter what caused the bug, the work-around I describe here has been working for me. Please let me know if you have this same issue as well, or any insight into what I could have done to cause it.

The Problem:
I need to launch a background-application from inside the application-support directory. The OS is Mac OS X 10.5.1 build 9B18.
[[NSWorkspace sharedWorkspace] openFile:path];
where path is the correct path to the application, in my case “/Users/user/Library/Application Support/IMLocation/IMLocationHelper.app.”, works about 3 out out 4 times, but intermittently fails. It also reports success without actually launching IMLocationHelper.app, but this is far more rare.

Details:
When openFile: explicitly fails, it prints, “LSOpenFromURLSpec() returned -600 for application (null) path /Users/user/Library/Application Support/IMLocation/IMLocationHelper.app.” to stderr.

Error -600 is procNotFound /*no eligible process with specified descriptor*/.

The path is correct, the application exists there. I verified this by
assert([[NSFileManager defaultManager] fileExistsAtPath:path]);
before calling openFile:.

Calling openFile: again immediately after a failure often works

Also, while(![[NSWorkspace sharedWorkspace] openFile:path]);
will terminate, meaning it reported success at some point, but it will not always have actually launched the application at path.

(EDITED TO ADD: At first I thought that, NSWorkspace was probably opening IMLocationHelper, but not blocking until it has finished being launched. But sleeping, to give IMLocationHelper time to finish launching, did not solve the problem.)

performSelectorOnMainThread did not solve anything ether.

I verify that IMLocationHelper has been launched by
assert([[ShellTask executeShellCommandSynchronously:@"ps -axww | grep IMLocationHelper | grep -v grep"] length] > 0);

A Workaround:
[[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:[NSArray arrayWithObject:path] waitUntilExit];.

This calls the open command to launch the application. I have not been able to make it fail when path exists.

Design Process: Current Location “Headline”

November 10, 2007

This is a bit of the design process behind one line of one settings panel inside IMLocation.

The “Locations” panel controls everything having to do with to locations. The pane’s “headline”, outlined in red, shows what is assumed to be the current location.
The Locations Panel

It reads like “Your current location is home”. It does not say “You are: Home”, or “You are at: Home”, even though that’s shorter, and closer to the familiar “you are here” stickers. “You are at:” is out, because people need to be free to choose whatever names work best for them. Naming a job-location “working” should not turn the headline into nonsense like “You are at: working”. I choose not to go with “you are: …”, because it felt too imperative — like it was dictating what the user was doing. I wanted the copy to say “This is where the program assumes you are”. I’m still not 100% sure that this was the right phrase to use, but it is clear, and it works well enough.

Immediately to the right of the headline is a button, “That’s not where i am…”, which lets the user fix things if the presumed location isn’t where they are. The button is on the same line as the the headline, because I think this makes it a little more clear that the button corrects the current location. Putting it under the headline would separate it from the current location.

I wanted clicking on the headline to select the current location, so it could be edited. This seemed like a very intuitive action to me, but affording it turned out to be surprisingly tricky.

Starting with v0.20, I made the entire headline a giant button, with a different style then the “That’s not where I am…” button:
Current Location v0.20

This showed that it was clickable, but looked kind of ugly, and testing showed it wasn’t clear to all users what clicking it did (“Why is that a button?”).

To clarify, I made only phrases that meant “the current location” clickable.

Mock-up 1:
mockup1

Mock-up 2:
mockup2

This was a big improvement, but still not good enough. The button’s borders broke up the text, making it choppy and slow to read. The “current location” button looked ugly and wrong, because normally buttons in OSX start with a capital letter. But capitalizing words in the middle of a phrase would be even more dissonant.

At this point I realized traditional buttons just weren’t a good fit. Every other button in the interface modified a location, but the buttons in the headline just select something. They don’t change anything. Every button I’ve ever seen in a good interface makes something happen — it changes data, or how data is presented, or searches billions of web-pages. I needed something a little less “heavy duty” then a button, that still afforded clicking, but didn’t break the flow of text.

Hyperlinks were a great fit. Clicking them means “show me that” — which is exactly what clicking the location headline was supposed to do — show something. They afford clicking, without breaking the flow of text.

So starting with v0.27 I made the key phrases links inside the headline.
v2_7_tiger.jpg
I also put the “That’s not where I am…” button and the headline together in a box, to help re-inforce their relationship, and to give the headline some emphasis by giving it a border.

Under Leopard, the headline looks like:
v2_7_leopard.jpg

Leopard introduced a new button style, called “Recessed Button” in Interface Builder, that is a perfect fit. It has no border, and hilights on mouse-over, just like a hyperlink. (Basically, it’s what is used in the Safari Bookmarks bar).

Right now I’m leaning towards dropping support for Tiger, so that I can take advantage of the UI improvements in Leopard. I just wish I had a better understanding of how many users that move would alienate.

Interfacing With (Hacking) iChat in Leopard

November 1, 2007

Before you consider using any of the unsupported hacks I’m about to discuss, check to see if existing frameworks , or iChat’s AppleScript interface, will do what you need. Any software update can break unsupported code in unpredictable ways at any time. When Leopard came out, all the hacks I had been using to interface with iChat broke — causing a deadlock, that was harder to track down then a crash. Hacks can have catastrophic consequences.

(I’m not going to go in-depth into the hacks I was using under Tiger. But for the benefit of people who’s code has suddenly broken: The FZDaemon/FZDaemonListener protocols have been significantly changed in Leopard. Every function in FZDaemon is now oneway void for example. My attempts to get a connection to com.apple.iChatAgent were deadlocking. The same approach will not work on both Leopard and Tiger, as far as I can tell. The good news is that the Leopard APIs are a lot cleaner.)

Reverse Engineering iChat and InstantMessage.framework

The first thing I did was check to see if Leopard introduced a supported way of doing what I needed. There were some, like the IMMyStatusChangedNotification, but still no way to set the user’s status message. After reading, I knew what frameworks I needed to poke at. I also perused the iChat, and iChatAgent bundles for further clues.

I used class-dump, otx and strings on: iChat, iChatAgent, InstantMessage.framework, and IMUtils.framework (inside InstantMessage.framework). This gave me private interface declarations, disassembled code, and portentous strings to pour over.

I also tried using gdb on iChat, to see just how it was communicating with iChatAgent. This was not very productive for me at all. I got much further by statically analyzing disassembled code and interfaces. GDB can be a great tool, but you have to know a lot about what you are looking for before you can find it — otherwise you don’t know where to set breakpoints. I’ve noticed myself using gdb less and less over the last few years. (As silly as it sounds, gdb just hasn’t been as much fun after the intel switch, now that all the assembler is x86 goobely-garb.)

Trying things out in my own “tester” project really helped me puzzle out exactly what was going on, why, and how. I had to explicitly add the IMUtils.framework to the “Linked Frameworks” group to get some stuff to link.

I learned a lot by listening to any - (NSNotificationCenter*) notificationCenter that an object exposed. For example,

[[IMService notificationCenter] addObserver:self selector:@selector(iChatNotification:) name:nil object:nil];

where,
- (void) iChatNotification:(NSNotification*)notification{
NSLog(@"iChatNotification: %@", notification);
}

Will print every notification passing through the IMService notification center. Passing in a name: or object: parameter filters what notifications your method receives. Different classes often share the same notification center, so I would verify that one was distinct before printing all it’s notifications.

It turns out that the public [IMService notificationCenter] sends an undocumented notification named “IMMyInfoChangedNotification” when the user’s status message changes. However, the documented IMMyStatusChangedNotification appears to always be sent in such cases — I could not contrive an example where the status message was changed, and it was not sent.

InstantMessagePrivate.h will let you use access private interfaces in InstantMessage.framework. It is a refinement o the class-dump of InstantMessage.framework.

IMServiceAgentImpl is the most feature-rich class for controlling iChat (see also: IMServiceAgent). [IMServiceAgentImpl sharedAgent] will give you the shared instance. Some of the most useful methods are:
- (void)setMyStatus:(IMPersonStatus)statusType message:(NSString*)statusMessage;
- (NSString*) myStatusMessage;
- (NSData*) myPictureData;
- (void) setMyPictureData:(NSData*)newPictureData;
- (NSString*) myProfile;
- (void) setMyProfile:(NSString*)newProfile;
- (NSArray*)myAvailableMessages;
- (NSArray*)myAwayMessages;

Accessor methods will not work correctly, unless you have a connection to the iChatAgent daemon.
[[IMServiceAgentImpl sharedAgent] connectWithLaunch:YES];
[[IMServiceAgentImpl sharedAgent] _blockUntilConnected];
ensures that there is a connection to the daemon. Only calling connectWithLaunch: or _blockUntilConnected didn’t do it for me, I had to call both in order. There may be a better way to get a connection, but I’m not aware of it

Unfortunately, I have not yet figured out how to get iChat to set the status to invisible. Right now I’m concentrating to getting IMLocation working on Leopard, so I’ll look into this later.

iChat’s a damn big program for “just a chat client”, It’s executable and class-dump are actually about 2x as big as Safari’s.

InstantMessagePrivate.h

Getting Mac OS X Version Information At Runtime

October 31, 2007

This article has been updated, and republished here.

Assert Outlet Connections

October 16, 2007

This article has been updated and moved here.

Better AppleScript Through Expressions

October 13, 2007

When writing a short AppleScript to complete a specific task, do not write a procedure that checks if A exists, then operates on it if it does. Instead, write an expression that operates on A, and ignore any exceptions caused by A not existing. Surrounding the statement in a try-block will ignore any exceptions it generates. The where and whose commands can help. This almost always leads to cleaner, and faster, AppleScripts.

Here is an example, a re-write of code in EyeTV version 2.5 (if you have it, you can find this script at EyeTV.app/Contents/Resources/English.lproj/EyeTV\ Help/openreadme.scpt).

When run, this opens a readme inside the EyeTV application bundle, iff the EyeTV application is running. I did not write this, and I do not know exactly how it is used, or what constraints it was written under — just what it does.

on run
set readmePath to "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd"

tell application "Finder"
 set allApps to every application process
 
 repeat with i from 1 to number of items in allApps
  set thisApp to item i of allApps
  
  if creator type of thisApp is "EyTV" then
   open file (((application file of thisApp) as string) & readmePath)
   exit repeat
  end if
 end repeat
end tell
end run

Re-writing the code by making it an expression that assumes the correct application process exists has a dramatic difference.

on run
try
 tell application "Finder"
  set theEyeTVProcessBundle to file of first item of (every application process where creator type is "EyTV") as string
  open (theEyeTVProcessBundle & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd") as alias
 end tell
end try
end run

We no longer need if or repeat blocks — they are procedural constructs that were used to find the correct application process, and guard against trying to open a file that does not exist. (I also removed the variable readmePath because I think the a path to “eyetvreadme.rtfd” is clearly a readme-path).

But it gets better. We could try removing the run and try blocks as well, and just ignore any errors the interpreter complains about. That brings the the script down to four lines. We can make it one-line: tell application "Finder" to open ((file of first item of (every application process where creator type is "EyTV") as string) & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd") as alias, but I think that makes the script too hard to understand, because the name “theEyeTVProcessBundle” documents the long expression needed to reliably find the application bundle of the running process.

One line or four lines, that’s still a big reduction from 13 lines. Now it’s short enough to embed directly in Cocoa code with NSAppleScript.

Here’s another example. This is from IMLocation‘s uninstaller code, it It removes the IMLocationHelper from the list of items that will be opened at login. Here’s the original code:

tell application "System Events"
 if (login item "IMLocationHelper") exists then
  delete login item "IMLocationHelper"
 end if
end tell

Applying the approach of re-writing the procedure as an expression

try
 tell application "System Events" to delete (login item "IMLocationHelper")
end try

Taking things a step further, by removing the try-block, and just ignoring errors when running the one-line script: tell application "System Events" to delete (get login item "IMLocationHelper"). This makes a big difference in the clarity of the surrounding Cocoa code.

AppleScript is a pretty poor language on it’s own. Usually I see it being used to get some specific thing done. In other words, generally AppleScripts are essentially expressions, used to get or set a single value; rarely are they sub-programs. This is whyExpressions are such a good fit.

A big thanks to Uli Kusterer for correcting my doubleplusungood code! (details in comments)

NSApplicationName Inconsistencies

October 11, 2007

This article has been updated, and moved here.

NSAlert Sheet Not Having Focus

October 11, 2007

EDITED TO ADD: Several months later, I ran into this same bug again!

I had an issue where if I used
beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:
to put up an NSAlert as a sheet inside a window, and I was not running on the main thread, the sheet would not have focus. I’m not sure exactly why, but this solution worked for me: Put the call to beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo: inside another method, showMyAlert, then use performSelectorOnMainThread:withObject:waitUntilDone: to make sure showMyAlert is called on the main thread.

Strangely, if i used runModal to display the NSAlert as a separate modal window, it would have focus.

The whole incident feels funny to me. I suspect there may be some deeper issue at work that I am not aware of. When I have time to investigate further I shall update this post. Unfortunately I don’t have time to look into ‘solved’ bugs today.