hitTestForEvent:inRect:ofView is invoked twice in my NSOutlineView cells - objective-c

I've subclassed the cells of a NSOutlineView, by setting the custom class in interface builder.
I've implemented this delegate method to configure the cells:
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
Also, I've implemented this method in my custom cell class:
- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView
which is invoked twice every time I click on the cell. I'm wondering why not just once. The event type is always MouseDown.
I don't know if this matters, but it is invoked twice even if the cell has not parents or children. So it can't be the cells hierarchy.
If I can't rely on hitTestForEvent to trigger an action when a specific area of my cell is clicked, which method should I use ?
Thanks

-hitTestForEvent:inRect:ofView is entirely the wrong method to be using to trigger actions. You should be using -trackMouse:inRect:ofView:untilMouseUp:, or -startTrackingAt:inView:, -continueTracking:at:inView: and -stopTracking:at:inView:mouseIsUp:.
Important Note: If you implement your own mouse tracking loop in -trackMouse:inRect:ofView:untilMouseUp:, you should document this fact somewhere, because generally speaking it will preclude the use of the other three methods. Some of the NSCell subclasses in the AppKit framework do this and fail to document that they have done so (with the result that you’ll ponder for hours why it is that -startTrackingAt:inView: never gets called).
How do you implement your own tracking loop? Like this:
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame
ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
NSPoint pos = [controlView convertPoint:[theEvent locationInWindow]
fromView:nil];
if ([theEvent type] == NSLeftMouseDown && NSPointInRect (pos, myClickRect)) {
NSWindow *window = [controlView window];
NSEvent *myEvent;
NSDate *endDate = [NSDate distantFuture];
while ((myEvent = [window nextEventMatchingMask:(NSLeftMouseDragged
|NSLeftMouseUp)
untilDate:endDate
inMode:NSEventTrackingRunLoopMode
dequeue:YES])) {
if ([myEvent type] != NSLeftMouseUp)
continue;
pos = [controlView convertPoint:[theEvent locationInWindow]
fromView:nil];
if (NSPointInRect (pos, myClickRect)) {
// React somehow
}
return YES;
}
}
return [super trackMouse:theEvent inRect:cellFrame ofView:controlView
untilMouseUp:untilMouseUp];
}
(The above code was just typed in here, so the usual caveats apply; it assumes the existence of an NSRect called myClickRect that defines the active area of your cell. You might need to calculate that from cellFrame at the head of the method.)
Obviously you can watch for and handle other events too, if they are relevant to you.
Perhaps I should also add that the three method approach, while conceptually cleaner, tends to be quite a bit slower, which generally leads me to prefer overriding -trackMouse:inRect:ofView:untilMouseUp: as shown above.

Related

How to properly show the current selection in an NSCollectionView?

