Detecting the Next Left Mouse Button Down Event

I’m looking to write AppleScript0bjC code that listens for the either the next “left mouse button down” event or the next “tablet point” event, does something in response, and then stops listening.

In my random walk through the Cocoa documentation, I’ve noticed “CGEventTapCreate” and “addGlobalMonitorForEventsMatchingMask:handler:”. Is there another way? Both these two look challenging to implement and non-compatible with AppleScript0bjC.

If I want to use “addGlobalMonitorForEventsMatchingMask:handler:”, I’m guessing I need to:

Create an Objective-C class in which I add a global monitor whose block sends a notification when one of the events occurs.

Have a handler in my AppleScript0bjC code which receives the notification, does something, and then sends a message to the Objective-C class telling it to release the global monitor.

I’m talking through my hat here since I know nothing about blocks or notifications, but am I on the right track?

The mouseDown: method listens for left mouse down events. This program logs the first event when the user clicks the left mouse button anywhere in the content view, and then ignores any further clicks. Notice that the parent class is NSView, and in IB you must set the class of the main content view to your program’s AppDelegate – in my example that would be MouseListenerAppDelegate.

script MouseListenerAppDelegate
	property parent : class "NSView"
	property counter : 0
	
	on mouseDown_(theEvent)
		if counter = 0 then log theEvent
		set counter to 1
	end mouseDown_
    
	end script

Ric

I was unclear. I need AppleScript0bjC code that listens for the either the next “left mouse button down” event or the next “tablet point” event anywhere on the display, does something in response, and then stops listening. Hence I ended up looking at “addGlobalMonitorForEventsMatchingMask:handler:”.

Thanks for the clarification – Now I see why you went to the method you did.
Have you been able to get “addGlobalMonitorForEventsMatchingMask:handler:” to work? Does anyone know whether this can be made to work in ASOC alone, or do you need to use Objective-C also? I can’t figure out how to specify the “handler” part of the method in ASOC.

Ric

Blocks have to be done in Objective-C. Which is probably just as well in this case, given that the code that is going to be called very, very often – not really a job for AppleScript.

Shane, thanks for that insight. I have one more question on this function, and some others that use masks or constants. What is the meaning of this syntax in the masks for event types section of the NSEvent class reference: NSLeftMouseDownMask = 1 << NSLeftMouseDown. It seems like in ASOC you have to convert these masks and constants to numbers for them to work. In the modifier keys section of NSEvent class reference, I found by trial and error that the syntax “NSControlKeyMask = 1 << 18” equates to 2^18.

Ric

If you look up NSMouseLeftDown, you’ll see it represents 18.

“1 << 18” means set the 18th bit of the NSUInteger to 1. If no other bit in the NSUInteger is set, then the number is 2^18.

Rather than trying to work out the values, just use the enums. If it’s just one value you want to use, you can pass “current application’s NSMouseLeftDownMask”. If you want more than one, coerce them to integers then add them:

(((current application’s NSLeftMouseDownMask) as integer) + ((current application’s NSKeyDownMask) as integer)

It’s more typing, but you don’t have to worry about what the numbers mean, and it’s also much more understandable code when you look at it in two months’ time.

Here is one approach to detecting mouse (or key) events in other applications. It works roughly as imagined in the original post.

Add an Objective-C class: GlobalMonitor

GlobalMonitor.h

#import <Cocoa/Cocoa.h>

static id myNextMonitor;
@interface GlobalMonitor : NSObject 

+(id) monitorEvery: (NSEventMask) eventMask;

+(void) monitorNext: (NSEventMask) eventMask;
	
+(void) removeMonitor: (id) monitor;

@end

GlobalMonitor.m

#import "GlobalMonitor.h"
@implementation GlobalMonitor

+(id) monitorEvery: (NSEventMask) eventMask {
	id myMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
		
		NSLog(@"EveryMonitorEvent");
		[[NSNotificationCenter defaultCenter] postNotificationName:@"eventNotification" object:nil];
		
	}];	
	return myMonitor;	
}

+(void) monitorNext: (NSEventMask) eventMask {
	 myNextMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
		 
		NSLog(@"NextMonitorEvent");
		[NSEvent removeMonitor: myNextMonitor];				
		[[NSNotificationCenter defaultCenter] postNotificationName:@"eventNotification" object:nil];
		 
	}];	
}

+(void) removeMonitor: (id) monitor {	
	[NSEvent removeMonitor: monitor];	
}

@end

and use it with an AppleScript application delegate like the following:

global myMonitor

property GlobalMonitor : class "GlobalMonitor"

