Skip to content

10.5 Release Note Gems

Here are some things I’ve found amusing as I’ve read through the release notes.

Foundation:

Use of NSCalendarDate discouraged

Developers should abandon hope for NSCalendarDate bug fixes.

Avoid ‘long double’ and ‘_Complex’ types

We strongly recommend avoiding all use of the “long double” type and the various C99 “_Complex” types other than — at most — purely local computational usage. In particular, we recommend:

  • not using it for parameter types;
  • not using it for return values;
  • not using or passing pointers to long double or _Complex types;
  • not using arrays of long doubles or _Complex types or arrays of pointers to long doubles or _Complex types as parameters;
  • not embedding long doubles or _Complex types or pointers to long doubles or _Complex types inside structures which are passed as parameters or returned as return values, nor using pointers to such structures;
  • not using long doubles or _Complex types as object instance variable types;
  • and so on.
  • Do not mix Objective C method calls or variables with any long double or _Complex type usage;
  • in other words, do not attempt to use long doubles or _Complex types with any Cocoa API

You may notice that Cocoa does not offer any “long double” or “_Complex” type APIs (such as in NSNumber) — we are avoiding them too.

Part of the issue is that the compiler emits “d” (double) as the type encoding string for @encode(long double), so there is no way to distinguish between the two, and this affects everything which uses Objective C type encoding strings, including key-value coding, forwarding and archiving. If the compiler were ever to fix that now, an app using long double will have binary compatibility problems.

Similarly for the _Complex types, the compiler emits “” (an empty string) for @encode(_Complex {float,double,long double}). Thus these types are completely invisible in method parameters and structure encodings and so on. This causes no end of havoc.

Hmm, are we supposed to avoid long double and _Complex? Actually, this probably explains the remaining problem class-dump has with Grapher.app. Some of the methods are probably returning an _Complex type, which gets an empty string for the return value type.

Foundation profile binary

Foundation no longer provides a version of the binary built for profiling. Of course, Foundation used to be one of the few libraries that actually did provide one. Use dtrace instead.

Someone sounds bitter :-)

Performance/RN-vecLib:

Accelerate Release Notes (10.5)

No new features of note. We took the year off and had a great time!

In our spare time, however, we did manage to rewrite/reoptimize in excess of 7000 entrypoints for three new architectures (21000 in total), i386, ppc64 and x86_64.

Enjoy!

class-dump 3.1.2 is now available

A new version of class-dump is now available. This release includes several bug fixes and a few minor enhancements. It’s recommended for all users of Mac OS X 10.4 or later. You can download it from the links on the class-dump project page, and see a detailed summary of the changes.

New Features

My favorite new feature is the -f option. This lets you search for methods and display them in context, showing the class, category or protocol that contains the matching methods. Before this, I would just class-dump a framework and search for the method in the output. In classes with a lot of methods, though, I’d have to scroll up to see what class contained the method, and it was difficult to get a good picture of the results. Now you can just use the -f option and get a nice short summary. This is great for discovering what classes implement some bit of private API. For example:


% class-dump /System/Library/Frameworks/AppKit.framework -f leed
/*
 *     Generated by class-dump 3.1.2.
 *
 *     class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2007 by Steve Nygard.
 */

@interface _NSBrowserScrollView : NSScrollView
- (id)_focusRingBleedRegion;

@interface NSButton : NSControl <NSUserInterfaceValidations>
- (id)_focusRingBleedRegion;

@interface NSTableView : NSControl <NSUserInterfaceValidations>
- (id)_focusRingBleedRegion;

@interface NSView (NSPrivateFocusRingSupport)
- (id)_focusRingBleedRegion;

@interface NSWindow : NSResponder <NSAnimatablePropertyContainer, NSUserInterfaceValidations>
- (void)_setLastFocusRingView:(id)fp8 bleedRegion:(id)fp12;
- (void)_getRetainedLastFocusRingView:(id *)fp8 bleedRegion:(id *)fp12;

The string is case sensitive and doesn’t support regular expressions. I just needed something simple, and it works quite well.