I have an NSCollectionView that is showing some images. I have implemented an NSCollectionViewDelegate to tell it which items should be selected and/or highlighted. I'm using a stock NSCollectionViewItem to draw the images and their names. When the user selects an item, my delegate gets the messages about highlight state changes:
- (void)collectionView:(NSCollectionView *)collectionView
didChangeItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths
toHighlightState:(NSCollectionViewItemHighlightState)highlightState
{
[collectionView reloadItemsAtIndexPaths:indexPaths];
}
I do a similar thing for didSelect/didDeselect:
- (void)collectionView:(NSCollectionView *)collectionView
didSelectItemsAtIndexPaths:(nonnull NSSet<NSIndexPath *> *)indexPaths
{
[collectionView reloadItemsAtIndexPaths:indexPaths];
}
In the NSCollectionViewItems view, I do the following:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* bgColor = [[self window] backgroundColor];
NSColor* highlightColor = [NSColor selectedControlColor];
NSRect frame = [self bounds];
NSCollectionViewItemHighlightState hlState = [collectionViewItem highlightState];
BOOL selected = [collectionViewItem isSelected];
if ((hlState == NSCollectionViewItemHighlightForSelection) || (selected))
{
[highlightColor setFill];
}
else
{
[bgColor setFill];
}
[NSBezierPath fillRect:frame];
}
The problem I'm seeing is that drawing the highlight or selection appears to be random. When it does draw the selection, it's almost always on the items the user has actually selected (though it often leaves off the last item for some reason). Occasionally, it will select a different item the user did not click on or drag over. Often, though, it just doesn't draw.
I've added printing to verify that it is calling -didChangeItemsAtIndexPaths:toHighlightState: and -didSelectItemsAtIndexPaths:. Is there anything I'm doing wrong here?
I've added some logging to the view's -drawRect: method, and it doesn't appear to be getting called on all transitions, even though I'm calling -reloadItemsAtIndexPaths: in the -didChange* methods. Why not?
I've also noticed that the delegate's -should/didDeselectItemsAtIndexPaths: does not seem to get called ever, even though the -should/didSelectItemsAtIndexPaths: does get called. Why is that?
The problem turned out to be calling [collectionView reloadItemsAtIndexPaths:]. When you do that, it removes the existing NSCollectionViewItem and creates a new one (by calling your data source's collectionView:itemForRepresentedObjectAt:). That immediately sets the new collection view item to not selected (or rather it doesn't set it to be selected). When that happens, it won't call your should/didDeselect methods because the existing item doesn't exist anymore, and the new one is not selected.
The real solution turned out to be to subclass NSCollectionViewItem and override -setSelected: to do the following:
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
[self.view setNeedsDisplay:YES];
}
When the view's -drawRect: method gets called, it asks the item if it's selected and draws appropriately.
Therefore, I could completely remove all of the should/did/select/Deselect methods from the delegate without any problem, and it all just worked!

Cocoa NSOpenGLView - [win setDelegate:view ] shows error. How to delegate manually?

I'm programming in Eclipse (not Xcode) on Yosemita 10.10...
I try to catch MouseMoved event, but it not called (mouseDown, mouseDragged - works fine). So I'm using this example code from here
http://lists.apple.com/archives/mac-opengl/2003/Feb/msg00069.html
but compiller show error on
[app setDelegate: view];
(- cannot initialize a parameter of type 'id' with an lvalue of type 'NSView *')
If I comment this line - it's work, but mouseMoved don't calling.
Please help! I'm newbie in objective-c
OS X does not automatically track the mouse movement events for you unless you request them.
In order to receive mouseMoved: events, you should add an NSTrackingArea to your subclass of NSOpenGLView. For example:
- (void)awakeFromNib {
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.frame options:NSTrackingActiveAlways|NSTrackingMouseMoved owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
}
After that, your mouseMoved: method will be called.
- (void)mouseMoved:(NSEvent *)theEvent {
NSLog(#"moved");
}
You can optionally implement updateTrackingAreas if you need to update your tracking area manually when the view resizes. For details, please refer to Using Tracking-Area Objects.

Toggling the selected state of a TableView cell with the mouse

By default, NSTableView allows the user to clear the rows selection by clicking anywhere in the blank area of the table view. This however is not always intuitive and sometimes isn’t even possible (for example, when the table view doesn’t actually have any empty area inside itself).
So how do you allow the user to deselect the row by simply clicking on it again? No regular delegate methods (like -tableView:shouldSelectRow:) are called in this case so you can’t capture the click on a row that is already selected, this way.
You want to define your own subclass of NSTableView and set up -mouseDown: like so:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint globalLocation = [theEvent locationInWindow];
NSPoint localLocation = [self convertPoint:globalLocation fromView:nil];
NSInteger clickedRow = [self rowAtPoint:localLocation];
BOOL wasPreselected = (self.selectedRow == clickedRow);
[super mouseDown:theEvent];
if (wasPreselected)
[self deselectRow:self.selectedRow];
}

NSControl and NSCell: Manage the cell state the right way

