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