The -H option now generates protocol header files. These are named <protocol-name>-Protocol.h, which differs from the imports generated by the previous version. The extra dash in the name helps make them stand out against regular classes that end in Protocol, such as NSURLProtocol.

I’ve also added a –list-arches option that prints the architectures contained in the given file. I added this for my regression testing, so that I can dump all architectures that are available. Unlike lipo -info, it only lists the architecture names, which makes it easier to use in shell scripts.

Bug Fixes

I’ve added several fixes for files that use C++, so a bunch of old warnings should go away. This version of class-dump also recognizes Apple protected binaries, and won’t spew a bunch of garbage when you try to dump these files.

The Code

There are two big changes in the source code. First, I’ve implemented a visitor pattern for going through the Objective-C data to generate output. When I first implemented the -f option, I just copied the old generation code and added the pattern check. It was a quick and easy proof of concept, but it wasn’t maintainable. With the visitors most of the actual output is contained in one or two classes, instead of being spread out among many classes. I like it a lot.

The other change was converting the unit tests from ObjCUnit to OCUnit/SenTestKit. It makes sense to use OCUnit since it’s bundled with the developer tools, and ObjCUnit was crashing, so it was just as easy to switch. After converting the unit tests it was apparent that the two frameworks were practically identical in usage. Just import a different header file, subclass from a different test case class, and use slightly different asserts. My main reason for picking ObjCUnit originally was that it was easier to control the order of the unit tests, but I’ve managed to get that working with OCUnit.

Next Version

The next version will be 10.5 only so that I can start using some of the new features that are available. I think I can add support for @rpath, and supporting 64-bit files is on my list of things to do.

As always, you can send bug reports to me at class-dump@codethecode.com.

Installing Trac on Leopard

Here are some notes on installing Trac, so that I have a good reference to help me the next time I need to install it, and maybe it’ll help someone else too.

The latest version of trac is 0.10.4. This still uses ClearSilver, so we need to install that. (ClearSilver gets replaced by Genshi in Trac 0.11.)

I’ll gloss over permission and ownership issues. Python packages get installed in /Library/Python/2.5/site-packages, so you need write permission to that. Other things may end up in /usr/local, so you also need permissions there.

Python and Subversion

Mac OS X 10.5 ships with Python 2.5.1, Subversion 1.4.4, and the SWIG bindings. The SWIG bindings are required for the cool subversion repository integration.

ClearSilver

The latest version of ClearSilver is 0.10.5. Something in the Ruby module doesn’t build (complaining it can’t find i386 arches on ppc), so just disable it:


% ./configure --disable-ruby
% make
% sudo make install

This should install three libraries in /usr/local/lib: libneo_cgi.a, libneo_cs.a, libneo_utl.a.

(Somewhere something installs a stray Library directory, this might be it. I watch for it the next time I install.)

This install file says this:

The make install is relatively new, and just installs the
libraries/header files (and probably the perl and python modules, but
that’s a guess)

Uh, thanks for guessing, but it doesn’t install the python modules that Trac needs. Change into the python directory and do this to install:


% sudo python setup.py install

This should create /Library/Python/2.5/site-packages/clearsilver-0.10.5-py2.5-macosx-10.5-ppc.egg.

You can verify by running python and trying to import neo_cgi.

Postgres

I use Postgres for a database, I assume that’s already been installed.

Psycopg 2

This is a Postgres database adaptor for Python that Trac uses.

Here is latest version of psycopg 2

Installation is simple:


% python setup.py build
% sudo python setup.py install

Trac

Installing trac itself is pretty easy. It’s another case of running “python setup.py install” as root and hoping it does the right thing. In this case there’s one small hitch—it wants to put the man pages and wiki templates in /System/Library/Frameworks/Python.framework/Versions/2.5. (This is sys.prefix). setup.py uses distutils, and I couldn’t figure out if you can use a command-line argument to fix it (–prefix=/usr/local didn’t do the right thing), so here’s the least invasive solution I found:


# cd /System/Library/Frameworks/Python.framework/Versions/2.5
# ln -s /usr/local/share
# chmod -h 777 share

(Yes, symbolic links have had permissions since 10.4, and now we can change their permissions after they’ve been created.)


# python setup.py install

