How can I get in Mac OS X "global" mouse position - I mean how can I in cocoa/cf/whatever find out cursor position even if it's outside the window, and even if my window is inactive?
I know it's somehow possible (even without admin permissions), because I've seen something like that in Java - but I want to write it in ObjC
Sorry for my English - I hope you'll understand what I mean ;)
NSPoint mouseLoc;
mouseLoc = [NSEvent mouseLocation]; //get current mouse position
NSLog(#"Mouse location: %f %f", mouseLoc.x, mouseLoc.y);
If you want it to continuously get the coordinates then make sure you have an NSTimer or something similar
Matt S. is correct that if you can use the NSEvent API to get the mouse location. However, you don't have to poll in order to continuously get coordinates. You can use a CGEventTap instead:
- (void) startEventTap {
//eventTap is an ivar on this class of type CFMachPortRef
eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, kCGEventMaskForAllEvents, myCGEventCallback, NULL);
CGEventTapEnable(eventTap, true);
}
CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
if (type == kCGEventMouseMoved) {
NSLog(#"%#", NSStringFromPoint([NSEvent mouseLocation]));
}
return event;
}
This way, your function myCGEventCallback will fire every time the mouse moves (regardless of whether your app is frontmost or not), without you having to poll for the information. Don't forget to CGEventTapEnable(eventTap, false) and CFRelease(eventTap) when you're done.
If you're not in cocoa land and an event tap is not appropriate for the situation,
CGEventRef event = CGEventCreate(nil);
CGPoint loc = CGEventGetLocation(event);
CFRelease(event);
works.
For a continuous update on the global cursor position, here is what the code looks like (Swift 4.0.3):
override func viewDidLoad()
{
super.viewDidLoad()
NSEvent.addGlobalMonitorForEvents(matching: NSEvent.EventTypeMask.mouseMoved, handler: {(mouseEvent:NSEvent) in
let position = mouseEvent.locationInWindow
})
}
Note, the variable 'position' is an NSPoint object with x and y coordinates (among many other attributes). This will not return the coordinates if your application is in focus or when you are clicking and dragging. There are separate events for all of those: simply change the NSEvent.EventTypeMask.mouseMoved to a different EventTypeMask
Matt is correct, but in order to continuously get the coordinates, I believe the other option is to use event monitor provided by NSEvent. Try addGlobalMonitorForEventsMatchingMask:handler: and addLocalMonitorForEventsMatchingMask:handler:. Check NSEvent Class Reference for more detail.
Related
I'm writing a "UIElement" app that shows a status window on the side of the screen, similar to the Dock.
Now, when a program takes over the entire screen, I need to hide my status window, just like the Dock does.
What are my options to detect this and the inverse event?
I like to avoid polling via a timed event and also cannot use undocumented tricks (such as suggested here)
What doesn't work:
Registering a Carbon Event Handler for the kEventAppSystemUIModeChanged event isn't sufficient - it works to detect VLC's full screen mode, but not for modern Cocoa apps that use the new fullscreen widget at the top right corner of their windows.
Similarly, following Apple's instructions about the NSApplication presentationOptions API by observing changes to the currentSystemPresentationOptions property does not help, either - again, it only informs about VLC's fullscreen mode, but not about apps using the window' top right fullscreen widget.
Monitoring changes to the screen configuration using CGDisplayRegisterReconfigurationCallback is not working because there aren't any callbacks for these fullscreen modes.
Based on #Chuck's suggestion, I've come up with a solution that works somewhat, but may not be foolproof.
The solution is based on the assumption that 10.7's new fullscreen mode for windows moves these windows to a new Screen Space. Therefore, we subscribe to notifications for changes to the active space. In that notification handler, we check the window list to detect whether the menubar is included. If it is not, it probably means that we're in a fullscreen space.
Checking for the presence of the "Menubar" window is the best test I could come up with based on Chuck's idea. I don't like it too much, though, because it makes assumptions on the naming and presence of internally managed windows.
Here's the test code that goes inside AppDelegate.m, which also includes the test for the other app-wide fullscreen mode:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSApplication *app = [NSApplication sharedApplication];
// Observe full screen mode from apps setting SystemUIMode
// or invoking 'setPresentationOptions'
[app addObserver:self
forKeyPath:#"currentSystemPresentationOptions"
options:NSKeyValueObservingOptionNew
context:NULL];
// Observe full screen mode from apps using a separate space
// (i.e. those providing the fullscreen widget at the right
// of their window title bar).
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification
object:NULL queue:NULL
usingBlock:^(NSNotification *note)
{
// The active space changed.
// Now we need to detect if this is a fullscreen space.
// Let's look at the windows...
NSArray *windows = CFBridgingRelease(CGWindowListCopyWindowInfo
(kCGWindowListOptionOnScreenOnly, kCGNullWindowID));
//NSLog(#"active space change: %#", windows);
// We detect full screen spaces by checking if there's a menubar
// in the window list.
// If not, we assume it's in fullscreen mode.
BOOL hasMenubar = NO;
for (NSDictionary *d in windows) {
if ([d[#"kCGWindowOwnerName"] isEqualToString:#"Window Server"]
&& [d[#"kCGWindowName"] isEqualToString:#"Menubar"]) {
hasMenubar = YES;
break;
}
}
NSLog(#"fullscreen: %#", hasMenubar ? #"No" : #"Yes");
}
];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:#"currentSystemPresentationOptions"]) {
NSLog(#"currentSystemPresentationOptions: %#", [change objectForKey:NSKeyValueChangeNewKey]); // a value of 4 indicates fullscreen mode
}
}
Since my earlier answer doesn't work for detecting full screen mode between apps, I did some experimentation. Starting with the solution that Thomas Tempelmann came up with of checking the presence of menu bar, I found a variation that I think could be more reliable.
The problem with checking for the menu bar is that in full screen mode you can move the mouse cursor to the top of the screen to make the menu bar appear, but you're still in full screen mode. I did some crawling through the CGWindow info, and discovered that when I enter full screen, there is window named "Fullscreen Backdrop" owned by the "Dock", and it's not there when not in full screen mode.
This is on Catalina (10.15.6) in an Xcode playground, so it should be tested in a real app, and on Big Sur (or whatever the current OS is, when you're reading this).
Here's the code (in Swift... easier to quickly test something)
func isFullScreen() -> Bool
{
guard let windows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) else {
return false
}
for window in windows as NSArray
{
guard let winInfo = window as? NSDictionary else { continue }
if winInfo["kCGWindowOwnerName"] as? String == "Dock",
winInfo["kCGWindowName"] as? String == "Fullscreen Backdrop"
{
return true
}
}
return false
}
EDIT NOTE: This answer unfortunately doesn't provide a solution for detecting full screen in a different app, which is what the OP was asking. I'm leaving it because it does answer the question for detecting in in the same app going full screen - for example in a generic library that needs to know to automatically update keyEquivalents and title for an explicitly added "Enter Full Screen" menu item rather than Apple's automatically added menu item.
Although this question is quite old now, I've had to detect full screen mode in Swift recently. While it's not as simple as querying some flag in NSWindow, as we would hope for, there is an easy and reliable solution that has been available since macOS 10.7.
Whenever a window is about to enter full screen mode NSWindow sends a willEnterFullScreenNotification notification, and when it is about to exit full screen mode, it sends willExitFullScreenNotification. So you add an observer for those notifications. In the following code, I use them to set a global boolean flag.
import Cocoa
/*
Since notification closures can be run concurrently, we need to guard against
races on the Boolean flag. We could use DispatchSemaphore, but it's kind
over-kill for guarding a simple read/write to a boolean variable.
os_unfair_lock is appropriate for nanosecond-level contention. If the wait
could be milliseconds or longer, DispatchSemaphore is the thing to use.
This extension is just to make using it easier and safer to use.
*/
extension os_unfair_lock
{
mutating func withLock<R>(block: () throws -> R) rethrows -> R
{
os_unfair_lock_lock(&self)
defer { os_unfair_lock_unlock(&self) }
return try block()
}
}
fileprivate var fullScreenLock = os_unfair_lock()
public fileprivate(set) var isFullScreen: Bool = false
// Call this function in the app delegate's applicationDidFinishLaunching method
func initializeFullScreenDetection()
{
_ = NotificationCenter.default.addObserver(
forName: NSWindow.willEnterFullScreenNotification,
object: nil,
queue: nil)
{ _ in
fullScreenLock.withLock { isFullScreen = true }
}
_ = NotificationCenter.default.addObserver(
forName: NSWindow.willExitFullScreenNotification,
object: nil,
queue: nil)
{ _ in
fullScreenLock.withLock { isFullScreen = false }
}
}
Since observer closures can be run concurrently, I use os_unfair_lock to guard races on the _isFullScreen property. You could use DispatchSemaphore, though it's a bit heavy weight for just guarding a Boolean flag. Back when the question was first asked, OSSpinLock would have been the equivalent, but it's been deprecated since 10.12.
Just make sure to call initializeFullScreenDetection() in your application delegate's applicationDidFinishLaunching() method.
I have the code below for a subclassed NSWindow, there is an animated view which can be scaled and I want to accept click when it is clicked at the right spot and reject (click through) if it is outside.
The code below works nice except the window does not let clicks through.
- (void)mouseDragged:(NSEvent *)theEvent {
if (allowDrag) {
NSRect screenVisibleFrame = [[NSScreen mainScreen] visibleFrame];
NSRect windowFrame = [self frame];
NSPoint newOrigin = windowFrame.origin;
// Get the mouse location in window coordinates.
NSPoint currentLocation = [theEvent locationInWindow];
// Update the origin with the difference between the new mouse location and the old mouse location.
newOrigin.x += (currentLocation.x - initialMouseLocation.x);
newOrigin.y += (currentLocation.y - initialMouseLocation.y);
if ((newOrigin.y + windowFrame.size.height) > (screenVisibleFrame.origin.y + screenVisibleFrame.size.height)) {
newOrigin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.size.height - windowFrame.size.height);
}
// Move the window to the new location
[self setFrameOrigin:newOrigin];
}
}
- (void)mouseDown:(NSEvent *)theEvent
{
screenResolution = [[NSScreen mainScreen] frame];
initialMouseLocation = [theEvent locationInWindow];
float scale = [[NSUserDefaults standardUserDefaults] floatForKey:#"widgetScale"]/100;
float pX = initialMouseLocation.x;
float pY = initialMouseLocation.y;
float fX = self.frame.size.width;
float fY = self.frame.size.height;
if (pX>(fX-fX*scale)/2 && pX<(fX+fX*scale)/2 && pY>(fY+fY*scale)/2) {
allowDrag = YES;
} else {
allowDrag = NO;
}
}
In Cocoa, you have two basic choices: 1) you can make a whole window pass clicks through with [window setIgnoresMouseEvents:YES], or 2) you can make parts of your window transparent and clicks will pass through by default.
The limitation is that the window server makes the decision about which app to deliver the event to once. After it has delivered the event to your app, there is no way to make it take the event back and deliver it to another app.
One possible solution might be to use Quartz Event Taps. The idea is that you make your window ignore mouse events, but set up an event tap that will see all events for the login session. If you want to make an event that's going through your window actually stop at your window, you process it manually and then discard it. You don't let the event continue on to the app it would otherwise reach. I expect that this would be very tricky to do right. For example, you don't want to intercept events for another app's window that is in front of yours.
If at all possible, I recommend that you use the Cocoa-supported techniques. I would think you would only want clicks to go through your window where it's transparent anyway, since otherwise how would the user know what they are clicking on?
Please invoke an transparent overlay CHILD WINDOW for accepting control and make the main window -setIgnoresMouseEvents:YES as Ken directed.
I used this tricky on my app named "Overlay".
I would like to be able to assign a key on my keyboard to be equivalent to a left mouse click.
Ideally it needs to act such that holding the key down is also equivalent to holding the left mouse button down.
I'd like this capability as a user, additionally a programmatic solution (cocoa/applescript etc) would be great too.
Not exactly what you want, but in the System preferences -> Universal access you can turn on mouse keys - and with them you can move (and click) mouse by keyboard. docs here:
http://docs.info.apple.com/article.html?path=Mac/10.6/en/cdb_moskys.html
http://docs.info.apple.com/article.html?path=Mac/10.6/en/8565.html
http://docs.info.apple.com/article.html?artnum=61472
Or,
With the "ControllerMate.app" is possible to do this, but it is commercial app.
This can be done by writing some code:
Write a global handler to receive the type of event you want to watch
[NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask
handler:^(NSEvent *event){
NSLog(#"%i", [event keyCode]);
//todo invoke mouse clicking code;
}];
Then write the mouse click code:
// get current mouse pos
CGEventRef ourEvent = CGEventCreate(NULL);
CGPoint point = CGEventGetLocation(ourEvent);
NSLog(#"Location? x= %f, y = %f", (float)point.x, (float)point.y);
// perform a click
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState);
CGEventRef theEvent = CGEventCreateMouseEvent(source, kCGEventLeftMouseDown, point, kCGMouseButtonLeft);
CGEventSetType(theEvent, kCGEventLeftMouseDown);
CGEventPost(kCGHIDEventTap, theEvent);
CFRelease(theEvent);
I have an NSButton subclass that I would like to make work with right mouse button clicks. Just overloading -rightMouseDown: won't cut it, as I would like the same kind of behaviour as for regular clicks (e.g. the button is pushed down, the user can cancel by leaving the button, the action is sent when the mouse is released, etc.).
What I have tried so far is overloading -rightMouse{Down,Up,Dragged}, changing the events to indicate the left mouse button clicks and then sending it to -mouse{Down,Up,Dragged}. Now this would clearly be a hack at best, and as it turns out Mac OS X did not like it all. I can click the button, but upon release, the button remains pushed in.
I could mimic the behaviour myself, which shouldn't be too complicated. However, I don't know how to make the button look pushed in.
Before you say "Don't! It's an unconventional Mac OS X behaviour and should be avoided": I have considered this and a right click could vastly improve the workflow. Basically the button cycles through 4 states, and I would like a right click to make it cycle in reverse. It's not an essential feature, but it would be nice. If you still feel like saying "Don't!", then let me know your thoughts. I appreciate it!
Thanks!
EDIT: This was my attempt of changing the event (you can't change the type, so I made a new one, copying all information across. I mean, I know this is the framework clearly telling me Don't Do This, but I gave it a go, as you do):
// I've contracted all three for brevity
- (void)rightMouse{Down,Up,Dragging}:(NSEvent *)theEvent {
NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouse{Down,Up,Dragging} location:[theEvent locationInWindow] modifierFlags:[theEvent modifierFlags] timestamp:[theEvent timestamp] windowNumber:[theEvent windowNumber] context:[theEvent context] eventNumber:[theEvent eventNumber] clickCount:[theEvent clickCount] pressure:[theEvent pressure]];
[self mouse{Down,Up,Dragging}:event];
}
UPDATE: I noticed that -mouseUp: was never sent to NSButton, and if I changed it to an NSControl, it was. I couldn't figure out why this was, until Francis McGrew pointed out that it contains its own event handling loop. Now, this also made sense to why before I could reroute the -rightMouseDown:, but the button wouldn't go up on release. This is because it was fetching new events on its own, that I couldn't intercept and convert from right to left mouse button events.
NSButton is entering a mouse tracking loop. To change this you will have to subclass NSButton and create your own custom tracking loop. Try this code:
- (void) rightMouseDown:(NSEvent *)theEvent
{
NSEvent *newEvent = theEvent;
BOOL mouseInBounds = NO;
while (YES)
{
mouseInBounds = NSPointInRect([newEvent locationInWindow], [self convertRect:[self frame] fromView:nil]);
[self highlight:mouseInBounds];
newEvent = [[self window] nextEventMatchingMask:NSRightMouseDraggedMask | NSRightMouseUpMask];
if (NSRightMouseUp == [newEvent type])
{
break;
}
}
if (mouseInBounds) [self performClick:nil];
}
This is how I do it; Hopefully it will work for you.
I've turned a left mouse click-and-hold into a fake right mouse down on a path control. I'm not sure this will solve all your problems, but I found that the key difference when I did this was changing the timestamp:
NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouseDown
location:[theEvent locationInWindow]
modifierFlags:[theEvent modifierFlags]
timestamp:CFAbsoluteGetTimeCurrent()
windowNumber:[theEvent windowNumber]
context:[theEvent context]
// I was surprised to find eventNumber didn't seem to need to be faked
eventNumber:[theEvent eventNumber]
clickCount:[theEvent clickCount]
pressure:[theEvent pressure]];
The other thing is that depending on your button type, its state may be the value that is making it appear pushed or not, so you might trying poking at that.
UPDATE: I think I've figured out why rightMouseUp: never gets called. Per the -[NSControl mouseDown:] docs, the button starts tracking the mouse when it gets a mouseDown event, and it doesn't stop tracking until it gets mouseUp. While it's tracking, it can't do anything else. I just tried, for example, at the end of a custom mouseDown::
[self performSelector:#selector(mouseUp:) withObject:myFakeMouseUpEvent afterDelay:1.0];
but this gets put off until a normal mouseUp: gets triggered some other way. So, if you've clicked the right mouse button, you can't (with the mouse) send a leftMouseUp, thus the button is still tracking, and won't accept a rightMouseUp event. I still don't know what the solution is, but I figured that would be useful information.
Not much to add to the answers above, but for those working in Swift, you may have trouble finding the constants for the event mask, buried deep in the documentation, and still more trouble finding a way to combine (OR) them in a way that the compiler accepts, so this may save you some time. Is there a neater way? This goes in your subclass -
var rightAction: Selector = nil
// add a new property, by analogy with action property
override func rightMouseDown(var theEvent: NSEvent!) {
var newEvent: NSEvent!
let maskUp = NSEventMask.RightMouseUpMask.rawValue
let maskDragged = NSEventMask.RightMouseDraggedMask.rawValue
let mask = Int( maskUp | maskDragged ) // cast from UInt
do {
newEvent = window!.nextEventMatchingMask(mask)
}
while newEvent.type == .RightMouseDragged
My loop has become a do..while, as it always has to execute at least once, and I never liked writing while true, and I don't need to do anything with the dragging events.
I had endless trouble getting meaningful results from convertRect(), perhaps because my controls were embedded in a table view. Thanks to Gustav Larsson above for my ending up with this for the last part -
let whereUp = newEvent.locationInWindow
let p = convertPoint(whereUp, fromView: nil)
let mouseInBounds = NSMouseInRect(p, bounds, flipped) // bounds, not frame
if mouseInBounds {
sendAction(rightAction, to: target)
// assuming rightAction and target have been allocated
}
}
I have a custom NSView (it's one of many and they all live inside an NSCollectionView — I don't think that's relevant, but who knows). When I click the view, I want it to change its selection state (and redraw itself accordingly); when I double-click the view, I want it to pop up a larger preview window for the object that was just double-clicked.
My first looked like this:
- (void)mouseUp: (NSEvent *)theEvent {
if ([theEvent clickCount] == 1) [model setIsSelected: ![model isSelected]];
else if ([theEvent clickCount] == 2) if ([model hasBeenDownloaded]) [mainWindowController showPreviewWindowForPicture:model];
}
which mostly worked fine. Except, when I double-click the view, the selection state changes and the window pops up. This is not exactly what I want.
It seems like I have two options. I can either revert the selection state when responding to a double-click (undoing the errant single-click) or I can finagle some sort of NSTimer solution to build in a delay before responding to the single click. In other words, I can make sure that a second click is not forthcoming before changing the selection state.
This seemed more elegant, so it was the approach I took at first. The only real guidance I found from Google was on an unnamed site with a hyphen in its name. This approach mostly works with one big caveat.
The outstanding question is "How long should my NSTimer wait?". The unnamed site suggests using the Carbon function GetDblTime(). Aside from being unusable in 64-bit apps, the only documentation I can find for it says that it's returning clock-ticks. And I don't know how to convert those into seconds for NSTimer.
So what's the "correct" answer here? Fumble around with GetDblTime()? "Undo" the selection on a double-click? I can't figure out the Cocoa-idiomatic approach.
Delaying the changing of the selection state is (from what I've seen) the recommended way of doing this.
It's pretty simple to implement:
- (void)mouseUp:(NSEvent *)theEvent
{
if([theEvent clickCount] == 1) {
[model performSelector:#selector(toggleSelectedState) afterDelay:[NSEvent doubleClickInterval]];
}
else if([theEvent clickCount] == 2)
{
if([model hasBeenDownloaded])
{
[NSRunLoop cancelPreviousPerformRequestsWithTarget: model];
[mainWindowController showPreviewWindowForPicture:model];
}
}
}
(Notice that in 10.6, the double click interval is accessible as a class method on NSEvent)
If your single-click and double-click operations are really separate and unrelated, you need to use a timer on the first click and wait to see if a double-click is going to happen. That is true on any platform.
But that introduces an awkward delay in your single-click operation that users typically don't like. So you don't see that approach used very often.
A better approach is to have your single-click and double-click operations be related and complementary. For example, if you single-click an icon in Finder it is selected (immediately), and if you double-click an icon it is selected and opened (immediately). That is the behavior you should aim for.
In other words, the consequences of a single-click should be related to your double-click command. That way, you can deal with the effects of the single-click in your double-click handler without having to resort to using a timer.
Personally, I think you need to ask yourself why you want this non-standard behaviour.
Can you point to any other application which treats the first click in a double-click as being different from a single-click? I can't think of any...
Add two properties to your custom view.
// CustomView.h
#interface CustomView : NSView {
#protected
id m_target;
SEL m_doubleAction;
}
#property (readwrite) id target;
#property (readwrite) SEL doubleAction;
#end
Overwrite the mouseUp: method in your custom view.
// CustomView.m
#pragma mark - MouseEvents
- (void)mouseUp:(NSEvent*)event {
if (event.clickCount == 2) {
if (m_target && m_doubleAction && [m_target respondsToSelector:m_doubleAction]) {
[m_target performSelector:m_doubleAction];
}
}
}
Register your controller as the target with an doubleAction.
// CustomController.m
- (id)init {
self = [super init];
if (self) {
// Register self for double click events.
[(CustomView*)m_myView setTarget:self];
[(CustomView*)m_myView setDoubleAction:#selector(doubleClicked:)];
}
return self;
}
Implement what should be done when a double click happens.
// CustomController.m
- (void)doubleClicked:(id)sender {
// DO SOMETHING.
}
#Dave DeLong's solution in Swift 4.2 (Xcode 10, macOS 10.13), amended for use with event.location(in: view)
var singleClickPoint: CGPoint?
override func mouseDown(with event: NSEvent) {
singleClickPoint = event.location(in: self)
perform(#selector(GameScene.singleClickAction), with: nil, afterDelay: NSEvent.doubleClickInterval)
if event.clickCount == 2 {
RunLoop.cancelPreviousPerformRequests(withTarget: self)
singleClickPoint = nil
//do whatever you want on double-click
}
}
#objc func singleClickAction(){
guard let singleClickPoint = singleClickPoint else {return}
//do whatever you want on single-click
}
The reason I'm not using singleClickAction(at point: CGPoint) and calling it with: event.location(in: self) is that any point I pass in - including CGPoint.zero - ends up arriving in the singleClick Action as (0.0, 9.223372036854776e+18). I will be filing a radar for that, but for now, bypassing perform is the way to go. (Other objects seem to work just fine, but CGPoints do not.)