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