NSButtonCell inside custom NSCell
Asked Answered
G

2

10

in my cocoa application, I need a custom NSCell for an NSTableView. This NSCell subclass contains a custom NSButtonCell for handling a click (and two or three NSTextFieldCells for textual contents). You'll find a simplified example of my code below.

@implementation TheCustomCell

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
   // various NSTextFieldCells
   NSTextFieldCell *titleCell = [[NSTextFieldCell alloc] init];
   ....
   // my custom NSButtonCell
   MyButtonCell *warningCell = [[MyButtonCell alloc] init];
   [warningCell setTarget:self];
   [warningCell setAction:@selector(testButton:)];
   [warningCell drawWithFrame:buttonRect inView:controlView];
}

The problem I'm stuck with is: what is the best/right way to get that Button (more precisely: the NSButtonCell) inside this NSCell to work properly? "work" means: trigger the assigned action message and show the alternate image when clicked. Out of the box, the button doesn't do anything when clicked.

Information / readings on this topic is hard to find. The only posts I found on the net pointed me to implementing

- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp; 

Is this the correct way to do it??? Implement trackMouse: in my containing NSCell? And then forward the event to the NSButtonCell? I would have expected the NSButtonCell itself to know what to do when it's being clicked (and I saw the trackMouse: methods more in cunjunction with really tracking mouse movements - not as a training wheel for 'standard' click behaviour). But it seems like it doesn't do this when included in a cell itself... It seems I haven't grasped the big picture on custom cells, yet ;-)

I'd be glad if someone could answer this (or point me to some tutorial or the like) out of his own experience - and tell me if I'm on the right track.

Thanks in advance, Tobi

Genuflection answered 17/2, 2010 at 11:53 Comment(0)
S
8

The minimal requirements are:

  • After left mouse down on the button, it must appear pressed whenever the mouse is over it.
  • If the mouse then releases over the button, your cell must send the appropriate action message.

To make the button look pressed, you need to update the button cell's highlighted property as appropriate. Changing the state alone will not accomplish this, but what you want is for the button to be highlighted if, and only if, its states is NSOnState.

To send the action message, you need to be aware of when the mouse is released, and then use -[NSApplication sendAction:to:from:] to send the message.

In order to be in position to send these messages, you will need to hook into the event tracking methods provided by NSCell. Notice that all those tracking methods, except the final, -stopTracking:... method, return a Boolean to answer the question, "Do you want to keep receiving tracking messages?"

The final twist is that, in order to be sent any tracking messages at all, you need to implement -hitTestForEvent:inRect:ofView: and return an appropriate bitmask of NSCellHit... values. Specifically, if the value returned doesn't have the NSCellHitTrackableArea value in it, you won't get any tracking messages!

So, at a high level, your implementation will look something like:

- (NSUInteger)hitTestForEvent:(NSEvent *)event
                       inRect:(NSRect)cellFrame
                       ofView:(NSView *)controlView {
    NSUInteger hitType = [super hitTestForEvent:event inRect:cellFrame ofView:controlView];

    NSPoint location = [event locationInWindow];
    location = [controlView convertPointFromBase:location];
    // get the button cell's |buttonRect|, then
    if (NSMouseInRect(location, buttonRect, [controlView isFlipped])) {
        // We are only sent tracking messages for trackable areas.
        hitType |= NSCellHitTrackableArea;
    }
    return hitType;
}

+ (BOOL)prefersTrackingUntilMouseUp {
   // you want a single, long tracking "session" from mouse down till up
   return YES;
}

- (BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView {
   // use NSMouseInRect and [controlView isFlipped] to test whether |startPoint| is on the button
   // if so, highlight the button
   return YES;  // keep tracking
}

- (BOOL)continueTracking:(NSPoint)lastPoint at:(NSPoint)currentPoint inView:(NSView *)controlView {
   // if |currentPoint| is in the button, highlight it
   // otherwise, unhighlight it
   return YES;  // keep on tracking
}

- (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag {
   // if |flag| and mouse in button's rect, then
   [[NSApplication sharedApplication] sendAction:self.action to:self.target from:controlView];
   // and, finally,
   [buttonCell setHighlighted:NO];
}
Serendipity answered 17/7, 2010 at 4:43 Comment(2)
Where in there do you tell the table to tell its dataSource that the button was checked?Reaction
@Jeremy W. Sherman: This is probably a stupid question, but how do you get "the button cell's |buttonRect|"? I tried various things such as [button frame], but that doesn't seem to work...Coimbra
S
5

The point of NSCell subclasses is to separate responsibility for rendering and handling common UI elements (the controls) from the visual- and event-hierarchy responsibilities of the NSView classes. This pairing permits each one to provide greater specialization and variability without burdening the other. Look at the large number of NSButton instances one can create in Cocoa. Imagine the number of NSButton sub-classes that would exist if this split in functionality were absent!

Using design pattern language to describe the roles: an NSControl acts as a façade, hiding details of its composition from its clients and passing events and rendering messages to its NSCell instance which acts as a delegate.

Because your NSCell subclass includes other NSCell subclass instances within its composition, they no longer directly receive these event messages from the NSControl instance which is in the view hierarchy. Thus, in order for these cell instances to receive event messages from the event responder chain (of the view hierarchy), your cell instance needs to pass along those relevant events. You are recreating the work of the NSView hierarchy.

This isn't necessarily a bad thing. By replicating the behavior of NSControl (and its NSView superclass) but in an NSCell form, you can filter the events passed on to your sub-cells by location, event type, or other criteria. The drawback is replicating the work of NSView/NSControl in building the filtering & management mechanism.

So in designing your interface, you need to consider whether the NSButtonCell (and NSTextFieldCells) are better off in NSControls in the normal view hierarchy, or as sub-cells in your NSCell subclass. It's better to leverage the functionality which already exists for you in a codebase than to re-invent it (and continue maintaining it later) unnecessarily.

Secondbest answered 26/2, 2010 at 20:26 Comment(4)
@Secondbest so if I was going to implement a custom cell for a table (that is pre Lion with the new table NSView cell capability), I am forced to basically mimic the behavior of each control I embed and I just use the cell for the drawing part really. Is that correct?Either
@David, I'm not clear on your question. If you're asking about embedding an NSView subclass in a custom cell in a table view, your cell would need to conform to the NSControl protocol, either handling messages from the view subclass itself, passing them on to the table view, or leaving them stubbed. You shouldn't need to embed an NSControl subclass in a custom cell as you can usually use the control's cell itself. As far as the cell's behavior, you can use the cell to draw parts, find parts clicked on, etc.Secondbest
@Secondbest what I am wanting to learn, is how to create say a cell that contains other "controls". It seems that I could use an NSButtonCell to draw the "control", but the actual button functionality would have to be recreated, i.e. I would have to detect clicks (for a button) mouse drags for an NSSlider etc. Seems like a lot of work for something I thought would be more straightforward/reusable. Am I missing something?Either
@David, there are two classes of objects involved here: controls and cells. Cells (like NSButtonCell, NSImageCell, NSTokenFieldCell, etc.) do the bulk of the work: drawing, hit-testing, computing cell size, etc. What they lack, however, is connection to the view hierarchy: on-screen position, getting events from the view (a subclass of NSResponder) hierarchy, etc. That's provided by NSControl, which acts as a proxy in redirecting messages to the cell, and as a dispatcher if it has more than one. Your cell can contain either cells or controls, but you have to relay the right messages to either.Secondbest

© 2022 - 2024 — McMap. All rights reserved.