script Test_Global_MonitorAppDelegate
	property parent : class "NSObject"
	
	on monitorNext_(sender)
		GlobalMonitor's monitorNext_(current application's NSLeftMouseDownMask)
	end monitorNext_
	
	on monitorOn_(sender)
		set myMonitor to GlobalMonitor's monitorEvery_(current application's NSLeftMouseDownMask)
	end monitorOn_
	
	on monitorOff_(sender)
		GlobalMonitor's removeMonitor_(myMonitor)
		set myMonitor to missing value
	end monitorOff_
	
	on eventHappens_(aNotification)
		say "left mouse down"
	end eventHappens_
	
	on applicationWillFinishLaunching_(aNotification)
		-- Insert code here to initialize your application before any files are opened
		set myMonitor to missing value
		set noteCenter to current application's NSNotificationCenter's defaultCenter
		noteCenter's addObserver_selector_name_object_(me, "eventHappens:", "eventNotification", missing value)
	end applicationWillFinishLaunching_
	
	on applicationShouldTerminate_(sender)
		-- Insert code here to do any housekeeping before your application quits 
		return current application's NSTerminateNow
	end applicationShouldTerminate_
	
end script

However, if anyone has a better approach, let me know.

EricN

I’ve discovered a slightly better approach which provides for ASobjC all of the functionality of “addGlobalMonitorForEventsMatchingMask:handler:”.

As above, you add an Objective-C class, GlobalMonitor:

GlobalMonitor.h

#import <Cocoa/Cocoa.h>

static id myNextMonitor;
@interface GlobalMonitor : NSObject 

+(id) monitorEvery: (NSEventMask) eventMask performSelector: (SEL) aSelector target: (id) target;

+(void) monitorNext: (NSEventMask) eventMask performSelector: (SEL) aSelector target: (id) target;
	
+(void) removeMonitor: (id) monitor;

@end

GlobalMonitor.m

#import "GlobalMonitor.h"

@implementation GlobalMonitor

+(id) monitorEvery: (NSEventMask) eventMask performSelector: (SEL) aSelector target: (id) target {
	id myHotKey = [NSEvent addGlobalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
		NSLog(@"EveryMonitorEvent");
		[target performSelector: aSelector withObject: event];
		
	}];	
	return myHotKey;	
}

+(void) monitorNext: (NSEventMask) eventMask performSelector: (SEL) aSelector target: (id) target {
	 myNextMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
		NSLog(@"NextMonitorEvent");
		[NSEvent removeMonitor: myNextMonitor];				
		[target performSelector: aSelector withObject: event];
		 
	}];	
}

+(void) removeMonitor: (id) monitor {	
	[NSEvent removeMonitor: monitor];	
}

@end

This class has three class methods:

+(void) monitorNext: (NSEventMask) eventMask performSelector: (SEL) aSelector target: (id) target;
which listens for the next event matching the NSEventMask, performs the selector (a one argument handler) of the target (your script)

To use this in ASobjC,

GlobalMonitor's monitorNext_performSelector_target_(current application's NSLeftMouseDownMask, "eventHappens:", me)

listens for the next left mouse down and when it occurs, runs the hander: eventHappens_(theEvent).

The second method,
+(id) monitorEvery: (NSEventMask) eventMask performSelector: (SEL) aSelector target: (id) target;
works just like the last, but listens for every matching event and returns a reference to itself so that the third method,

+(void) removeMonitor: (id) monitor;
can turn it off.

For example in ASobjC,

set myMonitor to GlobalMonitor's monitorEvery_performSelector_target_(current application's NSLeftMouseDownMask, "eventHappens:", me)

turns on a monitor and

GlobalMonitor's removeMonitor_(myMonitor)

turns it off.

Finally the selector (handler) that you use has one parameter so that it can be passed a reference to the event. For example, this might be the selector:

on eventHappens_(theEvent)
		if (theEvent's type) as integer is current application's NSLeftMouseDown then
			say "left mouse down"
		else if (theEvent's type) as integer is current application's NSLeftMouseUp then
			say "left mouse up"
		end if
end eventHappens_

EricN

Thanks for posting the code – I think it could be useful to me. I tried out the methods, and it doesn’t seem that the GlobalMonitor’s removeMonitor_(myMonitor) statement stops the monitoring – I do get a message saying that myMonitor is no longer defined, but the log from the objectiveC class and the talking from the AS keep happening.

Ric

-rdelmar

I’m not sure what the problem with the removeMonitor method might be. Rather than guess, I put up a rudimentary sample project at GitHub that appears to work: https://github.com/CdLbB/NSEvent-s-addGlobalMonitor-for-AppleScriptObjC

See if works for you.

Incidentally, I made a minor change in the “monitorNext:performSelector:target:” method ” it now returns a reference to itself so it also can be removed using “removeMonitor:”.

EricN

EricN - thanks for this great example code. I’ve been trying to convert it from GlobalMonitor to LocalMonitor, but can’t get the changes to compile.

I can see they’re declared slightly differently in the NSEvent class reference, but I can’t figure out how to make these changes. Any help appreciated!

  • (id)addGlobalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(void (^)(NSEvent*))block
  • (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* (^)(NSEvent*))block

Cancel that, have got it working!