« Dem Fingertipp sei Dank |  main  | My first S.M.A.R.T. failure »

Bugs, Child Windows and Spaces

Posted by dom on Tue Jan 22 12:04:35 +0100 2008

During my recent SubEthaEdit bugfixing efforts I came along some pretty annoying Cocoa Child Window issues.

First a little preamble, why did SubEthaEdit use child windows – this could be filed under “it was a bad idea anyways”. I got the idea searching for a good and safe way of displaying the little SSL-lock icon in the upper right of the window if a connection is SSL-encrypted. Safe meaning a way that wouldn’t break if something fundamental in the way how the window title is displayed would change in the future. Since most of the displaying in the titlebar window area is private API, the results I found when looking around did use some mean subclassing. I didn’t want to do that, so I thought having a tiny transparent child window on top of my window would be a very safe thing to do. So I did. And I was wrong.

Bug number one: Spaces breaks

This seems to be a known issue, when I filed it it almost instantly got duplicated to this one: Child windows and Spaces [rdar://5593261] .

If you have a window with a child window it behaves strangely on spaces. If you open spaces and drag such a window to another screen, then it does not move to the point you dragged it, but to the point it was on the previous screen – much like when you press shift or apple while dragging windows in the spaces view. That in itself wouldn’t be a major problem, but here you go: if you have multiple windows with child windows, all of them gather together on the screen you activate after moving a window around. Hurray, no more spaces for you puny little app :(. So if your app behaves this way, that might be the problem. This was not easy to find out, I found it by chance after making sure that both the main window and child window did have the same collectionBehavior. (P.S.: if you want to put a view in the titlebar, simply ask a standard button for its superview and frame and add some view relative to that. Thanks to Panic for this idea which sounds quite safe too.)

id superview = [[[self window] standardWindowButton:NSWindowToolbarButton] superview];

Bug number two: Your app crashes on quit in certain situations

We had this nasty little bug that every now and then crashed SubEthaEdit on quit. Quite an annoying habit, but we did not find a way to reproduce it. Thankfully amongst the many bug reporters who sent us the crash log via the built in reporter, there was one that actually found out a way to reproduce it. The crash only happens if you quit the application while it is in background. Then the child window is somehow overreleased. You can see the setup code of my sample NSDocument here:

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
    [super windowControllerDidLoadNib:aController];
    // Add any code here that needs to be executed once the windowController has loaded the document's window.
        // add a transparent child window to display a lock next to the toolbar button
        NSImage *lockImage = [NSImage imageNamed:NSImageNameInfo];
        NSRect frame=NSZeroRect;
        frame.size = [lockImage size];
        frame.origin = [[aController window] frame].origin;
        frame.origin.x += 100;
        NSWindow *childWindow = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
        [childWindow setIgnoresMouseEvents:YES];
        NSImageView *imageView = [[NSImageView alloc] initWithFrame:frame];
        [imageView setImage:lockImage];
        [childWindow setContentView:imageView];
        [imageView release];
        [childWindow setOpaque:NO];
        [childWindow setBackgroundColor:[NSColor clearColor]];
        [[aController window] addChildWindow:childWindow ordered:NSWindowAbove];
        [childWindow release];
        NSLog(@"%s %@ %@ %@",__FUNCTION__, childWindow, imageView, lockImage);

This is reproducible both in Leopard and in Tiger and a very bad thing. Not really grasping what goes wrong exactly in that case (if you close the windows or quit normally everything is fine, hence no general memory management problem – update: look at the bottom of the article to see that I was wrong nevertheless) I fixed this one in the 3.0.2 release by changing the lifecycle of the child window. I did so by putting the child window into an instance variable of my window controller subclass, and autorelease it on the dealloc method of it.

What I did not know then was that this didn’t fix it on Tiger in a special occasion: If on Tiger you quit SubEthaEdit while it was not active and had unsaved changes, and then without activating SubEthaEdit first directly click on the “don’t save” button. On PPC-Tiger that is. So I revisited the bug after a bug reporter finally added this magic information of reproducibility. What I did to fix it first was retain all child windows in the AppControllers applicationWillTerminate: delegate method. Which interestingly enough needed me to make an extra class for theses windows to identifiy them, because in the applicationWillTerminate: method the child windows were still alive – the parent windows weren’t. However I finally got rid of all of this by using a different and better method of adding the SSL-ock icon to the window, but some of you that might not be in the luxury of removing their child windows might find my trip valuable.

This one hasn’t been made a duplicate yet and is quite annoying. So if you have the same problem, report a bug referencing this one: Child window overrelease [rdar://5686435] .

Sample Code

If you want to reproduce it, here is my ChildWindow sample I wrote for the bug report. If you want to reproduce the last part of bug number two you have to add an NSWindowController subclass to the sample. Thanks for listening.

Addendum isReleasedWhenClosed

I have just been informed by the nice guys at Apple that programatically created windows default to a isReleasedWhenClosed state of YES which explains the overrelease. However why the heck it doesn’t crash when a window is closed does still not seem right to me. And what that means is that if I add the window without autoreleasing or releasing it beforehand that my app leaks windows. Which I verfied using ObjectAlloc – so it is still a bug, but a different one. If you want to do it right you have to setReleaesedWhenClosed:NO on the child window before you add it and everything is fine.

3 responses to 'Bugs, Child Windows and Spaces'

  1. Why so complicated? Using -[NSWindow standardWindowButton:] you can easily add custom buttons in your custom window class:

    - (float)toolbarHeight
        return NSHeight([NSWindow contentRectForFrameRect:[self frame] styleMask:[self styleMask]]) - NSHeight([[self contentView] frame]);
    - (float)titleBarHeight
        return NSHeight([self frame]) - NSHeight([[self contentView] frame]) - [self toolbarHeight];
    - (NSButton *)addButtonToTitleBarWithSize:(NSSize)size
        id superview = [[self standardWindowButton:NSWindowToolbarButton] superview];
        NSRect toolbarButtonFrame = [[self standardWindowButton:NSWindowToolbarButton] frame];
        NSRect frame;
        frame.size = size;
        frame.origin.y = -1.0 + NSMaxY([superview frame]) - (frame.size.height + ceil(([self titleBarHeight] - frame.size.height) / 2.0));
        frame.origin.x = NSMinX(toolbarButtonFrame) - frame.size.width - kIconSpacing;
        NSButton *button = [[[NSButton alloc] initWithFrame:frame] autorelease];
        [button setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin];
        [superview addSubview:button];
        return button;

  2. that is the “it was a bad idea anyways” part as i write in the P.S. of bug number one, this is the way i do this now.

    But thanks for posting the complete code to put a generic button on the upper right hand side of the window anyways.

  3. This is a bit of a nuisance. I have a good use for a child window, and I have it working nicely, but you’re right, it breaks with Spaces.

    Interestingly I don’t seem to be having the overrelease problem that you mention; or at least, I can’t reproduce it. Do you have any hints other than the application needing to be in the background?

write a comment.... (textile enabled)