This should put tracd and trac-admin in /usr/local/bin, and the python packages in /Library/Python/2.5/site-packages, and the trac page templates in /usr/local/share/trac/templates.

Testing Trac

Create a trac database user, if necessary:


% sudo -u postgres psql
postgres=# create user trac password 'tracpassword';
postgres=# \du     (to list the users)
postgres=# create database "trac-test1" owner trac;
postgres=# \l      (to list the databases)
postgres=# \q

Create a test environment and run tracd to see if it works:


% trac-admin /tmp/test1 initenv
% tracd --port 8000 /tmp/test1

From there, configure trac the way you want it. The postgres connection string looks something like “postgres://trac:tracpassword@127.0.0.1/trac-test1″.

Starting Trac Automatically

There are a few options on running Trac. I use tracd, which is very simple and works well for a single developer.

Here’s a configuration for a trac launch agent. Save this file as ~/Library/LaunchAgents/org.edgewall.trac.plist and it will automatically start trac when you log in.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>EnvironmentVariables</key>
	<dict>
		<key>LC_TIME</key>
		<string>en_CA</string>
	</dict>
	<key>KeepAlive</key>
	<true/>
	<key>Label</key>
	<string>org.edgewall.trac</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/local/bin/tracd</string>
		<string>--port</string>
		<string>8000</string>
		<string>--auth</string>
		<string>*,/usr/local/trac/users.htdigest,example.com</string>
		<string>/usr/local/trac/test1</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>UserName</key>
	<string>nygard</string>
</dict>
</plist>

Trac makes it extremely difficult to change the date format. They expect you to find some other language that uses the date format you want, and then set the LC_TIME environment variable. In my case, I want dates like 2007-10-31, and I couldn’t find anything matching that. So, I set LC_TIME to en_CA, and then edit the date format in the system locale. That is, I make a copy of /usr/share/locale/en_CA/LC_TIME (it’s a symlink), and change %d/%m/%Y to %Y-%m-%d.

You can use locale LC_TIME to see the formats for your current locale.

The program arguments are the ones you use on the command line. Be sure you don’t use -d, since daemons started by launchd must not daemonize themselves. RunAtLoad makes it start when you log in, and UserName will run it as the specified user.

Since you’re already logged in, you can start it now with launchctl load org.edgewall.trac.plist. launchctl automatically searches for the file and finds it in ~/Library/LaunchAgents. Since RunAtLoad is true, we don’t need to use launchctl start org.edgewall.trac (this is the Label from the .plist) to start it—it starts as soon as it’s loaded. You can see what’s loaded with launchctl load

Backing up

I’ll update this part after I’ve set it up.

Disabling accesskeys

[Update: 2007-11-15] Here’s one last thing I forgot about. HTML has a terrible misfeature called accesskeys. You can use an accesskey attribute on anchors and input elements, and then pressing control and that key will, for example, move the focus to the input, or activate the link. Safari has no option do disable this. What this means is that the standard system-wide control-key combinations that you use for editing text can be usurped by a web page to do something totally unexpected and undesired.

The solution for a local Trac installation is simple enough. Search the python code for any reference to “accesskey” and remove it. It’s probably best to do this before you install it, so that the .pyc files get the update too. Next you have to do the same for all of the template files.

For non-local sites accesskeys are more of a problem. I’ve gone as far as replacing all “accesskey” strings in Safari and it’s supporting frameworks with a different name of the same length, but it wasn’t successful.

Obscure bug with /usr/bin/open

I was busy removing a bunch of old files while I waited for Leopard to arrive. This involved opening a bunch of files to see if I should keep them.

I discovered that if you do “open one.pdf two.html“, both Preview and Safari open one.pdf. On the other hand, if you switch the order… “open two.html one.pdf“, Preview opens one.pdf and Safari opens two.html, as expected.

It’s still a bug in Leopard. I wonder how long this bug has been around?

Crashing Terminal in Leopard

Terminal in Leopard now has tabs. When you add the first tab, the tabs appear and the window grows in height to accommodate the tab view. When you remove this tab, the window shrinks. However, it also reduces the height of the terminal by one row. This is easy to see when you show the size of the terminal in the title bar.

