Using NSTrackingArea in an NSView that is unattached to a window? - objective-c

Basically, I want an "invisible" NSView covering my entire screen. I will add an NSTrackingArea to that, so that I get global mouse events as my cursor moves about the screen.
-(void)setTrackingArea
{
view = [[NSView alloc] initWithFrame:[NSScreen currentScreenForPoint:[NSEvent mouseLocation]].frame];
NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:[NSScreen currentScreenForPoint:[NSEvent mouseLocation]].frame options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways owner:view userInfo:nil];
[view addTrackingArea:area];
[area release];
//[[window contentView] addSubview:view];
//I don't want to add the view to a window, as all tutorials say.
}
- (void)mouseExited:(NSEvent *)theEvent
{
NSLog(#"Exit"); //Never firing
}
Is this possible? Using NSViews and NSTracking Areas without a window?

Using an invisible view is definitely not something you want to do. Look into the addGlobalMonitorForEventsMatchingMask:: class method on NSEvent.
For example, here's how you would add a monitor for a movement of the mouse:
[NSEvent addGlobalMonitorForEventsMatchingMask:NSMouseMovedMask handler:^(NSEvent *mouseMovedEvent) {
//do something with that event
}];

Related

When to override updateTrackingAreas

So I have a quite big NSScrollView with several custom views in it. In these custom views i'm overriding -(void)updateTrackingAreas like this:
- (void)updateTrackingAreas
{
[self removeTrackingArea:trackingArea];
trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:(NSTrackingCursorUpdate |
NSTrackingActiveWhenFirstResponder |
NSTrackingInVisibleRect)
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
But somehow I feel this is a little unefficient since I have about 50 instances of my custom view in the NSScrollView and only about 5 of them are visible at a time and this way I'm updating the tracking areas of non-visible views.
So I thought I would skip updating the tracking areas if the view is not in NSScrollView's visible rect. Something like:
- (void)updateTrackingAreas
{
if(!NSIntersectsRect([self frame], [[self superview] visibleRect]))
{
return;
}
// ...
}
This seems to be working out well but I'm not sure if this is safe.
Does anyone have some advice on this matter?

Why are Tracking Areas Not Respected

I've been experimenting with tracking area, and having some problems, so I created this simple program as a test. I create one tracking area in the lower left corner of my view (which is the window's content view), but I receive mouseEntered and exited messages no matter where I enter or exit the view. I've also tried putting this code in the init method and awakeFromNib with the same results.
#implementation Parent //This view is the contentView of the main window
-(void)viewDidMoveToWindow{
NSLog(#"In viwDidMoveToWindow");
NSTrackingArea *area = [[NSTrackingArea alloc]initWithRect:NSMakeRect(0,0,50,50) options:NSTrackingInVisibleRect |NSTrackingMouseEnteredAndExited |NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area];
}
-(void)mouseEntered:(NSEvent *)theEvent {
NSLog(#"Entered");
}
-(void)mouseExited:(NSEvent *)theEvent {
NSLog(#"Exited");
}
#end
Why is the tracking area not being respected?
It has to do with the options you are using, try instead using
options:NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited

mouseExited isn't called when mouse leaves trackingArea while scrolling

Why mouseExited/mouseEntered isn't called when mouse exits from NStrackingArea by scrolling or doing animation?
I create code like this:
Mouse entered and exited:
-(void)mouseEntered:(NSEvent *)theEvent {
NSLog(#"Mouse entered");
}
-(void)mouseExited:(NSEvent *)theEvent
{
NSLog(#"Mouse exited");
}
Tracking area:
-(void)updateTrackingAreas
{
if(trackingArea != nil) {
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
More details:
I have added NSViews as subviews in NSScrollView's view. Each NSView have his own tracking area and when I scroll my scrollView and leave tracking area "mouseExited" isn't called but without scrolling everything works fine. Problem is that when I scroll "updateTrackingAreas" is called and I think this makes problems.
* Same problem with just NSView without adding it as subview so that's not a problem.
As you noted in the title of the question, mouseEntered and mouseExited are only called when the mouse moves. To see why this is the case, let's first look at the process of adding NSTrackingAreas for the first time.
As a simple example, let's create a view that normally draws a white background, but if the user hovers over the view, it draws a red background. This example uses ARC.
#interface ExampleView
- (void) createTrackingArea
#property (nonatomic, retain) backgroundColor;
#property (nonatomic, retain) trackingArea;
#end
#implementation ExampleView
#synthesize backgroundColor;
#synthesize trackingArea
- (id) awakeFromNib
{
[self setBackgroundColor: [NSColor whiteColor]];
[self createTrackingArea];
}
- (void) createTrackingArea
{
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
- (void) drawRect: (NSRect) rect
{
[[self backgroundColor] set];
NSRectFill(rect);
}
- (void) mouseEntered: (NSEvent*) theEvent
{
[self setBackgroundColor: [NSColor redColor]];
}
- (void) mouseEntered: (NSEvent*) theEvent
{
[self setBackgroundColor: [NSColor whiteColor]];
}
#end
There are two problems with this code. First, when -awakeFromNib is called, if the mouse is already inside the view, -mouseEntered is not called. This means that the background will still be white, even though the mouse is over the view. This is actually mentioned in the NSView documentation for the assumeInside parameter of -addTrackingRect:owner:userData:assumeInside:
If YES, the first event will be generated when the cursor leaves aRect, regardless if the cursor is inside aRect when the tracking rectangle is added. If NO the first event will be generated when the cursor leaves aRect if the cursor is initially inside aRect, or when the cursor enters aRect if the cursor is initially outside aRect.
In both cases, if the mouse is inside the tracking area, no events will be generated until the mouse leaves the tracking area.
So to fix this, when we add the tracking area, we need to find out if the cursor is within in the tracking area. Our -createTrackingArea method thus becomes
- (void) createTrackingArea
{
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
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];
}
}
The second problem is scrolling. When scrolling or moving a view, we need to recalculate the NSTrackingAreas in that view. This is done by removing the tracking areas and then adding them back in. As you noted, -updateTrackingAreas is called when you scroll the view. This is the place to remove and re-add the area.
- (void) updateTrackingAreas
{
[self removeTrackingArea:trackingArea];
[self createTrackingArea];
[super updateTrackingAreas]; // Needed, according to the NSView documentation
}
And that should take care of your problem. Admittedly, needing to find the mouse location and then convert it to view coordinates every time you add a tracking area is something that gets old quickly, so I would recommend creating a category on NSView that handles this automatically. You won't always be able to call [self mouseEntered: nil] or [self mouseExited: nil], so you might want to make the category accept a couple blocks. One to run if the mouse is in the NSTrackingArea, and one to run if it is not.
#Michael offers a great answer, and solved my problem. But there is one thing,
if (CGRectContainsPoint([self bounds], mouseLocation))
{
[self mouseEntered: nil];
}
else
{
[self mouseExited: nil];
}
I found CGRectContainsPoint works in my box, not CGPointInRect,

Two finger tap on UIButton

I've got a UIButton in my application called myButton. I'd like to react to a single finger tap and a two finger tap on the button differently. However, from what I understand UIButton objects are only able to detect single finger touches touchDown, touchUpInside, etc. After doing some research, it looks like I'll have to use the touchesBegan method and just check to see if both of the fingers are within myButton frame. Is there any easier way to do this? Thanks!
Yes it is! By using UITapGestureRecognizer you can do something like this
- (void)viewDidLoad {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tap:)]
tap.numberOfTapsRequired = 2;
[myButton addGestureRecognizer:tap];
[tap release];
}
- (void)tap:(UITapGestureRecognizer *)gesture {
NSLog(#"Tap!");
}

Is there a way to make a custom NSWindow work with Spaces

I'm writing an app that has a custom, transparent NSWindow created using a NSWindow subclass with the following:
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
if (self)
{
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
}
return self;
}
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
I have everything working perfectly, including dragging and resizing, except the window doesn't work with Spaces. I cannot move the window to another space by either holding the window while switching spaces via keyboard shortcut, or by dragging to the bottom/top/left/right of the window. Is there anyway to have a custom window behave exactly like a normal window with regards to Spaces?
After a long time I found a solution to this annoying problem.
Indeed [window setMovableByWindowBackground:YES]; conflicts with my own resizing methods, the window trembles, it looks awful!
But overriding mouse event methods like below solved the problem in my case :)
- (void)mouseMoved:(NSEvent *)event
{
//set movableByWindowBackground to YES **ONLY** when the mouse is on the title bar
NSPoint mouseLocation = [event locationInWindow];
if (NSPointInRect(mouseLocation, [titleBar frame])){
[self setMovableByWindowBackground:YES];
}else{
[self setMovableByWindowBackground:NO];
}
//This is a good place to set the appropriate cursor too
}
- (void)mouseDown:(NSEvent *)event
{
//Just in case there was no mouse movement before the click AND
//is inside the title bar frame then setMovableByWindowBackground:YES
NSPoint mouseLocation = [event locationInWindow];
if (NSPointInRect(mouseLocation, [titleBar frame])){
[self setMovableByWindowBackground:YES];
}else if (NSPointInRect(mouseLocation, bottomRightResizingCornerRect)){
[self doBottomRightResize:event];
}//... do all other resizings here. There are 6 more in OSX 10.7!
}
- (void)mouseUp:(NSEvent *)event
{
//movableByBackground must be set to YES **ONLY**
//when the mouse is inside the titlebar.
//Disable it here :)
[self setMovableByWindowBackground:NO];
}
All my resizing methods start in mouseDown:
- (void)doBottomRightResize:(NSEvent *)event {
//This is a good place to push the appropriate cursor
NSRect r = [self frame];
while ([event type] != NSLeftMouseUp) {
event = [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
//do a little bit of maths and adjust rect r
[self setFrame:r display:YES];
}
//This is a good place to pop the cursor :)
//Dispatch unused NSLeftMouseUp event object
if ([event type] == NSLeftMouseUp) {
[self mouseUp:event];
}
}
Now I have my Custom window and plays nice with Spaces :)
Two things here.
You need to set the window to allow dragging by background, [window setMovableByWindowBackground:YES];
And If your custom window areas you expect to be draggable are custom NSView subclasses, you must override the method - (BOOL)mouseDownCanMoveWindow to return YES in any NSView subclass that needs to be able to move the window by dragging.
Did you override isMovable?
The Apple documentation says, that it changes Spaces behavior:
If a window returns NO, that means it
can only be dragged between spaces in
F8 mode, ...
Another method that might be related:
NSWindow setCollectionBehavior