a mouse event problem on macOS


James Walker
 

On 9/20/2017 5:17 PM, Graham Cox wrote:

      
On 21 Sep 2017, at 3:53 am, James Walker <list2@...> wrote:

Thank you very much.  Your code didn't work for me at first, perhaps because of a complication that I did not mention in my original message:  The original mouse down happens in a Carbon window.  Anyway, the mouse events returned by -[NSWindow nextEventMatchingMask:untilDate:inMode:dequeue:] are for the wrong window and hence have the wrong mouse coordinates, so I had to convert the coordinates and create a new NSEvent for the right window before forwarding it on to my view's mouseDragged: method.  But it's working now.
OK, sounds a bit smelly, but whatever works, I guess.

The red flag here is the need to “create an event”. I don’t believe there’s ever any reason to do that in mainstream (i.e. app level) code.

NSView has a reference to its window, and the events for that window should have the -locationInWindow coordinate correct for the window. From that you need to convert to the view’s local coordinates using -convertPoint:fromView:, but other than that there’s nothing special to do. I’m not sure why Carbon windows make a difference (surprised they’re still a thing), and since you are creating the overlay window, then everything should be local to that. The original mouse down in the Carbon window is irrelevant in this case - you can simply discard it.

NSView’s trio of mouse methods - mouseDown: -mouseDragged: and -mouseUp: are conveniences - NSView internally implements a loop just like the one I showed which calls out to those methods, but you are not obliged to use them.

The docs on -[NSWindow nextEventMatchingMask:untilDate:inMode:dequeue:] says it forwards the message to -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:].  So, how does NSApplication know what window to send the event to?  It could check what's under the location of the mouse, but as Quincey Morris said elsewhere in this thread, maybe it just sends mouse dragged and mouse up events to the same target as the mouse down.


Graham Cox
 

On 21 Sep 2017, at 3:53 am, James Walker <list2@...> wrote:

Thank you very much. Your code didn't work for me at first, perhaps because of a complication that I did not mention in my original message: The original mouse down happens in a Carbon window. Anyway, the mouse events returned by -[NSWindow nextEventMatchingMask:untilDate:inMode:dequeue:] are for the wrong window and hence have the wrong mouse coordinates, so I had to convert the coordinates and create a new NSEvent for the right window before forwarding it on to my view's mouseDragged: method. But it's working now.
OK, sounds a bit smelly, but whatever works, I guess.

The red flag here is the need to “create an event”. I don’t believe there’s ever any reason to do that in mainstream (i.e. app level) code.

NSView has a reference to its window, and the events for that window should have the -locationInWindow coordinate correct for the window. From that you need to convert to the view’s local coordinates using -convertPoint:fromView:, but other than that there’s nothing special to do. I’m not sure why Carbon windows make a difference (surprised they’re still a thing), and since you are creating the overlay window, then everything should be local to that. The original mouse down in the Carbon window is irrelevant in this case - you can simply discard it.

NSView’s trio of mouse methods - mouseDown: -mouseDragged: and -mouseUp: are conveniences - NSView internally implements a loop just like the one I showed which calls out to those methods, but you are not obliged to use them.


—Graham


James Walker
 

On 9/19/2017 6:43 PM, Graham Cox wrote:
The mouseDragged goes to the view that handled the mouseDown. Since that’s not the same view, you don’t get the mouseDragged.

But it’s pretty easy to fix this. On the mouseDown, create and show your overlay view. Then call its mouseDown from your mouseDown.

Within the overlay’s mouseDown, implement your own mouse tracking loop which keeps control until the mouse goes up. This approach is sufficiently general that I have a handy category on NSView to deal with it. It’s also a documented and officially supported way to do this.

Thank you very much.  Your code didn't work for me at first, perhaps because of a complication that I did not mention in my original message:  The original mouse down happens in a Carbon window.  Anyway, the mouse events returned by -[NSWindow nextEventMatchingMask:untilDate:inMode:dequeue:] are for the wrong window and hence have the wrong mouse coordinates, so I had to convert the coordinates and create a new NSEvent for the right window before forwarding it on to my view's mouseDragged: method.  But it's working now.


