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