How to make NSTableRowView drawBackgroundInRect, based upon trackingAreas, update fast enough on scroll? - cocoa

I'm using a view-based NSTableView with a custom NSTableRowView. I would like to use custom row background drawing via drawBackgroundInRect, based upon mouse location using trackingAreas. The goal is to draw a custom background for the unselected row the mouse is currently hovering over.
This is virtually identical to the HoverTableView example from the WWDC 2011 session View Based NSTableView Basic to Advanced. You can see that behavior in action in the Mail, Contacts & Calendars System Preferences Pane in the account types table view on the right.
Unlike the examples, I have thousands of rows in my table view. Everything works as in the examples unless I scroll the table view rapidly (e.g., with a two-finger flick via trackpad). In this case, it seems that updateTrackingAreas is not called fast enough. Rows that scroll under the mouse get highlighted but are never notified that the mouse left their tracking area and therefore remain highlighted. The result is mulitple rows showing the mouse-over highlight and, due to the reuse queue, these will scroll off one end of the table view and reappear on the other (with different data of course) still highlighted as if they are moused-over. Scrolling slowly eliminates the problem; but considering I expect to scroll thousands and thousands of rows, scrolling slowly is not an expected user behavior.
I've tried various combinations of NSTrackingAreaOptions to no avail and am now stumped. Any suggestions on to solve this issue would be appreciated.

I think the answer to the question is "you cannot," i.e., that updateTrackingAreas for NSTableRowView in a fast-scrolling NSTableView does not happen consistently fast enough on the run loop to rely upon it for determining if the pointer is inside a row view or not. Again, see the HoverTableView example code to see where updateTrackingAreas is being used.
I do think I have a suitable solution though. I noticed that Twitter for Mac (RIP) has mouse-over views that appear with mouse movement but disappear on scroll, very similar to the mouse-over highglight I was hoping to achieve.
To execute this, I basically made my custom NSTableRowView have a delegate (my custom NSTableViewController) whom it would ask if it should highlight on hover. I used a custom NSScrollView for my NSTableView and called
[self.contentView setPostsBoundsChangedNotifications:YES];
in its awakeFromNib and also made it register self as the observer of that notification. On receiving that notification, which implies that my table view is scrolling, my custom NSScrollView forwards a message to my NSTableViewController.
When my NSTableViewController receives the message that the table view is scrolling, it disables highlighting on mouse-over and, if there is not already a valid timer running from a previous notification, fires a short timer to reenable highlight on mouse-over once scrolling has stopped. As an extra precaution, at state transitions between enable and disable highlight on mouse-over, my NSTableViewController uses enumerateAvailableRowViewsUsingBlock to clear mouseInside for every row view.
Not sure if this is necessarily the best way, but it achieves the effect I wanted.

The solution for this issue is described here: mouseExited isn't called when mouse leaves trackingArea while scrolling
My updateTrackingAreas method now looks like:
- (void)updateTrackingAreas {
if (trackingArea)
[self removeTrackingArea:trackingArea];
[trackingArea release];
trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
options:NSTrackingInVisibleRect |
NSTrackingActiveAlways |
NSTrackingMouseEnteredAndExited
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
NSPoint mouseLocation = [[self window] mouseLocationOutsideOfEventStream];
mouseLocation = [self convertPoint: mouseLocation fromView: nil];
if (NSPointInRect(mouseLocation, [self bounds]))
[self mouseEntered:nil];
else
[self mouseExited:nil];
[super updateTrackingAreas];
}

Related

NSScrollView in NSPrintOperation accessory view doesn't scroll properly

I have to do a bit of processing before my app can start printing, so I do that on a thread, and once it is all done, I do
dispatch_async(dispatch_get_main_queue(),^(){
ULIPrintableView *viewForPrinting = [[ULIPrintableView alloc] initWithData:data];
NSPrintOperation *operation = [NSPrintOperation printOperationWithView:viewForPrinting];
NSPrintPanel *panel = operation.printPanel;
ULIPrintAccessoryViewController *settingsVC = [ULIPrintAccessoryViewController new];
viewForPrinting.printOperation = operation;
settingsVC.printView = viewForPrinting;
[panel addAccessoryController:settingsVC];
[operation runOperation];
});
All works fine, except that for an NSScrollView (wrapping an NSTableView) in the accessory view.
That view scrolls perfectly with a regular wired PC mouse attached to the Mac, but with an Apple trackpad (both MacBook or the external one) and two-finger scrolling, the scroll view does not update until you remove your fingers from the trackpad.
It seems as if some sort of touch-moved events were not being delivered.
If I change the last line in the block from -runOperation to -runOperationModalForWindow:delegate:didRunSelector:contextInfo: scrolling is OK again, but I don't really have a window I could show this sheet on in my use case.
If I call either runOperation method directly instead of doing my threading the scroll view scrolls fine.
So I have a workaround and a suspicion:
If I use -performSelectorOnMainThread:withObject: instead of dispatch_async(), the scroll view works.
I suspect that -performSelectorOnMainThread:withObject: doesn't use dispatch_async() under the hood, but that delivery of touch-moved events for wheel scrolling does. So when I -runOperation, I block the main dispatch queue and those events pile up "behind" my modal run loop.
Not too happy with this workaround though. What guarantees that -performSelectorOnMainThread:withObject: won't some day block the main queue as well?