typedef void(^GCEventTrackingBlock)( NSEvent* event, BOOL* shouldEndTracking );


@interface NSView (EventTrackingBlock)

- (void)	trackMouseEvent:(NSEvent*) initialMouseDownEvent usingBlock:(GCEventTrackingBlock) eventBlock;
- (void)	trackEvents:(NSEventMask) eventTypes usingBlock:(GCEventTrackingBlock) eventBlock;
- (void)	trackEvents:(NSEventMask) eventTypes untilDate:(NSDate*) date dequeue:(BOOL) dequeue usingBlock:(GCEventTrackingBlock) eventBlock;

@end




@implementation NSView (EventTrackingBlock)


- (void)	trackMouseEvent:(NSEvent*) initialMouseDownEvent usingBlock:(GCEventTrackingBlock) eventBlock
{
	// designed to be called from a -mouseDown: method, the initial event is passed to the block, and then unless the block cancelled it, it will
	// enter a tracking loop for all other LEFT mouse events.
	
	BOOL shouldEndTracking = NO;
	
	eventBlock( initialMouseDownEvent, &shouldEndTracking );
	
	if( !shouldEndTracking )
	{
		// tracks left mouse events plus flagsChanged. Tracks mouseMoved events if the view is set to report them.
		
		[self trackEvents:NSLeftMouseDownMask | NSLeftMouseDraggedMask | NSLeftMouseUpMask | NSMouseMovedMask | NSFlagsChangedMask usingBlock:eventBlock];
	}
}




- (void)	trackEvents:(NSEventMask) eventTypes usingBlock:(GCEventTrackingBlock) eventBlock
{
	[self trackEvents:eventTypes untilDate:[NSDate distantFuture] dequeue:YES usingBlock:eventBlock];
}


- (void)	trackEvents:(NSEventMask) eventTypes untilDate:(NSDate*) date dequeue:(BOOL) dequeue usingBlock:(GCEventTrackingBlock) eventBlock
{
	BOOL shouldEndTracking = NO;

	while( !shouldEndTracking )
	{
		NSEvent* event = [self.window nextEventMatchingMask:eventTypes untilDate:date inMode:NSEventTrackingRunLoopMode dequeue:dequeue];
		[self.window discardEventsMatchingMask:NSAnyEventMask beforeEvent:event];
		
		// if a timeout occurred and event is nil, the loop will end after invoking the block unless the block resets the flag to NO.
		
		if( event == nil )
			shouldEndTracking = YES;
	
		eventBlock( event, &shouldEndTracking );
	}
}

@end

The event tracking block needs to look at event.type to see whether it’s a drag, mouse up, flags changed, etc. Usually you do that in a switch statement.


—Graham





On 20 Sep 2017, at 10:54 am, James Walker <list2@...> wrote:

In response to a mouse-down event, I want to display an overlay window and have a view in that window process mouseDragged messages.  However, it does not get any mouseDragged messages, however much I wiggle the mouse.   If I let the mouse button up and click again, I can then get a mouseDragged, but that's not the user interface I'm going for.  I suppose it has something to do with the fact that my original mouseDown was not in the view in my overlay.  I tried making a fake NSLeftMouseDown event and calling -[NSApplication sendEvent:], but that didn't help.  Any ideas?



Graham Cox
 

The mouseDragged goes to the view that handled the mouseDown. Since that’s not the same view, you don’t get the mouseDragged.

But it’s pretty easy to fix this. On the mouseDown, create and show your overlay view. Then call its mouseDown from your mouseDown.

Within the overlay’s mouseDown, implement your own mouse tracking loop which keeps control until the mouse goes up. This approach is sufficiently general that I have a handy category on NSView to deal with it. It’s also a documented and officially supported way to do this.


typedef void(^GCEventTrackingBlock)( NSEvent* event, BOOL* shouldEndTracking );


@interface NSView (EventTrackingBlock)

