September 09, 2005

NSOutlineView, reloading items, and the expansion state

NSOutlineView requires all of the items in it to be pointer unique. If they are not, strange things happen. However, they can be equal (meaning [NSObject isEqual] may return YES).

However, there is a small exception to that rule. If you do a reload, the expansion state of items will be preserved. This expansion state is done by placing the current expanded items into an NSMutableSet. After a reload happens, if an item is in that set it is shown expanded.

What does this mean? Well, to see if an item should be expanded or not, it is looked up in the set. This is done via the item's hashcode and an isEqual comparision. Ahh ha! NSOutlineView is doing some non-pointer unique things here. This means you could potentially switch out items during a reload, and they would still appear expanded after the reload (as long as they have the same hashcode and are isEqual). Another nasty side effect: any items which mutate their hashtable will not appear expanded after a reload! For instance, if you modify an NSDictionary, its hash code will change. Take this point into consideration: the objects you put into an NSOutlineView are NSMutableDictionaries. You do a reload, and after the reload you add a child to one of the items via a key in the NSDictionary. Therefore, the NSMutableDictionary now has a different hashcode, and it will no longer appear expanded! The easy way to work around this is to use a non-mutable object in the outlineview (such as your own object, which might have a dictionary inside of it to keep track of things). Just a neat tip, for those who care or run into this.

Posted by corbin at 03:54 PM | Comments (0)

August 01, 2005

Using Xcode to become a faster programmer

You can utilize some of Xcode's cool features to become a faster Mac OS X programmer. Here are some things which you may not know:

1. Use Code Completion (Code Sense). However, the default keyboard shortcut is lame. Go to the Xcode preferences, Key Bindings:

Keybindingsinxcode

and change the Code Sense Completion List binding to be Ctrl-Space (I use Option-Space, but really it is the ctrl-key, because I like to swap my ctrl and option keys):

Xcodecodesensebinding

While you are there, set the Code Sense Select Next Placeholder to be Ctrl-/ (that is control forward slash, and again, I have mine set to be option-/ because I swap my keys):
Codesensenextcompletion

2. Now that you have it properly setup, use it! Here is how you should be using it:

You want to call a particular method in your current class (self). Type the first few characters:

Usingcodesense1

Hit Ctrl-Space to bring up the Code Sense window:

Usingcodesense2

Type a few more characters to narrow down what you want, and arrow key down to select the signature you want to use:

Usingcodesense3

Hit enter to add the template into your source code:

Usingcodesense4

Fill in the first parameter:

Usingcodesense5

Then, hit Ctrl-/ to go the next completion item and highlight it:

Usingcodesense6

Repeat with more Ctrl-/ commands until you are done.

3. Congratulations! You are now a faster programmer.

--corbin

Posted by corbin at 10:44 PM | Comments (2)

Cmd - click. The subtle secret of Mac OS X.

It seems that a lot of people don't know how cmd click works in a lot of Mac OS X Cocoa applications. To put it simply, Cmd-clicking on a non key window (or application for that matter) will act like a normal click without making the window key! This is really cool for testing UI things while debugging or testing for memory leaks.

Here is how I use this technique while debugging:
1. I'm debugging my application with Xcode. Inside of Xcode I have a “hot” breakpoint that I don't want to enable, since enabling it will make Xcode key, and the key/focus switching will hit the breakpoint at the wrong time.
2. Because of this, I keep my application key, and Cmd-click on the title bar in Xcode to move the window to where I can see the breakpoint.
3. Then, I Cmd-click the breakpoint, enabling it without setting the window key and never leaving focus from my application that i want to debug.
4. Now, I perform the operation that invokes the breakpoint and debug away.

Sure, you could do this by figuring out how many times the breakpoint was hit, and in gdb ignore the breakpoint for X times, but sometimes that is a pain to do.

I also use this technique while trying to find memory leaks with Object Alloc. Frequently, focus switching causes numerous allocations, and by Cmd-clicking the “Mark” button in Object Alloc I can prevent those allocations from happening.

Note that cmd click doesn't work 100% correctly with certain components. For instance, NSTableView won't change the selected item unless it is key (note that that bug will be fixed).

--corbin

Posted by corbin at 03:01 PM | Comments (1)

July 28, 2005

Drag and Drop in an NSTableView

Drag and Drop in an NSTableView is easy to do. However, I think the documentation (Table Views: Using Drag and Drop in Tables) for it isn't particularly great. It misses a few points, so I'm going to go over the basic steps on how to add drag and drop to your TableView. Here, I'll assume you have a TableView with your source code controller class set as the delegate.

Declare your custom pasteboard format:

#define BasicTableViewDragAndDropDataType @"BasicTableViewDragAndDropDataType"

In awakeFromNib you must register for the drag types you want to receive (you could have others here):

- (void)awakeFromNib {

    [myTableView registerForDraggedTypes:[NSArray arrayWithObjects:BasicTableViewDragAndDropDataType, nil]];

}

Then, you must implement writeRowsWithIndexes to add your data to the pasteboard:

- (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pboard {

    // Drag and drop support

    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];

    [pboard declareTypes:[NSArray arrayWithObject:BasicTableViewDragAndDropDataType] owner:self];

    [pboard setData:data forType:BasicTableViewDragAndDropDataType];

    return YES;

}

Next you will validate the drop:

- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)op {

    // Add code here to validate the drop

    NSLog(@"validate Drop");

    return NSDragOperationEvery;    

}

Finally, you must have a method that accepts the drop. Here you could access the BasicTableViewDragAndDropDataType and look at the rows that were dragged.

- (BOOL)tableView:(NSTableView*)tv acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)op {

    NSLog(@"acceptDrop");

    // Add code here to accept the drop

    return YES;    

}

And that is it! It is pretty easy...

Technorati Tags:

Posted by corbin at 06:44 PM | Comments (1) | TrackBack

July 26, 2005

Dynamically populating an NSPopUpButtonCell in an NSTableView

It is quite common. You have a PopUpButton (NSPopUpButtonCell) in an NSTableView and you want to dynamically change the contents based on the selected row:

Popupcellpicture1

How do you do this? There are a few tricky steps. First, add a menu to the nib and set the delegate for the menu to be your Controller:

Menusdelegatepopupbutton

Okay. Next is the tricky part. In IB, when you have a column in a tableview selected, it has a little white triangle in the corner:

Picture 1-3

Clicking on that will allow you to modify properties of the cell (in this case, the NSPopUpButtonCell). However, we want to set the menu for the NSPopUpButtonCell, so drag from that little triangle to your menu and set the menu outlet:

Popupcellhookup

Okay, good! Now, all your controller needs is a little bit of code to dynamically populate the menu:

- (void)menuNeedsUpdate:(NSMenu *)menu {

    // remove all previous items

    while ([menu numberOfItems] > 0) {

        [menu removeItemAtIndex:0];

    }

    // dynamically build the menu

    int i;

    int selectedRow = [tableViewStuff selectedRow];

    for (i = 0; i &lt;= selectedRow; i++) {

        NSMenuItem *item = [[NSMenuItem alloc] 

            initWithTitle:[NSString stringWithFormat:@“Menu %d”, i] 

                   action:@selector(menuClicked:) keyEquivalent:@“”];

        [item setTarget:self];

        [menu addItem:item];

    }

}

Okay, that should work, right? Normally, yes. But in a tableview when a cell is tracked, it is first copied. Unfortunately, a small bug in the menu code doesn't copy the delegate. Therefore, we must fix it up in the nstableview's delegate method:

- (void)tableView:(NSTableView *)tableView 

  willDisplayCell:(id)cell 

   forTableColumn:(NSTableColumn *)tableColumn 

              row:(int)row 

{

    if ([cell isKindOfClass:[NSPopUpButtonCell class]]) {

        [[cell menu] setDelegate:self];

    }

}

That's it! Have fun...happy coding.

Posted by corbin at 04:14 PM | Comments (0)

July 08, 2005

Adding a popup menu to any NSButton

It is easy to add a popup to any button (NSButton).
Picture 1-2

Here are the steps:

  1. In your nib, drop down an NSPopupButton and add the items you want to its popup list.
  2. Set the NSPopupButton to be hidden.
  3. Add an outlet for the NSPopupButton (named popupButton in this case)
  4. Drop down an NSButton.
  5. Set the image (or whatever else you want) for the button
  6. In the action for the button, run the following code:
    [[popupButton cell] performClickWithFrame:[sender frame] inView:[sender superview]];    

That is it!

Posted by corbin at 04:07 PM | Comments (8)

June 30, 2005

Different cells on each row in an NSTableView or NSOutlineView

Some people have asked me how to dynamically change the cell that is displayed for each row in an NSTableView or NSOutlineView. Generally, the same cell is used for each row, but it is possible to use a different cell for each row, if you like. NSTableColumn can change the cell that is used for each row. NSTableView calls -[NSTableColumn dataCellForRow:(int)row] for each row. Now, consider this code:

@interface NSObject (VariableCellColumnDelegate)
- (id)tableColumn:(NSTableColumn *)column inTableView:(NSTableView *)tableView dataCellForRow:(int)row;
@end

@interface VariableCellColumn : NSTableColumn
@end

@implementation VariableCellColumn

- (id)dataCellForRow:(int)row {
    id delegate = [[self tableView] delegate];
    if ([delegate respondsToSelector:@selector(tableColumn:inTableView:dataCellForRow:)]) {
        return [delegate tableColumn:self inTableView:[self tableView] dataCellForRow:row];
    } else {
        return [super dataCellForRow:row];
    }    
}
@end

If the delegate simply implements the method:

- (id)tableColumn:(NSTableColumn *)column inTableView:(NSTableView *)tableView dataCellForRow:(int)row;

...you will be able to dynamically change the cells, with very little effort! In IB, you can set the class for the NSTableColumn to be VariableCellColumn (after dragging the header into IB), and you won't have to do any work of dynamically creating NSTableColumn's.