Hmm, interesting… I wonder how small I can make it. Resizing the window normally you can shrink it until it’s only 6 rows high. Create a tab and remove it, now it’s only 5 rows, ha! 4.. 3.. 2.. 1.. 0.. Woohoo, a terminal window zero rows high! Once more and you’ll crash Terminal.

And you know what? You can still execute commands in the zero height terminal window.

Disabling ptrace’s PT_DENY_ATTACH

Mac OS X has a “feature” that allows a process to disallow debugging. All you need to do is call ptrace(PT_DENY_ATTACH, 0, 0, 0) or perhaps ptrace(PT_DENY_ATTACH, getpid(), 0, 0). The man page for ptrace has this to say about PT_DENY_ATTACH:

This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.

I first encountered it in July 2004, when someone on Cocoa-dev discovered that DVDPlayback.framework will not initialize under debugger. I was curious, since I didn’t know of anything that could do this, so I looked at what was happening and discovered PT_DENY_ATTACH.

Once I knew what was responsible, it was easy to find it in the Darwin source code. This comes from xnu-792.17.14, but I don’t think it has changed since I first looked at it. In bsd/kern/mach_process.c in ptrace(), we have this code near the top of the function:


if (uap->req == PT_DENY_ATTACH) {
    if (ISSET(p->p_flag, P_TRACED)) {
        exit1(p, W_EXITCODE(ENOTSUP, 0), retval);
        /* drop funnel before we return */
        thread_funnel_set(kernel_flock, FALSE);
        thread_exception_return();
        /* NOTREACHED */
    }
    SET(p->p_flag, P_NOATTACH);

    return(0);
}

And then it checks the P_NOATTACH flag in a couple places to be very unfriendly if you’re trying to use a debugger on one of these processes.

Now imagine you’re a developer, and your app uses the DVDPlayback framework, and you need to debug your app but can’t because the framework is disallowing debugging. That’s going to suck. You can and should file a bug with Apple telling them that this makes your development more difficult. But realistically they’re not going to change this, so you need alternatives.

Solutions

There’s a good description about what is happening in debugging itunes with gdb. The comments have a link to Fixing ptrace() and they have source code to a kext that disables that part of ptrace(). I couldn’t get it to compile (I didn’t try very hard), and it looks like temp_patch_ptrace() will disappear in Leopard, so I didn’t use this method. From bsd/dev/ppc/systemcalls.c:


/*
 * WARNING - this is a temporary workaround for binary compatibility issues
 * with anti-piracy software that relies on patching ptrace (3928003).
 * This KPI will be removed in the system release after Tiger.
 */

In theory, we should be able to use the source code in Darwin to rebuild the kernel. In practice I’ve never tried to do this, so editing /mach_kernel directly was easier for me. Sure, you’ll need to change it every time the kernal gets updated, but it’s so simple that I can live with it.

What to change?

Use otool -tvV /mach_kernel and search for ptrace to see the disassembly of the function. There are some calls to _audit_arg_* that matches with the source code pretty well. Then comes the interesting part—this is from 10.4.10 (8R218):


0027c790        lwz     r0,0x4(r29)
0027c794        cmpwi   cr7,r0,0x1f                ; check for PT_DENY_ATTACH
0027c798        bne+    cr7,0x27c7d8
0027c79c        lwz     r0,0x1c(r28)
0027c7a0        andi.   r2,r0,0x800                ; check for P_TRACED flag
0027c7a4        beq+    0x27c7d0
0027c7a8        or      r5,r27,r27
0027c7ac        li      r4,0x2d00
0027c7b0        or      r3,r28,r28
0027c7b4        bl      _exit1
0027c7b8        lis     r3,0x3f
0027c7bc        lwz     r3,0x5290(r3)
0027c7c0        li      r4,_lowGlo
0027c7c4        bl      _thread_funnel_set
0027c7c8        bl      _thread_exception_return
0027c7cc        lwz     r0,0x1c(r28)
0027c7d0        oris    r0,r0,0x400                ; set P_NOATTACH
0027c7d4        b       0x27c844                   ; skip to the end of the function
0027c7d8        cmpwi   cr7,r0,0x1e

