Changing existing Help indexes to be local only

Today Pierre Igot writes about problems with built-in Help in Pages and how it downloads some help over the network. He found that when he clicked on a link in the help, it took up to a minute to do anything, with no indication about what was happening (downloading content from the internet), how long it would take or any way to stop the action.

There are three options that a developer can use when creating a help index that change this behavior. The Help Book content can be Internet-only, Internet-primary, or local-primary. (See Internet-Based Help Book Content.)

I looked at the help index for Pages. It’s located at “/Applications/iWork ‘08/Pages.app/Contents/Resources/English.lproj/Pages 3 Help/Pages 3 Help.helpindex”. This is just a binary typed stream that only contains standard objects, so it was easy to write a tool to read the file. I tweaked it so that it doesn’t show the bulk of the index data. Here’s what part of Pages’ help index looks like:


"SKI_PREFER_NETWORK_FILES" = 1;
"SKI_REMOTE_ROOT" = "http://helposx.apple.com/pageshelpr3/English/";
"SKI_USE_REMOTE_ROOT" = 1;
"SKI_VERSIONS" =     {
    "SKI_CORE_FOUNDATION" = 368;
    "SKI_FOUNDATION" = 567;
    "SKI_HELP_INDEXER" = 17;
    "SKI_SEARCH_KIT" = 147;
    "SKI_SYSTEM_BUILD" = 8P135;
};

This is pretty easy to understand, but with a little experimentation with Help Indexer I verified that the first three keys (SKI_PREFER_NETWORK_FILES, SKI_REMOTE_ROOT, SKI_USE_REMOTE_ROOT) are the only changes between the three different options. Removing the first two keys and changing the value of SKI_USE_REMOTE_ROOT to 0 changes it to a local-only version.

HelpIndexTool

This is the little tool I created. Running it with no arguments shows the usage. Running it with a single argument (the path the a help index) will display the contents of the file (excluding the actual index data). Running it with two arguments will create a local-only version of the help index at the path specified by the second argument.

So if you wanted to try this on the Pages help index, you would do:


./HelpIndexTool "/Applications/iWork '08/Pages.app/Contents/Resources/English.lproj/Pages 3 Help/Pages 3 Help.helpindex" ~/out.helpindex

And then backup the original file, and replace the original “Pages 3 Help.helpindex” with the out.helpindex file in your home directory. Relaunch Pages and it should be good to go.

You can download HelpIndexTool-2008-02-01.dmg here. It includes the project source and compiled tool (Universal).

If you have the developer tools installed, you could try just recreating the help index with Help Indexer, but there are other options that you’d want to make sure were the same as the original. In particular, you can specify the language for the stop words, or specify a custom file of stop words. My tool doesn’t change any of these other options.

Mac OS X

Comments (0)

Permalink

Working around opendiff race conditions for Subversion integration

When I started using Subversion, I wrote a little Python script to act as the diff command, calling opendiff. This makes ’svn diff’ show all the changes in FileMerge. It worked, but I soon ran into a problem. Sometimes it would launch more than one copy of FileMerge. For a time I just made sure FileMerge was running before I did a diff, but that was a pain.

So last year I looked at it, and came up with a solution. I wrote a simple program that used Launch Services (via NSWorkspace) to launch FileMerge synchronously, and ran that before I ran opendiff.

This worked great until 10.5 came out, but then I noticed the multiple FileMerges occurring some of the time. I finally took a closer look at this.

With a little debugging I discovered that opendiff doesn’t use Launch Services at all. Instead, it checks for the existence of a named port, ‘com.apple.DiffMergeController4.0′. If it exists, it uses Distributed Objects to tell FileMerge to compare the files. Otherwise, it uses an NSTask to run FileMerge with ‘-left <left-file> -right <right-file>’ arguments. There seems to be no protection against launching FileMerge more than once.

You can see this for yourself by making sure FileMerge isn’t running, and then doing ‘/Developer/Applications/Utilities/FileMerge.app/Contents/MacOS/FileMerge & opendiff file1.txt file2.txt’. This is almost guaranteed to leave two FileMerges running.

My solution was to update my Launcher tool to wait until the named port existed before returning. So far this seems to work.


NSPort *port;

port = [[NSPortNameServer systemDefaultPortNameServer] portForName:@”com.apple.DiffMergeController4.0″];

