ui/cocoa: capture all keys and combos when mouse is grabbed
Applications such as Gnome may use Alt-Tab and Super-Tab for different purposes, some use Ctrl-arrows so we want to allow qemu to handle everything when it captures the mouse/keyboard. However, Mac OS handles some combos like Command-Tab and Ctrl-arrows at an earlier part of the event handling chain, not letting qemu see it. We add a global Event Tap that allows qemu to see all events when the mouse is grabbed. Note that this requires additional permissions. See: https://developer.apple.com/documentation/coregraphics/1454426-cgeventtapcreate?language=objc#discussion https://support.apple.com/en-in/guide/mac-help/mh32356/mac Acked-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Gustavo Noronha Silva <gustavo@noronha.dev.br> Message-Id: <20210713213200.2547-2-gustavo@noronha.dev.br> Signed-off-by: Akihiko Odaki <akihiko.odaki@gmail.com> Message-Id: <20220306121119.45631-2-akihiko.odaki@gmail.com> Reviewed-by: Will Cohen <wwcohen@gmail.com> Signed-off-by: Philippe Mathieu-Daudé <f4bug@amsat.org>
This commit is contained in:
		
				
					committed by
					
						 Philippe Mathieu-Daudé
						Philippe Mathieu-Daudé
					
				
			
			
				
	
			
			
			
						parent
						
							69221df8cd
						
					
				
				
					commit
					f844cdb997
				
			| @@ -1270,11 +1270,17 @@ | |||||||
