I have created an NSButton class, and when rolling over my buttons it happily detects mouseEntered and mouseExited events. But as soon as the mouseDown event occurs, so long as the mouse it down, mouseEntered events are no longer called until the mouse button is lifted.
So, when the mouseDown event is called, mouseEntered or MouseExited events are no longer called, nor is mouseDown called when rolling over other buttons until I let go of the initial mouseDown.
So I would like to detect when my mouse enters while the mouse is down as well.
Turns out I just needed to add NSTrackingEnabledDuringMouseDrag to my NSTrackingAreaOptions. mouseEntered and mouseExited events now fire when dragging with mouse down.
When an NSButton receives a mouse down event, it enters a private tracking loop, handling all the mouse events that are posted until it gets a mouse up. You can set up your own tracking loop to do things based on the mouse location:
- (void) mouseDown:(NSEvent *)event {
BOOL keepTracking = YES;
NSEvent * nextEvent = event;
while( keepTracking ){
NSPoint mouseLocation = [self convertPoint:[nextEvent locationInWindow]
fromView:nil];
BOOL mouseInside = [self mouse:mouseLocation inRect:[self bounds]];
// Draw highlight conditional upon mouse being in bounds
[self highlight:mouseInside];
switch( [nextEvent type] ){
case NSLeftMouseDragged:
/* Do something interesting, testing mouseInside */
break;
case NSLeftMouseUp:
if( mouseInside ) [self performClick:nil];
keepTracking = NO;
break;
default:
break;
}
nextEvent = [[self window] nextEventMatchingMask:NSLeftMouseDraggedMask | NSLeftMouseUpMask];
}
}
When the left mouse button is down, dragging begins. If I remember correctly, mouse moved events aren't sent during drags, and that's probably one reason that you don't get mouseEntered and mouseExited messages. However, if you implement the NSDraggingDestination protocol and register your view as a possible recipient of the type of data being dragged, you'll get draggingEntered and draggingExited messages instead.
Read about it in the Dragging Destinations section of Drag and Drop Programming Topics.
Related
In Cocoa I developed app which is agent (runs only as icon on upper status bar).
It can display popover window which is basically subclass of NSWindow with NSView as it's content.
Into another NSView subclass (which represents icon on status bar) I'm adding
self.settingsPopoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask|NSRightMouseDownMask handler:^(NSEvent *event) {
[selfReference hideSettingsPopover];
}];
So when user clicks outside popover windows it hides.
I want to implement similar behaviour when user swipes with four fingers up/down (so when Exposé or Mission Control are being launch).
I tried with lot of mask that are available in NSEvent.h but none of them helped.
First enable touch events in your NSView:
view.acceptsTouchEvents = YES
In your NSView subclass, override the touch event methods declared in the NSResponder superclass. If you want to recognize a 4-finger swipe gesture, you probably want -swipeWithEvent:. Since this will fire for swipe events that have any number of fingers, you would want to filter it to 4-finger gestures only. The method -[NSEvent touchesMatchingPhase:inView:] will return an array of NSTouch objects, one for each finger (ie. the count of the array is equivalent to the number of fingers).
To summarize, the implementation would look something like this:
- (void)swipeWithEvent:(NSEvent *)event
{
NSArray *touches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self];
if (touches.count == 4) {
// Handle event here
}
}
I have an IBAction which should, when activated, wait until the mouse is clicked and then print the location. How do I do that properly? My attempt:
BOOL finished = NO;
while (!finished) {
if ([NSEvent pressedMouseButtons] == 1) {
finished = YES;
NSLog(#"%f | %f", [NSEvent mouseLocation].x, [NSEvent mouseLocation].y);
}
}
does work but will show a busy mouse pointer. So I guess there must be a much better way.
Both the IBaction and Click events occur on a Main Thread only. It seems you are blocking the main thread with a while loop in IBAction until the mouse click happens, which is like putting the Main thread busy.
You should run this while loop in a secondary thread so that Main thread will be free of this while loop checking.
I have a borderless NSPanel, when I first launch it and it has focus I can drag it around and the mousedragged method gets triggered correctly, however when I switch focus to another app and then return to the NSPanel (which is set with an NSNonactivatingPanelMask) I no longer receive the mousedragged events.
I do still receive mouseup and mousedown events, so I'm at a loss of why the mousedragged event isn't executed.
Any ideas?
Here's how it gets initialized:
_panel = [[MyPanel alloc] initWithContentRect:frame
styleMask:NSBorderlessWindowMask | NSNonactivatingPanelMask
backing:NSBackingStoreBuffered
defer:NO];
I've also tried adding all these methods to my panel class:
- (BOOL)canBecomeKeyWindow {
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (BOOL)acceptsFirstMouse
{
return YES;
}
And making it first responder in the mouse down (which it still receives):
- (void)mouseDown:(NSEvent *)theEvent
{
[self makeFirstResponder:self];
[self makeKeyWindow];
[self setBackgroundColor:[NSColor redColor]];
[self display];
}
The mousedragged simply contains this:
- (void)mouseDragged:(NSEvent *)theEvent
{
[self setBackgroundColor:[NSColor greenColor]];
[self display];
NSLog(#"dragged");
}
I do not want the window to get focus. (The focus should remain on the third party app below).
Update: I've added a sample project, download here: http://users.telenet.be/prullen/MovingPanel.zip
As you will see, when you first run the app, and drag it will output "dragged" in the console.log continuously (and the background color will be green). If you then switch to another app and then back to the movingpanel app, dragging will not output anything any more. (and the background color will be red, which is set in the mousedown event handler).
Without the NSNonactivatingPanelMask this works as it should, but it is vital that the window below my panel remains active. If there is another way to accomplish this, please do share.
One thing I have also noticed, if you double click on the panel (fast), and then drag, it turns green (so the mousedragged: is being called), but it's not moving... Not sure what to think of that.
I also noticed that if I do [self setMovableByWindowBackground:NO]; then it will also work correctly. I am betting that the way setMovableByWindowBackground works is interfering with the mouseDragged being called. It's probably a bug that it is called at all.
I would guess one possible solution would be to implement your own window dragging.
If what you are really interested in is responding to when the window is moving, this question and answer may provide what you need.
How to receive notifications when moving Window by mouse?
I have several views of class MyView (subclass of NSView) inside another NSView. MyView implements -mouseEntered:, -mouseExited:, -mouseDown:, -mouseDragged:, and -mouseUp:.
Almost always, when a MyView receives a mouse-down event, all subsequent mouse-dragged events are received by the same MyView until the next mouse-up event. Even if the cursor goes outside of the MyView. That is the expected behavior.
Occasionally, a MyView will receive a mouse-down event, but will only receive mouse-dragged and mouse-up events while the cursor remains inside the MyView. If the cursor moves onto a different MyView, then that MyView starts receiving mouse-dragged events (without first receiving a mouse-down event) and can receive the subsequent mouse-up event.
In case it matters, the mouse-down event creates a FooView (subclass of NSView) on top of the MyView, and the mouse-dragged events resize the frame of the FooView. This might be related, as I've only been able to reproduce the problem after one of these FooViews has been created. FooView does not implement any of the mouse event methods.
I've been messing with this for a while now and haven't been able to either purposely reproduce the problem or recreate the problem in a simple example. I'd be happy to answer any questions about my code, I'm just not sure what would be the relevant part to post.
Not sure what the root issue is (this Cocoa behavior seems inconsistent to me)... but here's one possible workaround:
In the superview, create an instance variable tracking the MyView instance in which the -mouseDown: occurred.
When you receive a -mouseDragged: in MyView, instead of operating on self, operate on the MyView instance reference stored in the superview.
...then you'll be able to consistently track which object is being dragged, without having to run your own event loop.
You need to run you own mouse tracking loop in the view until mouse is up. You can extend it to process more types of events by passing them into nextEventMatchingMask:.
- (void)mouseDown:(NSEvent*)event
{
CGPoint hitPoint = [self pointInViewSpaceFromEvent:event];
BOOL isDragging = NO;
BOOL isTracking = YES;
while (isTracking)
{
switch ([event type])
{
case NSLeftMouseDown:
[self singleMouseDownAtPoint:hitPoint withModifierFlags:[event modifierFlags]];
break;
case NSLeftMouseUp:
isTracking = NO;
if (isDragging)
[self mouseDraggingDidEndAtPoint:hitPoint];
else
[self singleMouseUpAtPoint:hitPoint withEvent:event];
break;
case NSLeftMouseDragged:
if (isDragging)
[self mouseDraggingAtPoint:hitPoint withModifierFlags:[event modifierFlags]];
else
isDragging = YES;
break;
default:
break;
}
if (isTracking)
{
event = [[self window] nextEventMatchingMask:NSLeftMouseDraggedMask | NSLeftMouseUpMask];
hitPoint = [self pointInViewSpaceFromEvent:event];
}
}
}
I have simple global monitor for mouse clicks:
[NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask handler:^(NSEvent *event){
if ([event type] == NSLeftMouseDown) {
[self mouseDown];
if (self.lockMouse) {
// Cancel event
}
}
}];
Is there any way to cancel global mouse events, so clicking only notifies my app?
I.e.: After clicking on any button on screen (that don't belong to my app) while "locked", event goes here, but not to button that was below cursor. Something like event.preventDefault() in JavaScript.
Not with this API, no. From the documentation, it says that the block is:
The event handler block object. It is passed the event to monitor. You are unable to change the event, merely observe it.
If you want to intercept the event and prevent it from propagating, you need to use a CGEventTap instead.