The test at 0x0027c794 checks for PT_DENY_ATTACH (0x1f), so the code up to 0x0027c7d4 is what we’re interested in. oris r0,r0,0x400 sets the flag, so we don’t want that to happen, but we need to make sure r0 is loaded with the proper value or bad things will happen. (Mmm, kernel panic with my first attempt when I launched iTunes. Oops! :-) ) So at 0x0027c7a0 we want to branch unconditionally to 0x0027c7d4.

I can find the values for some branches by loading /mach_kernel into gdb:


% gdb /mach_kernel
(gdb) x/x 0x27c798
0x27c798 <ptrace+136>:  0x40be0040
(gdb) x/x 0x0027c7d4
0x27c7d4 <ptrace+196>:  0x48000070
(gdb) x/x 0x0027c7a0
0x27c7a0 <ptrace+144>:  0x70020800

So 0x40be.... is a conditional branch, and 0x4800.... is unconditional. With a little trial I see we need to change the instruction at 0x0027c7a0 from 0x70020800 to 0x48000034. We need some context for the bytes we’re going to change:


(gdb) x/32xb 0x0027c798
0x27c798 <ptrace+136>:  0x40    0xbe    0x00    0x40    0x80    0x1c    0x00    0x1c
0x27c7a0 <ptrace+144>:  0x70    0x02    0x08    0x00    0x41    0xa2    0x00    0x2c
0x27c7a8 <ptrace+152>:  0x7f    0x65    0xdb    0x78    0x38    0x80    0x2d    0x00
0x27c7b0 <ptrace+160>:  0x7f    0x83    0xe3    0x78    0x4b    0xfe    0x9e    0x89

With that in hand you can use your favorite hex editor to edit a copy of the kernel. I used Emacs’ hexl mode, but HexFiend is probably a better choice. Disassemble the changed file to verify:


0027c790        lwz     r0,0x4(r29)
0027c794        cmpwi   cr7,r0,0x1f
0027c798        bne+    cr7,0x27c7d8
0027c79c        lwz     r0,0x1c(r28)
0027c7a0        b       0x27c7d4
0027c7a4        beq+    0x27c7d0
0027c7a8        or      r5,r27,r27
0027c7ac        li      r4,0x2d00
0027c7b0        or      r3,r28,r28
0027c7b4        bl      _exit1
0027c7b8        lis     r3,0x3f
0027c7bc        lwz     r3,0x5290(r3)
0027c7c0        li      r4,_lowGlo
0027c7c4        bl      _thread_funnel_set
0027c7c8        bl      _thread_exception_return
0027c7cc        lwz     r0,0x1c(r28)
0027c7d0        oris    r0,r0,0x400
0027c7d4        b       0x27c844
0027c7d8        cmpwi   cr7,r0,0x1e

That looks good, it skips over the nasty code now. Replace the original /mach_kernel with the new version. Be sure to save the original and keep the same permissions/ownership on it. Make sure you can recover from a mistake—another bootable partition or another machine that you can attach via FireWire, and then reboot. Start up iTunes in gdb and watch as it doesn’t segfault now.

Optimizing Drawing in a Source List View

I came upon Mark Alldritt’s SourceListView again after reading Marc Charbonneau’s Blog. I remember reading Mark’s blog entry before, but I never looked at the source code. Tonight I decided to check it out.

LNSSourceListView is a subclass of NSOutlineView which highlights selected rows differently. There are two styles, an iTunes style that uses a gradient background, and a Numbers style with a flat background.

The view is using [self setNeedsDisplay:YES] in three places. This forces the entire view to be redrawn, so you want to avoid calling that method if at all possible to get faster drawing. Calling setNeedsDisplayInRect: is better.

So I decided to see if I could improve the drawing. The two cases in question happen with the Numbers appearance of the the source list:


- (void)outlineViewSelectionDidChange:(NSNotification *)notification;
{
    if (mAppearance == kSourceList_NumbersAppearance
        && [[self selectedRowIndexes] count] > 1)
        [self setNeedsDisplay:YES];
}

- (void)outlineViewSelectionIsChanging:(NSNotification *)notification;
{
    if (mAppearance == kSourceList_NumbersAppearance
        && [[self selectedRowIndexes] count] > 1)
        [self setNeedsDisplay:YES];
}