- (void) trackMouseEvent:(NSEvent*) initialMouseDownEvent usingBlock:(GCEventTrackingBlock) eventBlock;
- (void) trackEvents:(NSEventMask) eventTypes usingBlock:(GCEventTrackingBlock) eventBlock;
- (void) trackEvents:(NSEventMask) eventTypes untilDate:(NSDate*) date dequeue:(BOOL) dequeue usingBlock:(GCEventTrackingBlock) eventBlock;

@end




@implementation NSView (EventTrackingBlock)


- (void) trackMouseEvent:(NSEvent*) initialMouseDownEvent usingBlock:(GCEventTrackingBlock) eventBlock
{
// designed to be called from a -mouseDown: method, the initial event is passed to the block, and then unless the block cancelled it, it will
// enter a tracking loop for all other LEFT mouse events.

BOOL shouldEndTracking = NO;

eventBlock( initialMouseDownEvent, &shouldEndTracking );

if( !shouldEndTracking )
{
// tracks left mouse events plus flagsChanged. Tracks mouseMoved events if the view is set to report them.

[self trackEvents:NSLeftMouseDownMask | NSLeftMouseDraggedMask | NSLeftMouseUpMask | NSMouseMovedMask | NSFlagsChangedMask usingBlock:eventBlock];
}
}




- (void) trackEvents:(NSEventMask) eventTypes usingBlock:(GCEventTrackingBlock) eventBlock
{
[self trackEvents:eventTypes untilDate:[NSDate distantFuture] dequeue:YES usingBlock:eventBlock];
}


- (void) trackEvents:(NSEventMask) eventTypes untilDate:(NSDate*) date dequeue:(BOOL) dequeue usingBlock:(GCEventTrackingBlock) eventBlock
{
BOOL shouldEndTracking = NO;

while( !shouldEndTracking )
{
NSEvent* event = [self.window nextEventMatchingMask:eventTypes untilDate:date inMode:NSEventTrackingRunLoopMode dequeue:dequeue];
[self.window discardEventsMatchingMask:NSAnyEventMask beforeEvent:event];

// if a timeout occurred and event is nil, the loop will end after invoking the block unless the block resets the flag to NO.

if( event == nil )
shouldEndTracking = YES;

eventBlock( event, &shouldEndTracking );
}
}

@end

The event tracking block needs to look at event.type to see whether it’s a drag, mouse up, flags changed, etc. Usually you do that in a switch statement.


—Graham

On 20 Sep 2017, at 10:54 am, James Walker <list2@...> wrote:

In response to a mouse-down event, I want to display an overlay window and have a view in that window process mouseDragged messages. However, it does not get any mouseDragged messages, however much I wiggle the mouse. If I let the mouse button up and click again, I can then get a mouseDragged, but that's not the user interface I'm going for. I suppose it has something to do with the fact that my original mouseDown was not in the view in my overlay. I tried making a fake NSLeftMouseDown event and calling -[NSApplication sendEvent:], but that didn't help. Any ideas?


Quincey Morris
 

On Sep 19, 2017, at 17:54 , James Walker <list2@...> wrote:

I suppose it has something to do with the fact that my original mouseDown was not in the view in my overlay.

I would assume that mouseDragged and mouseUp are sent to the NSResponder object that handled the mouseDown, but I couldn’t find anything documenting that. The other potential problem is that some views that respond to mouseDown don’t use a mouseDragged override, but handle events in a local modal event loop.

Are you trying to consume the mouseDragged events or monitor them? If you’re trying to monitor them, you can install a local event monitor via a NSEvent class method.


James Walker
 

In response to a mouse-down event, I want to display an overlay window and have a view in that window process mouseDragged messages.  However, it does not get any mouseDragged messages, however much I wiggle the mouse.   If I let the mouse button up and click again, I can then get a mouseDragged, but that's not the user interface I'm going for.  I suppose it has something to do with the fact that my original mouseDown was not in the view in my overlay.  I tried making a fake NSLeftMouseDown event and calling -[NSApplication sendEvent:], but that didn't help.  Any ideas?