if (port == nil) {
    result = [[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:@”com.apple.FileMerge”
                                            options:NSWorkspaceLaunchWithoutActivation
                                            additionalEventParamDescriptor:nil
                                            launchIdentifier:NULL];
    if (result) {
        port = [[NSPortNameServer systemDefaultPortNameServer] portForName:@”com.apple.DiffMergeController4.0″];
        while (port == nil) {
            usleep(50000);
            port = [[NSPortNameServer systemDefaultPortNameServer] portForName:@”com.apple.DiffMergeController4.0″];
        }
    }
}

(I probably don’t even need to bother checking the port first.)

So I have my opendiff2 script installed somewhere in my search path, and configured ~/.subversion/config with the following:


[helpers]
diff-cmd = opendiff2

I’ve also compiled my Launcher project and have it in the search path.

opendiff2 creates a /tmp/opendiff2 directory, and copies the files to be compared into directories under that, named after the parent process id. This effectively groups files compared by one ’svn diff’ session together.

Leopard
Mac OS X
Programming
Python

Comments (0)

Permalink

Private API to the rescue

I started using a new method, -[NSView viewWillDraw], in one of my views for performing layout. This is a really handy method. It supports further dirtying of the view and frame changes.

But while I was converting my code I discovered a case where my entire view was being redrawn. This was odd, so I made a note to look at it after my conversion was done. I go to quite some trouble to make sure I only dirty the parts of my view that I need to. When I looked through my logs, I didn’t see any calls to -setNeedsDisplay: or -setNeedsDisplayInRect: that would explain why it was redrawing the whole view.

I was ready to blame -viewWillDraw, so I set out to create a simple test case for it. Fortunately that method isn’t at fault. What’s really happening is that changing the frame causes the entire view to be redisplayed!

This is different from the 10.4 behaviour. The documentation for -[NSView setFrame:] says:

This method, in setting the frame rectangle, repositions and resizes the receiver within the coordinate system of its superview. It neither redisplays the receiver nor marks it as needing display. You must do this yourself with display or setNeedsDisplay:.

I ran my test app on 10.4, and it did what I expected. But it didn’t explain what was causing this effect, and how to fix it.

I was reminded of instrumentObjcMessageSends() by a post on Dave Dribin’s blog: Tracing Objective-C messages. So I enabled logging just before I changed the frame, ran the test on 10.5 and 10.4, and compared the results.

There are several changes. In the 10.5 run I see this:


- MyView NSView setFrameSize:
- MyView NSView inLiveResize
- MyView NSView _autoInvalidateBeforeFrameSizeChange
- MyView NSView _setWindowNeedsDisplayInViewsDrawableRect

And a bit later:


- MyView NSView inLiveResize
- MyView NSView _autoInvalidateAfterFrameSizeChange
- MyView NSView _setWindowNeedsDisplayInViewsDrawableRect
- MyView NSView visibleRect

Well, I wonder what _autoInvalidateAfterFrameSizeChange does. First let’s class-dump it and see what classes implement it:


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

@interface NSTextView : NSText <NSTextInput, NSUserInterfaceValidations, NSTextInputClient>
- (void)_autoInvalidateBeforeFrameSizeChange;	// IMP=0x0055d938
- (void)_autoInvalidateAfterFrameSizeChange;	// IMP=0x0055d93c

@interface NSView (NSInternal)
- (void)_autoInvalidateBeforeFrameSizeChange;	// IMP=0x005c504c
- (void)_autoInvalidateAfterFrameSizeChange;	// IMP=0x005c5058

Interesting. I wonder what NSTextView’s implementation of these do.


(gdb) x/20i 0x0055d938
0x55d938 <-[NSTextView _autoInvalidateBeforeFrameSizeChange]>:     blr
0×55d93c <-[NSTextView _autoInvalidateAfterFrameSizeChange]>:      blr
0×55d940 <-[NSTextView setBoundsSize:]>:     mflr    r0

Aha! It doesn’t do anything. I like it. Indeed, testing in both my sample and real applications shows that this fixes the problem.


- (void)_autoInvalidateBeforeFrameSizeChange;
{
    // Do nothing
}

- (void)_autoInvalidateAfterFrameSizeChange;
{
    // Do nothing
}

Sure, Apple doesn’t want us to use private API, but as this illustrates there are cases where they leave us no choice.

By the way, the AppKit release notes say nothing about this new behaviour, not even a hint like there was for the focus ring bleed regions.

I did notice the following in the documentation for -[NSView setFrameSize:]:

In Mac OS X version 10.4 and later, you can override this method to support content preservation during live resizing. In your overridden implementation, include some conditional code to be executed only during a live resize operation. Your code must invalidate any portions of your view that need to be redrawn.

So maybe the bug is mixed up with that. I’ve filed a bug report. In the meantime I’m just happy there’s such an easy workaround.

Bugs
Leopard
Mac OS X
Programming

Comments (0)

Permalink

Using NSTrackingArea for cursor rects

As I said before, Leopard adds a new class, NSTrackingArea, that is now the preferred way of setting up cursor rectangles. It can also handle mouse entered/exited and mouse moved events, but right now I’m only interested in cursor updates.

I didn’t really gain anything from using NSTrackingArea in this case. I still have to clip the rects against the visible area.

I replaced -resetCursorRects with -updateTrackingAreas. The old tracking areas aren’t discarded before this is called, so I have to remove the old ones first. I store the cursor in the NSTrackingArea userInfo so that I can get it in -cursorUpdate:.


- (void)updateTrackingAreas;
{
    NSRect visibleRect;
    NSTrackingArea *trackingArea;

    [super updateTrackingAreas];

    for (trackingArea in [self trackingAreas])
        [self removeTrackingArea:trackingArea];

    visibleRect = [self visibleRect];

    for (STTableColumn *tableColumn in [self tableColumns]) {
        NSRect cursorRect;

        cursorRect = NSIntersectionRect([self resizeCursorFrameForTableColumn:tableColumn], visibleRect);
        if (NSIsEmptyRect(cursorRect) == NO) {
            NSCursor *cursor;

            cursor = [tableColumn resizeCursor];
            if (cursor != nil) {
                trackingArea = [[NSTrackingArea alloc] initWithRect:cursorRect
                                                       options:(NSTrackingCursorUpdate|NSTrackingActiveInKeyWindow)
                                                       owner:self
                                                       userInfo:[NSDictionary dictionaryWithObject:cursor forKey:@"cursor"]];
                [self addTrackingArea:trackingArea];
                [trackingArea release];
            }
        }
    }
}

I’ve removed the -disableCursorRects/-enableCursorRects, and it seems to work fine without a replacement. If I did need to disable them, since I control cursor updates in -cursorUpdate: I could just set a flag and not update them when the flag is set.

I call -updateTrackingAreas directly instead of using -[NSWindow invalidateCursorRectsForView:].

Finally, here’s my -cursorUpdate: method.


- (void)cursorUpdate:(NSEvent *)event;
{
    NSPoint hitPoint;
    NSTrackingArea *trackingArea;
    NSCursor *cursor;

    trackingArea = [event trackingArea];
    cursor = [[trackingArea userInfo] objectForKey:@”cursor”];

    hitPoint = [self convertPoint:[event locationInWindow] fromView:nil];
    if (cursor != nil && [self mouse:hitPoint inRect:[trackingArea rect]]) {
        [cursor set];
    } else {
        [[NSCursor arrowCursor] set];
    }
}

It’s important to use -[NSView mouse:inRect:] instead of NSPointInRect(), otherwise slowly moving the cursor across the top or bottom edge of the tracking area won’t properly update the cursor. And if the tracking area doesn’t cover the entire view, you also get a -cursorUpdate: when the mouse leaves a tracking area, so you need to watch for this case and use a default cursor.

I think in most cases you want to make sure the tracking areas are clipped to the visible rect. The -cursorUpdate: is only called at the transition from one tracking area to another, and it gets sent down the responder chain starting at the view under the mouse. So if you have a tracking area that extends above the top edge of a view, your view won’t get a cursor update when you cross the top edge of the tracking area, and so you won’t change the cursor. You don’t get another update when the mouse enters your view, because it’s still in the same tracking area.

This is slightly different from the old way, because NSWindow was setting the cursor for you. But off the top of my head I can’t imagine a case where you would want the tracking area to extend outside of the view, so this shouldn’t be a problem. Just something to keep in mind.

That’s it, there really wasn’t much to change.

Cocoa
Leopard
Mac OS X
Programming

Comments (0)

Permalink

Old style cursor rects

Leopard adds a new class, NSTrackingArea, that is now the preferred way of setting up cursor rectangles. I’m looking at switching some old code to use the new method, but first I’ll describe how it currently works.

The old way

Your NSView subclass implements -resetCursorRects to add the cursor rects. The old ones have already been removed, so you just need to use -[NSView addCursorRect:cursor:] to add the new ones. When the view changes in a way that makes them invalid, such as changing the frame, the cursor rects are invalidated and -resetCursorRects will be called again to set them up. If you make other changes to your view that affect the position of the cursor rects, you can use -[NSWindow invalidateCursorRectsForView:] to invalidate them.

There are two things you need to be careful with. First, you should intersect the cursor rects with the visible rect before adding them, since you won’t get mouse events in these hidden areas and it’s misleading to change the cursor for them. Second, if you change the frame in a modal event loop, the cursors will be reset for each change and the cursor will flicker oddly. You can disable cursor rect updates by calling -[NSWindow disableCursorRects] before your loop, and enable them at the end with -[NSWindow enableCursorRects].

An example

I’ll use a custom table header view as an example. It needs to show resize cursors at the right edge of the columns. This is used in a custom table view, not subclassed from NSTableView, so while the classes correspond to their Cocoa counterparts, they don’t inherit any behavior from them.

The table columns have a minimum and maximum size, and so they need to show different cursors based upon whether they can resize left, right, or in both directions.


@implementation STTableColumn

- (NSCursor *)resizeCursor;
{
    BOOL canShrink, canGrow;

    canShrink = width > minimumWidth;
    canGrow = width < maximumWidth;
    if (canShrink && canGrow) {
        return [NSCursor resizeLeftRightCursor];
    } else if (canShrink) {
        return [NSCursor resizeLeftCursor];
    } else if (canGrow) {
        return [NSCursor resizeRightCursor];
    }

    return nil;
}

@end

The following method clips the proposed cursor rect against the visible rect.


@interface STTableHeaderView

- (void)resetCursorRects;
{
    NSRect visibleRect;

    visibleRect = [self visibleRect];

    for (STTableColumn *tableColumn in [self tableColumns]) {
        NSRect cursorRect;

        cursorRect = NSIntersectionRect([self resizeCursorFrameForTableColumn:tableColumn], visibleRect);
        if (NSIsEmptyRect(cursorRect) == NO) {
	    NSCursor *cursor;

	    cursor = [tableColumn resizeCursor];
	    if (cursor != nil)
                [self addCursorRect:cursorRect cursor:cursor];
        }
    }
}

While editing a column title, the field editor covers the title and so we need to make sure the cursor rect doesn’t overlap the field editor.


- (NSRect)resizeCursorFrameForTableColumn:(STTableColumn *)aTableColumn;
{
    NSRect rect;

    if (aTableColumn == nil)
        return NSZeroRect;

    rect = [self bounds];
    if ([aTableColumn index] == editingColumnIndex) {
        rect.size.width = 2;
        rect.origin.x = [aTableColumn maxXPosition];
    } else if ([aTableColumn index] + 1 == editingColumnIndex) {
        rect.size.width = 3;
        rect.origin.x = [aTableColumn maxXPosition] - 3;
    } else {
        rect.size.width = 5;
        rect.origin.x = [aTableColumn maxXPosition] - 3;
    }

    return rect;
}

Changing a column’s width changes the frame, so we don’t need to invalidate cursor rects in that case. But during the modal event loop we do disable cursor updates to avoid the cursor flickering (both the frame change and auto-scrolling would otherwise reset the cursor). When we do change the width, we manually reset the cursor. Here are the interesting parts:


- (void)startResizingColumn:(NSEvent *)mouseDownEvent;
{
    ...
    [[self window] disableCursorRects];

    while (1) {
        NSEvent *event;
        event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask | NSPeriodicMask)
                       untilDate:[NSDate distantFuture]
                       inMode:NSEventTrackingRunLoopMode
                       dequeue:YES];

        …

        if ([event type] == NSLeftMouseDragged) {
            …
            if ([tableColumn width] != newWidth) {
                [tableColumn setWidth:newWidth];
                [self autoscroll:event];

                // Make sure the cursor changes as we hit min/max widths
                [[tableColumn resizeCursor] set];
            }
        }
        …
    }

    [[self window] enableCursorRects];
}