Posted by corbin at 03:28 PM | Comments (0)

June 20, 2005

Debugging OCTest bundles

To debug OCTest bundles:

1. Add a new executable to your Xcode project pointing it to "otest" at /Developer/Tools/otest
2. Double click on the executable, and add two run params (requires Xcode 2.1):
  a. -SenTest Self
  b. $(BUILT_PRODUCTS_DIR)/YourBundleName.octest

This makes it REALLY easy to debug OCUnit tests. You can debug an individual test suite with:

  -SenTest UnitTestClassName

or an individual test case with:

  -SenTest UnitTestClassName/testMethodName

For instance, here is what one of my arguments looks like in Xcode 2.1:


Unittestingoptionsshot


I can easily test/debug all tests, or an individual test by turning on/off arguments. Note that it doesn't have the "$(BUILT_PRODUCTS_DIR)/YourBundleName.octest" option because this is for an executable, not a bundle.

Posted by corbin at 03:00 PM | Comments (0)

June 19, 2005

Drawing a “mail like” border on items in an NSTableView

Mail has a cool way of making unread messages stand out. It is really easy to do this type of thing with NSTableView/NSOutlineView. Subclass the one you want, and override drawRow. Toss in the code you see below, and it should give a cool highlight on all expandable rows in an NSOutlineView (you will have to modify it to have it work in NSTableView -- just remove the call to isExpandable).

mail_like_highlighting


- (void)drawRow:(int)row clipRect:(NSRect)clipRect {
    if (([self isExpandable:[self itemAtRow:row]]) && (![self isRowSelected:row])) {
        // Draw a light-blue “mail like” border around the row, if not selected
        NSRect rect = [self rectOfRow:row];
        [[[NSColor blueColor] colorWithAlphaComponent:50/255.0] set];
        NSBezierPath *path = [NSBezierPath bezierPath];
        [path setLineCapStyle:NSRoundLineCapStyle];
        [path setLineWidth:rect.size.height - 3];
        int rowLevel = [self levelForRow:row];
        float x = rect.origin.x + 10.0 + (rowLevel * [self indentationPerLevel]);
        float y = rect.origin.y + (rect.size.height / 2.0);
        [path moveToPoint:NSMakePoint(x,y)];
        [path lineToPoint:NSMakePoint(x + rect.size.width - 2*10.0, y)];
        [path stroke];
    }    
    [super drawRow:row clipRect:clipRect];
}

Posted by corbin at 02:37 PM | Comments (3)

June 17, 2005

Finding memory leaks with Object Alloc

So, your app leaks? Here's how to fix it:

1. Open your app in Object Alloc
2. Start the process, and check to have retain events:

Focus1

3. Run till in a known state

4. Check "Show since mark":

Focus2

5. Click auto-sort:

Focus3

6. Click the "Current" column header to sort on that automatically:

Focus4

7. Click the Mark button:

Focus5

7. Do the offending memory leak operation (to warm it up)
8. Click Mark again, and repeat the offending memory leak operation.

9. Take a look at the Instance Browser, and find your objects that are leaked:

Focus6

10. Pause the app, and in the right hand browser column will be the allocation events. Double click on them and you can see the stack of the allocation/retain/release event, and figure out who isn't "playing nice" by seeing who should have done a release for a corresponding alloc/retain/copy:

Focus8

Have fun!
--corbin

Posted by corbin at 02:20 PM | Comments (1)

June 15, 2005

Changing the disclosure triangle in an NSOutlineView

At WWDC I was asked how to remove the disclosure triangle in an NSOutlineView. Well, first things first. You can change it with this bit of code in your delegate:

- (void)outlineView:(NSOutlineView *)ov
  willDisplayOutlineCell:(NSButtonCell *)cell
  forTableColumn:(NSTableColumn *)tableColumn
  item:(id)item
{
  [cell setImage:[NSImage imageNamed:@"collapsedglyph.tiff"]];
  [cell setAlternateImage:[NSImage imageNamed:@"expandedglyph.tiff"]];
}

If you want to set it to nil, you will have to create an image that is empty.

--corbin

Posted by corbin at 05:47 PM | Comments (0)

June 10, 2005

Tooltips for NSTableView cell's in Tiger

At WWDC, I quickly mentioned how easy it is to add tooltip's to an NSCell for an NSTableView/NSOutlineView.

Here is a quick snippet of code on how to do this only if the text doesn't fill up the entire cell:

- (NSString *)tableView:(NSTableView *)tv toolTipForCell:(NSCell *)cell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)tc row:(int)row mouseLocation:(NSPoint)mouseLocation {
    if ([cell isKindOfClass:[NSTextFieldCell class]]) {
        if ([[cell attributedStringValue] size].width > rect->size.width) {
            return [cell stringValue];
        }
    }
    return nil;
}

You will obviously have to set the delegate for the tableview to be whatever class implements the above method, and this will only work on Tiger. But, it is REALLY easy to do.

Technorati Tags:

Posted by corbin at 03:32 PM | Comments (9)