1 /* 2 * QEMU Cocoa CG display driver 3 * 4 * Copyright (c) 2008 Mike Kronenberg 5 * 6 * Permission is hereby granted, free of charge, to any person obtaining a copy 7 * of this software and associated documentation files (the "Software"), to deal 8 * in the Software without restriction, including without limitation the rights 9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 * copies of the Software, and to permit persons to whom the Software is 11 * furnished to do so, subject to the following conditions: 12 * 13 * The above copyright notice and this permission notice shall be included in 14 * all copies or substantial portions of the Software. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 * THE SOFTWARE. 23 */ 24 25 #include "qemu/osdep.h" 26 27 #import <Cocoa/Cocoa.h> 28 #include <crt_externs.h> 29 30 #include "qemu/help-texts.h" 31 #include "qemu-main.h" 32 #include "ui/clipboard.h" 33 #include "ui/console.h" 34 #include "ui/input.h" 35 #include "ui/kbd-state.h" 36 #include "sysemu/sysemu.h" 37 #include "sysemu/runstate.h" 38 #include "sysemu/runstate-action.h" 39 #include "sysemu/cpu-throttle.h" 40 #include "qapi/error.h" 41 #include "qapi/qapi-commands-block.h" 42 #include "qapi/qapi-commands-machine.h" 43 #include "qapi/qapi-commands-misc.h" 44 #include "sysemu/blockdev.h" 45 #include "qemu-version.h" 46 #include "qemu/cutils.h" 47 #include "qemu/main-loop.h" 48 #include "qemu/module.h" 49 #include "qemu/error-report.h" 50 #include <Carbon/Carbon.h> 51 #include "hw/core/cpu.h" 52 53 #ifndef MAC_OS_X_VERSION_10_13 54 #define MAC_OS_X_VERSION_10_13 101300 55 #endif 56 57 #ifndef MAC_OS_VERSION_14_0 58 #define MAC_OS_VERSION_14_0 140000 59 #endif 60 61 /* 10.14 deprecates NSOnState and NSOffState in favor of 62 * NSControlStateValueOn/Off, which were introduced in 10.13. 63 * Define for older versions 64 */ 65 #if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13 66 #define NSControlStateValueOn NSOnState 67 #define NSControlStateValueOff NSOffState 68 #endif 69 70 //#define DEBUG 71 72 #ifdef DEBUG 73 #define COCOA_DEBUG(...) { (void) fprintf (stdout, __VA_ARGS__); } 74 #else 75 #define COCOA_DEBUG(...) ((void) 0) 76 #endif 77 78 #define cgrect(nsrect) (*(CGRect *)&(nsrect)) 79 80 #define UC_CTRL_KEY "\xe2\x8c\x83" 81 #define UC_ALT_KEY "\xe2\x8c\xa5" 82 83 typedef struct { 84 int width; 85 int height; 86 } QEMUScreen; 87 88 static void cocoa_update(DisplayChangeListener *dcl, 89 int x, int y, int w, int h); 90 91 static void cocoa_switch(DisplayChangeListener *dcl, 92 DisplaySurface *surface); 93 94 static void cocoa_refresh(DisplayChangeListener *dcl); 95 96 static const DisplayChangeListenerOps dcl_ops = { 97 .dpy_name = "cocoa", 98 .dpy_gfx_update = cocoa_update, 99 .dpy_gfx_switch = cocoa_switch, 100 .dpy_refresh = cocoa_refresh, 101 }; 102 static DisplayChangeListener dcl = { 103 .ops = &dcl_ops, 104 }; 105 static QKbdState *kbd; 106 static int cursor_hide = 1; 107 static int left_command_key_enabled = 1; 108 static bool swap_opt_cmd; 109 110 static CGInterpolationQuality zoom_interpolation = kCGInterpolationNone; 111 static NSTextField *pauseLabel; 112 113 static bool allow_events; 114 115 static NSInteger cbchangecount = -1; 116 static QemuClipboardInfo *cbinfo; 117 static QemuEvent cbevent; 118 119 // Utility functions to run specified code block with the BQL held 120 typedef void (^CodeBlock)(void); 121 typedef bool (^BoolCodeBlock)(void); 122 123 static void with_bql(CodeBlock block) 124 { 125 bool locked = bql_locked(); 126 if (!locked) { 127 bql_lock(); 128 } 129 block(); 130 if (!locked) { 131 bql_unlock(); 132 } 133 } 134 135 static bool bool_with_bql(BoolCodeBlock block) 136 { 137 bool locked = bql_locked(); 138 bool val; 139 140 if (!locked) { 141 bql_lock(); 142 } 143 val = block(); 144 if (!locked) { 145 bql_unlock(); 146 } 147 return val; 148 } 149 150 // Mac to QKeyCode conversion 151 static const int mac_to_qkeycode_map[] = { 152 [kVK_ANSI_A] = Q_KEY_CODE_A, 153 [kVK_ANSI_B] = Q_KEY_CODE_B, 154 [kVK_ANSI_C] = Q_KEY_CODE_C, 155 [kVK_ANSI_D] = Q_KEY_CODE_D, 156 [kVK_ANSI_E] = Q_KEY_CODE_E, 157 [kVK_ANSI_F] = Q_KEY_CODE_F, 158 [kVK_ANSI_G] = Q_KEY_CODE_G, 159 [kVK_ANSI_H] = Q_KEY_CODE_H, 160 [kVK_ANSI_I] = Q_KEY_CODE_I, 161 [kVK_ANSI_J] = Q_KEY_CODE_J, 162 [kVK_ANSI_K] = Q_KEY_CODE_K, 163 [kVK_ANSI_L] = Q_KEY_CODE_L, 164 [kVK_ANSI_M] = Q_KEY_CODE_M, 165 [kVK_ANSI_N] = Q_KEY_CODE_N, 166 [kVK_ANSI_O] = Q_KEY_CODE_O, 167 [kVK_ANSI_P] = Q_KEY_CODE_P, 168 [kVK_ANSI_Q] = Q_KEY_CODE_Q, 169 [kVK_ANSI_R] = Q_KEY_CODE_R, 170 [kVK_ANSI_S] = Q_KEY_CODE_S, 171 [kVK_ANSI_T] = Q_KEY_CODE_T, 172 [kVK_ANSI_U] = Q_KEY_CODE_U, 173 [kVK_ANSI_V] = Q_KEY_CODE_V, 174 [kVK_ANSI_W] = Q_KEY_CODE_W, 175 [kVK_ANSI_X] = Q_KEY_CODE_X, 176 [kVK_ANSI_Y] = Q_KEY_CODE_Y, 177 [kVK_ANSI_Z] = Q_KEY_CODE_Z, 178 179 [kVK_ANSI_0] = Q_KEY_CODE_0, 180 [kVK_ANSI_1] = Q_KEY_CODE_1, 181 [kVK_ANSI_2] = Q_KEY_CODE_2, 182 [kVK_ANSI_3] = Q_KEY_CODE_3, 183 [kVK_ANSI_4] = Q_KEY_CODE_4, 184 [kVK_ANSI_5] = Q_KEY_CODE_5, 185 [kVK_ANSI_6] = Q_KEY_CODE_6, 186 [kVK_ANSI_7] = Q_KEY_CODE_7, 187 [kVK_ANSI_8] = Q_KEY_CODE_8, 188 [kVK_ANSI_9] = Q_KEY_CODE_9, 189 190 [kVK_ANSI_Grave] = Q_KEY_CODE_GRAVE_ACCENT, 191 [kVK_ANSI_Minus] = Q_KEY_CODE_MINUS, 192 [kVK_ANSI_Equal] = Q_KEY_CODE_EQUAL, 193 [kVK_Delete] = Q_KEY_CODE_BACKSPACE, 194 [kVK_CapsLock] = Q_KEY_CODE_CAPS_LOCK, 195 [kVK_Tab] = Q_KEY_CODE_TAB, 196 [kVK_Return] = Q_KEY_CODE_RET, 197 [kVK_ANSI_LeftBracket] = Q_KEY_CODE_BRACKET_LEFT, 198 [kVK_ANSI_RightBracket] = Q_KEY_CODE_BRACKET_RIGHT, 199 [kVK_ANSI_Backslash] = Q_KEY_CODE_BACKSLASH, 200 [kVK_ANSI_Semicolon] = Q_KEY_CODE_SEMICOLON, 201 [kVK_ANSI_Quote] = Q_KEY_CODE_APOSTROPHE, 202 [kVK_ANSI_Comma] = Q_KEY_CODE_COMMA, 203 [kVK_ANSI_Period] = Q_KEY_CODE_DOT, 204 [kVK_ANSI_Slash] = Q_KEY_CODE_SLASH, 205 [kVK_Space] = Q_KEY_CODE_SPC, 206 207 [kVK_ANSI_Keypad0] = Q_KEY_CODE_KP_0, 208 [kVK_ANSI_Keypad1] = Q_KEY_CODE_KP_1, 209 [kVK_ANSI_Keypad2] = Q_KEY_CODE_KP_2, 210 [kVK_ANSI_Keypad3] = Q_KEY_CODE_KP_3, 211 [kVK_ANSI_Keypad4] = Q_KEY_CODE_KP_4, 212 [kVK_ANSI_Keypad5] = Q_KEY_CODE_KP_5, 213 [kVK_ANSI_Keypad6] = Q_KEY_CODE_KP_6, 214 [kVK_ANSI_Keypad7] = Q_KEY_CODE_KP_7, 215 [kVK_ANSI_Keypad8] = Q_KEY_CODE_KP_8, 216 [kVK_ANSI_Keypad9] = Q_KEY_CODE_KP_9, 217 [kVK_ANSI_KeypadDecimal] = Q_KEY_CODE_KP_DECIMAL, 218 [kVK_ANSI_KeypadEnter] = Q_KEY_CODE_KP_ENTER, 219 [kVK_ANSI_KeypadPlus] = Q_KEY_CODE_KP_ADD, 220 [kVK_ANSI_KeypadMinus] = Q_KEY_CODE_KP_SUBTRACT, 221 [kVK_ANSI_KeypadMultiply] = Q_KEY_CODE_KP_MULTIPLY, 222 [kVK_ANSI_KeypadDivide] = Q_KEY_CODE_KP_DIVIDE, 223 [kVK_ANSI_KeypadEquals] = Q_KEY_CODE_KP_EQUALS, 224 [kVK_ANSI_KeypadClear] = Q_KEY_CODE_NUM_LOCK, 225 226 [kVK_UpArrow] = Q_KEY_CODE_UP, 227 [kVK_DownArrow] = Q_KEY_CODE_DOWN, 228 [kVK_LeftArrow] = Q_KEY_CODE_LEFT, 229 [kVK_RightArrow] = Q_KEY_CODE_RIGHT, 230 231 [kVK_Help] = Q_KEY_CODE_INSERT, 232 [kVK_Home] = Q_KEY_CODE_HOME, 233 [kVK_PageUp] = Q_KEY_CODE_PGUP, 234 [kVK_PageDown] = Q_KEY_CODE_PGDN, 235 [kVK_End] = Q_KEY_CODE_END, 236 [kVK_ForwardDelete] = Q_KEY_CODE_DELETE, 237 238 [kVK_Escape] = Q_KEY_CODE_ESC, 239 240 /* The Power key can't be used directly because the operating system uses 241 * it. This key can be emulated by using it in place of another key such as 242 * F1. Don't forget to disable the real key binding. 243 */ 244 /* [kVK_F1] = Q_KEY_CODE_POWER, */ 245 246 [kVK_F1] = Q_KEY_CODE_F1, 247 [kVK_F2] = Q_KEY_CODE_F2, 248 [kVK_F3] = Q_KEY_CODE_F3, 249 [kVK_F4] = Q_KEY_CODE_F4, 250 [kVK_F5] = Q_KEY_CODE_F5, 251 [kVK_F6] = Q_KEY_CODE_F6, 252 [kVK_F7] = Q_KEY_CODE_F7, 253 [kVK_F8] = Q_KEY_CODE_F8, 254 [kVK_F9] = Q_KEY_CODE_F9, 255 [kVK_F10] = Q_KEY_CODE_F10, 256 [kVK_F11] = Q_KEY_CODE_F11, 257 [kVK_F12] = Q_KEY_CODE_F12, 258 [kVK_F13] = Q_KEY_CODE_PRINT, 259 [kVK_F14] = Q_KEY_CODE_SCROLL_LOCK, 260 [kVK_F15] = Q_KEY_CODE_PAUSE, 261 262 // JIS keyboards only 263 [kVK_JIS_Yen] = Q_KEY_CODE_YEN, 264 [kVK_JIS_Underscore] = Q_KEY_CODE_RO, 265 [kVK_JIS_KeypadComma] = Q_KEY_CODE_KP_COMMA, 266 [kVK_JIS_Eisu] = Q_KEY_CODE_MUHENKAN, 267 [kVK_JIS_Kana] = Q_KEY_CODE_HENKAN, 268 269 /* 270 * The eject and volume keys can't be used here because they are handled at 271 * a lower level than what an Application can see. 272 */ 273 }; 274 275 static int cocoa_keycode_to_qemu(int keycode) 276 { 277 if (ARRAY_SIZE(mac_to_qkeycode_map) <= keycode) { 278 error_report("(cocoa) warning unknown keycode 0x%x", keycode); 279 return 0; 280 } 281 return mac_to_qkeycode_map[keycode]; 282 } 283 284 /* Displays an alert dialog box with the specified message */ 285 static void QEMU_Alert(NSString *message) 286 { 287 NSAlert *alert; 288 alert = [NSAlert new]; 289 [alert setMessageText: message]; 290 [alert runModal]; 291 } 292 293 /* Handles any errors that happen with a device transaction */ 294 static void handleAnyDeviceErrors(Error * err) 295 { 296 if (err) { 297 QEMU_Alert([NSString stringWithCString: error_get_pretty(err) 298 encoding: NSASCIIStringEncoding]); 299 error_free(err); 300 } 301 } 302 303 /* 304 ------------------------------------------------------ 305 QemuCocoaView 306 ------------------------------------------------------ 307 */ 308 @interface QemuCocoaView : NSView 309 { 310 QEMUScreen screen; 311 pixman_image_t *pixman_image; 312 BOOL isMouseGrabbed; 313 BOOL isAbsoluteEnabled; 314 CFMachPortRef eventsTap; 315 } 316 - (void) switchSurface:(pixman_image_t *)image; 317 - (void) grabMouse; 318 - (void) ungrabMouse; 319 - (void) setFullGrab:(id)sender; 320 - (void) handleMonitorInput:(NSEvent *)event; 321 - (bool) handleEvent:(NSEvent *)event; 322 - (bool) handleEventLocked:(NSEvent *)event; 323 - (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled; 324 /* The state surrounding mouse grabbing is potentially confusing. 325 * isAbsoluteEnabled tracks qemu_input_is_absolute() [ie "is the emulated 326 * pointing device an absolute-position one?"], but is only updated on 327 * next refresh. 328 * isMouseGrabbed tracks whether GUI events are directed to the guest; 329 * it controls whether special keys like Cmd get sent to the guest, 330 * and whether we capture the mouse when in non-absolute mode. 331 */ 332 - (BOOL) isMouseGrabbed; 333 - (BOOL) isAbsoluteEnabled; 334 - (QEMUScreen) gscreen; 335 - (void) raiseAllKeys; 336 @end 337 338 QemuCocoaView *cocoaView; 339 340 static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEventRef cgEvent, void *userInfo) 341 { 342 QemuCocoaView *view = userInfo; 343 NSEvent *event = [NSEvent eventWithCGEvent:cgEvent]; 344 if ([view isMouseGrabbed] && [view handleEvent:event]) { 345 COCOA_DEBUG("Global events tap: qemu handled the event, capturing!\n"); 346 return NULL; 347 } 348 COCOA_DEBUG("Global events tap: qemu did not handle the event, letting it through...\n"); 349 350 return cgEvent; 351 } 352 353 @implementation QemuCocoaView 354 - (id)initWithFrame:(NSRect)frameRect 355 { 356 COCOA_DEBUG("QemuCocoaView: initWithFrame\n"); 357 358 self = [super initWithFrame:frameRect]; 359 if (self) { 360 361 NSTrackingAreaOptions options = NSTrackingActiveInKeyWindow | 362 NSTrackingMouseEnteredAndExited | 363 NSTrackingMouseMoved | 364 NSTrackingInVisibleRect; 365 366 NSTrackingArea *trackingArea = 367 [[NSTrackingArea alloc] initWithRect:CGRectZero 368 options:options 369 owner:self 370 userInfo:nil]; 371 372 [self addTrackingArea:trackingArea]; 373 [trackingArea release]; 374 screen.width = frameRect.size.width; 375 screen.height = frameRect.size.height; 376 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_14_0 377 [self setClipsToBounds:YES]; 378 #endif 379 380 } 381 return self; 382 } 383 384 - (void) dealloc 385 { 386 COCOA_DEBUG("QemuCocoaView: dealloc\n"); 387 388 if (pixman_image) { 389 pixman_image_unref(pixman_image); 390 } 391 392 if (eventsTap) { 393 CFRelease(eventsTap); 394 } 395 396 [super dealloc]; 397 } 398 399 - (BOOL) isOpaque 400 { 401 return YES; 402 } 403 404 - (void) viewDidMoveToWindow 405 { 406 [self resizeWindow]; 407 } 408 409 - (void) selectConsoleLocked:(unsigned int)index 410 { 411 QemuConsole *con = qemu_console_lookup_by_index(index); 412 if (!con) { 413 return; 414 } 415 416 unregister_displaychangelistener(&dcl); 417 qkbd_state_switch_console(kbd, con); 418 dcl.con = con; 419 register_displaychangelistener(&dcl); 420 [self updateUIInfo]; 421 } 422 423 - (void) hideCursor 424 { 425 if (!cursor_hide) { 426 return; 427 } 428 [NSCursor hide]; 429 } 430 431 - (void) unhideCursor 432 { 433 if (!cursor_hide) { 434 return; 435 } 436 [NSCursor unhide]; 437 } 438 439 - (void) drawRect:(NSRect) rect 440 { 441 COCOA_DEBUG("QemuCocoaView: drawRect\n"); 442 443 // get CoreGraphic context 444 CGContextRef viewContextRef = [[NSGraphicsContext currentContext] CGContext]; 445 446 CGContextSetInterpolationQuality (viewContextRef, zoom_interpolation); 447 CGContextSetShouldAntialias (viewContextRef, NO); 448 449 // draw screen bitmap directly to Core Graphics context 450 if (!pixman_image) { 451 // Draw request before any guest device has set up a framebuffer: 452 // just draw an opaque black rectangle 453 CGContextSetRGBFillColor(viewContextRef, 0, 0, 0, 1.0); 454 CGContextFillRect(viewContextRef, NSRectToCGRect(rect)); 455 } else { 456 int w = pixman_image_get_width(pixman_image); 457 int h = pixman_image_get_height(pixman_image); 458 int bitsPerPixel = PIXMAN_FORMAT_BPP(pixman_image_get_format(pixman_image)); 459 int stride = pixman_image_get_stride(pixman_image); 460 CGDataProviderRef dataProviderRef = CGDataProviderCreateWithData( 461 NULL, 462 pixman_image_get_data(pixman_image), 463 stride * h, 464 NULL 465 ); 466 CGImageRef imageRef = CGImageCreate( 467 w, //width 468 h, //height 469 DIV_ROUND_UP(bitsPerPixel, 8) * 2, //bitsPerComponent 470 bitsPerPixel, //bitsPerPixel 471 stride, //bytesPerRow 472 CGColorSpaceCreateWithName(kCGColorSpaceSRGB), //colorspace 473 kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, //bitmapInfo 474 dataProviderRef, //provider 475 NULL, //decode 476 0, //interpolate 477 kCGRenderingIntentDefault //intent 478 ); 479 // selective drawing code (draws only dirty rectangles) (OS X >= 10.4) 480 const NSRect *rectList; 481 NSInteger rectCount; 482 int i; 483 CGImageRef clipImageRef; 484 CGRect clipRect; 485 486 [self getRectsBeingDrawn:&rectList count:&rectCount]; 487 for (i = 0; i < rectCount; i++) { 488 clipRect = rectList[i]; 489 clipRect.origin.y = (float)h - (clipRect.origin.y + clipRect.size.height); 490 clipImageRef = CGImageCreateWithImageInRect( 491 imageRef, 492 clipRect 493 ); 494 CGContextDrawImage (viewContextRef, cgrect(rectList[i]), clipImageRef); 495 CGImageRelease (clipImageRef); 496 } 497 CGImageRelease (imageRef); 498 CGDataProviderRelease(dataProviderRef); 499 } 500 } 501 502 - (NSSize)fixAspectRatio:(NSSize)max 503 { 504 NSSize scaled; 505 NSSize fixed; 506 507 scaled.width = screen.width * max.height; 508 scaled.height = screen.height * max.width; 509 510 /* 511 * Here screen is our guest's output size, and max is the size of the 512 * largest possible area of the screen we can display on. 513 * We want to scale up (screen.width x screen.height) by either: 514 * 1) max.height / screen.height 515 * 2) max.width / screen.width 516 * With the first scale factor the scale will result in an output height of 517 * max.height (i.e. we will fill the whole height of the available screen 518 * space and have black bars left and right) and with the second scale 519 * factor the scaling will result in an output width of max.width (i.e. we 520 * fill the whole width of the available screen space and have black bars 521 * top and bottom). We need to pick whichever keeps the whole of the guest 522 * output on the screen, which is to say the smaller of the two scale 523 * factors. 524 * To avoid doing more division than strictly necessary, instead of directly 525 * comparing scale factors 1 and 2 we instead calculate and compare those 526 * two scale factors multiplied by (screen.height * screen.width). 527 */ 528 if (scaled.width < scaled.height) { 529 fixed.width = scaled.width / screen.height; 530 fixed.height = max.height; 531 } else { 532 fixed.width = max.width; 533 fixed.height = scaled.height / screen.width; 534 } 535 536 return fixed; 537 } 538 539 - (NSSize) screenSafeAreaSize 540 { 541 NSSize size = [[[self window] screen] frame].size; 542 NSEdgeInsets insets = [[[self window] screen] safeAreaInsets]; 543 size.width -= insets.left + insets.right; 544 size.height -= insets.top + insets.bottom; 545 return size; 546 } 547 548 - (void) resizeWindow 549 { 550 [[self window] setContentAspectRatio:NSMakeSize(screen.width, screen.height)]; 551 552 if (!([[self window] styleMask] & NSWindowStyleMaskResizable)) { 553 [[self window] setContentSize:NSMakeSize(screen.width, screen.height)]; 554 [[self window] center]; 555 } else if ([[self window] styleMask] & NSWindowStyleMaskFullScreen) { 556 [[self window] setContentSize:[self fixAspectRatio:[self screenSafeAreaSize]]]; 557 [[self window] center]; 558 } else { 559 [[self window] setContentSize:[self fixAspectRatio:[self frame].size]]; 560 } 561 } 562 563 - (void) updateBounds 564 { 565 [self setBoundsSize:NSMakeSize(screen.width, screen.height)]; 566 } 567 568 - (void) updateUIInfoLocked 569 { 570 /* Must be called with the BQL, i.e. via updateUIInfo */ 571 NSSize frameSize; 572 QemuUIInfo info; 573 574 if (!qemu_console_is_graphic(dcl.con)) { 575 return; 576 } 577 578 if ([self window]) { 579 NSDictionary *description = [[[self window] screen] deviceDescription]; 580 CGDirectDisplayID display = [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]; 581 NSSize screenSize = [[[self window] screen] frame].size; 582 CGSize screenPhysicalSize = CGDisplayScreenSize(display); 583 bool isFullscreen = ([[self window] styleMask] & NSWindowStyleMaskFullScreen) != 0; 584 CVDisplayLinkRef displayLink; 585 586 frameSize = isFullscreen ? [self screenSafeAreaSize] : [self frame].size; 587 588 if (!CVDisplayLinkCreateWithCGDisplay(display, &displayLink)) { 589 CVTime period = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLink); 590 CVDisplayLinkRelease(displayLink); 591 if (!(period.flags & kCVTimeIsIndefinite)) { 592 update_displaychangelistener(&dcl, 593 1000 * period.timeValue / period.timeScale); 594 info.refresh_rate = (int64_t)1000 * period.timeScale / period.timeValue; 595 } 596 } 597 598 info.width_mm = frameSize.width / screenSize.width * screenPhysicalSize.width; 599 info.height_mm = frameSize.height / screenSize.height * screenPhysicalSize.height; 600 } else { 601 frameSize = [self frame].size; 602 info.width_mm = 0; 603 info.height_mm = 0; 604 } 605 606 info.xoff = 0; 607 info.yoff = 0; 608 info.width = frameSize.width; 609 info.height = frameSize.height; 610 611 dpy_set_ui_info(dcl.con, &info, TRUE); 612 } 613 614 - (void) updateUIInfo 615 { 616 if (!allow_events) { 617 /* 618 * Don't try to tell QEMU about UI information in the application 619 * startup phase -- we haven't yet registered dcl with the QEMU UI 620 * layer. 621 * When cocoa_display_init() does register the dcl, the UI layer 622 * will call cocoa_switch(), which will call updateUIInfo, so 623 * we don't lose any information here. 624 */ 625 return; 626 } 627 628 with_bql(^{ 629 [self updateUIInfoLocked]; 630 }); 631 } 632 633 - (void) switchSurface:(pixman_image_t *)image 634 { 635 COCOA_DEBUG("QemuCocoaView: switchSurface\n"); 636 637 int w = pixman_image_get_width(image); 638 int h = pixman_image_get_height(image); 639 640 if (w != screen.width || h != screen.height) { 641 // Resize before we trigger the redraw, or we'll redraw at the wrong size 642 COCOA_DEBUG("switchSurface: new size %d x %d\n", w, h); 643 screen.width = w; 644 screen.height = h; 645 [self resizeWindow]; 646 [self updateBounds]; 647 } 648 649 // update screenBuffer 650 if (pixman_image) { 651 pixman_image_unref(pixman_image); 652 } 653 654 pixman_image = image; 655 } 656 657 - (void) setFullGrab:(id)sender 658 { 659 COCOA_DEBUG("QemuCocoaView: setFullGrab\n"); 660 661 CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged); 662 eventsTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, 663 mask, handleTapEvent, self); 664 if (!eventsTap) { 665 warn_report("Could not create event tap, system key combos will not be captured.\n"); 666 return; 667 } else { 668 COCOA_DEBUG("Global events tap created! Will capture system key combos.\n"); 669 } 670 671 CFRunLoopRef runLoop = CFRunLoopGetCurrent(); 672 if (!runLoop) { 673 warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n"); 674 return; 675 } 676 677 CFRunLoopSourceRef tapEventsSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventsTap, 0); 678 if (!tapEventsSrc ) { 679 warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n"); 680 return; 681 } 682 683 CFRunLoopAddSource(runLoop, tapEventsSrc, kCFRunLoopDefaultMode); 684 CFRelease(tapEventsSrc); 685 } 686 687 - (void) toggleKey: (int)keycode { 688 qkbd_state_key_event(kbd, keycode, !qkbd_state_key_get(kbd, keycode)); 689 } 690 691 // Does the work of sending input to the monitor 692 - (void) handleMonitorInput:(NSEvent *)event 693 { 694 int keysym = 0; 695 int control_key = 0; 696 697 // if the control key is down 698 if ([event modifierFlags] & NSEventModifierFlagControl) { 699 control_key = 1; 700 } 701 702 /* translates Macintosh keycodes to QEMU's keysym */ 703 704 static const int without_control_translation[] = { 705 [0 ... 0xff] = 0, // invalid key 706 707 [kVK_UpArrow] = QEMU_KEY_UP, 708 [kVK_DownArrow] = QEMU_KEY_DOWN, 709 [kVK_RightArrow] = QEMU_KEY_RIGHT, 710 [kVK_LeftArrow] = QEMU_KEY_LEFT, 711 [kVK_Home] = QEMU_KEY_HOME, 712 [kVK_End] = QEMU_KEY_END, 713 [kVK_PageUp] = QEMU_KEY_PAGEUP, 714 [kVK_PageDown] = QEMU_KEY_PAGEDOWN, 715 [kVK_ForwardDelete] = QEMU_KEY_DELETE, 716 [kVK_Delete] = QEMU_KEY_BACKSPACE, 717 }; 718 719 static const int with_control_translation[] = { 720 [0 ... 0xff] = 0, // invalid key 721 722 [kVK_UpArrow] = QEMU_KEY_CTRL_UP, 723 [kVK_DownArrow] = QEMU_KEY_CTRL_DOWN, 724 [kVK_RightArrow] = QEMU_KEY_CTRL_RIGHT, 725 [kVK_LeftArrow] = QEMU_KEY_CTRL_LEFT, 726 [kVK_Home] = QEMU_KEY_CTRL_HOME, 727 [kVK_End] = QEMU_KEY_CTRL_END, 728 [kVK_PageUp] = QEMU_KEY_CTRL_PAGEUP, 729 [kVK_PageDown] = QEMU_KEY_CTRL_PAGEDOWN, 730 }; 731 732 if (control_key != 0) { /* If the control key is being used */ 733 if ([event keyCode] < ARRAY_SIZE(with_control_translation)) { 734 keysym = with_control_translation[[event keyCode]]; 735 } 736 } else { 737 if ([event keyCode] < ARRAY_SIZE(without_control_translation)) { 738 keysym = without_control_translation[[event keyCode]]; 739 } 740 } 741 742 // if not a key that needs translating 743 if (keysym == 0) { 744 NSString *ks = [event characters]; 745 if ([ks length] > 0) { 746 keysym = [ks characterAtIndex:0]; 747 } 748 } 749 750 if (keysym) { 751 QemuTextConsole *con = QEMU_TEXT_CONSOLE(dcl.con); 752 qemu_text_console_put_keysym(con, keysym); 753 } 754 } 755 756 - (bool) handleEvent:(NSEvent *)event 757 { 758 return bool_with_bql(^{ 759 return [self handleEventLocked:event]; 760 }); 761 } 762 763 - (bool) handleEventLocked:(NSEvent *)event 764 { 765 /* Return true if we handled the event, false if it should be given to OSX */ 766 COCOA_DEBUG("QemuCocoaView: handleEvent\n"); 767 InputButton button; 768 int keycode = 0; 769 NSUInteger modifiers = [event modifierFlags]; 770 771 /* 772 * Check -[NSEvent modifierFlags] here. 773 * 774 * There is a NSEventType for an event notifying the change of 775 * -[NSEvent modifierFlags], NSEventTypeFlagsChanged but these operations 776 * are performed for any events because a modifier state may change while 777 * the application is inactive (i.e. no events fire) and we don't want to 778 * wait for another modifier state change to detect such a change. 779 * 780 * NSEventModifierFlagCapsLock requires a special treatment. The other flags 781 * are handled in similar manners. 782 * 783 * NSEventModifierFlagCapsLock 784 * --------------------------- 785 * 786 * If CapsLock state is changed, "up" and "down" events will be fired in 787 * sequence, effectively updates CapsLock state on the guest. 788 * 789 * The other flags 790 * --------------- 791 * 792 * If a flag is not set, fire "up" events for all keys which correspond to 793 * the flag. Note that "down" events are not fired here because the flags 794 * checked here do not tell what exact keys are down. 795 * 796 * If one of the keys corresponding to a flag is down, we rely on 797 * -[NSEvent keyCode] of an event whose -[NSEvent type] is 798 * NSEventTypeFlagsChanged to know the exact key which is down, which has 799 * the following two downsides: 800 * - It does not work when the application is inactive as described above. 801 * - It malfactions *after* the modifier state is changed while the 802 * application is inactive. It is because -[NSEvent keyCode] does not tell 803 * if the key is up or down, and requires to infer the current state from 804 * the previous state. It is still possible to fix such a malfanction by 805 * completely leaving your hands from the keyboard, which hopefully makes 806 * this implementation usable enough. 807 */ 808 if (!!(modifiers & NSEventModifierFlagCapsLock) != 809 qkbd_state_modifier_get(kbd, QKBD_MOD_CAPSLOCK)) { 810 qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, true); 811 qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, false); 812 } 813 814 if (!(modifiers & NSEventModifierFlagShift)) { 815 qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT, false); 816 qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT_R, false); 817 } 818 if (!(modifiers & NSEventModifierFlagControl)) { 819 qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL, false); 820 qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL_R, false); 821 } 822 if (!(modifiers & NSEventModifierFlagOption)) { 823 if (swap_opt_cmd) { 824 qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false); 825 qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false); 826 } else { 827 qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); 828 qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); 829 } 830 } 831 if (!(modifiers & NSEventModifierFlagCommand)) { 832 if (swap_opt_cmd) { 833 qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false); 834 qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false); 835 } else { 836 qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false); 837 qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false); 838 } 839 } 840 841 switch ([event type]) { 842 case NSEventTypeFlagsChanged: 843 switch ([event keyCode]) { 844 case kVK_Shift: 845 if (!!(modifiers & NSEventModifierFlagShift)) { 846 [self toggleKey:Q_KEY_CODE_SHIFT]; 847 } 848 break; 849 850 case kVK_RightShift: 851 if (!!(modifiers & NSEventModifierFlagShift)) { 852 [self toggleKey:Q_KEY_CODE_SHIFT_R]; 853 } 854 break; 855 856 case kVK_Control: 857 if (!!(modifiers & NSEventModifierFlagControl)) { 858 [self toggleKey:Q_KEY_CODE_CTRL]; 859 } 860 break; 861 862 case kVK_RightControl: 863 if (!!(modifiers & NSEventModifierFlagControl)) { 864 [self toggleKey:Q_KEY_CODE_CTRL_R]; 865 } 866 break; 867 868 case kVK_Option: 869 if (!!(modifiers & NSEventModifierFlagOption)) { 870 if (swap_opt_cmd) { 871 [self toggleKey:Q_KEY_CODE_META_L]; 872 } else { 873 [self toggleKey:Q_KEY_CODE_ALT]; 874 } 875 } 876 break; 877 878 case kVK_RightOption: 879 if (!!(modifiers & NSEventModifierFlagOption)) { 880 if (swap_opt_cmd) { 881 [self toggleKey:Q_KEY_CODE_META_R]; 882 } else { 883 [self toggleKey:Q_KEY_CODE_ALT_R]; 884 } 885 } 886 break; 887 888 /* Don't pass command key changes to guest unless mouse is grabbed */ 889 case kVK_Command: 890 if (isMouseGrabbed && 891 !!(modifiers & NSEventModifierFlagCommand) && 892 left_command_key_enabled) { 893 if (swap_opt_cmd) { 894 [self toggleKey:Q_KEY_CODE_ALT]; 895 } else { 896 [self toggleKey:Q_KEY_CODE_META_L]; 897 } 898 } 899 break; 900 901 case kVK_RightCommand: 902 if (isMouseGrabbed && 903 !!(modifiers & NSEventModifierFlagCommand)) { 904 if (swap_opt_cmd) { 905 [self toggleKey:Q_KEY_CODE_ALT_R]; 906 } else { 907 [self toggleKey:Q_KEY_CODE_META_R]; 908 } 909 } 910 break; 911 } 912 return true; 913 case NSEventTypeKeyDown: 914 keycode = cocoa_keycode_to_qemu([event keyCode]); 915 916 // forward command key combos to the host UI unless the mouse is grabbed 917 if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) { 918 return false; 919 } 920 921 // default 922 923 // handle control + alt Key Combos (ctrl+alt+[1..9,g] is reserved for QEMU) 924 if (([event modifierFlags] & NSEventModifierFlagControl) && ([event modifierFlags] & NSEventModifierFlagOption)) { 925 NSString *keychar = [event charactersIgnoringModifiers]; 926 if ([keychar length] == 1) { 927 char key = [keychar characterAtIndex:0]; 928 switch (key) { 929 930 // enable graphic console 931 case '1' ... '9': 932 [self selectConsoleLocked:key - '0' - 1]; /* ascii math */ 933 return true; 934 935 // release the mouse grab 936 case 'g': 937 [self ungrabMouse]; 938 return true; 939 } 940 } 941 } 942 943 if (qemu_console_is_graphic(dcl.con)) { 944 qkbd_state_key_event(kbd, keycode, true); 945 } else { 946 [self handleMonitorInput: event]; 947 } 948 return true; 949 case NSEventTypeKeyUp: 950 keycode = cocoa_keycode_to_qemu([event keyCode]); 951 952 // don't pass the guest a spurious key-up if we treated this 953 // command-key combo as a host UI action 954 if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) { 955 return true; 956 } 957 958 if (qemu_console_is_graphic(dcl.con)) { 959 qkbd_state_key_event(kbd, keycode, false); 960 } 961 return true; 962 case NSEventTypeScrollWheel: 963 /* 964 * Send wheel events to the guest regardless of window focus. 965 * This is in-line with standard Mac OS X UI behaviour. 966 */ 967 968 /* Determine if this is a scroll up or scroll down event */ 969 if ([event deltaY] != 0) { 970 button = ([event deltaY] > 0) ? 971 INPUT_BUTTON_WHEEL_UP : INPUT_BUTTON_WHEEL_DOWN; 972 } else if ([event deltaX] != 0) { 973 button = ([event deltaX] > 0) ? 974 INPUT_BUTTON_WHEEL_LEFT : INPUT_BUTTON_WHEEL_RIGHT; 975 } else { 976 /* 977 * We shouldn't have got a scroll event when deltaY and delta Y 978 * are zero, hence no harm in dropping the event 979 */ 980 return true; 981 } 982 983 qemu_input_queue_btn(dcl.con, button, true); 984 qemu_input_event_sync(); 985 qemu_input_queue_btn(dcl.con, button, false); 986 qemu_input_event_sync(); 987 988 return true; 989 default: 990 return false; 991 } 992 } 993 994 - (void) handleMouseEvent:(NSEvent *)event button:(InputButton)button down:(bool)down 995 { 996 if (!isMouseGrabbed) { 997 return; 998 } 999 1000 with_bql(^{ 1001 qemu_input_queue_btn(dcl.con, button, down); 1002 }); 1003 1004 [self handleMouseEvent:event]; 1005 } 1006 1007 - (void) handleMouseEvent:(NSEvent *)event 1008 { 1009 if (!isMouseGrabbed) { 1010 return; 1011 } 1012 1013 with_bql(^{ 1014 if (isAbsoluteEnabled) { 1015 CGFloat d = (CGFloat)screen.height / [self frame].size.height; 1016 NSPoint p = [event locationInWindow]; 1017 1018 /* Note that the origin for Cocoa mouse coords is bottom left, not top left. */ 1019 qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x * d, 0, screen.width); 1020 qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y * d, 0, screen.height); 1021 } else { 1022 qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, [event deltaX]); 1023 qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, [event deltaY]); 1024 } 1025 1026 qemu_input_event_sync(); 1027 }); 1028 } 1029 1030 - (void) mouseExited:(NSEvent *)event 1031 { 1032 if (isAbsoluteEnabled && isMouseGrabbed) { 1033 [self ungrabMouse]; 1034 } 1035 } 1036 1037 - (void) mouseEntered:(NSEvent *)event 1038 { 1039 if (isAbsoluteEnabled && !isMouseGrabbed) { 1040 [self grabMouse]; 1041 } 1042 } 1043 1044 - (void) mouseMoved:(NSEvent *)event 1045 { 1046 [self handleMouseEvent:event]; 1047 } 1048 1049 - (void) mouseDown:(NSEvent *)event 1050 { 1051 [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:true]; 1052 } 1053 1054 - (void) rightMouseDown:(NSEvent *)event 1055 { 1056 [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:true]; 1057 } 1058 1059 - (void) otherMouseDown:(NSEvent *)event 1060 { 1061 [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:true]; 1062 } 1063 1064 - (void) mouseDragged:(NSEvent *)event 1065 { 1066 [self handleMouseEvent:event]; 1067 } 1068 1069 - (void) rightMouseDragged:(NSEvent *)event 1070 { 1071 [self handleMouseEvent:event]; 1072 } 1073 1074 - (void) otherMouseDragged:(NSEvent *)event 1075 { 1076 [self handleMouseEvent:event]; 1077 } 1078 1079 - (void) mouseUp:(NSEvent *)event 1080 { 1081 if (!isMouseGrabbed) { 1082 [self grabMouse]; 1083 } 1084 1085 [self handleMouseEvent:event button:INPUT_BUTTON_LEFT down:false]; 1086 } 1087 1088 - (void) rightMouseUp:(NSEvent *)event 1089 { 1090 [self handleMouseEvent:event button:INPUT_BUTTON_RIGHT down:false]; 1091 } 1092 1093 - (void) otherMouseUp:(NSEvent *)event 1094 { 1095 [self handleMouseEvent:event button:INPUT_BUTTON_MIDDLE down:false]; 1096 } 1097 1098 - (void) grabMouse 1099 { 1100 COCOA_DEBUG("QemuCocoaView: grabMouse\n"); 1101 1102 if (qemu_name) 1103 [[self window] setTitle:[NSString stringWithFormat:@"QEMU %s - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)", qemu_name]]; 1104 else 1105 [[self window] setTitle:@"QEMU - (Press " UC_CTRL_KEY " " UC_ALT_KEY " G to release Mouse)"]; 1106 [self hideCursor]; 1107 CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); 1108 isMouseGrabbed = TRUE; // while isMouseGrabbed = TRUE, QemuCocoaApp sends all events to [cocoaView handleEvent:] 1109 } 1110 1111 - (void) ungrabMouse 1112 { 1113 COCOA_DEBUG("QemuCocoaView: ungrabMouse\n"); 1114 1115 if (qemu_name) 1116 [[self window] setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]]; 1117 else 1118 [[self window] setTitle:@"QEMU"]; 1119 [self unhideCursor]; 1120 CGAssociateMouseAndMouseCursorPosition(TRUE); 1121 isMouseGrabbed = FALSE; 1122 [self raiseAllButtons]; 1123 } 1124 1125 - (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled { 1126 isAbsoluteEnabled = tIsAbsoluteEnabled; 1127 if (isMouseGrabbed) { 1128 CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled); 1129 } 1130 } 1131 - (BOOL) isMouseGrabbed {return isMouseGrabbed;} 1132 - (BOOL) isAbsoluteEnabled {return isAbsoluteEnabled;} 1133 - (QEMUScreen) gscreen {return screen;} 1134 1135 /* 1136 * Makes the target think all down keys are being released. 1137 * This prevents a stuck key problem, since we will not see 1138 * key up events for those keys after we have lost focus. 1139 */ 1140 - (void) raiseAllKeys 1141 { 1142 with_bql(^{ 1143 qkbd_state_lift_all_keys(kbd); 1144 }); 1145 } 1146 1147 - (void) raiseAllButtons 1148 { 1149 with_bql(^{ 1150 qemu_input_queue_btn(dcl.con, INPUT_BUTTON_LEFT, false); 1151 qemu_input_queue_btn(dcl.con, INPUT_BUTTON_RIGHT, false); 1152 qemu_input_queue_btn(dcl.con, INPUT_BUTTON_MIDDLE, false); 1153 }); 1154 } 1155 @end 1156 1157 1158 1159 /* 1160 ------------------------------------------------------ 1161 QemuCocoaAppController 1162 ------------------------------------------------------ 1163 */ 1164 @interface QemuCocoaAppController : NSObject 1165 <NSWindowDelegate, NSApplicationDelegate> 1166 { 1167 } 1168 - (void)doToggleFullScreen:(id)sender; 1169 - (void)showQEMUDoc:(id)sender; 1170 - (void)zoomToFit:(id) sender; 1171 - (void)displayConsole:(id)sender; 1172 - (void)pauseQEMU:(id)sender; 1173 - (void)resumeQEMU:(id)sender; 1174 - (void)displayPause; 1175 - (void)removePause; 1176 - (void)restartQEMU:(id)sender; 1177 - (void)powerDownQEMU:(id)sender; 1178 - (void)ejectDeviceMedia:(id)sender; 1179 - (void)changeDeviceMedia:(id)sender; 1180 - (BOOL)verifyQuit; 1181 - (void)openDocumentation:(NSString *)filename; 1182 - (IBAction) do_about_menu_item: (id) sender; 1183 - (void)adjustSpeed:(id)sender; 1184 @end 1185 1186 @implementation QemuCocoaAppController 1187 - (id) init 1188 { 1189 NSWindow *window; 1190 1191 COCOA_DEBUG("QemuCocoaAppController: init\n"); 1192 1193 self = [super init]; 1194 if (self) { 1195 1196 // create a view and add it to the window 1197 cocoaView = [[QemuCocoaView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 640.0, 480.0)]; 1198 if(!cocoaView) { 1199 error_report("(cocoa) can't create a view"); 1200 exit(1); 1201 } 1202 1203 // create a window 1204 window = [[NSWindow alloc] initWithContentRect:[cocoaView frame] 1205 styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskClosable 1206 backing:NSBackingStoreBuffered defer:NO]; 1207 if(!window) { 1208 error_report("(cocoa) can't create window"); 1209 exit(1); 1210 } 1211 [window setAcceptsMouseMovedEvents:YES]; 1212 [window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; 1213 [window setTitle:qemu_name ? [NSString stringWithFormat:@"QEMU %s", qemu_name] : @"QEMU"]; 1214 [window setContentView:cocoaView]; 1215 [window makeKeyAndOrderFront:self]; 1216 [window center]; 1217 [window setDelegate: self]; 1218 1219 /* Used for displaying pause on the screen */ 1220 pauseLabel = [NSTextField new]; 1221 [pauseLabel setBezeled:YES]; 1222 [pauseLabel setDrawsBackground:YES]; 1223 [pauseLabel setBackgroundColor: [NSColor whiteColor]]; 1224 [pauseLabel setEditable:NO]; 1225 [pauseLabel setSelectable:NO]; 1226 [pauseLabel setStringValue: @"Paused"]; 1227 [pauseLabel setFont: [NSFont fontWithName: @"Helvetica" size: 90]]; 1228 [pauseLabel setTextColor: [NSColor blackColor]]; 1229 [pauseLabel sizeToFit]; 1230 } 1231 return self; 1232 } 1233 1234 - (void) dealloc 1235 { 1236 COCOA_DEBUG("QemuCocoaAppController: dealloc\n"); 1237 1238 if (cocoaView) 1239 [cocoaView release]; 1240 [super dealloc]; 1241 } 1242 1243 - (void)applicationDidFinishLaunching: (NSNotification *) note 1244 { 1245 COCOA_DEBUG("QemuCocoaAppController: applicationDidFinishLaunching\n"); 1246 allow_events = true; 1247 } 1248 1249 - (void)applicationWillTerminate:(NSNotification *)aNotification 1250 { 1251 COCOA_DEBUG("QemuCocoaAppController: applicationWillTerminate\n"); 1252 1253 with_bql(^{ 1254 shutdown_action = SHUTDOWN_ACTION_POWEROFF; 1255 qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); 1256 }); 1257 1258 /* 1259 * Sleep here, because returning will cause OSX to kill us 1260 * immediately; the QEMU main loop will handle the shutdown 1261 * request and terminate the process. 1262 */ 1263 [NSThread sleepForTimeInterval:INFINITY]; 1264 } 1265 1266 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication 1267 { 1268 return YES; 1269 } 1270 1271 - (NSApplicationTerminateReply)applicationShouldTerminate: 1272 (NSApplication *)sender 1273 { 1274 COCOA_DEBUG("QemuCocoaAppController: applicationShouldTerminate\n"); 1275 return [self verifyQuit]; 1276 } 1277 1278 - (void)windowDidChangeScreen:(NSNotification *)notification 1279 { 1280 [cocoaView updateUIInfo]; 1281 } 1282 1283 - (void)windowDidEnterFullScreen:(NSNotification *)notification 1284 { 1285 [cocoaView grabMouse]; 1286 } 1287 1288 - (void)windowDidExitFullScreen:(NSNotification *)notification 1289 { 1290 [cocoaView resizeWindow]; 1291 [cocoaView ungrabMouse]; 1292 } 1293 1294 - (void)windowDidResize:(NSNotification *)notification 1295 { 1296 [cocoaView updateBounds]; 1297 [cocoaView updateUIInfo]; 1298 } 1299 1300 /* Called when the user clicks on a window's close button */ 1301 - (BOOL)windowShouldClose:(id)sender 1302 { 1303 COCOA_DEBUG("QemuCocoaAppController: windowShouldClose\n"); 1304 [NSApp terminate: sender]; 1305 /* If the user allows the application to quit then the call to 1306 * NSApp terminate will never return. If we get here then the user 1307 * cancelled the quit, so we should return NO to not permit the 1308 * closing of this window. 1309 */ 1310 return NO; 1311 } 1312 1313 - (NSApplicationPresentationOptions) window:(NSWindow *)window 1314 willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions; 1315 1316 { 1317 return (proposedOptions & ~(NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar)) | 1318 NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar; 1319 } 1320 1321 /* 1322 * Called when QEMU goes into the background. Note that 1323 * [-NSWindowDelegate windowDidResignKey:] is used here instead of 1324 * [-NSApplicationDelegate applicationWillResignActive:] because it cannot 1325 * detect that the window loses focus when the deck is clicked on macOS 13.2.1. 1326 */ 1327 - (void) windowDidResignKey: (NSNotification *)aNotification 1328 { 1329 COCOA_DEBUG("%s\n", __func__); 1330 [cocoaView ungrabMouse]; 1331 [cocoaView raiseAllKeys]; 1332 } 1333 1334 /* We abstract the method called by the Enter Fullscreen menu item 1335 * because Mac OS 10.7 and higher disables it. This is because of the 1336 * menu item's old selector's name toggleFullScreen: 1337 */ 1338 - (void) doToggleFullScreen:(id)sender 1339 { 1340 [[cocoaView window] toggleFullScreen:sender]; 1341 } 1342 1343 - (void) setFullGrab:(id)sender 1344 { 1345 COCOA_DEBUG("QemuCocoaAppController: setFullGrab\n"); 1346 1347 [cocoaView setFullGrab:sender]; 1348 } 1349 1350 /* Tries to find then open the specified filename */ 1351 - (void) openDocumentation: (NSString *) filename 1352 { 1353 /* Where to look for local files */ 1354 NSString *path_array[] = {@"../share/doc/qemu/", @"../doc/qemu/", @"docs/"}; 1355 NSString *full_file_path; 1356 NSURL *full_file_url; 1357 1358 /* iterate thru the possible paths until the file is found */ 1359 int index; 1360 for (index = 0; index < ARRAY_SIZE(path_array); index++) { 1361 full_file_path = [[NSBundle mainBundle] executablePath]; 1362 full_file_path = [full_file_path stringByDeletingLastPathComponent]; 1363 full_file_path = [NSString stringWithFormat: @"%@/%@%@", full_file_path, 1364 path_array[index], filename]; 1365 full_file_url = [NSURL fileURLWithPath: full_file_path 1366 isDirectory: false]; 1367 if ([[NSWorkspace sharedWorkspace] openURL: full_file_url] == YES) { 1368 return; 1369 } 1370 } 1371 1372 /* If none of the paths opened a file */ 1373 NSBeep(); 1374 QEMU_Alert(@"Failed to open file"); 1375 } 1376 1377 - (void)showQEMUDoc:(id)sender 1378 { 1379 COCOA_DEBUG("QemuCocoaAppController: showQEMUDoc\n"); 1380 1381 [self openDocumentation: @"index.html"]; 1382 } 1383 1384 /* Stretches video to fit host monitor size */ 1385 - (void)zoomToFit:(id) sender 1386 { 1387 NSWindowStyleMask styleMask = [[cocoaView window] styleMask] ^ NSWindowStyleMaskResizable; 1388 1389 [[cocoaView window] setStyleMask:styleMask]; 1390 [sender setState:styleMask & NSWindowStyleMaskResizable ? NSControlStateValueOn : NSControlStateValueOff]; 1391 [cocoaView resizeWindow]; 1392 } 1393 1394 - (void)toggleZoomInterpolation:(id) sender 1395 { 1396 if (zoom_interpolation == kCGInterpolationNone) { 1397 zoom_interpolation = kCGInterpolationLow; 1398 [sender setState: NSControlStateValueOn]; 1399 } else { 1400 zoom_interpolation = kCGInterpolationNone; 1401 [sender setState: NSControlStateValueOff]; 1402 } 1403 } 1404 1405 /* Displays the console on the screen */ 1406 - (void)displayConsole:(id)sender 1407 { 1408 with_bql(^{ 1409 [cocoaView selectConsoleLocked:[sender tag]]; 1410 }); 1411 } 1412 1413 /* Pause the guest */ 1414 - (void)pauseQEMU:(id)sender 1415 { 1416 with_bql(^{ 1417 qmp_stop(NULL); 1418 }); 1419 [sender setEnabled: NO]; 1420 [[[sender menu] itemWithTitle: @"Resume"] setEnabled: YES]; 1421 [self displayPause]; 1422 } 1423 1424 /* Resume running the guest operating system */ 1425 - (void)resumeQEMU:(id) sender 1426 { 1427 with_bql(^{ 1428 qmp_cont(NULL); 1429 }); 1430 [sender setEnabled: NO]; 1431 [[[sender menu] itemWithTitle: @"Pause"] setEnabled: YES]; 1432 [self removePause]; 1433 } 1434 1435 /* Displays the word pause on the screen */ 1436 - (void)displayPause 1437 { 1438 /* Coordinates have to be calculated each time because the window can change its size */ 1439 int xCoord, yCoord, width, height; 1440 xCoord = ([cocoaView frame].size.width - [pauseLabel frame].size.width)/2; 1441 yCoord = [cocoaView frame].size.height - [pauseLabel frame].size.height - ([pauseLabel frame].size.height * .5); 1442 width = [pauseLabel frame].size.width; 1443 height = [pauseLabel frame].size.height; 1444 [pauseLabel setFrame: NSMakeRect(xCoord, yCoord, width, height)]; 1445 [cocoaView addSubview: pauseLabel]; 1446 } 1447 1448 /* Removes the word pause from the screen */ 1449 - (void)removePause 1450 { 1451 [pauseLabel removeFromSuperview]; 1452 } 1453 1454 /* Restarts QEMU */ 1455 - (void)restartQEMU:(id)sender 1456 { 1457 with_bql(^{ 1458 qmp_system_reset(NULL); 1459 }); 1460 } 1461 1462 /* Powers down QEMU */ 1463 - (void)powerDownQEMU:(id)sender 1464 { 1465 with_bql(^{ 1466 qmp_system_powerdown(NULL); 1467 }); 1468 } 1469 1470 /* Ejects the media. 1471 * Uses sender's tag to figure out the device to eject. 1472 */ 1473 - (void)ejectDeviceMedia:(id)sender 1474 { 1475 NSString * drive; 1476 drive = [sender representedObject]; 1477 if(drive == nil) { 1478 NSBeep(); 1479 QEMU_Alert(@"Failed to find drive to eject!"); 1480 return; 1481 } 1482 1483 __block Error *err = NULL; 1484 with_bql(^{ 1485 qmp_eject([drive cStringUsingEncoding: NSASCIIStringEncoding], 1486 NULL, false, false, &err); 1487 }); 1488 handleAnyDeviceErrors(err); 1489 } 1490 1491 /* Displays a dialog box asking the user to select an image file to load. 1492 * Uses sender's represented object value to figure out which drive to use. 1493 */ 1494 - (void)changeDeviceMedia:(id)sender 1495 { 1496 /* Find the drive name */ 1497 NSString * drive; 1498 drive = [sender representedObject]; 1499 if(drive == nil) { 1500 NSBeep(); 1501 QEMU_Alert(@"Could not find drive!"); 1502 return; 1503 } 1504 1505 /* Display the file open dialog */ 1506 NSOpenPanel * openPanel; 1507 openPanel = [NSOpenPanel openPanel]; 1508 [openPanel setCanChooseFiles: YES]; 1509 [openPanel setAllowsMultipleSelection: NO]; 1510 if([openPanel runModal] == NSModalResponseOK) { 1511 NSString * file = [[[openPanel URLs] objectAtIndex: 0] path]; 1512 if(file == nil) { 1513 NSBeep(); 1514 QEMU_Alert(@"Failed to convert URL to file path!"); 1515 return; 1516 } 1517 1518 __block Error *err = NULL; 1519 with_bql(^{ 1520 qmp_blockdev_change_medium([drive cStringUsingEncoding: 1521 NSASCIIStringEncoding], 1522 NULL, 1523 [file cStringUsingEncoding: 1524 NSASCIIStringEncoding], 1525 "raw", 1526 true, false, 1527 false, 0, 1528 &err); 1529 }); 1530 handleAnyDeviceErrors(err); 1531 } 1532 } 1533 1534 /* Verifies if the user really wants to quit */ 1535 - (BOOL)verifyQuit 1536 { 1537 NSAlert *alert = [NSAlert new]; 1538 [alert autorelease]; 1539 [alert setMessageText: @"Are you sure you want to quit QEMU?"]; 1540 [alert addButtonWithTitle: @"Cancel"]; 1541 [alert addButtonWithTitle: @"Quit"]; 1542 if([alert runModal] == NSAlertSecondButtonReturn) { 1543 return YES; 1544 } else { 1545 return NO; 1546 } 1547 } 1548 1549 /* The action method for the About menu item */ 1550 - (IBAction) do_about_menu_item: (id) sender 1551 { 1552 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 1553 char *icon_path_c = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/512x512/apps/qemu.png"); 1554 NSString *icon_path = [NSString stringWithUTF8String:icon_path_c]; 1555 g_free(icon_path_c); 1556 NSImage *icon = [[NSImage alloc] initWithContentsOfFile:icon_path]; 1557 NSString *version = @"QEMU emulator version " QEMU_FULL_VERSION; 1558 NSString *copyright = @QEMU_COPYRIGHT; 1559 NSDictionary *options; 1560 if (icon) { 1561 options = @{ 1562 NSAboutPanelOptionApplicationIcon : icon, 1563 NSAboutPanelOptionApplicationVersion : version, 1564 @"Copyright" : copyright, 1565 }; 1566 [icon release]; 1567 } else { 1568 options = @{ 1569 NSAboutPanelOptionApplicationVersion : version, 1570 @"Copyright" : copyright, 1571 }; 1572 } 1573 [NSApp orderFrontStandardAboutPanelWithOptions:options]; 1574 [pool release]; 1575 } 1576 1577 /* Used by the Speed menu items */ 1578 - (void)adjustSpeed:(id)sender 1579 { 1580 int throttle_pct; /* throttle percentage */ 1581 NSMenu *menu; 1582 1583 menu = [sender menu]; 1584 if (menu != nil) 1585 { 1586 /* Unselect the currently selected item */ 1587 for (NSMenuItem *item in [menu itemArray]) { 1588 if (item.state == NSControlStateValueOn) { 1589 [item setState: NSControlStateValueOff]; 1590 break; 1591 } 1592 } 1593 } 1594 1595 // check the menu item 1596 [sender setState: NSControlStateValueOn]; 1597 1598 // get the throttle percentage 1599 throttle_pct = [sender tag]; 1600 1601 with_bql(^{ 1602 cpu_throttle_set(throttle_pct); 1603 }); 1604 COCOA_DEBUG("cpu throttling at %d%c\n", cpu_throttle_get_percentage(), '%'); 1605 } 1606 1607 @end 1608 1609 @interface QemuApplication : NSApplication 1610 @end 1611 1612 @implementation QemuApplication 1613 - (void)sendEvent:(NSEvent *)event 1614 { 1615 COCOA_DEBUG("QemuApplication: sendEvent\n"); 1616 if (![cocoaView handleEvent:event]) { 1617 [super sendEvent: event]; 1618 } 1619 } 1620 @end 1621 1622 static void create_initial_menus(void) 1623 { 1624 // Add menus 1625 NSMenu *menu; 1626 NSMenuItem *menuItem; 1627 1628 [NSApp setMainMenu:[[NSMenu alloc] init]]; 1629 [NSApp setServicesMenu:[[NSMenu alloc] initWithTitle:@"Services"]]; 1630 1631 // Application menu 1632 menu = [[NSMenu alloc] initWithTitle:@""]; 1633 [menu addItemWithTitle:@"About QEMU" action:@selector(do_about_menu_item:) keyEquivalent:@""]; // About QEMU 1634 [menu addItem:[NSMenuItem separatorItem]]; //Separator 1635 menuItem = [menu addItemWithTitle:@"Services" action:nil keyEquivalent:@""]; 1636 [menuItem setSubmenu:[NSApp servicesMenu]]; 1637 [menu addItem:[NSMenuItem separatorItem]]; 1638 [menu addItemWithTitle:@"Hide QEMU" action:@selector(hide:) keyEquivalent:@"h"]; //Hide QEMU 1639 menuItem = (NSMenuItem *)[menu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; // Hide Others 1640 [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption|NSEventModifierFlagCommand)]; 1641 [menu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; // Show All 1642 [menu addItem:[NSMenuItem separatorItem]]; //Separator 1643 [menu addItemWithTitle:@"Quit QEMU" action:@selector(terminate:) keyEquivalent:@"q"]; 1644 menuItem = [[NSMenuItem alloc] initWithTitle:@"Apple" action:nil keyEquivalent:@""]; 1645 [menuItem setSubmenu:menu]; 1646 [[NSApp mainMenu] addItem:menuItem]; 1647 [NSApp performSelector:@selector(setAppleMenu:) withObject:menu]; // Workaround (this method is private since 10.4+) 1648 1649 // Machine menu 1650 menu = [[NSMenu alloc] initWithTitle: @"Machine"]; 1651 [menu setAutoenablesItems: NO]; 1652 [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Pause" action: @selector(pauseQEMU:) keyEquivalent: @""] autorelease]]; 1653 menuItem = [[[NSMenuItem alloc] initWithTitle: @"Resume" action: @selector(resumeQEMU:) keyEquivalent: @""] autorelease]; 1654 [menu addItem: menuItem]; 1655 [menuItem setEnabled: NO]; 1656 [menu addItem: [NSMenuItem separatorItem]]; 1657 [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Reset" action: @selector(restartQEMU:) keyEquivalent: @""] autorelease]]; 1658 [menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Power Down" action: @selector(powerDownQEMU:) keyEquivalent: @""] autorelease]]; 1659 menuItem = [[[NSMenuItem alloc] initWithTitle: @"Machine" action:nil keyEquivalent:@""] autorelease]; 1660 [menuItem setSubmenu:menu]; 1661 [[NSApp mainMenu] addItem:menuItem]; 1662 1663 // View menu 1664 menu = [[NSMenu alloc] initWithTitle:@"View"]; 1665 [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Enter Fullscreen" action:@selector(doToggleFullScreen:) keyEquivalent:@"f"] autorelease]]; // Fullscreen 1666 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom To Fit" action:@selector(zoomToFit:) keyEquivalent:@""] autorelease]; 1667 [menuItem setState: [[cocoaView window] styleMask] & NSWindowStyleMaskResizable ? NSControlStateValueOn : NSControlStateValueOff]; 1668 [menu addItem: menuItem]; 1669 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom Interpolation" action:@selector(toggleZoomInterpolation:) keyEquivalent:@""] autorelease]; 1670 [menuItem setState: zoom_interpolation == kCGInterpolationLow ? NSControlStateValueOn : NSControlStateValueOff]; 1671 [menu addItem: menuItem]; 1672 menuItem = [[[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""] autorelease]; 1673 [menuItem setSubmenu:menu]; 1674 [[NSApp mainMenu] addItem:menuItem]; 1675 1676 // Speed menu 1677 menu = [[NSMenu alloc] initWithTitle:@"Speed"]; 1678 1679 // Add the rest of the Speed menu items 1680 int p, percentage, throttle_pct; 1681 for (p = 10; p >= 0; p--) 1682 { 1683 percentage = p * 10 > 1 ? p * 10 : 1; // prevent a 0% menu item 1684 1685 menuItem = [[[NSMenuItem alloc] 1686 initWithTitle: [NSString stringWithFormat: @"%d%%", percentage] action:@selector(adjustSpeed:) keyEquivalent:@""] autorelease]; 1687 1688 if (percentage == 100) { 1689 [menuItem setState: NSControlStateValueOn]; 1690 } 1691 1692 /* Calculate the throttle percentage */ 1693 throttle_pct = -1 * percentage + 100; 1694 1695 [menuItem setTag: throttle_pct]; 1696 [menu addItem: menuItem]; 1697 } 1698 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Speed" action:nil keyEquivalent:@""] autorelease]; 1699 [menuItem setSubmenu:menu]; 1700 [[NSApp mainMenu] addItem:menuItem]; 1701 1702 // Window menu 1703 menu = [[NSMenu alloc] initWithTitle:@"Window"]; 1704 [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease]]; // Miniaturize 1705 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease]; 1706 [menuItem setSubmenu:menu]; 1707 [[NSApp mainMenu] addItem:menuItem]; 1708 [NSApp setWindowsMenu:menu]; 1709 1710 // Help menu 1711 menu = [[NSMenu alloc] initWithTitle:@"Help"]; 1712 [menu addItem: [[[NSMenuItem alloc] initWithTitle:@"QEMU Documentation" action:@selector(showQEMUDoc:) keyEquivalent:@"?"] autorelease]]; // QEMU Help 1713 menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease]; 1714 [menuItem setSubmenu:menu]; 1715 [[NSApp mainMenu] addItem:menuItem]; 1716 } 1717 1718 /* Returns a name for a given console */ 1719 static NSString * getConsoleName(QemuConsole * console) 1720 { 1721 g_autofree char *label = qemu_console_get_label(console); 1722 1723 return [NSString stringWithUTF8String:label]; 1724 } 1725 1726 /* Add an entry to the View menu for each console */ 1727 static void add_console_menu_entries(void) 1728 { 1729 NSMenu *menu; 1730 NSMenuItem *menuItem; 1731 int index = 0; 1732 1733 menu = [[[NSApp mainMenu] itemWithTitle:@"View"] submenu]; 1734 1735 [menu addItem:[NSMenuItem separatorItem]]; 1736 1737 while (qemu_console_lookup_by_index(index) != NULL) { 1738 menuItem = [[[NSMenuItem alloc] initWithTitle: getConsoleName(qemu_console_lookup_by_index(index)) 1739 action: @selector(displayConsole:) keyEquivalent: @""] autorelease]; 1740 [menuItem setTag: index]; 1741 [menu addItem: menuItem]; 1742 index++; 1743 } 1744 } 1745 1746 /* Make menu items for all removable devices. 1747 * Each device is given an 'Eject' and 'Change' menu item. 1748 */ 1749 static void addRemovableDevicesMenuItems(void) 1750 { 1751 NSMenu *menu; 1752 NSMenuItem *menuItem; 1753 BlockInfoList *currentDevice, *pointerToFree; 1754 NSString *deviceName; 1755 1756 currentDevice = qmp_query_block(NULL); 1757 pointerToFree = currentDevice; 1758 1759 menu = [[[NSApp mainMenu] itemWithTitle:@"Machine"] submenu]; 1760 1761 // Add a separator between related groups of menu items 1762 [menu addItem:[NSMenuItem separatorItem]]; 1763 1764 // Set the attributes to the "Removable Media" menu item 1765 NSString *titleString = @"Removable Media"; 1766 NSMutableAttributedString *attString=[[NSMutableAttributedString alloc] initWithString:titleString]; 1767 NSColor *newColor = [NSColor blackColor]; 1768 NSFontManager *fontManager = [NSFontManager sharedFontManager]; 1769 NSFont *font = [fontManager fontWithFamily:@"Helvetica" 1770 traits:NSBoldFontMask|NSItalicFontMask 1771 weight:0 1772 size:14]; 1773 [attString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [titleString length])]; 1774 [attString addAttribute:NSForegroundColorAttributeName value:newColor range:NSMakeRange(0, [titleString length])]; 1775 [attString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt: 1] range:NSMakeRange(0, [titleString length])]; 1776 1777 // Add the "Removable Media" menu item 1778 menuItem = [NSMenuItem new]; 1779 [menuItem setAttributedTitle: attString]; 1780 [menuItem setEnabled: NO]; 1781 [menu addItem: menuItem]; 1782 1783 /* Loop through all the block devices in the emulator */ 1784 while (currentDevice) { 1785 deviceName = [[NSString stringWithFormat: @"%s", currentDevice->value->device] retain]; 1786 1787 if(currentDevice->value->removable) { 1788 menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Change %s...", currentDevice->value->device] 1789 action: @selector(changeDeviceMedia:) 1790 keyEquivalent: @""]; 1791 [menu addItem: menuItem]; 1792 [menuItem setRepresentedObject: deviceName]; 1793 [menuItem autorelease]; 1794 1795 menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Eject %s", currentDevice->value->device] 1796 action: @selector(ejectDeviceMedia:) 1797 keyEquivalent: @""]; 1798 [menu addItem: menuItem]; 1799 [menuItem setRepresentedObject: deviceName]; 1800 [menuItem autorelease]; 1801 } 1802 currentDevice = currentDevice->next; 1803 } 1804 qapi_free_BlockInfoList(pointerToFree); 1805 } 1806 1807 @interface QemuCocoaPasteboardTypeOwner : NSObject<NSPasteboardTypeOwner> 1808 @end 1809 1810 @implementation QemuCocoaPasteboardTypeOwner 1811 1812 - (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSPasteboardType)type 1813 { 1814 if (type != NSPasteboardTypeString) { 1815 return; 1816 } 1817 1818 with_bql(^{ 1819 QemuClipboardInfo *info = qemu_clipboard_info_ref(cbinfo); 1820 qemu_event_reset(&cbevent); 1821 qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT); 1822 1823 while (info == cbinfo && 1824 info->types[QEMU_CLIPBOARD_TYPE_TEXT].available && 1825 info->types[QEMU_CLIPBOARD_TYPE_TEXT].data == NULL) { 1826 bql_unlock(); 1827 qemu_event_wait(&cbevent); 1828 bql_lock(); 1829 } 1830 1831 if (info == cbinfo) { 1832 NSData *data = [[NSData alloc] initWithBytes:info->types[QEMU_CLIPBOARD_TYPE_TEXT].data 1833 length:info->types[QEMU_CLIPBOARD_TYPE_TEXT].size]; 1834 [sender setData:data forType:NSPasteboardTypeString]; 1835 [data release]; 1836 } 1837 1838 qemu_clipboard_info_unref(info); 1839 }); 1840 } 1841 1842 @end 1843 1844 static QemuCocoaPasteboardTypeOwner *cbowner; 1845 1846 static void cocoa_clipboard_notify(Notifier *notifier, void *data); 1847 static void cocoa_clipboard_request(QemuClipboardInfo *info, 1848 QemuClipboardType type); 1849 1850 static QemuClipboardPeer cbpeer = { 1851 .name = "cocoa", 1852 .notifier = { .notify = cocoa_clipboard_notify }, 1853 .request = cocoa_clipboard_request 1854 }; 1855 1856 static void cocoa_clipboard_update_info(QemuClipboardInfo *info) 1857 { 1858 if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { 1859 return; 1860 } 1861 1862 if (info != cbinfo) { 1863 NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 1864 qemu_clipboard_info_unref(cbinfo); 1865 cbinfo = qemu_clipboard_info_ref(info); 1866 cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner]; 1867 [pool release]; 1868 } 1869 1870 qemu_event_set(&cbevent); 1871 } 1872 1873 static void cocoa_clipboard_notify(Notifier *notifier, void *data) 1874 { 1875 QemuClipboardNotify *notify = data; 1876 1877 switch (notify->type) { 1878 case QEMU_CLIPBOARD_UPDATE_INFO: 1879 cocoa_clipboard_update_info(notify->info); 1880 return; 1881 case QEMU_CLIPBOARD_RESET_SERIAL: 1882 /* ignore */ 1883 return; 1884 } 1885 } 1886 1887 static void cocoa_clipboard_request(QemuClipboardInfo *info, 1888 QemuClipboardType type) 1889 { 1890 NSAutoreleasePool *pool; 1891 NSData *text; 1892 1893 switch (type) { 1894 case QEMU_CLIPBOARD_TYPE_TEXT: 1895 pool = [[NSAutoreleasePool alloc] init]; 1896 text = [[NSPasteboard generalPasteboard] dataForType:NSPasteboardTypeString]; 1897 if (text) { 1898 qemu_clipboard_set_data(&cbpeer, info, type, 1899 [text length], [text bytes], true); 1900 } 1901 [pool release]; 1902 break; 1903 default: 1904 break; 1905 } 1906 } 1907 1908 /* 1909 * The startup process for the OSX/Cocoa UI is complicated, because 1910 * OSX insists that the UI runs on the initial main thread, and so we 1911 * need to start a second thread which runs the qemu_default_main(): 1912 * in main(): 1913 * in cocoa_display_init(): 1914 * assign cocoa_main to qemu_main 1915 * create application, menus, etc 1916 * in cocoa_main(): 1917 * create qemu-main thread 1918 * enter OSX run loop 1919 */ 1920 1921 static void *call_qemu_main(void *opaque) 1922 { 1923 int status; 1924 1925 COCOA_DEBUG("Second thread: calling qemu_default_main()\n"); 1926 bql_lock(); 1927 status = qemu_default_main(); 1928 bql_unlock(); 1929 COCOA_DEBUG("Second thread: qemu_default_main() returned, exiting\n"); 1930 [cbowner release]; 1931 exit(status); 1932 } 1933 1934 static int cocoa_main(void) 1935 { 1936 QemuThread thread; 1937 1938 COCOA_DEBUG("Entered %s()\n", __func__); 1939 1940 bql_unlock(); 1941 qemu_thread_create(&thread, "qemu_main", call_qemu_main, 1942 NULL, QEMU_THREAD_DETACHED); 1943 1944 // Start the main event loop 1945 COCOA_DEBUG("Main thread: entering OSX run loop\n"); 1946 [NSApp run]; 1947 COCOA_DEBUG("Main thread: left OSX run loop, which should never happen\n"); 1948 1949 abort(); 1950 } 1951 1952 1953 1954 #pragma mark qemu 1955 static void cocoa_update(DisplayChangeListener *dcl, 1956 int x, int y, int w, int h) 1957 { 1958 COCOA_DEBUG("qemu_cocoa: cocoa_update\n"); 1959 1960 dispatch_async(dispatch_get_main_queue(), ^{ 1961 NSRect rect = NSMakeRect(x, [cocoaView gscreen].height - y - h, w, h); 1962 [cocoaView setNeedsDisplayInRect:rect]; 1963 }); 1964 } 1965 1966 static void cocoa_switch(DisplayChangeListener *dcl, 1967 DisplaySurface *surface) 1968 { 1969 pixman_image_t *image = surface->image; 1970 1971 COCOA_DEBUG("qemu_cocoa: cocoa_switch\n"); 1972 1973 // The DisplaySurface will be freed as soon as this callback returns. 1974 // We take a reference to the underlying pixman image here so it does 1975 // not disappear from under our feet; the switchSurface method will 1976 // deref the old image when it is done with it. 1977 pixman_image_ref(image); 1978 1979 dispatch_async(dispatch_get_main_queue(), ^{ 1980 [cocoaView switchSurface:image]; 1981 }); 1982 } 1983 1984 static void cocoa_refresh(DisplayChangeListener *dcl) 1985 { 1986 NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 1987 1988 COCOA_DEBUG("qemu_cocoa: cocoa_refresh\n"); 1989 graphic_hw_update(dcl->con); 1990 1991 if (qemu_input_is_absolute(dcl->con)) { 1992 dispatch_async(dispatch_get_main_queue(), ^{ 1993 if (![cocoaView isAbsoluteEnabled]) { 1994 if ([cocoaView isMouseGrabbed]) { 1995 [cocoaView ungrabMouse]; 1996 } 1997 } 1998 [cocoaView setAbsoluteEnabled:YES]; 1999 }); 2000 } 2001 2002 if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) { 2003 qemu_clipboard_info_unref(cbinfo); 2004 cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); 2005 if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) { 2006 cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; 2007 } 2008 qemu_clipboard_update(cbinfo); 2009 cbchangecount = [[NSPasteboard generalPasteboard] changeCount]; 2010 qemu_event_set(&cbevent); 2011 } 2012 2013 [pool release]; 2014 } 2015 2016 static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts) 2017 { 2018 NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 2019 2020 COCOA_DEBUG("qemu_cocoa: cocoa_display_init\n"); 2021 2022 qemu_main = cocoa_main; 2023 2024 // Pull this console process up to being a fully-fledged graphical 2025 // app with a menubar and Dock icon 2026 ProcessSerialNumber psn = { 0, kCurrentProcess }; 2027 TransformProcessType(&psn, kProcessTransformToForegroundApplication); 2028 2029 [QemuApplication sharedApplication]; 2030 2031 // Create an Application controller 2032 QemuCocoaAppController *controller = [[QemuCocoaAppController alloc] init]; 2033 [NSApp setDelegate:controller]; 2034 2035 /* if fullscreen mode is to be used */ 2036 if (opts->has_full_screen && opts->full_screen) { 2037 [[cocoaView window] toggleFullScreen: nil]; 2038 } 2039 if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) { 2040 [controller setFullGrab: nil]; 2041 } 2042 2043 if (opts->has_show_cursor && opts->show_cursor) { 2044 cursor_hide = 0; 2045 } 2046 if (opts->u.cocoa.has_swap_opt_cmd) { 2047 swap_opt_cmd = opts->u.cocoa.swap_opt_cmd; 2048 } 2049 2050 if (opts->u.cocoa.has_left_command_key && !opts->u.cocoa.left_command_key) { 2051 left_command_key_enabled = 0; 2052 } 2053 2054 if (opts->u.cocoa.has_zoom_to_fit && opts->u.cocoa.zoom_to_fit) { 2055 [cocoaView window].styleMask |= NSWindowStyleMaskResizable; 2056 } 2057 2058 if (opts->u.cocoa.has_zoom_interpolation && opts->u.cocoa.zoom_interpolation) { 2059 zoom_interpolation = kCGInterpolationLow; 2060 } 2061 2062 create_initial_menus(); 2063 /* 2064 * Create the menu entries which depend on QEMU state (for consoles 2065 * and removable devices). These make calls back into QEMU functions, 2066 * which is OK because at this point we know that the second thread 2067 * holds the BQL and is synchronously waiting for us to 2068 * finish. 2069 */ 2070 add_console_menu_entries(); 2071 addRemovableDevicesMenuItems(); 2072 2073 dcl.con = qemu_console_lookup_default(); 2074 kbd = qkbd_state_init(dcl.con); 2075 2076 // register vga output callbacks 2077 register_displaychangelistener(&dcl); 2078 [cocoaView updateUIInfo]; 2079 2080 qemu_event_init(&cbevent, false); 2081 cbowner = [[QemuCocoaPasteboardTypeOwner alloc] init]; 2082 qemu_clipboard_peer_register(&cbpeer); 2083 2084 [pool release]; 2085 } 2086 2087 static QemuDisplay qemu_display_cocoa = { 2088 .type = DISPLAY_TYPE_COCOA, 2089 .init = cocoa_display_init, 2090 }; 2091 2092 static void register_cocoa(void) 2093 { 2094 qemu_display_register(&qemu_display_cocoa); 2095 } 2096 2097 type_init(register_cocoa); 2098