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