From e28a909a191f00645d101511e5e9e132662aa290 Mon Sep 17 00:00:00 2001 From: Carwyn Ellis Date: Fri, 10 Nov 2023 16:17:29 +0000 Subject: [PATCH 01/12] ui/cocoa: add zoom-interpolation display option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides a new display option, zoom-interpolation, that enables interpolation of the scaled display when zoom-to-fit is enabled. Also provides a corresponding view menu item to allow this to be toggled as required. Signed-off-by: Carwyn Ellis Reviewed-by: Akihiko Odaki Message-ID: <20231110161729.36822-2-carwynellis@gmail.com> [PMD: QAPI @zoom-interpolation since 9.0] Signed-off-by: Philippe Mathieu-Daudé --- qapi/ui.json | 6 +++++- ui/cocoa.m | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/qapi/ui.json b/qapi/ui.json index e3999b7c07..096a2ad26f 100644 --- a/qapi/ui.json +++ b/qapi/ui.json @@ -1428,6 +1428,9 @@ # turned off the host window will be resized instead. Defaults to # "off". (Since 8.2) # +# @zoom-interpolation: Apply interpolation to smooth output when +# zoom-to-fit is enabled. Defaults to "off". (Since 9.0) +# # Since: 7.0 ## { 'struct': 'DisplayCocoa', @@ -1435,7 +1438,8 @@ '*left-command-key': 'bool', '*full-grab': 'bool', '*swap-opt-cmd': 'bool', - '*zoom-to-fit': 'bool' + '*zoom-to-fit': 'bool', + '*zoom-interpolation': 'bool' } } ## diff --git a/ui/cocoa.m b/ui/cocoa.m index eb99064bee..b7ca0ed94b 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -105,6 +105,7 @@ static int left_command_key_enabled = 1; static bool swap_opt_cmd; static bool stretch_video; +static CGInterpolationQuality zoom_interpolation = kCGInterpolationNone; static NSTextField *pauseLabel; static bool allow_events; @@ -455,7 +456,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven // get CoreGraphic context CGContextRef viewContextRef = [[NSGraphicsContext currentContext] CGContext]; - CGContextSetInterpolationQuality (viewContextRef, kCGInterpolationNone); + CGContextSetInterpolationQuality (viewContextRef, zoom_interpolation); CGContextSetShouldAntialias (viewContextRef, NO); // draw screen bitmap directly to Core Graphics context @@ -1411,6 +1412,17 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } +- (void)toggleZoomInterpolation:(id) sender +{ + if (zoom_interpolation == kCGInterpolationNone) { + zoom_interpolation = kCGInterpolationLow; + [sender setState: NSControlStateValueOn]; + } else { + zoom_interpolation = kCGInterpolationNone; + [sender setState: NSControlStateValueOff]; + } +} + /* Displays the console on the screen */ - (void)displayConsole:(id)sender { @@ -1673,6 +1685,9 @@ static void create_initial_menus(void) menuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom To Fit" action:@selector(zoomToFit:) keyEquivalent:@""] autorelease]; [menuItem setState: stretch_video ? NSControlStateValueOn : NSControlStateValueOff]; [menu addItem: menuItem]; + menuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom Interpolation" action:@selector(toggleZoomInterpolation:) keyEquivalent:@""] autorelease]; + [menuItem setState: zoom_interpolation == kCGInterpolationLow ? NSControlStateValueOn : NSControlStateValueOff]; + [menu addItem: menuItem]; menuItem = [[[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""] autorelease]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; @@ -2070,6 +2085,10 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) stretch_video = true; } + if (opts->u.cocoa.has_zoom_interpolation && opts->u.cocoa.zoom_interpolation) { + zoom_interpolation = kCGInterpolationLow; + } + create_initial_menus(); /* * Create the menu entries which depend on QEMU state (for consoles From f5af80271aad356233b2bea2369b3b2211fa395d Mon Sep 17 00:00:00 2001 From: David Parsons Date: Sat, 24 Feb 2024 14:06:20 +0000 Subject: [PATCH 02/12] ui/cocoa: Fix window clipping on macOS 14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS Sonoma changes the NSView.clipsToBounds to false by default where it was true in earlier version of macOS. This causes the window contents to be occluded by the frame at the top of the window. This fixes the issue by conditionally compiling the clipping on Sonoma to true. NSView only exposes the clipToBounds in macOS 14 and so has to be fixed via conditional compilation. Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1994 Signed-off-by: David Parsons Reviewed-by: Akihiko Odaki Message-ID: <20240224140620.39200-1-dave@daveparsons.net> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/cocoa.m b/ui/cocoa.m index b7ca0ed94b..5618d294c4 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -54,6 +54,10 @@ #define MAC_OS_X_VERSION_10_13 101300 #endif +#ifndef MAC_OS_VERSION_14_0 +#define MAC_OS_VERSION_14_0 140000 +#endif + /* 10.14 deprecates NSOnState and NSOffState in favor of * NSControlStateValueOn/Off, which were introduced in 10.13. * Define for older versions @@ -366,6 +370,9 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven screen.width = frameRect.size.width; screen.height = frameRect.size.height; kbd = qkbd_state_init(dcl.con); +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_14_0 + [self setClipsToBounds:YES]; +#endif } return self; From af4efbccda6729e2986ae225718bd60657d00592 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:32 +0900 Subject: [PATCH 03/12] ui/cocoa: Split [-QemuCocoaView handleEventLocked:] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently [-QemuCocoaView handleEventLocked:] parses the passed event, stores operations to be done to variables, and perform them according to the variables. This construct will be cluttered with variables and hard to read when we need more different operations for different events. Split the methods so that we can call appropriate methods depending on events instead of relying on variables. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-1-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 86 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index 5618d294c4..aecd60df2a 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -1102,45 +1102,61 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } if (mouse_event) { - /* Don't send button events to the guest unless we've got a - * mouse grab or window focus. If we have neither then this event - * is the user clicking on the background window to activate and - * bring us to the front, which will be done by the sendEvent - * call below. We definitely don't want to pass that click through - * to the guest. - */ - if ((isMouseGrabbed || [[self window] isKeyWindow]) && - (last_buttons != buttons)) { - static uint32_t bmap[INPUT_BUTTON__MAX] = { - [INPUT_BUTTON_LEFT] = MOUSE_EVENT_LBUTTON, - [INPUT_BUTTON_MIDDLE] = MOUSE_EVENT_MBUTTON, - [INPUT_BUTTON_RIGHT] = MOUSE_EVENT_RBUTTON - }; - qemu_input_update_buttons(dcl.con, bmap, last_buttons, buttons); - last_buttons = buttons; - } - if (isMouseGrabbed) { - if (isAbsoluteEnabled) { - /* Note that the origin for Cocoa mouse coords is bottom left, not top left. - * The check on screenContainsPoint is to avoid sending out of range values for - * clicks in the titlebar. - */ - if ([self screenContainsPoint:p]) { - qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x, 0, screen.width); - qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y, 0, screen.height); - } - } else { - qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, (int)[event deltaX]); - qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, (int)[event deltaY]); - } - } else { - return false; - } - qemu_input_event_sync(); + return [self handleMouseEvent:event buttons:buttons]; } return true; } +- (bool) handleMouseEvent:(NSEvent *)event buttons:(uint32_t)buttons +{ + /* Don't send button events to the guest unless we've got a + * mouse grab or window focus. If we have neither then this event + * is the user clicking on the background window to activate and + * bring us to the front, which will be done by the sendEvent + * call below. We definitely don't want to pass that click through + * to the guest. + */ + if ((isMouseGrabbed || [[self window] isKeyWindow]) && + (last_buttons != buttons)) { + static uint32_t bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = MOUSE_EVENT_LBUTTON, + [INPUT_BUTTON_MIDDLE] = MOUSE_EVENT_MBUTTON, + [INPUT_BUTTON_RIGHT] = MOUSE_EVENT_RBUTTON + }; + qemu_input_update_buttons(dcl.con, bmap, last_buttons, buttons); + last_buttons = buttons; + } + + return [self handleMouseEvent:event]; +} + +- (bool) handleMouseEvent:(NSEvent *)event +{ + if (!isMouseGrabbed) { + return false; + } + + if (isAbsoluteEnabled) { + NSPoint p = [self screenLocationOfEvent:event]; + + /* Note that the origin for Cocoa mouse coords is bottom left, not top left. + * The check on screenContainsPoint is to avoid sending out of range values for + * clicks in the titlebar. + */ + if ([self screenContainsPoint:p]) { + qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x, 0, screen.width); + qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y, 0, screen.height); + } + } else { + qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, (int)[event deltaX]); + qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, (int)[event deltaY]); + } + + qemu_input_event_sync(); + + return true; +} + - (void) grabMouse { COCOA_DEBUG("QemuCocoaView: grabMouse\n"); From 0f7be47abe86aaf5be791373434bdcf013a1f2ee Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:33 +0900 Subject: [PATCH 04/12] ui/cocoa: Immediately call [-QemuCocoaView handleMouseEvent:buttons:] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of using mouse_event variable to tell to handle a mouse event later, immediately call [-QemuCocoaView handleMouseEvent:buttons:]. Signed-off-by: Akihiko Odaki Reviewed-by: Peter Maydell Tested-by: Rene Engel Message-ID: <20240224-cocoa-v12-2-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 87 +++++++++++++++++++----------------------------------- 1 file changed, 30 insertions(+), 57 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index aecd60df2a..ff6486093c 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -807,9 +807,8 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven { /* Return true if we handled the event, false if it should be given to OSX */ COCOA_DEBUG("QemuCocoaView: handleEvent\n"); - int buttons = 0; + InputButton button; int keycode = 0; - bool mouse_event = false; // Location of event in virtual screen coordinates NSPoint p = [self screenLocationOfEvent:event]; NSUInteger modifiers = [event modifierFlags]; @@ -955,7 +954,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } break; } - break; + return true; case NSEventTypeKeyDown: keycode = cocoa_keycode_to_qemu([event keyCode]); @@ -991,7 +990,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } else { [self handleMonitorInput: event]; } - break; + return true; case NSEventTypeKeyUp: keycode = cocoa_keycode_to_qemu([event keyCode]); @@ -1004,7 +1003,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven if (qemu_console_is_graphic(NULL)) { qkbd_state_key_event(kbd, keycode, false); } - break; + return true; case NSEventTypeMouseMoved: if (isAbsoluteEnabled) { // Cursor re-entered into a window might generate events bound to screen coordinates @@ -1020,34 +1019,20 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } } - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:0]; case NSEventTypeLeftMouseDown: - buttons |= MOUSE_EVENT_LBUTTON; - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:MOUSE_EVENT_LBUTTON]; case NSEventTypeRightMouseDown: - buttons |= MOUSE_EVENT_RBUTTON; - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:MOUSE_EVENT_RBUTTON]; case NSEventTypeOtherMouseDown: - buttons |= MOUSE_EVENT_MBUTTON; - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:MOUSE_EVENT_MBUTTON]; case NSEventTypeLeftMouseDragged: - buttons |= MOUSE_EVENT_LBUTTON; - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:MOUSE_EVENT_LBUTTON]; case NSEventTypeRightMouseDragged: - buttons |= MOUSE_EVENT_RBUTTON; - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:MOUSE_EVENT_RBUTTON]; case NSEventTypeOtherMouseDragged: - buttons |= MOUSE_EVENT_MBUTTON; - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:MOUSE_EVENT_MBUTTON]; case NSEventTypeLeftMouseUp: - mouse_event = true; if (!isMouseGrabbed && [self screenContainsPoint:p]) { /* * In fullscreen mode, the window of cocoaView may not be the @@ -1058,53 +1043,41 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven [self grabMouse]; } } - break; + return [self handleMouseEvent:event buttons:0]; case NSEventTypeRightMouseUp: - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:0]; case NSEventTypeOtherMouseUp: - mouse_event = true; - break; + return [self handleMouseEvent:event buttons:0]; case NSEventTypeScrollWheel: /* * Send wheel events to the guest regardless of window focus. * This is in-line with standard Mac OS X UI behaviour. */ - /* - * We shouldn't have got a scroll event when deltaY and delta Y - * are zero, hence no harm in dropping the event - */ - if ([event deltaY] != 0 || [event deltaX] != 0) { /* Determine if this is a scroll up or scroll down event */ - if ([event deltaY] != 0) { - buttons = ([event deltaY] > 0) ? + if ([event deltaY] != 0) { + button = ([event deltaY] > 0) ? INPUT_BUTTON_WHEEL_UP : INPUT_BUTTON_WHEEL_DOWN; - } else if ([event deltaX] != 0) { - buttons = ([event deltaX] > 0) ? + } else if ([event deltaX] != 0) { + button = ([event deltaX] > 0) ? INPUT_BUTTON_WHEEL_LEFT : INPUT_BUTTON_WHEEL_RIGHT; - } - - qemu_input_queue_btn(dcl.con, buttons, true); - qemu_input_event_sync(); - qemu_input_queue_btn(dcl.con, buttons, false); - qemu_input_event_sync(); + } else { + /* + * We shouldn't have got a scroll event when deltaY and delta Y + * are zero, hence no harm in dropping the event + */ + return true; } - /* - * Since deltaX/deltaY also report scroll wheel events we prevent mouse - * movement code from executing. - */ - mouse_event = false; - break; + qemu_input_queue_btn(dcl.con, button, true); + qemu_input_event_sync(); + qemu_input_queue_btn(dcl.con, button, false); + qemu_input_event_sync(); + + return true; default: return false; } - - if (mouse_event) { - return [self handleMouseEvent:event buttons:buttons]; - } - return true; } - (bool) handleMouseEvent:(NSEvent *)event buttons:(uint32_t)buttons From f4de9688d4fa3d4371cedc74a48362069b9ecd0f Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:34 +0900 Subject: [PATCH 05/12] ui/cocoa: Release specific mouse buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ui/cocoa used to release all mouse buttons when it sees NSEventTypeLeftMouseUp, NSEventTypeRightMouseUp, or NSEventTypeOtherMouseUp, but it can instead release specific one according to the delivered event. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-3-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index ff6486093c..e156527082 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -103,7 +103,6 @@ static const DisplayChangeListenerOps dcl_ops = { static DisplayChangeListener dcl = { .ops = &dcl_ops, }; -static int last_buttons; static int cursor_hide = 1; static int left_command_key_enabled = 1; static bool swap_opt_cmd; @@ -1019,19 +1018,19 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } } - return [self handleMouseEvent:event buttons:0]; + return [self handleMouseEvent:event]; case NSEventTypeLeftMouseDown: - return [self handleMouseEvent:event buttons:MOUSE_EVENT_LBUTTON]; + return [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:true]; case NSEventTypeRightMouseDown: - return [self handleMouseEvent:event buttons:MOUSE_EVENT_RBUTTON]; + return [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:true]; case NSEventTypeOtherMouseDown: - return [self handleMouseEvent:event buttons:MOUSE_EVENT_MBUTTON]; + return [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:true]; case NSEventTypeLeftMouseDragged: - return [self handleMouseEvent:event buttons:MOUSE_EVENT_LBUTTON]; + return [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:true]; case NSEventTypeRightMouseDragged: - return [self handleMouseEvent:event buttons:MOUSE_EVENT_RBUTTON]; + return [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:true]; case NSEventTypeOtherMouseDragged: - return [self handleMouseEvent:event buttons:MOUSE_EVENT_MBUTTON]; + return [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:true]; case NSEventTypeLeftMouseUp: if (!isMouseGrabbed && [self screenContainsPoint:p]) { /* @@ -1043,11 +1042,11 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven [self grabMouse]; } } - return [self handleMouseEvent:event buttons:0]; + return [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:false]; case NSEventTypeRightMouseUp: - return [self handleMouseEvent:event buttons:0]; + return [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:false]; case NSEventTypeOtherMouseUp: - return [self handleMouseEvent:event buttons:0]; + return [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:false]; case NSEventTypeScrollWheel: /* * Send wheel events to the guest regardless of window focus. @@ -1080,7 +1079,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } -- (bool) handleMouseEvent:(NSEvent *)event buttons:(uint32_t)buttons +- (bool) handleMouseEvent:(NSEvent *)event button:(InputButton)button down:(bool)down { /* Don't send button events to the guest unless we've got a * mouse grab or window focus. If we have neither then this event @@ -1089,17 +1088,12 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven * call below. We definitely don't want to pass that click through * to the guest. */ - if ((isMouseGrabbed || [[self window] isKeyWindow]) && - (last_buttons != buttons)) { - static uint32_t bmap[INPUT_BUTTON__MAX] = { - [INPUT_BUTTON_LEFT] = MOUSE_EVENT_LBUTTON, - [INPUT_BUTTON_MIDDLE] = MOUSE_EVENT_MBUTTON, - [INPUT_BUTTON_RIGHT] = MOUSE_EVENT_RBUTTON - }; - qemu_input_update_buttons(dcl.con, bmap, last_buttons, buttons); - last_buttons = buttons; + if (!isMouseGrabbed && ![[self window] isKeyWindow]) { + return false; } + qemu_input_queue_btn(dcl.con, button, down); + return [self handleMouseEvent:event]; } From fcb03de7e19306619c424f3cce5689b3b43f2043 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:35 +0900 Subject: [PATCH 06/12] ui/cocoa: Scale with NSView instead of Core Graphics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core Graphics is not accelerated and slow. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-4-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index e156527082..6e8cd24e88 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -504,10 +504,8 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven [self getRectsBeingDrawn:&rectList count:&rectCount]; for (i = 0; i < rectCount; i++) { - clipRect.origin.x = rectList[i].origin.x / cdx; - clipRect.origin.y = (float)h - (rectList[i].origin.y + rectList[i].size.height) / cdy; - clipRect.size.width = rectList[i].size.width / cdx; - clipRect.size.height = rectList[i].size.height / cdy; + clipRect = rectList[i]; + clipRect.origin.y = (float)h - (clipRect.origin.y + clipRect.size.height); clipImageRef = CGImageCreateWithImageInRect( imageRef, clipRect @@ -553,6 +551,11 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } +- (void) updateBounds +{ + [self setBoundsSize:NSMakeSize(screen.width, screen.height)]; +} + - (void) updateUIInfoLocked { /* Must be called with the BQL, i.e. via updateUIInfo */ @@ -642,6 +645,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven screen.height = h; [self setContentDimensions]; [self setFrame:NSMakeRect(cx, cy, cw, ch)]; + [self updateBounds]; } // update screenBuffer @@ -1305,6 +1309,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven - (void)windowDidResize:(NSNotification *)notification { + [cocoaView updateBounds]; [cocoaView updateUIInfo]; } @@ -1967,16 +1972,7 @@ static void cocoa_update(DisplayChangeListener *dcl, COCOA_DEBUG("qemu_cocoa: cocoa_update\n"); dispatch_async(dispatch_get_main_queue(), ^{ - NSRect rect; - if ([cocoaView cdx] == 1.0) { - rect = NSMakeRect(x, [cocoaView gscreen].height - y - h, w, h); - } else { - rect = NSMakeRect( - x * [cocoaView cdx], - ([cocoaView gscreen].height - y - h) * [cocoaView cdy], - w * [cocoaView cdx], - h * [cocoaView cdy]); - } + NSRect rect = NSMakeRect(x, [cocoaView gscreen].height - y - h, w, h); [cocoaView setNeedsDisplayInRect:rect]; }); } From 1a4b64a5f5a7dd96fcc84aedb157700d9da8c54d Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:36 +0900 Subject: [PATCH 07/12] ui/cocoa: Fix pause label coordinates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A subview is positioned in the superview so the superview's frame should be used instead of one of the window to determine the coordinates. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-5-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index 6e8cd24e88..a2e52ceabc 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -1451,8 +1451,8 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven { /* Coordinates have to be calculated each time because the window can change its size */ int xCoord, yCoord, width, height; - xCoord = ([normalWindow frame].size.width - [pauseLabel frame].size.width)/2; - yCoord = [normalWindow frame].size.height - [pauseLabel frame].size.height - ([pauseLabel frame].size.height * .5); + xCoord = ([cocoaView frame].size.width - [pauseLabel frame].size.width)/2; + yCoord = [cocoaView frame].size.height - [pauseLabel frame].size.height - ([pauseLabel frame].size.height * .5); width = [pauseLabel frame].size.width; height = [pauseLabel frame].size.height; [pauseLabel setFrame: NSMakeRect(xCoord, yCoord, width, height)]; From 91aa508d0274deec8b38ac7aeaa7a33f7ffea74a Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:37 +0900 Subject: [PATCH 08/12] ui/cocoa: Let the platform toggle fullscreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It allows making the window full screen by clicking full screen button provided by the platform (the left-top green button) and save some code. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-6-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 420 ++++++++++++++++++++++++----------------------------- 1 file changed, 187 insertions(+), 233 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index a2e52ceabc..20d73098e2 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -308,20 +308,17 @@ static void handleAnyDeviceErrors(Error * err) */ @interface QemuCocoaView : NSView { + NSTrackingArea *trackingArea; QEMUScreen screen; - NSWindow *fullScreenWindow; - float cx,cy,cw,ch,cdx,cdy; pixman_image_t *pixman_image; QKbdState *kbd; BOOL isMouseGrabbed; - BOOL isFullscreen; BOOL isAbsoluteEnabled; CFMachPortRef eventsTap; } - (void) switchSurface:(pixman_image_t *)image; - (void) grabMouse; - (void) ungrabMouse; -- (void) toggleFullScreen:(id)sender; - (void) setFullGrab:(id)sender; - (void) handleMonitorInput:(NSEvent *)event; - (bool) handleEvent:(NSEvent *)event; @@ -337,8 +334,6 @@ static void handleAnyDeviceErrors(Error * err) */ - (BOOL) isMouseGrabbed; - (BOOL) isAbsoluteEnabled; -- (float) cdx; -- (float) cdy; - (QEMUScreen) gscreen; - (void) raiseAllKeys; @end @@ -399,46 +394,43 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven return YES; } -- (BOOL) screenContainsPoint:(NSPoint) p +- (void) removeTrackingRect { - return (p.x > -1 && p.x < screen.width && p.y > -1 && p.y < screen.height); + if (trackingArea) { + [self removeTrackingArea:trackingArea]; + [trackingArea release]; + trackingArea = nil; + } } -/* Get location of event and convert to virtual screen coordinate */ -- (CGPoint) screenLocationOfEvent:(NSEvent *)ev +- (void) frameUpdated { - NSWindow *eventWindow = [ev window]; - // XXX: Use CGRect and -convertRectFromScreen: to support macOS 10.10 - CGRect r = CGRectZero; - r.origin = [ev locationInWindow]; - if (!eventWindow) { - if (!isFullscreen) { - return [[self window] convertRectFromScreen:r].origin; - } else { - CGPoint locationInSelfWindow = [[self window] convertRectFromScreen:r].origin; - CGPoint loc = [self convertPoint:locationInSelfWindow fromView:nil]; - if (stretch_video) { - loc.x /= cdx; - loc.y /= cdy; - } - return loc; - } - } else if ([[self window] isEqual:eventWindow]) { - if (!isFullscreen) { - return r.origin; - } else { - CGPoint loc = [self convertPoint:r.origin fromView:nil]; - if (stretch_video) { - loc.x /= cdx; - loc.y /= cdy; - } - return loc; - } - } else { - return [[self window] convertRectFromScreen:[eventWindow convertRectToScreen:r]].origin; + [self removeTrackingRect]; + + if ([self window]) { + NSTrackingAreaOptions options = NSTrackingActiveInKeyWindow | + NSTrackingMouseEnteredAndExited | + NSTrackingMouseMoved; + trackingArea = [[NSTrackingArea alloc] initWithRect:[self frame] + options:options + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; + [self updateUIInfo]; } } +- (void) viewDidMoveToWindow +{ + [self resizeWindow]; + [self frameUpdated]; +} + +- (void) viewWillMoveToWindow:(NSWindow *)newWindow +{ + [self removeTrackingRect]; +} + - (void) hideCursor { if (!cursor_hide) { @@ -518,36 +510,25 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } -- (void) setContentDimensions +- (NSSize) screenSafeAreaSize { - COCOA_DEBUG("QemuCocoaView: setContentDimensions\n"); + NSSize size = [[[self window] screen] frame].size; + NSEdgeInsets insets = [[[self window] screen] safeAreaInsets]; + size.width -= insets.left + insets.right; + size.height -= insets.top + insets.bottom; + return size; +} - if (isFullscreen) { - cdx = [[NSScreen mainScreen] frame].size.width / (float)screen.width; - cdy = [[NSScreen mainScreen] frame].size.height / (float)screen.height; +- (void) resizeWindow +{ + [[self window] setContentAspectRatio:NSMakeSize(screen.width, screen.height)]; - /* stretches video, but keeps same aspect ratio */ - if (stretch_video == true) { - /* use smallest stretch value - prevents clipping on sides */ - if (MIN(cdx, cdy) == cdx) { - cdy = cdx; - } else { - cdx = cdy; - } - } else { /* No stretching */ - cdx = cdy = 1; - } - cw = screen.width * cdx; - ch = screen.height * cdy; - cx = ([[NSScreen mainScreen] frame].size.width - cw) / 2.0; - cy = ([[NSScreen mainScreen] frame].size.height - ch) / 2.0; - } else { - cx = 0; - cy = 0; - cw = screen.width; - ch = screen.height; - cdx = 1.0; - cdy = 1.0; + if (!stretch_video) { + [[self window] setContentSize:NSMakeSize(screen.width, screen.height)]; + [[self window] center]; + } else if ([[self window] styleMask] & NSWindowStyleMaskFullScreen) { + [[self window] setContentSize:[self screenSafeAreaSize]]; + [[self window] center]; } } @@ -571,9 +552,10 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven CGDirectDisplayID display = [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]; NSSize screenSize = [[[self window] screen] frame].size; CGSize screenPhysicalSize = CGDisplayScreenSize(display); + bool isFullscreen = ([[self window] styleMask] & NSWindowStyleMaskFullScreen) != 0; CVDisplayLinkRef displayLink; - frameSize = isFullscreen ? screenSize : [self frame].size; + frameSize = isFullscreen ? [self screenSafeAreaSize] : [self frame].size; if (!CVDisplayLinkCreateWithCGDisplay(display, &displayLink)) { CVTime period = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLink); @@ -620,31 +602,19 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven }); } -- (void)viewDidMoveToWindow -{ - [self updateUIInfo]; -} - - (void) switchSurface:(pixman_image_t *)image { COCOA_DEBUG("QemuCocoaView: switchSurface\n"); int w = pixman_image_get_width(image); int h = pixman_image_get_height(image); - /* cdx == 0 means this is our very first surface, in which case we need - * to recalculate the content dimensions even if it happens to be the size - * of the initial empty window. - */ - bool isResize = (w != screen.width || h != screen.height || cdx == 0.0); - int oldh = screen.height; - if (isResize) { + if (w != screen.width || h != screen.height) { // Resize before we trigger the redraw, or we'll redraw at the wrong size COCOA_DEBUG("switchSurface: new size %d x %d\n", w, h); screen.width = w; screen.height = h; - [self setContentDimensions]; - [self setFrame:NSMakeRect(cx, cy, cw, ch)]; + [self resizeWindow]; [self updateBounds]; } @@ -654,51 +624,6 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } pixman_image = image; - - // update windows - if (isFullscreen) { - [[fullScreenWindow contentView] setFrame:[[NSScreen mainScreen] frame]]; - [normalWindow setFrame:NSMakeRect([normalWindow frame].origin.x, [normalWindow frame].origin.y - h + oldh, w, h + [normalWindow frame].size.height - oldh) display:NO animate:NO]; - } else { - if (qemu_name) - [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; - [normalWindow setFrame:NSMakeRect([normalWindow frame].origin.x, [normalWindow frame].origin.y - h + oldh, w, h + [normalWindow frame].size.height - oldh) display:YES animate:NO]; - } - - if (isResize) { - [normalWindow center]; - } -} - -- (void) toggleFullScreen:(id)sender -{ - COCOA_DEBUG("QemuCocoaView: toggleFullScreen\n"); - - if (isFullscreen) { // switch from fullscreen to desktop - isFullscreen = FALSE; - [self ungrabMouse]; - [self setContentDimensions]; - [fullScreenWindow close]; - [normalWindow setContentView: self]; - [normalWindow makeKeyAndOrderFront: self]; - [NSMenu setMenuBarVisible:YES]; - } else { // switch from desktop to fullscreen - isFullscreen = TRUE; - [normalWindow orderOut: nil]; /* Hide the window */ - [self grabMouse]; - [self setContentDimensions]; - [NSMenu setMenuBarVisible:NO]; - fullScreenWindow = [[NSWindow alloc] initWithContentRect:[[NSScreen mainScreen] frame] - styleMask:NSWindowStyleMaskBorderless - backing:NSBackingStoreBuffered - defer:NO]; - [fullScreenWindow setAcceptsMouseMovedEvents: YES]; - [fullScreenWindow setHasShadow:NO]; - [fullScreenWindow setBackgroundColor: [NSColor blackColor]]; - [self setFrame:NSMakeRect(cx, cy, cw, ch)]; - [[fullScreenWindow contentView] addSubview: self]; - [fullScreenWindow makeKeyAndOrderFront:self]; - } } - (void) setFullGrab:(id)sender @@ -812,8 +737,6 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven COCOA_DEBUG("QemuCocoaView: handleEvent\n"); InputButton button; int keycode = 0; - // Location of event in virtual screen coordinates - NSPoint p = [self screenLocationOfEvent:event]; NSUInteger modifiers = [event modifierFlags]; /* @@ -1007,50 +930,6 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven qkbd_state_key_event(kbd, keycode, false); } return true; - case NSEventTypeMouseMoved: - if (isAbsoluteEnabled) { - // Cursor re-entered into a window might generate events bound to screen coordinates - // and `nil` window property, and in full screen mode, current window might not be - // key window, where event location alone should suffice. - if (![self screenContainsPoint:p] || !([[self window] isKeyWindow] || isFullscreen)) { - if (isMouseGrabbed) { - [self ungrabMouse]; - } - } else { - if (!isMouseGrabbed) { - [self grabMouse]; - } - } - } - return [self handleMouseEvent:event]; - case NSEventTypeLeftMouseDown: - return [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:true]; - case NSEventTypeRightMouseDown: - return [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:true]; - case NSEventTypeOtherMouseDown: - return [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:true]; - case NSEventTypeLeftMouseDragged: - return [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:true]; - case NSEventTypeRightMouseDragged: - return [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:true]; - case NSEventTypeOtherMouseDragged: - return [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:true]; - case NSEventTypeLeftMouseUp: - if (!isMouseGrabbed && [self screenContainsPoint:p]) { - /* - * In fullscreen mode, the window of cocoaView may not be the - * key window, therefore the position relative to the virtual - * screen alone will be sufficient. - */ - if(isFullscreen || [[self window] isKeyWindow]) { - [self grabMouse]; - } - } - return [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:false]; - case NSEventTypeRightMouseUp: - return [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:false]; - case NSEventTypeOtherMouseUp: - return [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:false]; case NSEventTypeScrollWheel: /* * Send wheel events to the guest regardless of window focus. @@ -1083,61 +962,118 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } } -- (bool) handleMouseEvent:(NSEvent *)event button:(InputButton)button down:(bool)down -{ - /* Don't send button events to the guest unless we've got a - * mouse grab or window focus. If we have neither then this event - * is the user clicking on the background window to activate and - * bring us to the front, which will be done by the sendEvent - * call below. We definitely don't want to pass that click through - * to the guest. - */ - if (!isMouseGrabbed && ![[self window] isKeyWindow]) { - return false; - } - - qemu_input_queue_btn(dcl.con, button, down); - - return [self handleMouseEvent:event]; -} - -- (bool) handleMouseEvent:(NSEvent *)event +- (void) handleMouseEvent:(NSEvent *)event button:(InputButton)button down:(bool)down { if (!isMouseGrabbed) { - return false; + return; } - if (isAbsoluteEnabled) { - NSPoint p = [self screenLocationOfEvent:event]; + with_bql(^{ + qemu_input_queue_btn(dcl.con, button, down); + }); - /* Note that the origin for Cocoa mouse coords is bottom left, not top left. - * The check on screenContainsPoint is to avoid sending out of range values for - * clicks in the titlebar. - */ - if ([self screenContainsPoint:p]) { - qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x, 0, screen.width); - qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y, 0, screen.height); + [self handleMouseEvent:event]; +} + +- (void) handleMouseEvent:(NSEvent *)event +{ + if (!isMouseGrabbed) { + return; + } + + with_bql(^{ + if (isAbsoluteEnabled) { + CGFloat d = (CGFloat)screen.height / [self frame].size.height; + NSPoint p = [event locationInWindow]; + + /* Note that the origin for Cocoa mouse coords is bottom left, not top left. */ + qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x * d, 0, screen.width); + qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y * d, 0, screen.height); + } else { + qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, [event deltaX]); + qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, [event deltaY]); } - } else { - qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, (int)[event deltaX]); - qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, (int)[event deltaY]); + + qemu_input_event_sync(); + }); +} + +- (void) mouseExited:(NSEvent *)event +{ + if (isAbsoluteEnabled && isMouseGrabbed) { + [self ungrabMouse]; + } +} + +- (void) mouseEntered:(NSEvent *)event +{ + if (isAbsoluteEnabled && !isMouseGrabbed) { + [self grabMouse]; + } +} + +- (void) mouseMoved:(NSEvent *)event +{ + [self handleMouseEvent:event]; +} + +- (void) mouseDown:(NSEvent *)event +{ + [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:true]; +} + +- (void) rightMouseDown:(NSEvent *)event +{ + [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:true]; +} + +- (void) otherMouseDown:(NSEvent *)event +{ + [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:true]; +} + +- (void) mouseDragged:(NSEvent *)event +{ + [self handleMouseEvent:event]; +} + +- (void) rightMouseDragged:(NSEvent *)event +{ + [self handleMouseEvent:event]; +} + +- (void) otherMouseDragged:(NSEvent *)event +{ + [self handleMouseEvent:event]; +} + +- (void) mouseUp:(NSEvent *)event +{ + if (!isMouseGrabbed) { + [self grabMouse]; } - qemu_input_event_sync(); + [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:false]; +} - return true; +- (void) rightMouseUp:(NSEvent *)event +{ + [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:false]; +} + +- (void) otherMouseUp:(NSEvent *)event +{ + [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:false]; } - (void) grabMouse { COCOA_DEBUG("QemuCocoaView: grabMouse\n"); - if (!isFullscreen) { - if (qemu_name) - [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)", qemu_name]]; - else - [normalWindow setTitle:@"QEMU - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)"]; - } + if (qemu_name) + [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)", qemu_name]]; + else + [normalWindow setTitle:@"QEMU - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)"]; [self hideCursor]; CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); isMouseGrabbed = TRUE; // while isMouseGrabbed = TRUE, QemuCocoaApp sends all events to [cocoaView handleEvent:] @@ -1147,15 +1083,14 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven { COCOA_DEBUG("QemuCocoaView: ungrabMouse\n"); - if (!isFullscreen) { - if (qemu_name) - [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; - else - [normalWindow setTitle:@"QEMU"]; - } + if (qemu_name) + [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; + else + [normalWindow setTitle:@"QEMU"]; [self unhideCursor]; CGAssociateMouseAndMouseCursorPosition(TRUE); isMouseGrabbed = FALSE; + [self raiseAllButtons]; } - (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled { @@ -1166,8 +1101,6 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } - (BOOL) isMouseGrabbed {return isMouseGrabbed;} - (BOOL) isAbsoluteEnabled {return isAbsoluteEnabled;} -- (float) cdx {return cdx;} -- (float) cdy {return cdy;} - (QEMUScreen) gscreen {return screen;} /* @@ -1181,6 +1114,15 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven qkbd_state_lift_all_keys(kbd); }); } + +- (void) raiseAllButtons +{ + with_bql(^{ + qemu_input_queue_btn(dcl.con, INPUT_BUTTON_LEFT, false); + qemu_input_queue_btn(dcl.con, INPUT_BUTTON_RIGHT, false); + qemu_input_queue_btn(dcl.con, INPUT_BUTTON_MIDDLE, false); + }); +} @end @@ -1195,7 +1137,6 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven { } - (void)doToggleFullScreen:(id)sender; -- (void)toggleFullScreen:(id)sender; - (void)showQEMUDoc:(id)sender; - (void)zoomToFit:(id) sender; - (void)displayConsole:(id)sender; @@ -1237,7 +1178,8 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven exit(1); } [normalWindow setAcceptsMouseMovedEvents:YES]; - [normalWindow setTitle:@"QEMU"]; + [normalWindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + [normalWindow setTitle:qemu_name ? [NSString stringWithFormat:@"QEMU %s", qemu_name] : @"QEMU"]; [normalWindow setContentView:cocoaView]; [normalWindow makeKeyAndOrderFront:self]; [normalWindow center]; @@ -1307,10 +1249,21 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven [cocoaView updateUIInfo]; } +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + [cocoaView grabMouse]; +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + [cocoaView resizeWindow]; + [cocoaView ungrabMouse]; +} + - (void)windowDidResize:(NSNotification *)notification { [cocoaView updateBounds]; - [cocoaView updateUIInfo]; + [cocoaView frameUpdated]; } /* Called when the user clicks on a window's close button */ @@ -1326,6 +1279,14 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven return NO; } +- (NSApplicationPresentationOptions) window:(NSWindow *)window + willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions; + +{ + return (proposedOptions & ~(NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar)) | + NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar; +} + /* * Called when QEMU goes into the background. Note that * [-NSWindowDelegate windowDidResignKey:] is used here instead of @@ -1345,14 +1306,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven */ - (void) doToggleFullScreen:(id)sender { - [self toggleFullScreen:(id)sender]; -} - -- (void)toggleFullScreen:(id)sender -{ - COCOA_DEBUG("QemuCocoaAppController: toggleFullScreen\n"); - - [cocoaView toggleFullScreen:sender]; + [normalWindow toggleFullScreen:sender]; } - (void) setFullGrab:(id)sender @@ -1403,6 +1357,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven if (stretch_video == true) { [sender setState: NSControlStateValueOn]; } else { + [cocoaView resizeWindow]; [sender setState: NSControlStateValueOff]; } } @@ -2049,8 +2004,7 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) /* if fullscreen mode is to be used */ if (opts->has_full_screen && opts->full_screen) { - [NSApp activateIgnoringOtherApps: YES]; - [controller toggleFullScreen: nil]; + [normalWindow toggleFullScreen: nil]; } if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) { [controller setFullGrab: nil]; From 0c35886e80a9593231cf50c6adb903cf7b4ee58b Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:38 +0900 Subject: [PATCH 09/12] ui/cocoa: Remove normalWindow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QemuCocoaView used to have fullScreenWindow but now it's gone, so we do no longer have to call the window specifically "normalWindow". Instead, refer to it with [-QemuCocoaView window]. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-7-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index 20d73098e2..a891e76345 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -93,7 +93,6 @@ static void cocoa_switch(DisplayChangeListener *dcl, static void cocoa_refresh(DisplayChangeListener *dcl); -static NSWindow *normalWindow; static const DisplayChangeListenerOps dcl_ops = { .dpy_name = "cocoa", .dpy_gfx_update = cocoa_update, @@ -1071,9 +1070,9 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven COCOA_DEBUG("QemuCocoaView: grabMouse\n"); if (qemu_name) - [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)", qemu_name]]; + [[self window] setTitle:[NSString stringWithFormat:@"QEMU %s - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)", qemu_name]]; else - [normalWindow setTitle:@"QEMU - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)"]; + [[self window] setTitle:@"QEMU - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)"]; [self hideCursor]; CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); isMouseGrabbed = TRUE; // while isMouseGrabbed = TRUE, QemuCocoaApp sends all events to [cocoaView handleEvent:] @@ -1084,9 +1083,9 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven COCOA_DEBUG("QemuCocoaView: ungrabMouse\n"); if (qemu_name) - [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; + [[self window] setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; else - [normalWindow setTitle:@"QEMU"]; + [[self window] setTitle:@"QEMU"]; [self unhideCursor]; CGAssociateMouseAndMouseCursorPosition(TRUE); isMouseGrabbed = FALSE; @@ -1157,6 +1156,8 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven @implementation QemuCocoaAppController - (id) init { + NSWindow *window; + COCOA_DEBUG("QemuCocoaAppController: init\n"); self = [super init]; @@ -1170,20 +1171,20 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven } // create a window - normalWindow = [[NSWindow alloc] initWithContentRect:[cocoaView frame] + window = [[NSWindow alloc] initWithContentRect:[cocoaView frame] styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskClosable backing:NSBackingStoreBuffered defer:NO]; - if(!normalWindow) { + if(!window) { error_report("(cocoa) can't create window"); exit(1); } - [normalWindow setAcceptsMouseMovedEvents:YES]; - [normalWindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - [normalWindow setTitle:qemu_name ? [NSString stringWithFormat:@"QEMU %s", qemu_name] : @"QEMU"]; - [normalWindow setContentView:cocoaView]; - [normalWindow makeKeyAndOrderFront:self]; - [normalWindow center]; - [normalWindow setDelegate: self]; + [window setAcceptsMouseMovedEvents:YES]; + [window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + [window setTitle:qemu_name ? [NSString stringWithFormat:@"QEMU %s", qemu_name] : @"QEMU"]; + [window setContentView:cocoaView]; + [window makeKeyAndOrderFront:self]; + [window center]; + [window setDelegate: self]; /* Used for displaying pause on the screen */ pauseLabel = [NSTextField new]; @@ -1306,7 +1307,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven */ - (void) doToggleFullScreen:(id)sender { - [normalWindow toggleFullScreen:sender]; + [[cocoaView window] toggleFullScreen:sender]; } - (void) setFullGrab:(id)sender @@ -2004,7 +2005,7 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) /* if fullscreen mode is to be used */ if (opts->has_full_screen && opts->full_screen) { - [normalWindow toggleFullScreen: nil]; + [[cocoaView window] toggleFullScreen: nil]; } if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) { [controller setFullGrab: nil]; From b6ee03c229c5e4abe724add1b8c7ae09be7219ee Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:39 +0900 Subject: [PATCH 10/12] ui/cocoa: Make window resizable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window will be resizable when zoom-to-fit is on. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-8-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/cocoa.m b/ui/cocoa.m index a891e76345..6c9efa0c20 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -1356,8 +1356,10 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven { stretch_video = !stretch_video; if (stretch_video == true) { + [cocoaView window].styleMask |= NSWindowStyleMaskResizable; [sender setState: NSControlStateValueOn]; } else { + [cocoaView window].styleMask &= ~NSWindowStyleMaskResizable; [cocoaView resizeWindow]; [sender setState: NSControlStateValueOff]; } @@ -2024,6 +2026,7 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) if (opts->u.cocoa.has_zoom_to_fit && opts->u.cocoa.zoom_to_fit) { stretch_video = true; + [cocoaView window].styleMask |= NSWindowStyleMaskResizable; } if (opts->u.cocoa.has_zoom_interpolation && opts->u.cocoa.zoom_interpolation) { From 4b49f92ce1499eecf5be5365429e5d4c486354db Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:40 +0900 Subject: [PATCH 11/12] ui/cocoa: Call console_select() with the BQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [-QemuCocoaView displayConsole:] can be called anytime so explicitly take the BQL before it calls console_select(). Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-9-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index 6c9efa0c20..bc63043158 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -1379,7 +1379,9 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven /* Displays the console on the screen */ - (void)displayConsole:(id)sender { - console_select([sender tag]); + with_bql(^{ + console_select([sender tag]); + }); } /* Pause the guest */ From 5576663208b7c31766c580520df506375d00103e Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sat, 24 Feb 2024 21:43:41 +0900 Subject: [PATCH 12/12] ui/cocoa: Remove stretch_video flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluate [normalWindow styleMask] & NSWindowStyleMaskResizable instead. Signed-off-by: Akihiko Odaki Tested-by: Rene Engel Reviewed-by: Peter Maydell Message-ID: <20240224-cocoa-v12-10-e89f70bdda71@daynix.com> Signed-off-by: Philippe Mathieu-Daudé --- ui/cocoa.m | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/ui/cocoa.m b/ui/cocoa.m index bc63043158..fa879d7dcd 100644 --- a/ui/cocoa.m +++ b/ui/cocoa.m @@ -106,7 +106,6 @@ static int cursor_hide = 1; static int left_command_key_enabled = 1; static bool swap_opt_cmd; -static bool stretch_video; static CGInterpolationQuality zoom_interpolation = kCGInterpolationNone; static NSTextField *pauseLabel; @@ -522,7 +521,7 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven { [[self window] setContentAspectRatio:NSMakeSize(screen.width, screen.height)]; - if (!stretch_video) { + if (!([[self window] styleMask] & NSWindowStyleMaskResizable)) { [[self window] setContentSize:NSMakeSize(screen.width, screen.height)]; [[self window] center]; } else if ([[self window] styleMask] & NSWindowStyleMaskFullScreen) { @@ -1354,15 +1353,10 @@ static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEven /* Stretches video to fit host monitor size */ - (void)zoomToFit:(id) sender { - stretch_video = !stretch_video; - if (stretch_video == true) { - [cocoaView window].styleMask |= NSWindowStyleMaskResizable; - [sender setState: NSControlStateValueOn]; - } else { - [cocoaView window].styleMask &= ~NSWindowStyleMaskResizable; - [cocoaView resizeWindow]; - [sender setState: NSControlStateValueOff]; - } + NSWindowStyleMask styleMask = [[cocoaView window] styleMask] ^ NSWindowStyleMaskResizable; + + [[cocoaView window] setStyleMask:styleMask]; + [sender setState:styleMask & NSWindowStyleMaskResizable ? NSControlStateValueOn : NSControlStateValueOff]; } - (void)toggleZoomInterpolation:(id) sender @@ -1638,7 +1632,7 @@ static void create_initial_menus(void) menu = [[NSMenu alloc] initWithTitle:@"View"]; [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Enter Fullscreen" action:@selector(doToggleFullScreen:) keyEquivalent:@"f"] autorelease]]; // Fullscreen menuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom To Fit" action:@selector(zoomToFit:) keyEquivalent:@""] autorelease]; - [menuItem setState: stretch_video ? NSControlStateValueOn : NSControlStateValueOff]; + [menuItem setState: [[cocoaView window] styleMask] & NSWindowStyleMaskResizable ? NSControlStateValueOn : NSControlStateValueOff]; [menu addItem: menuItem]; menuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom Interpolation" action:@selector(toggleZoomInterpolation:) keyEquivalent:@""] autorelease]; [menuItem setState: zoom_interpolation == kCGInterpolationLow ? NSControlStateValueOn : NSControlStateValueOff]; @@ -2027,7 +2021,6 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) } if (opts->u.cocoa.has_zoom_to_fit && opts->u.cocoa.zoom_to_fit) { - stretch_video = true; [cocoaView window].styleMask |= NSWindowStyleMaskResizable; }