NSTableView displaying incorrectly in NSSplitView

I have an NStableView embedded in an NSSplitview.
The table will display, but when it does, the first three or so rows are not visible until I reize the window and/or split view. Then, it will snap into place and function perfectly fine until I quit.
Has this ever happened to anyone? Is there a simple method I can call on the view or table to get it to redraw?
This is how it displays when the view is first loaded (note: the user can scroll the table up and see the top row highlighted, but never get to it)
after resizing the window, the table view suddenly snaps into place and appears as it should:
You could try a [_yourSplitView display] to force a redraw of the NSSplitView. If I remeber correctly the SplitView will redraw all its subviews.
Try experimenting with where you use this, as result may vary depending on where in the init order you call this.
I actually got this working by calling the subview and then just resetting the position of the splitview divider.
NSView *v = [vc view];
[self.superDisplayView addSubview:v];
[self.SourceListSplitView setPosition:250 ofDividerAtIndex:0];

Cocoa: NSView drawRect painting over IBOutlets

I have an NSView in IB which sits above the app window. I have a subclass of NSView (AddSource) which I assign to the NSView.
On awakeFromNib I instantiate the view:
//add a new Add Source class
addSourceView = [[AddSource alloc] initWithFrame:NSMakeRect(0.0, 959.0, 307.0, 118.0)];
[[winMain contentView] addSubview:addSourceView];
in addSourceView's drawRect method I am adding a white background to the view:
[[NSColor whiteColor] set];
NSRectFill(rect);
[self setNeedsDisplay:YES];//added this to see if it might solve the problem
In winMain's contentView I have a NSButton that when clicked slides the addSourceView onto the window:
NSRect addSourceViewFrame = [addSourceView frame];
addSourceViewFrame.origin.y = 841.0;
[[addSourceView animator] setFrame:addSourceViewFrame];
But it seems as if the app is painting over the IBOutlets I placed on the NSView in IB. If, in IB, I repoistion the NSView so that it is on screen when the app launches everything works fine, the IBOutlets are there as well as the background color.
I'm not sure why this is happening. I've done this before with no problems. I must be doing something different this time.
Thanks for any help.
*note - on the 3rd screen capture, when I say this is what the app looks like when opened, that's when I hard code the Y position of the NSView. When it is functioning correctly it should open as screen capture 1.
Most likely your buttons and custom view are siblings, i.e. they are both subviews of your window's content view. Since siblings are "Stacked" depending on the order in which they are added, when you add the view in code it is being added on top of the buttons. You should be able to fix it by explicitly specifying where the view should be positioned relative to its new siblings like so:
[[winMain contentView] addSubview:addSourceView positioned:NSWindowBelow relativeTo:nil];
which should place it below any existing subviews of your window's content view. Also, remove the setNeedsDisplay: line in drawRect, that leads to unncessary, possibly infinite, redrawing.
EDIT: OK I see what you're doing.
I would suggest creating a standalove view in the NIB by dragging a "Custom View" object into the left hand side (the vertically-aligned archived objects section) and adding your controls there, that should ensure the controls are actualy subviews of the view, then you can just create a reference to the archived view in code, and add/remove it dynamically as needed.
Honestly though, you should probably be using a sheet for these kinds of modal dialogs. Why reinvent the wheel, and make your app uglier in the process?
You added TWO AddSource views to the window. You added one in IB - this view contains your textFields and buttons that are connected to the IBOutlets and it is positioned outside the window.
Then in -awakeFromNib you create another, blank AddSource view (containing nothing) and animate it into the window.
I can't recommend highly enough the Hillegass as the best introduction to IB and the correct way to build Cocoa Apps.
Also, Assertions can be useful to make sure what you think is happening is actually what is happening.
If you are certain you added a button to your view in IB, assert it is so:-
- (void)awakeFromNib {
NSAssert( myButton, #"did i hook up the outlet?");
}
NSAssert is a macro that has zero overhead in a release build.
Calling [self setNeedsDisplay:YES] from -drawRect just causes the same -drawRect to be called again. This will give you big problems.

NSView overlapping

My cocoa app has a "dashboard" style layout.
When the app starts, the main window contains 6 views which display graphs.
When I click on any part of the bottom most view, I have another NSView instance popping up
as an annotation. The problem I run into is that is the pop up NSView is large enough dimension wise,
the other views in the window overlap the pop up view. Currently, I do that with:
[[self superview] addSubview:annotationView ]; where 'superview' is the window.
Im not sure why this would be the case, I have tried removing the the pop up view from the "view stack"
and making it change positions but that didnt work.
[[[self window] contentView] insertView:popupView atIndex:0];
This will insert the view at the top level, if you still can't see it you will need to add a subview to the superview of the NSWindow's contentview.
If all the views are added as subviews of [self superview] then you must make sure that they do not overlap. Cocoa doesn’t guarantee correct behaviour in case there are overlapping sibling views.
If you want a popup view, consider using a child window instead. Since it’s a different window, the popup lies in a different view hierarchy, hence you won’t have the overlapping sibling views problem.
A good example of using child windows for additional information is Matt Gemmell’s MAAttachedWindow.

NSButtonCell inside custom NSCell

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
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];
}
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.

Resources