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