I changed it to use that appearance and then watched the drawing with Quartz Debug. Sure enough, when more than one row is selected the entire view gets redrawn.

Were those method calls really necessary? I disabled them to see what would happen. The result was some darker lines drawn between the selected rows.

The highlighting occurs in highlightSelectionInClipRect:. It sets up a couple of colors, highlightColor and highlightFrameColor. I wanted to make it easy to see where these were used, so I changed them to something that would stand out:


highlightColor = [NSColor redColor];
highlightFrameColor = [NSColor blackColor];

From this I saw that it draws a one pixel high line in the highlightFrameColor above and below each contiguous set of selected rows.

Even with the setNeedsDisplay:YES enabled, I noticed a couple drawing bugs now that the colors really stood out. If you have two adjacent rows selected and deselect the second row (either by selecting the first row, or command-selecting the second row), the bottom border of the first row isn’t redrawn. Also, if you have three adjacent rows selected and then click on the second selected row, neither the top or bottom borders get redrawn.

The highlighting is a little hard to follow, but it’s using [self rowsInRect:clipRect] to find the range of rows to check, highlighting the selected ones and doing tests for drawing the top or bottom borders. I suspect this won’t work properly when the clip rect doesn’t cover an entire range of selected rows.

Cutting out the drawing for the top and bottom borders leaves me with this:


- (void)highlightSelectionInClipRect:(NSRect)clipRect;
{
    switch (mAppearance) {
      ...
      case kSourceList_NumbersAppearance:
        NSRange rows = [self rowsInRect:clipRect];
        ...
        for (row = rows.location; row < maxRow; ++row) {
            if ([self isRowSelected:row]) {
                NSRect selectRect = [self rectOfRow:row];
                 if (NSIntersectsRect(selectRect, clipRect)) {
                    [highlightColor set];
                    NSRectFill(selectRect);
                }
            }
        }
        ...
    }
}

That's easy to understand, but now we need to draw the borders again. There should be a line above the first row and below the last row of each range of contiguously selected rows. Hmm. Well, if we know indexes of all of the rows starting these ranges, we can easily draw above them. Similarly for the rows ending the ranges.


@implementation NSIndexSet (STExtensions)

- (void)findRangeStartIndexes:(NSMutableIndexSet *)startIndexes
                   endIndexes:(NSMutableIndexSet *)endIndexes;
{
    unsigned int currentIndex;

    for (currentIndex = [self firstIndex]; currentIndex != NSNotFound;
         currentIndex = [self indexGreaterThanIndex:currentIndex]) {
        if (currentIndex == 0 || [self containsIndex:currentIndex - 1] == NO)
            [startIndexes addIndex:currentIndex];
        if ([self containsIndex:currentIndex + 1] == NO)
            [endIndexes addIndex:currentIndex];
    }
}

That handles finding the start and end indexes. Drawing these borders now looks like this:


{
    NSMutableIndexSet *startIndexes, *endIndexes;
    NSRect selectRect;
    unsigned int currentIndex;

    [highlightFrameColor set];

    startIndexes = [[NSMutableIndexSet alloc] init];
    endIndexes = [[NSMutableIndexSet alloc] init];
    [[self selectedRowIndexes] findRangeStartIndexes:startIndexes
                                          endIndexes:endIndexes];

    for (currentIndex = [startIndexes firstIndex]; currentIndex != NSNotFound;
         currentIndex = [startIndexes indexGreaterThanIndex:currentIndex]) {
             selectRect = [self rectOfRow:currentIndex];
             selectRect.origin.y -= 1.0;
             selectRect.size.height = 1.0;
             if (NSIntersectsRect(selectRect, clipRect))
                 NSRectFill(selectRect);
    }

    for (currentIndex = [endIndexes firstIndex]; currentIndex != NSNotFound;
         currentIndex = [endIndexes indexGreaterThanIndex:currentIndex]) {
             selectRect = [self rectOfRow:currentIndex];
             selectRect.origin.y = NSMaxY(selectRect) - 1.0;
             selectRect.size.height = 1.0;
             if (NSIntersectsRect(selectRect, clipRect))
                 NSRectFill(selectRect);
    }

    [startIndexes release];
    [endIndexes release];
}

