/* * QEMU Cocoa CG display driver * * Copyright (c) 2008 Mike Kronenberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "qemu/osdep.h" #import #include #include "qemu/help-texts.h" #include "qemu-main.h" #include "ui/clipboard.h" #include "ui/console.h" #include "ui/input.h" #include "ui/kbd-state.h" #include "sysemu/sysemu.h" #include "sysemu/runstate.h" #include "sysemu/runstate-action.h" #include "sysemu/cpu-throttle.h" #include "qapi/error.h" #include "qapi/qapi-commands-block.h" #include "qapi/qapi-commands-machine.h" #include "qapi/qapi-commands-misc.h" #include "sysemu/blockdev.h" #include "qemu-version.h" #include "qemu/cutils.h" #include "qemu/main-loop.h" #include "qemu/module.h" #include "qemu/error-report.h" #include #include "hw/core/cpu.h" #ifndef MAC_OS_X_VERSION_10_13 #define MAC_OS_X_VERSION_10_13 101300 #endif /* 10.14 deprecates NSOnState and NSOffState in favor of * NSControlStateValueOn/Off, which were introduced in 10.13. * Define for older versions */ #if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13 #define NSControlStateValueOn NSOnState #define NSControlStateValueOff NSOffState #endif //#define DEBUG #ifdef DEBUG #define COCOA_DEBUG(...) { (void) fprintf (stdout, __VA_ARGS__); } #else #define COCOA_DEBUG(...) ((void) 0) #endif #define cgrect(nsrect) (*(CGRect *)&(nsrect)) #define UC_CTRL_KEY "\xe2\x8c\x83" #define UC_ALT_KEY "\xe2\x8c\xa5" typedef struct { int width; int height; } QEMUScreen; static void cocoa_update(DisplayChangeListener *dcl, int x, int y, int w, int h); static void cocoa_switch(DisplayChangeListener *dcl, DisplaySurface *surface); static void cocoa_refresh(DisplayChangeListener *dcl); static NSWindow *normalWindow; static const DisplayChangeListenerOps dcl_ops = { .dpy_name = "cocoa", .dpy_gfx_update = cocoa_update, .dpy_gfx_switch = cocoa_switch, .dpy_refresh = cocoa_refresh, }; 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; static bool stretch_video; static NSTextField *pauseLabel; static bool allow_events; static NSInteger cbchangecount = -1; static QemuClipboardInfo *cbinfo; static QemuEvent cbevent; // Utility functions to run specified code block with the BQL held typedef void (^CodeBlock)(void); typedef bool (^BoolCodeBlock)(void); static void with_bql(CodeBlock block) { bool locked = bql_locked(); if (!locked) { bql_lock(); } block(); if (!locked) { bql_unlock(); } } static bool bool_with_bql(BoolCodeBlock block) { bool locked = bql_locked(); bool val; if (!locked) { bql_lock(); } val = block(); if (!locked) { bql_unlock(); } return val; } // Mac to QKeyCode conversion static const int mac_to_qkeycode_map[] = { [kVK_ANSI_A] = Q_KEY_CODE_A, [kVK_ANSI_B] = Q_KEY_CODE_B, [kVK_ANSI_C] = Q_KEY_CODE_C, [kVK_ANSI_D] = Q_KEY_CODE_D, [kVK_ANSI_E] = Q_KEY_CODE_E, [kVK_ANSI_F] = Q_KEY_CODE_F, [kVK_ANSI_G] = Q_KEY_CODE_G, [kVK_ANSI_H] = Q_KEY_CODE_H, [kVK_ANSI_I] = Q_KEY_CODE_I, [kVK_ANSI_J] = Q_KEY_CODE_J, [kVK_ANSI_K] = Q_KEY_CODE_K, [kVK_ANSI_L] = Q_KEY_CODE_L, [kVK_ANSI_M] = Q_KEY_CODE_M, [kVK_ANSI_N] = Q_KEY_CODE_N, [kVK_ANSI_O] = Q_KEY_CODE_O, [kVK_ANSI_P] = Q_KEY_CODE_P, [kVK_ANSI_Q] = Q_KEY_CODE_Q, [kVK_ANSI_R] = Q_KEY_CODE_R, [kVK_ANSI_S] = Q_KEY_CODE_S, [kVK_ANSI_T] = Q_KEY_CODE_T, [kVK_ANSI_U] = Q_KEY_CODE_U, [kVK_ANSI_V] = Q_KEY_CODE_V, [kVK_ANSI_W] = Q_KEY_CODE_W, [kVK_ANSI_X] = Q_KEY_CODE_X, [kVK_ANSI_Y] = Q_KEY_CODE_Y, [kVK_ANSI_Z] = Q_KEY_CODE_Z, [kVK_ANSI_0] = Q_KEY_CODE_0, [kVK_ANSI_1] = Q_KEY_CODE_1, [kVK_ANSI_2] = Q_KEY_CODE_2, [kVK_ANSI_3] = Q_KEY_CODE_3, [kVK_ANSI_4] = Q_KEY_CODE_4, [kVK_ANSI_5] = Q_KEY_CODE_5, [kVK_ANSI_6] = Q_KEY_CODE_6, [kVK_ANSI_7] = Q_KEY_CODE_7, [kVK_ANSI_8] = Q_KEY_CODE_8, [kVK_ANSI_9] = Q_KEY_CODE_9, [kVK_ANSI_Grave] = Q_KEY_CODE_GRAVE_ACCENT, [kVK_ANSI_Minus] = Q_KEY_CODE_MINUS, [kVK_ANSI_Equal] = Q_KEY_CODE_EQUAL, [kVK_Delete] = Q_KEY_CODE_BACKSPACE, [kVK_CapsLock] = Q_KEY_CODE_CAPS_LOCK, [kVK_Tab] = Q_KEY_CODE_TAB, [kVK_Return] = Q_KEY_CODE_RET, [kVK_ANSI_LeftBracket] = Q_KEY_CODE_BRACKET_LEFT, [kVK_ANSI_RightBracket] = Q_KEY_CODE_BRACKET_RIGHT, [kVK_ANSI_Backslash] = Q_KEY_CODE_BACKSLASH, [kVK_ANSI_Semicolon] = Q_KEY_CODE_SEMICOLON, [kVK_ANSI_Quote] = Q_KEY_CODE_APOSTROPHE, [kVK_ANSI_Comma] = Q_KEY_CODE_COMMA, [kVK_ANSI_Period] = Q_KEY_CODE_DOT, [kVK_ANSI_Slash] = Q_KEY_CODE_SLASH, [kVK_Space] = Q_KEY_CODE_SPC, [kVK_ANSI_Keypad0] = Q_KEY_CODE_KP_0, [kVK_ANSI_Keypad1] = Q_KEY_CODE_KP_1, [kVK_ANSI_Keypad2] = Q_KEY_CODE_KP_2, [kVK_ANSI_Keypad3] = Q_KEY_CODE_KP_3, [kVK_ANSI_Keypad4] = Q_KEY_CODE_KP_4, [kVK_ANSI_Keypad5] = Q_KEY_CODE_KP_5, [kVK_ANSI_Keypad6] = Q_KEY_CODE_KP_6, [kVK_ANSI_Keypad7] = Q_KEY_CODE_KP_7, [kVK_ANSI_Keypad8] = Q_KEY_CODE_KP_8, [kVK_ANSI_Keypad9] = Q_KEY_CODE_KP_9, [kVK_ANSI_KeypadDecimal] = Q_KEY_CODE_KP_DECIMAL, [kVK_ANSI_KeypadEnter] = Q_KEY_CODE_KP_ENTER, [kVK_ANSI_KeypadPlus] = Q_KEY_CODE_KP_ADD, [kVK_ANSI_KeypadMinus] = Q_KEY_CODE_KP_SUBTRACT, [kVK_ANSI_KeypadMultiply] = Q_KEY_CODE_KP_MULTIPLY, [kVK_ANSI_KeypadDivide] = Q_KEY_CODE_KP_DIVIDE, [kVK_ANSI_KeypadEquals] = Q_KEY_CODE_KP_EQUALS, [kVK_ANSI_KeypadClear] = Q_KEY_CODE_NUM_LOCK, [kVK_UpArrow] = Q_KEY_CODE_UP, [kVK_DownArrow] = Q_KEY_CODE_DOWN, [kVK_LeftArrow] = Q_KEY_CODE_LEFT, [kVK_RightArrow] = Q_KEY_CODE_RIGHT, [kVK_Help] = Q_KEY_CODE_INSERT, [kVK_Home] = Q_KEY_CODE_HOME, [kVK_PageUp] = Q_KEY_CODE_PGUP, [kVK_PageDown] = Q_KEY_CODE_PGDN, [kVK_End] = Q_KEY_CODE_END, [kVK_ForwardDelete] = Q_KEY_CODE_DELETE, [kVK_Escape] = Q_KEY_CODE_ESC, /* The Power key can't be used directly because the operating system uses * it. This key can be emulated by using it in place of another key such as * F1. Don't forget to disable the real key binding. */ /* [kVK_F1] = Q_KEY_CODE_POWER, */ [kVK_F1] = Q_KEY_CODE_F1, [kVK_F2] = Q_KEY_CODE_F2, [kVK_F3] = Q_KEY_CODE_F3, [kVK_F4] = Q_KEY_CODE_F4, [kVK_F5] = Q_KEY_CODE_F5, [kVK_F6] = Q_KEY_CODE_F6, [kVK_F7] = Q_KEY_CODE_F7, [kVK_F8] = Q_KEY_CODE_F8, [kVK_F9] = Q_KEY_CODE_F9, [kVK_F10] = Q_KEY_CODE_F10, [kVK_F11] = Q_KEY_CODE_F11, [kVK_F12] = Q_KEY_CODE_F12, [kVK_F13] = Q_KEY_CODE_PRINT, [kVK_F14] = Q_KEY_CODE_SCROLL_LOCK, [kVK_F15] = Q_KEY_CODE_PAUSE, // JIS keyboards only [kVK_JIS_Yen] = Q_KEY_CODE_YEN, [kVK_JIS_Underscore] = Q_KEY_CODE_RO, [kVK_JIS_KeypadComma] = Q_KEY_CODE_KP_COMMA, [kVK_JIS_Eisu] = Q_KEY_CODE_MUHENKAN, [kVK_JIS_Kana] = Q_KEY_CODE_HENKAN, /* * The eject and volume keys can't be used here because they are handled at * a lower level than what an Application can see. */ }; static int cocoa_keycode_to_qemu(int keycode) { if (ARRAY_SIZE(mac_to_qkeycode_map) <= keycode) { error_report("(cocoa) warning unknown keycode 0x%x", keycode); return 0; } return mac_to_qkeycode_map[keycode]; } /* Displays an alert dialog box with the specified message */ static void QEMU_Alert(NSString *message) { NSAlert *alert; alert = [NSAlert new]; [alert setMessageText: message]; [alert runModal]; } /* Handles any errors that happen with a device transaction */ static void handleAnyDeviceErrors(Error * err) { if (err) { QEMU_Alert([NSString stringWithCString: error_get_pretty(err) encoding: NSASCIIStringEncoding]); error_free(err); } } /* ------------------------------------------------------ QemuCocoaView ------------------------------------------------------ */ @interface QemuCocoaView : NSView { 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; - (bool) handleEventLocked:(NSEvent *)event; - (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled; /* The state surrounding mouse grabbing is potentially confusing. * isAbsoluteEnabled tracks qemu_input_is_absolute() [ie "is the emulated * pointing device an absolute-position one?"], but is only updated on * next refresh. * isMouseGrabbed tracks whether GUI events are directed to the guest; * it controls whether special keys like Cmd get sent to the guest, * and whether we capture the mouse when in non-absolute mode. */ - (BOOL) isMouseGrabbed; - (BOOL) isAbsoluteEnabled; - (float) cdx; - (float) cdy; - (QEMUScreen) gscreen; - (void) raiseAllKeys; @end QemuCocoaView *cocoaView; static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEventRef cgEvent, void *userInfo) { QemuCocoaView *view = userInfo; NSEvent *event = [NSEvent eventWithCGEvent:cgEvent]; if ([view isMouseGrabbed] && [view 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 - (id)initWithFrame:(NSRect)frameRect { COCOA_DEBUG("QemuCocoaView: initWithFrame\n"); self = [super initWithFrame:frameRect]; if (self) { screen.width = frameRect.size.width; screen.height = frameRect.size.height; kbd = qkbd_state_init(dcl.con); } return self; } - (void) dealloc { COCOA_DEBUG("QemuCocoaView: dealloc\n"); if (pixman_image) { pixman_image_unref(pixman_image); } qkbd_state_free(kbd); if (eventsTap) { CFRelease(eventsTap); } [super dealloc]; } - (BOOL) isOpaque { return YES; } - (BOOL) screenContainsPoint:(NSPoint) p { return (p.x > -1 && p.x < screen.width && p.y > -1 && p.y < screen.height); } /* Get location of event and convert to virtual screen coordinate */ - (CGPoint) screenLocationOfEvent:(NSEvent *)ev { 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; } } - (void) hideCursor { if (!cursor_hide) { return; } [NSCursor hide]; } - (void) unhideCursor { if (!cursor_hide) { return; } [NSCursor unhide]; } - (void) drawRect:(NSRect) rect { COCOA_DEBUG("QemuCocoaView: drawRect\n"); // get CoreGraphic context CGContextRef viewContextRef = [[NSGraphicsContext currentContext] CGContext]; CGContextSetInterpolationQuality (viewContextRef, kCGInterpolationNone); CGContextSetShouldAntialias (viewContextRef, NO); // draw screen bitmap directly to Core Graphics context if (!pixman_image) { // Draw request before any guest device has set up a framebuffer: // just draw an opaque black rectangle CGContextSetRGBFillColor(viewContextRef, 0, 0, 0, 1.0); CGContextFillRect(viewContextRef, NSRectToCGRect(rect)); } else { int w = pixman_image_get_width(pixman_image); int h = pixman_image_get_height(pixman_image); int bitsPerPixel = PIXMAN_FORMAT_BPP(pixman_image_get_format(pixman_image)); int stride = pixman_image_get_stride(pixman_image); CGDataProviderRef dataProviderRef = CGDataProviderCreateWithData( NULL, pixman_image_get_data(pixman_image), stride * h, NULL ); CGImageRef imageRef = CGImageCreate( w, //width h, //height DIV_ROUND_UP(bitsPerPixel, 8) * 2, //bitsPerComponent bitsPerPixel, //bitsPerPixel stride, //bytesPerRow CGColorSpaceCreateWithName(kCGColorSpaceSRGB), //colorspace kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, //bitmapInfo dataProviderRef, //provider NULL, //decode 0, //interpolate kCGRenderingIntentDefault //intent ); // selective drawing code (draws only dirty rectangles) (OS X >= 10.4) const NSRect *rectList; NSInteger rectCount; int i; CGImageRef clipImageRef; CGRect clipRect; [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; clipImageRef = CGImageCreateWithImageInRect( imageRef, clipRect ); CGContextDrawImage (viewContextRef, cgrect(rectList[i]), clipImageRef); CGImageRelease (clipImageRef); } CGImageRelease (imageRef); CGDataProviderRelease(dataProviderRef); } } - (void) setContentDimensions { COCOA_DEBUG("QemuCocoaView: setContentDimensions\n"); if (isFullscreen) { cdx = [[NSScreen mainScreen] frame].size.width / (float)screen.width; cdy = [[NSScreen mainScreen] frame].size.height / (float)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; } } - (void) updateUIInfoLocked { /* Must be called with the BQL, i.e. via updateUIInfo */ NSSize frameSize; QemuUIInfo info; if (!qemu_console_is_graphic(dcl.con)) { return; } if ([self window]) { NSDictionary *description = [[[self window] screen] deviceDescription]; CGDirectDisplayID display = [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]; NSSize screenSize = [[[self window] screen] frame].size; CGSize screenPhysicalSize = CGDisplayScreenSize(display); CVDisplayLinkRef displayLink; frameSize = isFullscreen ? screenSize : [self frame].size; if (!CVDisplayLinkCreateWithCGDisplay(display, &displayLink)) { CVTime period = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLink); CVDisplayLinkRelease(displayLink); if (!(period.flags & kCVTimeIsIndefinite)) { update_displaychangelistener(&dcl, 1000 * period.timeValue / period.timeScale); info.refresh_rate = (int64_t)1000 * period.timeScale / period.timeValue; } } info.width_mm = frameSize.width / screenSize.width * screenPhysicalSize.width; info.height_mm = frameSize.height / screenSize.height * screenPhysicalSize.height; } else { frameSize = [self frame].size; info.width_mm = 0; info.height_mm = 0; } info.xoff = 0; info.yoff = 0; info.width = frameSize.width; info.height = frameSize.height; dpy_set_ui_info(dcl.con, &info, TRUE); } - (void) updateUIInfo { if (!allow_events) { /* * Don't try to tell QEMU about UI information in the application * startup phase -- we haven't yet registered dcl with the QEMU UI * layer. * When cocoa_display_init() does register the dcl, the UI layer * will call cocoa_switch(), which will call updateUIInfo, so * we don't lose any information here. */ return; } with_bql(^{ [self updateUIInfoLocked]; }); } - (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) { // 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)]; } // update screenBuffer if (pixman_image) { pixman_image_unref(pixman_image); } 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 { 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 { qkbd_state_key_event(kbd, keycode, !qkbd_state_key_get(kbd, keycode)); } // Does the work of sending input to the monitor - (void) handleMonitorInput:(NSEvent *)event { int keysym = 0; int control_key = 0; // if the control key is down if ([event modifierFlags] & NSEventModifierFlagControl) { control_key = 1; } /* translates Macintosh keycodes to QEMU's keysym */ static const int without_control_translation[] = { [0 ... 0xff] = 0, // invalid key [kVK_UpArrow] = QEMU_KEY_UP, [kVK_DownArrow] = QEMU_KEY_DOWN, [kVK_RightArrow] = QEMU_KEY_RIGHT, [kVK_LeftArrow] = QEMU_KEY_LEFT, [kVK_Home] = QEMU_KEY_HOME, [kVK_End] = QEMU_KEY_END, [kVK_PageUp] = QEMU_KEY_PAGEUP, [kVK_PageDown] = QEMU_KEY_PAGEDOWN, [kVK_ForwardDelete] = QEMU_KEY_DELETE, [kVK_Delete] = QEMU_KEY_BACKSPACE, }; static const int with_control_translation[] = { [0 ... 0xff] = 0, // invalid key [kVK_UpArrow] = QEMU_KEY_CTRL_UP, [kVK_DownArrow] = QEMU_KEY_CTRL_DOWN, [kVK_RightArrow] = QEMU_KEY_CTRL_RIGHT, [kVK_LeftArrow] = QEMU_KEY_CTRL_LEFT, [kVK_Home] = QEMU_KEY_CTRL_HOME, [kVK_End] = QEMU_KEY_CTRL_END, [kVK_PageUp] = QEMU_KEY_CTRL_PAGEUP, [kVK_PageDown] = QEMU_KEY_CTRL_PAGEDOWN, }; if (control_key != 0) { /* If the control key is being used */ if ([event keyCode] < ARRAY_SIZE(with_control_translation)) { keysym = with_control_translation[[event keyCode]]; } } else { if ([event keyCode] < ARRAY_SIZE(without_control_translation)) { keysym = without_control_translation[[event keyCode]]; } } // if not a key that needs translating if (keysym == 0) { NSString *ks = [event characters]; if ([ks length] > 0) { keysym = [ks characterAtIndex:0]; } } if (keysym) { qemu_text_console_put_keysym(NULL, keysym); } } - (bool) handleEvent:(NSEvent *)event { return bool_with_bql(^{ return [self handleEventLocked:event]; }); } - (bool) handleEventLocked:(NSEvent *)event { /* Return true if we handled the event, false if it should be given to OSX */ COCOA_DEBUG("QemuCocoaView: handleEvent\n"); int buttons = 0; int keycode = 0; bool mouse_event = false; // Location of event in virtual screen coordinates NSPoint p = [self screenLocationOfEvent:event]; NSUInteger modifiers = [event modifierFlags]; /* * Check -[NSEvent modifierFlags] here. * * There is a NSEventType for an event notifying the change of * -[NSEvent modifierFlags], NSEventTypeFlagsChanged but these operations * are performed for any events because a modifier state may change while * the application is inactive (i.e. no events fire) and we don't want to * wait for another modifier state change to detect such a change. * * NSEventModifierFlagCapsLock requires a special treatment. The other flags * are handled in similar manners. * * NSEventModifierFlagCapsLock * --------------------------- * * If CapsLock state is changed, "up" and "down" events will be fired in * sequence, effectively updates CapsLock state on the guest. * * The other flags * --------------- * * If a flag is not set, fire "up" events for all keys which correspond to * the flag. Note that "down" events are not fired here because the flags * checked here do not tell what exact keys are down. * * If one of the keys corresponding to a flag is down, we rely on * -[NSEvent keyCode] of an event whose -[NSEvent type] is * NSEventTypeFlagsChanged to know the exact key which is down, which has * the following two downsides: * - It does not work when the application is inactive as described above. * - It malfactions *after* the modifier state is changed while the * application is inactive. It is because -[NSEvent keyCode] does not tell * if the key is up or down, and requires to infer the current state from * the previous state. It is still possible to fix such a malfanction by * completely leaving your hands from the keyboard, which hopefully makes * this implementation usable enough. */ if (!!(modifiers & NSEventModifierFlagCapsLock) != qkbd_state_modifier_get(kbd, QKBD_MOD_CAPSLOCK)) { qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, true); qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, false); } if (!(modifiers & NSEventModifierFlagShift)) { qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT, false); qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT_R, false); } if (!(modifiers & NSEventModifierFlagControl)) { qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL, false); qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL_R, false); } if (!(modifiers & NSEventModifierFlagOption)) { if (swap_opt_cmd) { qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false); qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false); } else { qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); } } if (!(modifiers & NSEventModifierFlagCommand)) { if (swap_opt_cmd) { qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); } else { qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false); qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false); } } switch ([event type]) { case NSEventTypeFlagsChanged: switch ([event keyCode]) { case kVK_Shift: if (!!(modifiers & NSEventModifierFlagShift)) { [self toggleKey:Q_KEY_CODE_SHIFT]; } break; case kVK_RightShift: if (!!(modifiers & NSEventModifierFlagShift)) { [self toggleKey:Q_KEY_CODE_SHIFT_R]; } break; case kVK_Control: if (!!(modifiers & NSEventModifierFlagControl)) { [self toggleKey:Q_KEY_CODE_CTRL]; } break; case kVK_RightControl: if (!!(modifiers & NSEventModifierFlagControl)) { [self toggleKey:Q_KEY_CODE_CTRL_R]; } break; case kVK_Option: if (!!(modifiers & NSEventModifierFlagOption)) { if (swap_opt_cmd) { [self toggleKey:Q_KEY_CODE_META_L]; } else { [self toggleKey:Q_KEY_CODE_ALT]; } } break; case kVK_RightOption: if (!!(modifiers & NSEventModifierFlagOption)) { if (swap_opt_cmd) { [self toggleKey:Q_KEY_CODE_META_R]; } else { [self toggleKey:Q_KEY_CODE_ALT_R]; } } break; /* Don't pass command key changes to guest unless mouse is grabbed */ case kVK_Command: if (isMouseGrabbed && !!(modifiers & NSEventModifierFlagCommand) && left_command_key_enabled) { if (swap_opt_cmd) { [self toggleKey:Q_KEY_CODE_ALT]; } else { [self toggleKey:Q_KEY_CODE_META_L]; } } break; case kVK_RightCommand: if (isMouseGrabbed && !!(modifiers & NSEventModifierFlagCommand)) { if (swap_opt_cmd) { [self toggleKey:Q_KEY_CODE_ALT_R]; } else { [self toggleKey:Q_KEY_CODE_META_R]; } } break; } break; case NSEventTypeKeyDown: keycode = cocoa_keycode_to_qemu([event keyCode]); // forward command key combos to the host UI unless the mouse is grabbed if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) { return false; } // default // handle control + alt Key Combos (ctrl+alt+[1..9,g] is reserved for QEMU) if (([event modifierFlags] & NSEventModifierFlagControl) && ([event modifierFlags] & NSEventModifierFlagOption)) { NSString *keychar = [event charactersIgnoringModifiers]; if ([keychar length] == 1) { char key = [keychar characterAtIndex:0]; switch (key) { // enable graphic console case '1' ... '9': console_select(key - '0' - 1); /* ascii math */ return true; // release the mouse grab case 'g': [self ungrabMouse]; return true; } } } if (qemu_console_is_graphic(NULL)) { qkbd_state_key_event(kbd, keycode, true); } else { [self handleMonitorInput: event]; } break; case NSEventTypeKeyUp: keycode = cocoa_keycode_to_qemu([event keyCode]); // don't pass the guest a spurious key-up if we treated this // command-key combo as a host UI action if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) { return true; } if (qemu_console_is_graphic(NULL)) { qkbd_state_key_event(kbd, keycode, false); } break; 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]; } } } mouse_event = true; break; case NSEventTypeLeftMouseDown: buttons |= MOUSE_EVENT_LBUTTON; mouse_event = true; break; case NSEventTypeRightMouseDown: buttons |= MOUSE_EVENT_RBUTTON; mouse_event = true; break; case NSEventTypeOtherMouseDown: buttons |= MOUSE_EVENT_MBUTTON; mouse_event = true; break; case NSEventTypeLeftMouseDragged: buttons |= MOUSE_EVENT_LBUTTON; mouse_event = true; break; case NSEventTypeRightMouseDragged: buttons |= MOUSE_EVENT_RBUTTON; mouse_event = true; break; case NSEventTypeOtherMouseDragged: buttons |= MOUSE_EVENT_MBUTTON; mouse_event = true; break; case NSEventTypeLeftMouseUp: mouse_event = true; 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]; } } break; case NSEventTypeRightMouseUp: mouse_event = true; break; case NSEventTypeOtherMouseUp: mouse_event = true; break; 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) ? INPUT_BUTTON_WHEEL_UP : INPUT_BUTTON_WHEEL_DOWN; } else if ([event deltaX] != 0) { buttons = ([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(); } /* * Since deltaX/deltaY also report scroll wheel events we prevent mouse * movement code from executing. */ mouse_event = false; break; default: return false; } 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 true; } - (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)"]; } [self hideCursor]; CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); isMouseGrabbed = TRUE; // while isMouseGrabbed = TRUE, QemuCocoaApp sends all events to [cocoaView handleEvent:] } - (void) ungrabMouse { COCOA_DEBUG("QemuCocoaView: ungrabMouse\n"); if (!isFullscreen) { if (qemu_name) [normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; else [normalWindow setTitle:@"QEMU"]; } [self unhideCursor]; CGAssociateMouseAndMouseCursorPosition(TRUE); isMouseGrabbed = FALSE; } - (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled { isAbsoluteEnabled = tIsAbsoluteEnabled; if (isMouseGrabbed) { CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); } } - (BOOL) isMouseGrabbed {return isMouseGrabbed;} - (BOOL) isAbsoluteEnabled {return isAbsoluteEnabled;} - (float) cdx {return cdx;} - (float) cdy {return cdy;} - (QEMUScreen) gscreen {return screen;} /* * Makes the target think all down keys are being released. * This prevents a stuck key problem, since we will not see * key up events for those keys after we have lost focus. */ - (void) raiseAllKeys { with_bql(^{ qkbd_state_lift_all_keys(kbd); }); } @end /* ------------------------------------------------------ QemuCocoaAppController ------------------------------------------------------ */ @interface QemuCocoaAppController : NSObject { } - (void)doToggleFullScreen:(id)sender; - (void)toggleFullScreen:(id)sender; - (void)showQEMUDoc:(id)sender; - (void)zoomToFit:(id) sender; - (void)displayConsole:(id)sender; - (void)pauseQEMU:(id)sender; - (void)resumeQEMU:(id)sender; - (void)displayPause; - (void)removePause; - (void)restartQEMU:(id)sender; - (void)powerDownQEMU:(id)sender; - (void)ejectDeviceMedia:(id)sender; - (void)changeDeviceMedia:(id)sender; - (BOOL)verifyQuit; - (void)openDocumentation:(NSString *)filename; - (IBAction) do_about_menu_item: (id) sender; - (void)adjustSpeed:(id)sender; @end @implementation QemuCocoaAppController - (id) init { COCOA_DEBUG("QemuCocoaAppController: init\n"); self = [super init]; if (self) { // create a view and add it to the window cocoaView = [[QemuCocoaView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 640.0, 480.0)]; if(!cocoaView) { error_report("(cocoa) can't create a view"); exit(1); } // create a window normalWindow = [[NSWindow alloc] initWithContentRect:[cocoaView frame] styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskClosable backing:NSBackingStoreBuffered defer:NO]; if(!normalWindow) { error_report("(cocoa) can't create window"); exit(1); } [normalWindow setAcceptsMouseMovedEvents:YES]; [normalWindow setTitle:@"QEMU"]; [normalWindow setContentView:cocoaView]; [normalWindow makeKeyAndOrderFront:self]; [normalWindow center]; [normalWindow setDelegate: self]; /* Used for displaying pause on the screen */ pauseLabel = [NSTextField new]; [pauseLabel setBezeled:YES]; [pauseLabel setDrawsBackground:YES]; [pauseLabel setBackgroundColor: [NSColor whiteColor]]; [pauseLabel setEditable:NO]; [pauseLabel setSelectable:NO]; [pauseLabel setStringValue: @"Paused"]; [pauseLabel setFont: [NSFont fontWithName: @"Helvetica" size: 90]]; [pauseLabel setTextColor: [NSColor blackColor]]; [pauseLabel sizeToFit]; } return self; } - (void) dealloc { COCOA_DEBUG("QemuCocoaAppController: dealloc\n"); if (cocoaView) [cocoaView release]; [super dealloc]; } - (void)applicationDidFinishLaunching: (NSNotification *) note { COCOA_DEBUG("QemuCocoaAppController: applicationDidFinishLaunching\n"); allow_events = true; } - (void)applicationWillTerminate:(NSNotification *)aNotification { COCOA_DEBUG("QemuCocoaAppController: applicationWillTerminate\n"); with_bql(^{ shutdown_action = SHUTDOWN_ACTION_POWEROFF; qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); }); /* * Sleep here, because returning will cause OSX to kill us * immediately; the QEMU main loop will handle the shutdown * request and terminate the process. */ [NSThread sleepForTimeInterval:INFINITY]; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication { return YES; } - (NSApplicationTerminateReply)applicationShouldTerminate: (NSApplication *)sender { COCOA_DEBUG("QemuCocoaAppController: applicationShouldTerminate\n"); return [self verifyQuit]; } - (void)windowDidChangeScreen:(NSNotification *)notification { [cocoaView updateUIInfo]; } - (void)windowDidResize:(NSNotification *)notification { [cocoaView updateUIInfo]; } /* Called when the user clicks on a window's close button */ - (BOOL)windowShouldClose:(id)sender { COCOA_DEBUG("QemuCocoaAppController: windowShouldClose\n"); [NSApp terminate: sender]; /* If the user allows the application to quit then the call to * NSApp terminate will never return. If we get here then the user * cancelled the quit, so we should return NO to not permit the * closing of this window. */ return NO; } /* * Called when QEMU goes into the background. Note that * [-NSWindowDelegate windowDidResignKey:] is used here instead of * [-NSApplicationDelegate applicationWillResignActive:] because it cannot * detect that the window loses focus when the deck is clicked on macOS 13.2.1. */ - (void) windowDidResignKey: (NSNotification *)aNotification { COCOA_DEBUG("%s\n", __func__); [cocoaView ungrabMouse]; [cocoaView raiseAllKeys]; } /* We abstract the method called by the Enter Fullscreen menu item * because Mac OS 10.7 and higher disables it. This is because of the * menu item's old selector's name toggleFullScreen: */ - (void) doToggleFullScreen:(id)sender { [self toggleFullScreen:(id)sender]; } - (void)toggleFullScreen:(id)sender { COCOA_DEBUG("QemuCocoaAppController: toggleFullScreen\n"); [cocoaView toggleFullScreen:sender]; } - (void) setFullGrab:(id)sender { COCOA_DEBUG("QemuCocoaAppController: setFullGrab\n"); [cocoaView setFullGrab:sender]; } /* Tries to find then open the specified filename */ - (void) openDocumentation: (NSString *) filename { /* Where to look for local files */ NSString *path_array[] = {@"../share/doc/qemu/", @"../doc/qemu/", @"docs/"}; NSString *full_file_path; NSURL *full_file_url; /* iterate thru the possible paths until the file is found */ int index; for (index = 0; index < ARRAY_SIZE(path_array); index++) { full_file_path = [[NSBundle mainBundle] executablePath]; full_file_path = [full_file_path stringByDeletingLastPathComponent]; full_file_path = [NSString stringWithFormat: @"%@/%@%@", full_file_path, path_array[index], filename]; full_file_url = [NSURL fileURLWithPath: full_file_path isDirectory: false]; if ([[NSWorkspace sharedWorkspace] openURL: full_file_url] == YES) { return; } } /* If none of the paths opened a file */ NSBeep(); QEMU_Alert(@"Failed to open file"); } - (void)showQEMUDoc:(id)sender { COCOA_DEBUG("QemuCocoaAppController: showQEMUDoc\n"); [self openDocumentation: @"index.html"]; } /* Stretches video to fit host monitor size */ - (void)zoomToFit:(id) sender { stretch_video = !stretch_video; if (stretch_video == true) { [sender setState: NSControlStateValueOn]; } else { [sender setState: NSControlStateValueOff]; } } /* Displays the console on the screen */ - (void)displayConsole:(id)sender { console_select([sender tag]); } /* Pause the guest */ - (void)pauseQEMU:(id)sender { with_bql(^{ qmp_stop(NULL); }); [sender setEnabled: NO]; [[[sender menu] itemWithTitle: @"Resume"] setEnabled: YES]; [self displayPause]; } /* Resume running the guest operating system */ - (void)resumeQEMU:(id) sender { with_bql(^{ qmp_cont(NULL); }); [sender setEnabled: NO]; [[[sender menu] itemWithTitle: @"Pause"] setEnabled: YES]; [self removePause]; } /* Displays the word pause on the screen */ - (void)displayPause { /* 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); width = [pauseLabel frame].size.width; height = [pauseLabel frame].size.height; [pauseLabel setFrame: NSMakeRect(xCoord, yCoord, width, height)]; [cocoaView addSubview: pauseLabel]; } /* Removes the word pause from the screen */ - (void)removePause { [pauseLabel removeFromSuperview]; } /* Restarts QEMU */ - (void)restartQEMU:(id)sender { with_bql(^{ qmp_system_reset(NULL); }); } /* Powers down QEMU */ - (void)powerDownQEMU:(id)sender { with_bql(^{ qmp_system_powerdown(NULL); }); } /* Ejects the media. * Uses sender's tag to figure out the device to eject. */ - (void)ejectDeviceMedia:(id)sender { NSString * drive; drive = [sender representedObject]; if(drive == nil) { NSBeep(); QEMU_Alert(@"Failed to find drive to eject!"); return; } __block Error *err = NULL; with_bql(^{ qmp_eject([drive cStringUsingEncoding: NSASCIIStringEncoding], NULL, false, false, &err); }); handleAnyDeviceErrors(err); } /* Displays a dialog box asking the user to select an image file to load. * Uses sender's represented object value to figure out which drive to use. */ - (void)changeDeviceMedia:(id)sender { /* Find the drive name */ NSString * drive; drive = [sender representedObject]; if(drive == nil) { NSBeep(); QEMU_Alert(@"Could not find drive!"); return; } /* Display the file open dialog */ NSOpenPanel * openPanel; openPanel = [NSOpenPanel openPanel]; [openPanel setCanChooseFiles: YES]; [openPanel setAllowsMultipleSelection: NO]; if([openPanel runModal] == NSModalResponseOK) { NSString * file = [[[openPanel URLs] objectAtIndex: 0] path]; if(file == nil) { NSBeep(); QEMU_Alert(@"Failed to convert URL to file path!"); return; } __block Error *err = NULL; with_bql(^{ qmp_blockdev_change_medium([drive cStringUsingEncoding: NSASCIIStringEncoding], NULL, [file cStringUsingEncoding: NSASCIIStringEncoding], "raw", true, false, false, 0, &err); }); handleAnyDeviceErrors(err); } } /* Verifies if the user really wants to quit */ - (BOOL)verifyQuit { NSAlert *alert = [NSAlert new]; [alert autorelease]; [alert setMessageText: @"Are you sure you want to quit QEMU?"]; [alert addButtonWithTitle: @"Cancel"]; [alert addButtonWithTitle: @"Quit"]; if([alert runModal] == NSAlertSecondButtonReturn) { return YES; } else { return NO; } } /* The action method for the About menu item */ - (IBAction) do_about_menu_item: (id) sender { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; char *icon_path_c = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/512x512/apps/qemu.png"); NSString *icon_path = [NSString stringWithUTF8String:icon_path_c]; g_free(icon_path_c); NSImage *icon = [[NSImage alloc] initWithContentsOfFile:icon_path]; NSString *version = @"QEMU emulator version " QEMU_FULL_VERSION; NSString *copyright = @QEMU_COPYRIGHT; NSDictionary *options; if (icon) { options = @{ NSAboutPanelOptionApplicationIcon : icon, NSAboutPanelOptionApplicationVersion : version, @"Copyright" : copyright, }; [icon release]; } else { options = @{ NSAboutPanelOptionApplicationVersion : version, @"Copyright" : copyright, }; } [NSApp orderFrontStandardAboutPanelWithOptions:options]; [pool release]; } /* Used by the Speed menu items */ - (void)adjustSpeed:(id)sender { int throttle_pct; /* throttle percentage */ NSMenu *menu; menu = [sender menu]; if (menu != nil) { /* Unselect the currently selected item */ for (NSMenuItem *item in [menu itemArray]) { if (item.state == NSControlStateValueOn) { [item setState: NSControlStateValueOff]; break; } } } // check the menu item [sender setState: NSControlStateValueOn]; // get the throttle percentage throttle_pct = [sender tag]; with_bql(^{ cpu_throttle_set(throttle_pct); }); COCOA_DEBUG("cpu throttling at %d%c\n", cpu_throttle_get_percentage(), '%'); } @end @interface QemuApplication : NSApplication @end @implementation QemuApplication - (void)sendEvent:(NSEvent *)event { COCOA_DEBUG("QemuApplication: sendEvent\n"); if (![cocoaView handleEvent:event]) { [super sendEvent: event]; } } @end static void create_initial_menus(void) { // Add menus NSMenu *menu; NSMenuItem *menuItem; [NSApp setMainMenu:[[NSMenu alloc] init]]; [NSApp setServicesMenu:[[NSMenu alloc] initWithTitle:@"Services"]]; // Application menu menu = [[NSMenu alloc] initWithTitle:@""]; [menu addItemWithTitle:@"About QEMU" action:@selector(do_about_menu_item:) keyEquivalent:@""]; // About QEMU [menu addItem:[NSMenuItem separatorItem]]; //Separator menuItem = [menu addItemWithTitle:@"Services" action:nil keyEquivalent:@""]; [menuItem setSubmenu:[NSApp servicesMenu]]; [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:@"Hide QEMU" action:@selector(hide:) keyEquivalent:@"h"]; //Hide QEMU menuItem = (NSMenuItem *)[menu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; // Hide Others [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption|NSEventModifierFlagCommand)]; [menu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; // Show All [menu addItem:[NSMenuItem separatorItem]]; //Separator [menu addItemWithTitle:@"Quit QEMU" action:@selector(terminate:) keyEquivalent:@"q"]; menuItem = [[NSMenuItem alloc] initWithTitle:@"Apple" action:nil keyEquivalent:@""]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; [NSApp performSelector:@selector(setAppleMenu:) withObject:menu]; // Workaround (this method is private since 10.4+) // Machine menu menu = [[NSMenu alloc] initWithTitle: @"Machine"]; [menu setAutoenablesItems: NO]; [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Pause" action: @selector(pauseQEMU:) keyEquivalent: @""] autorelease]]; menuItem = [[[NSMenuItem alloc] initWithTitle: @"Resume" action: @selector(resumeQEMU:) keyEquivalent: @""] autorelease]; [menu addItem: menuItem]; [menuItem setEnabled: NO]; [menu addItem: [NSMenuItem separatorItem]]; [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Reset" action: @selector(restartQEMU:) keyEquivalent: @""] autorelease]]; [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Power Down" action: @selector(powerDownQEMU:) keyEquivalent: @""] autorelease]]; menuItem = [[[NSMenuItem alloc] initWithTitle: @"Machine" action:nil keyEquivalent:@""] autorelease]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; // View menu 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]; [menu addItem: menuItem]; menuItem = [[[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""] autorelease]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; // Speed menu menu = [[NSMenu alloc] initWithTitle:@"Speed"]; // Add the rest of the Speed menu items int p, percentage, throttle_pct; for (p = 10; p >= 0; p--) { percentage = p * 10 > 1 ? p * 10 : 1; // prevent a 0% menu item menuItem = [[[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"%d%%", percentage] action:@selector(adjustSpeed:) keyEquivalent:@""] autorelease]; if (percentage == 100) { [menuItem setState: NSControlStateValueOn]; } /* Calculate the throttle percentage */ throttle_pct = -1 * percentage + 100; [menuItem setTag: throttle_pct]; [menu addItem: menuItem]; } menuItem = [[[NSMenuItem alloc] initWithTitle:@"Speed" action:nil keyEquivalent:@""] autorelease]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; // Window menu menu = [[NSMenu alloc] initWithTitle:@"Window"]; [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease]]; // Miniaturize menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; [NSApp setWindowsMenu:menu]; // Help menu menu = [[NSMenu alloc] initWithTitle:@"Help"]; [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"QEMU Documentation" action:@selector(showQEMUDoc:) keyEquivalent:@"?"] autorelease]]; // QEMU Help menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease]; [menuItem setSubmenu:menu]; [[NSApp mainMenu] addItem:menuItem]; } /* Returns a name for a given console */ static NSString * getConsoleName(QemuConsole * console) { g_autofree char *label = qemu_console_get_label(console); return [NSString stringWithUTF8String:label]; } /* Add an entry to the View menu for each console */ static void add_console_menu_entries(void) { NSMenu *menu; NSMenuItem *menuItem; int index = 0; menu = [[[NSApp mainMenu] itemWithTitle:@"View"] submenu]; [menu addItem:[NSMenuItem separatorItem]]; while (qemu_console_lookup_by_index(index) != NULL) { menuItem = [[[NSMenuItem alloc] initWithTitle: getConsoleName(qemu_console_lookup_by_index(index)) action: @selector(displayConsole:) keyEquivalent: @""] autorelease]; [menuItem setTag: index]; [menu addItem: menuItem]; index++; } } /* Make menu items for all removable devices. * Each device is given an 'Eject' and 'Change' menu item. */ static void addRemovableDevicesMenuItems(void) { NSMenu *menu; NSMenuItem *menuItem; BlockInfoList *currentDevice, *pointerToFree; NSString *deviceName; currentDevice = qmp_query_block(NULL); pointerToFree = currentDevice; menu = [[[NSApp mainMenu] itemWithTitle:@"Machine"] submenu]; // Add a separator between related groups of menu items [menu addItem:[NSMenuItem separatorItem]]; // Set the attributes to the "Removable Media" menu item NSString *titleString = @"Removable Media"; NSMutableAttributedString *attString=[[NSMutableAttributedString alloc] initWithString:titleString]; NSColor *newColor = [NSColor blackColor]; NSFontManager *fontManager = [NSFontManager sharedFontManager]; NSFont *font = [fontManager fontWithFamily:@"Helvetica" traits:NSBoldFontMask|NSItalicFontMask weight:0 size:14]; [attString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [titleString length])]; [attString addAttribute:NSForegroundColorAttributeName value:newColor range:NSMakeRange(0, [titleString length])]; [attString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt: 1] range:NSMakeRange(0, [titleString length])]; // Add the "Removable Media" menu item menuItem = [NSMenuItem new]; [menuItem setAttributedTitle: attString]; [menuItem setEnabled: NO]; [menu addItem: menuItem]; /* Loop through all the block devices in the emulator */ while (currentDevice) { deviceName = [[NSString stringWithFormat: @"%s", currentDevice->value->device] retain]; if(currentDevice->value->removable) { menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Change %s...", currentDevice->value->device] action: @selector(changeDeviceMedia:) keyEquivalent: @""]; [menu addItem: menuItem]; [menuItem setRepresentedObject: deviceName]; [menuItem autorelease]; menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Eject %s", currentDevice->value->device] action: @selector(ejectDeviceMedia:) keyEquivalent: @""]; [menu addItem: menuItem]; [menuItem setRepresentedObject: deviceName]; [menuItem autorelease]; } currentDevice = currentDevice->next; } qapi_free_BlockInfoList(pointerToFree); } @interface QemuCocoaPasteboardTypeOwner : NSObject @end @implementation QemuCocoaPasteboardTypeOwner - (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSPasteboardType)type { if (type != NSPasteboardTypeString) { return; } with_bql(^{ QemuClipboardInfo *info = qemu_clipboard_info_ref(cbinfo); qemu_event_reset(&cbevent); qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT); while (info == cbinfo && info->types[QEMU_CLIPBOARD_TYPE_TEXT].available && info->types[QEMU_CLIPBOARD_TYPE_TEXT].data == NULL) { bql_unlock(); qemu_event_wait(&cbevent); bql_lock(); } if (info == cbinfo) { NSData *data = [[NSData alloc] initWithBytes:info->types[QEMU_CLIPBOARD_TYPE_TEXT].data length:info->types[QEMU_CLIPBOARD_TYPE_TEXT].size]; [sender setData:data forType:NSPasteboardTypeString]; [data release]; } qemu_clipboard_info_unref(info); }); } @end static QemuCocoaPasteboardTypeOwner *cbowner; static void cocoa_clipboard_notify(Notifier *notifier, void *data); static void cocoa_clipboard_request(QemuClipboardInfo *info, QemuClipboardType type); static QemuClipboardPeer cbpeer = { .name = "cocoa", .notifier = { .notify = cocoa_clipboard_notify }, .request = cocoa_clipboard_request }; static void cocoa_clipboard_update_info(QemuClipboardInfo *info) { if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { return; } if (info != cbinfo) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; qemu_clipboard_info_unref(cbinfo); cbinfo = qemu_clipboard_info_ref(info); cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner]; [pool release]; } qemu_event_set(&cbevent); } static void cocoa_clipboard_notify(Notifier *notifier, void *data) { QemuClipboardNotify *notify = data; switch (notify->type) { case QEMU_CLIPBOARD_UPDATE_INFO: cocoa_clipboard_update_info(notify->info); return; case QEMU_CLIPBOARD_RESET_SERIAL: /* ignore */ return; } } static void cocoa_clipboard_request(QemuClipboardInfo *info, QemuClipboardType type) { NSAutoreleasePool *pool; NSData *text; switch (type) { case QEMU_CLIPBOARD_TYPE_TEXT: pool = [[NSAutoreleasePool alloc] init]; text = [[NSPasteboard generalPasteboard] dataForType:NSPasteboardTypeString]; if (text) { qemu_clipboard_set_data(&cbpeer, info, type, [text length], [text bytes], true); } [pool release]; break; default: break; } } /* * The startup process for the OSX/Cocoa UI is complicated, because * OSX insists that the UI runs on the initial main thread, and so we * need to start a second thread which runs the qemu_default_main(): * in main(): * in cocoa_display_init(): * assign cocoa_main to qemu_main * create application, menus, etc * in cocoa_main(): * create qemu-main thread * enter OSX run loop */ static void *call_qemu_main(void *opaque) { int status; COCOA_DEBUG("Second thread: calling qemu_default_main()\n"); bql_lock(); status = qemu_default_main(); bql_unlock(); COCOA_DEBUG("Second thread: qemu_default_main() returned, exiting\n"); [cbowner release]; exit(status); } static int cocoa_main(void) { QemuThread thread; COCOA_DEBUG("Entered %s()\n", __func__); bql_unlock(); qemu_thread_create(&thread, "qemu_main", call_qemu_main, NULL, QEMU_THREAD_DETACHED); // Start the main event loop COCOA_DEBUG("Main thread: entering OSX run loop\n"); [NSApp run]; COCOA_DEBUG("Main thread: left OSX run loop, which should never happen\n"); abort(); } #pragma mark qemu static void cocoa_update(DisplayChangeListener *dcl, int x, int y, int w, int h) { 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]); } [cocoaView setNeedsDisplayInRect:rect]; }); } static void cocoa_switch(DisplayChangeListener *dcl, DisplaySurface *surface) { pixman_image_t *image = surface->image; COCOA_DEBUG("qemu_cocoa: cocoa_switch\n"); // The DisplaySurface will be freed as soon as this callback returns. // We take a reference to the underlying pixman image here so it does // not disappear from under our feet; the switchSurface method will // deref the old image when it is done with it. pixman_image_ref(image); dispatch_async(dispatch_get_main_queue(), ^{ [cocoaView updateUIInfo]; [cocoaView switchSurface:image]; }); } static void cocoa_refresh(DisplayChangeListener *dcl) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; COCOA_DEBUG("qemu_cocoa: cocoa_refresh\n"); graphic_hw_update(NULL); if (qemu_input_is_absolute(dcl->con)) { dispatch_async(dispatch_get_main_queue(), ^{ if (![cocoaView isAbsoluteEnabled]) { if ([cocoaView isMouseGrabbed]) { [cocoaView ungrabMouse]; } } [cocoaView setAbsoluteEnabled:YES]; }); } if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) { qemu_clipboard_info_unref(cbinfo); cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) { cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; } qemu_clipboard_update(cbinfo); cbchangecount = [[NSPasteboard generalPasteboard] changeCount]; qemu_event_set(&cbevent); } [pool release]; } static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; COCOA_DEBUG("qemu_cocoa: cocoa_display_init\n"); qemu_main = cocoa_main; // Pull this console process up to being a fully-fledged graphical // app with a menubar and Dock icon ProcessSerialNumber psn = { 0, kCurrentProcess }; TransformProcessType(&psn, kProcessTransformToForegroundApplication); [QemuApplication sharedApplication]; // Create an Application controller QemuCocoaAppController *controller = [[QemuCocoaAppController alloc] init]; [NSApp setDelegate:controller]; /* if fullscreen mode is to be used */ if (opts->has_full_screen && opts->full_screen) { [NSApp activateIgnoringOtherApps: YES]; [controller toggleFullScreen: nil]; } if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) { [controller setFullGrab: nil]; } if (opts->has_show_cursor && opts->show_cursor) { cursor_hide = 0; } if (opts->u.cocoa.has_swap_opt_cmd) { swap_opt_cmd = opts->u.cocoa.swap_opt_cmd; } if (opts->u.cocoa.has_left_command_key && !opts->u.cocoa.left_command_key) { left_command_key_enabled = 0; } if (opts->u.cocoa.has_zoom_to_fit && opts->u.cocoa.zoom_to_fit) { stretch_video = true; } create_initial_menus(); /* * Create the menu entries which depend on QEMU state (for consoles * and removable devices). These make calls back into QEMU functions, * which is OK because at this point we know that the second thread * holds the BQL and is synchronously waiting for us to * finish. */ add_console_menu_entries(); addRemovableDevicesMenuItems(); // register vga output callbacks register_displaychangelistener(&dcl); qemu_event_init(&cbevent, false); cbowner = [[QemuCocoaPasteboardTypeOwner alloc] init]; qemu_clipboard_peer_register(&cbpeer); [pool release]; } static QemuDisplay qemu_display_cocoa = { .type = DISPLAY_TYPE_COCOA, .init = cocoa_display_init, }; static void register_cocoa(void) { qemu_display_register(&qemu_display_cocoa); } type_init(register_cocoa);