| #                    host without sending this key to the guest when | #                    host without sending this key to the guest when | ||||||
| #                    "off". Defaults to "on" | #                    "off". Defaults to "on" | ||||||
| # | # | ||||||
|  | # @full-grab: Capture all key presses, including system combos. This | ||||||
|  | #             requires accessibility permissions, since it performs | ||||||
|  | #             a global grab on key events. (default: off) | ||||||
|  | #             See https://support.apple.com/en-in/guide/mac-help/mh32356/mac | ||||||
|  | # | ||||||
| # Since: 7.0 | # Since: 7.0 | ||||||
| ## | ## | ||||||
| { 'struct': 'DisplayCocoa', | { 'struct': 'DisplayCocoa', | ||||||
|   'data': { |   'data': { | ||||||
|       '*left-command-key': 'bool' |       '*left-command-key': 'bool', | ||||||
|  |       '*full-grab': 'bool' | ||||||
|   } } |   } } | ||||||
|  |  | ||||||
| ## | ## | ||||||
|   | |||||||
| @@ -1916,6 +1916,9 @@ DEF("display", HAS_ARG, QEMU_OPTION_display, | |||||||
| #if defined(CONFIG_CURSES) | #if defined(CONFIG_CURSES) | ||||||
|     "-display curses[,charset=<encoding>]\n" |     "-display curses[,charset=<encoding>]\n" | ||||||
| #endif | #endif | ||||||
|  | #if defined(CONFIG_COCOA) | ||||||
|  |     "-display cocoa[,full_grab=on|off]\n" | ||||||
|  | #endif | ||||||
| #if defined(CONFIG_OPENGL) | #if defined(CONFIG_OPENGL) | ||||||
|     "-display egl-headless[,rendernode=<file>]\n" |     "-display egl-headless[,rendernode=<file>]\n" | ||||||
| #endif | #endif | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								ui/cocoa.m
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								ui/cocoa.m
									
									
									
									
									
								
							| @@ -309,11 +309,13 @@ static void handleAnyDeviceErrors(Error * err) | |||||||
|     BOOL isMouseGrabbed; |     BOOL isMouseGrabbed; | ||||||
|     BOOL isFullscreen; |     BOOL isFullscreen; | ||||||
|     BOOL isAbsoluteEnabled; |     BOOL isAbsoluteEnabled; | ||||||
|  |     CFMachPortRef eventsTap; | ||||||
| } | } | ||||||
| - (void) switchSurface:(pixman_image_t *)image; | - (void) switchSurface:(pixman_image_t *)image; | ||||||
| - (void) grabMouse; | - (void) grabMouse; | ||||||
| - (void) ungrabMouse; | - (void) ungrabMouse; | ||||||
| - (void) toggleFullScreen:(id)sender; | - (void) toggleFullScreen:(id)sender; | ||||||
|  | - (void) setFullGrab:(id)sender; | ||||||
| - (void) handleMonitorInput:(NSEvent *)event; | - (void) handleMonitorInput:(NSEvent *)event; | ||||||
| - (bool) handleEvent:(NSEvent *)event; | - (bool) handleEvent:(NSEvent *)event; | ||||||
| - (bool) handleEventLocked:(NSEvent *)event; | - (bool) handleEventLocked:(NSEvent *)event; | ||||||
| @@ -336,6 +338,19 @@ static void handleAnyDeviceErrors(Error * err) | |||||||
|  |  | ||||||
| QemuCocoaView *cocoaView; | QemuCocoaView *cocoaView; | ||||||
|  |  | ||||||
|  | static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEventRef cgEvent, void *userInfo) | ||||||
|  | { | ||||||
|  |     QemuCocoaView *cocoaView = userInfo; | ||||||
|  |     NSEvent *event = [NSEvent eventWithCGEvent:cgEvent]; | ||||||
|  |     if ([cocoaView isMouseGrabbed] && [cocoaView handleEvent:event]) { | ||||||
|  |         COCOA_DEBUG("Global events tap: qemu handled the event, capturing!\n"); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     COCOA_DEBUG("Global events tap: qemu did not handle the event, letting it through...\n"); | ||||||
|  |  | ||||||
|  |     return cgEvent; | ||||||
|  | } | ||||||
|  |  | ||||||
| @implementation QemuCocoaView | @implementation QemuCocoaView | ||||||
| - (id)initWithFrame:(NSRect)frameRect | - (id)initWithFrame:(NSRect)frameRect | ||||||
| { | { | ||||||
| @@ -361,6 +376,11 @@ QemuCocoaView *cocoaView; | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     qkbd_state_free(kbd); |     qkbd_state_free(kbd); | ||||||
|  |  | ||||||
|  |     if (eventsTap) { | ||||||
|  |         CFRelease(eventsTap); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     [super dealloc]; |     [super dealloc]; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -655,6 +675,36 @@ QemuCocoaView *cocoaView; | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | - (void) setFullGrab:(id)sender | ||||||
|  | { | ||||||
|  |     COCOA_DEBUG("QemuCocoaView: setFullGrab\n"); | ||||||
|  |  | ||||||
|  |     CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged); | ||||||
|  |     eventsTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, | ||||||
|  |                                  mask, handleTapEvent, self); | ||||||
|  |     if (!eventsTap) { | ||||||
|  |         warn_report("Could not create event tap, system key combos will not be captured.\n"); | ||||||
|  |         return; | ||||||
|  |     } else { | ||||||
|  |         COCOA_DEBUG("Global events tap created! Will capture system key combos.\n"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     CFRunLoopRef runLoop = CFRunLoopGetCurrent(); | ||||||
|  |     if (!runLoop) { | ||||||
|  |         warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n"); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     CFRunLoopSourceRef tapEventsSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventsTap, 0); | ||||||
|  |     if (!tapEventsSrc ) { | ||||||
|  |         warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n"); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     CFRunLoopAddSource(runLoop, tapEventsSrc, kCFRunLoopDefaultMode); | ||||||
|  |     CFRelease(tapEventsSrc); | ||||||
|  | } | ||||||
|  |  | ||||||
| - (void) toggleKey: (int)keycode { | - (void) toggleKey: (int)keycode { | ||||||
|     qkbd_state_key_event(kbd, keycode, !qkbd_state_key_get(kbd, keycode)); |     qkbd_state_key_event(kbd, keycode, !qkbd_state_key_get(kbd, keycode)); | ||||||
| } | } | ||||||
| @@ -1281,6 +1331,13 @@ QemuCocoaView *cocoaView; | |||||||
|     [cocoaView toggleFullScreen:sender]; |     [cocoaView toggleFullScreen:sender]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | - (void) setFullGrab:(id)sender | ||||||
|  | { | ||||||
|  |     COCOA_DEBUG("QemuCocoaAppController: setFullGrab\n"); | ||||||
|  |  | ||||||
|  |     [cocoaView setFullGrab:sender]; | ||||||
|  | } | ||||||
|  |  | ||||||
| /* Tries to find then open the specified filename */ | /* Tries to find then open the specified filename */ | ||||||
| - (void) openDocumentation: (NSString *) filename | - (void) openDocumentation: (NSString *) filename | ||||||
| { | { | ||||||
| @@ -1994,11 +2051,17 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) | |||||||
|     qemu_sem_wait(&app_started_sem); |     qemu_sem_wait(&app_started_sem); | ||||||
|     COCOA_DEBUG("cocoa_display_init: app start completed\n"); |     COCOA_DEBUG("cocoa_display_init: app start completed\n"); | ||||||
|  |  | ||||||
|  |     QemuCocoaAppController *controller = (QemuCocoaAppController *)[[NSApplication sharedApplication] delegate]; | ||||||
|     /* if fullscreen mode is to be used */ |     /* if fullscreen mode is to be used */ | ||||||
|     if (opts->has_full_screen && opts->full_screen) { |     if (opts->has_full_screen && opts->full_screen) { | ||||||
|         dispatch_async(dispatch_get_main_queue(), ^{ |         dispatch_async(dispatch_get_main_queue(), ^{ | ||||||
|             [NSApp activateIgnoringOtherApps: YES]; |             [NSApp activateIgnoringOtherApps: YES]; | ||||||
|             [(QemuCocoaAppController *)[[NSApplication sharedApplication] delegate] toggleFullScreen: nil]; |             [controller toggleFullScreen: nil]; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) { | ||||||
|  |         dispatch_async(dispatch_get_main_queue(), ^{ | ||||||
|  |             [controller setFullGrab: nil]; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user