When columns are reordered, the frame doesn’t change so we do need to invalidate them.

And that’s all there is to it.

Cocoa
Mac OS X
Programming

Comments (0)

Permalink

iCal 3: Shake, Rattle, and Scroll

It’s not my fault. I just wanted to move the iCal window out of the way so that I could see Pierre Igot’s blog entry on iCal 3 hyphenation. I was going to add an appointment and watch the problem in action. But when I moved the window, the day view started scrolling up.

This works best if you have a fast mouse acceleration. Go into the day view, and scroll it to the bottom. Click in the toolbar area above the day view. Try to get close to the day view while still being able to drag the window around. Now, while you’re dragging the window, shake it down and then up again. If you’re fast enough the day view will start scrolling up.

Bugs
Leopard
Mac OS X

Comments (0)

Permalink

Leopard hates custom views that draw focus rings

The Cocoa release notes say this:

To help guarantee correct redraw of focus rings, AppKit may automatically draw additional parts of a window beyond those that application code marked as needing display. It may do this for the first responder view in the application’s key window, if that first responder view’s focusRingType property is set to a value other than NSFocusRingTypeNone. Any view that can become first responder, but doesn’t draw a focus ring, should have its focusRingType set to NSFocusRingTypeNone to avoid unnecessary additional redraw.

