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