I have a totally custom NSControl with its totally custom NSCell.
Now I want to implement some Mouse interaction. For example when user clicks over the control
I want to change the control state to highlight so the questions are:
1) Where I have to deal with the mouse event? In the NSControl or directly in the NSCell?
At the moment I'm working with this code in the NSCell subclass:
-(BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView{
[self setHighlighted:YES];
return YES;
}
-(void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag{
[self setHighlighted:NO];
}
2) Is the NSCell state automatically managed by the NSControl? If I set the NSControl stete to highlight it will be mirrored to the NSCell?
3) and what about the enabled attributes?
At the moment I wrote this code in the NSControl:
And this code in the NSControl
-(void)setEnabled:(BOOL)flag{
[super setEnabled:flag];
[[self cell]setEnabled:flag];
[self updateCell:[self cell]];
}
Have you particular suggestion to work with mouse event with a custom NSControl+NSCell?
You can do the following:
Create a NSTrackingArea, which will detect if the mouse got into your NSControl:
NSTrackingArea* trackingArea = [[[NSTrackingArea alloc] initWithRect:yourNSControlBoundsRect options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways owner:yourNSControl userInfo:nil];
Then you add your trackingArea to your NSControl:
[yourNSControl addTrackingArea:trackingArea];
In your NSControl subclass implement both
- (void)mouseEntered:(NSEvent *)theEvent
- (void)mouseExited:(NSEvent *)theEvent
There you can do something with your cell inside your control, or with your control itself.
Hope that helps

Access int variable from other class

I have two windows, my main window "window" and "help window" all inside my App Delegate. In my main window its view is subclassed and I want to draw a rect inside it. My help window has a rect also but it has an NSTracker on it. What I want to do is draw my rect in my window subclass with the x and y coordinates equal to my NSTracker position. The problem I am having is it crashes when I try to bring in the coordinates, any ideas of what I could be doing wrong? thanks
//My subclass of window is called CutoutView. This is all in draw rect
AppDelegate *controller = [[[NSApp delegate] window] contentView];
xValue = controller.mouseLoc.x;
yValue = controller.mouseLoc.y;
NSRectFillUsingOperation(NSMakeRect(xValue,yValue, 600, 400), NSCompositeClear);
[self update];
- (void)update
{
NSLog(#"test");
[self setNeedsDisplay:YES];
}
//My AppDelegate with the tracker helpView is a reference to the view of my second window "Help Window"
-(void)updateTrackingAreas
{
if(trackingArea != nil) {
[self.helpView removeTrackingArea:trackingArea];
[trackingArea release];
}
opts = (NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self.helpView bounds]
options:opts
owner:self
userInfo:nil];
[self.helpView addTrackingArea:trackingArea];
}
-(void)mouseMoved:(NSEvent *)theEvent
{
mouseLoc = [NSEvent mouseLocation];
NSLog(#"mouseMoved: %f %f", mouseLoc.x, mouseLoc.y);
}
in my CutoutView am i getting the AppController wrong because it is in a different window "helpWindow"? or does it have to do with my int values?
There are many things wrong with your code and it's obvious that you are fundamentally misunderstanding some basic concepts.
Firstly, you state that this code is in your drawRect: method;
AppDelegate *controller = [[[NSApp delegate] window] contentView];
xValue = controller.mouseLoc.x;
yValue = controller.mouseLoc.y;
NSRectFillUsingOperation(NSMakeRect(xValue,yValue, 600, 400), NSCompositeClear);
[self update];
There are several immediate flaws apparent. Firstly, why are you declaring controller to be of type AppController* when the method you are calling (-contentView) returns an NSView?
Your AppController is not a view (at least it should not be!), so you should be declaring the object as such:
NSView* mainView = [[[NSApp delegate] window] contentView];
If you are indeed using a view as a controller then this is completely wrong. See below for my note about MVC.
You don't specify where the mouseLoc property is coming from. We need to see where this is declared, because that will affect whether or not there are problems with it.
Your drawing code calls [self update], which simply tells the view to redraw itself. This will result in an infinite loop because every time the view draws it is forced to redraw. You should never call setNeedsDisplay: from inside drawRect:.
Even after making these changes, this code is very badly structured and the design is broken.
As it stands, your code violates the Model-View-Controller pattern. A views should not have knowledge of other views. You need to restructure things so that your views display properties of your controller without needing knowledge of other views. That means that you must store the mouse location in your controller (or a model object) and use some method for the view to access that information, preferably a datasource protocol or similar. In my answer to this other question I give an example of how to do that.
You need to read the Cocoa Drawing Guide. You also need to learn more core Cocoa concepts as it is clear you are misunderstanding how Cocoa is supposed to work.