What this means is that when your view calls NSSetFocusRingStyle(NSFocusRingOnly), this ends up calling -[NSWindow _setLastFocusRingView:bleedRegion:], with this view as the first argument. From that point forward, until another view becomes first responder, the entire view will be redrawn any time part of it becomes dirty. This effectively kills your drawing performance.

Setting the view’s focusRingType to NSFocusRingTypeNone has no effect.

There are a few private methods that AppKit is using here.


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

@interface NSView : NSResponder <NSAnimatablePropertyContainer>
- (NSRect)_focusRingVisibleRect;
@end

NSView’s implementation of _focusRingBleedRegion roughly does this:

  • Converts the visible rect to window base coordinates, scales it, and then insets it be -3 pixels on each side.
  • Calls -_focusRingClipAncestor, and converts that view’s _focusRingVisibleRect to window base coordinates.
  • Creates an NSRegion out of the intersection of two of these rectangles (not exactly sure which two).
  • Insets a rectangle and subtracts that from the region.
  • Returns this region.

The subclasses _NSBroswerScrollView, NSButton, and NSTableView, each implement this method, presumably because they need to. But if they need to, isn’t it likely that other views might need to as well? Except that it’s private, and uses an entirely private class, NSRegion.

Fortunately my workaround doesn’t need to use this method. A little experimentation showed that the focus view and region can be nil. So I just need to override -[NSWindow setLastFocusView:bleedRegion:] and pass on nil values for my particular view:


