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