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