This draws properly if the entire view is redrawn, but doesn't redraw the borders properly as-is. We'll fix that.

It looks like outlineViewSelectionIsChanging: is called during drag selection, after each change to the selection, and outlineViewSelectionDidChange: is called at the end. The latter method is also called after the selection changes by other means, such as using selectAll: or using the up and down arrows to move the selection. We need to know which start and end rows changed from the selection, so we need to know which rows were selected before the selection changed. Unfortunately there isn't a public method that's called *before* the selection changes. (The outlineView:shouldSelectItem: delegate method might be called at the right time, but I don't want to handle this in the delegate.)

After some experimentation I came up with this. The view will have an NSMutableIndexSet to record the selected rows before they change. Since initWithFrame: isn't called when the view is unarchived from a nib, we also create it in awakeFromNib.


@interface LNSSourceListView : NSOutlineView
{
    AppearanceKind  mAppearance;
    NSMutableIndexSet *previouslySelectedRows;
}

At the right times I'll call this method to update it:


- (void)_updatePreviouslySelectedRows;
{
    [previouslySelectedRows removeAllIndexes];
    [previouslySelectedRows addIndexes:[self selectedRowIndexes]];
}

And then when the selection changes, we dirty the borders of the start and end rows that changed from the change in selection. That is, a row that was a start row before but isn't now, or wasn't before but is now. Same for the end rows. Here's another NSIndexSet category method to help:


- (NSIndexSet *)exclusiveOrWithIndexSet:(NSIndexSet *)otherIndexSet;
{
    NSMutableIndexSet *resultSet, *allSet;
    unsigned int currentIndex;

    allSet = [[NSMutableIndexSet alloc] initWithIndexSet:self];
    [allSet addIndexes:otherIndexSet];

    resultSet = [NSMutableIndexSet indexSet];

    for (currentIndex = [allSet firstIndex]; currentIndex != NSNotFound;
         currentIndex = [allSet indexGreaterThanIndex:currentIndex]) {
        if (![self containsIndex:currentIndex]
            || ![otherIndexSet containsIndex:currentIndex])
            [resultSet addIndex:currentIndex];
    }

    [allSet release];

    return resultSet;
}

Here are the methods that dirty the border frames. We make sure they intersect the visible rect before calling setNeedsDisplayInRect: because that method can become expensive if called too many times.


- (void)_dirtyChangedRowFrames;
{
    NSMutableIndexSet *previousStartIndexes, *previousEndIndexes;
    NSMutableIndexSet *currentStartIndexes, *currentEndIndexes;

    previousStartIndexes = [[NSMutableIndexSet alloc] init];
    previousEndIndexes = [[NSMutableIndexSet alloc] init];
    currentStartIndexes = [[NSMutableIndexSet alloc] init];
    currentEndIndexes = [[NSMutableIndexSet alloc] init];

    [previouslySelectedRows findRangeStartIndexes:previousStartIndexes endIndexes:previousEndIndexes];
    [[self selectedRowIndexes] findRangeStartIndexes:currentStartIndexes endIndexes:currentEndIndexes];

    [self _dirtyStartFrameOfRowsAtIndexes:[previousStartIndexes exclusiveOrWithIndexSet:currentStartIndexes]];
    [self _dirtyEndFrameOfRowsAtIndexes:[previousEndIndexes exclusiveOrWithIndexSet:currentEndIndexes]];

    [previousStartIndexes release];
    [previousEndIndexes release];
    [currentStartIndexes release];
    [currentEndIndexes release];
}

- (void)_dirtyStartFrameOfRowsAtIndexes:(NSIndexSet *)indexes;
{
    unsigned int currentIndex;
    NSRect visibleRect, rect;

    visibleRect = [self visibleRect];
    for (currentIndex = [indexes firstIndex]; currentIndex != NSNotFound;
         currentIndex = [indexes indexGreaterThanIndex:currentIndex]) {
        rect = [self rectOfRow:currentIndex];
        rect.origin.y -= 1.0;
        rect.size.height = 1.0;
        if (NSIntersectsRect(rect, visibleRect))
            [self setNeedsDisplayInRect:rect];
    }
}

- (void)_dirtyStartFrameOfRowsAtIndexes:(NSIndexSet *)indexes;
{
    unsigned int currentIndex;
    NSRect visibleRect, rect;

    visibleRect = [self visibleRect];
    for (currentIndex = [indexes firstIndex]; currentIndex != NSNotFound;
         currentIndex = [indexes indexGreaterThanIndex:currentIndex]) {
        rect = [self rectOfRow:currentIndex];
        rect.origin.y -= 1.0;
        rect.size.height = 1.0;
        if (NSIntersectsRect(rect, visibleRect))
            [self setNeedsDisplayInRect:rect];
    }
}

Now we just need to call these methods at the appropriate times. It looks like mouseDown: is the easiest place to get the initial selection when using the mouse to select:


- (void)mouseDown:(NSEvent *)mouseEvent;
{
    if (mAppearance == kSourceList_NumbersAppearance)
        [self _updatePreviouslySelectedRows];

    [super mouseDown:mouseEvent];

    [previouslySelectedRows removeAllIndexes];
}

For the cases of moving the selection with the arrow keys we use keyDown:.


- (void)keyDown:(NSEvent *)keyEvent;
{
    if (mAppearance == kSourceList_NumbersAppearance)
        [self _updatePreviouslySelectedRows];

    [super keyDown:keyEvent];
}

But that doesn't get called for Select All. We just override selectAll: to handle that case:


- (void)selectAll:(id)sender;
{
    if (mAppearance == kSourceList_NumbersAppearance)
        [self _updatePreviouslySelectedRows];

    [super selectAll:sender];
}

And of course after each change we need to update it, after first dirtying the parts that changed:


- (void)outlineViewSelectionIsChanging:(NSNotification *) notification
{
    if (mAppearance == kSourceList_NumbersAppearance) {
        [self _dirtyChangedRowFrames];
        [self _updatePreviouslySelectedRows];
    }
}

That method isn't called for Select All or the keyboard changes to selection, so we also need this:


- (void)outlineViewSelectionDidChange:(NSNotification *) notification
{
    if (mAppearance == kSourceList_NumbersAppearance)
        [self _dirtyChangedRowFrames];
}

I think that's the last of the changes. You can watch with Quartz Debug and see that now it doesn't redraw the entire view when the selection changes. Plus the two drawing bugs noted earlier should be fixed.

Conclusion

When you're initially writing code, you're most concerned with getting the custom drawing to draw properly and it's easiest to just use setNeedsDisplay:YES. Once that's working you can go back and replace those with calls to setNeedsDisplayInRect:. This usually involves a lot more code because you need to know exactly what has changed. And you still need to be careful not to call it too much, since that can become a bottleneck, so checking against the visible rect is a good practice.

It can be a lot of work, but it's quite satisfying to watch the updates in Quartz Debug at the end.

This might be a blog

I switched to Dreamhost a few months ago to host my website. They have an easy one-click installation for WordPress, so I thought I’d try writing a blog. Here it is! If I update it regularly, it’s a blog. If I only post something every few months, it’s not. :-)

I plan to write about Cocoa programming, and show some code along the way. I have a bunch of things to write about, but it seems to take a while to turn them into a completed post.

This will also be a better place to announce updates of class-dump. I’ve retroactively added posts for the last three releases.

Minor bugfix, class-dump 3.1.1

This release fixes some old bugs with parsing C++ template types, which occurred frequently with applications like iPhoto. It doesn’t try to parse these types—it just scans for commas, nested < and >, and the closing >.

You can download class-dump 3.1.1. This version is built against the 10.4 Universal SDK and will not run on Mac OS X 10.3 or earlier.

Universal binary support

I’ve added universal binary support to class-dump. It will choose the host architecture, if available, or the first architecture otherwise. There is also a –arch command line option to select between ppc and i386. The selected architecture is displayed in the output.

You can download class-dump 3.1. This version is built against the 10.4 Universal SDK and will not run on Mac OS X 10.3 or earlier.