@implementation NSView (STExtensions)

// This is used on 10.5 by my NSWindow category to work around a problem with focus ring bleed regions.  The default is YES,
// meaning just do the normal behavior.  Subclasses can return NO to disable AppKit's automatic bleed region dirtying for them.
- (BOOL)canControlFocusRingBleedRegion;
{
    return YES;
}

@end

@implementation NSWindow (STExtensions)

+ (void)load;
{
    if (self == [NSWindow class]) {
        Method originalMethod, replacementMethod;

        originalMethod = class_getInstanceMethod(self, @selector(_setLastFocusRingView:bleedRegion:));
        replacementMethod = class_getInstanceMethod(self, @selector(replacement_setLastFocusRingView:bleedRegion:));
        method_exchangeImplementations(originalMethod, replacementMethod);
    }
}

- (void)replacement_setLastFocusRingView:(id)view bleedRegion:(id)region;
{
    if ([view canControlFocusRingBleedRegion]) {
        [self replacement_setLastFocusRingView:view bleedRegion:region]; // Not really recursive.
    } else {
        // We can’t control it, so disable it.
        [self replacement_setLastFocusRingView:nil bleedRegion:nil]; // Not really recursive.
    }
}

Then my view just has to return NO from -canControlFocusRingBleedRegion. This works like a charm, assuming your view draws it’s focus ring correctly in all cases, which mine does.

Here’s what the Cocoa release notes have to say about private methods:

The last point (Increased restricted visibility of private symbols in the runtime) is especially important, since it would prevent applications using these symbols from launching in future releases. As always: Please do not access undeclared APIs in your applications. This includes undeclared classes, methods, and instance variables. If you have a strong need for a private API and have no workaround, please let us know.

This is a perfect example of Apple leaving us no choice but to use private API. I’ve filed a bug report about this case.

Cocoa
Leopard
Mac OS X
Programming

Comments (0)

Permalink

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!

Leopard
Mac OS X
Programming

Comments (0)

Permalink

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.

Mac OS X
Programming
class-dump

Comments (0)

Permalink

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.

Leopard
Mac OS X
Python

Comments (0)

Permalink