mirror of
https://github.com/xemu-project/xemu.git
synced 2024-11-23 19:49:43 +00:00
52eaefd36c
Retrieve the refresh rate of the display and reflect it with dpy_set_ui_info() and update_displaychangelistener(), allowing the guest and DisplayChangeListener to consume the information. The information will be used as a hint how often the display should be updated. For example, when we run 30 Hz physical display updates it is pointless for the guest to update the screen at 60Hz frequency, the guest can spare some work instead. Signed-off-by: Akihiko Odaki <akihiko.odaki@gmail.com> Reviewed-by: Peter Maydell <peter.maydell@linaro.org> Message-Id: <20220702142519.12188-1-akihiko.odaki@gmail.com> Signed-off-by: Philippe Mathieu-Daudé <f4bug@amsat.org>
2137 lines
71 KiB
Objective-C
2137 lines
71 KiB
Objective-C
/*
|
|
* QEMU Cocoa CG display driver
|
|
*
|
|
* Copyright (c) 2008 Mike Kronenberg
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
#include <crt_externs.h>
|
|
|
|
#include "qemu/help-texts.h"
|
|
#include "qemu-main.h"
|
|
#include "ui/clipboard.h"
|
|
#include "ui/console.h"
|
|
#include "ui/input.h"
|
|
#include "ui/kbd-state.h"
|
|
#include "sysemu/sysemu.h"
|
|
#include "sysemu/runstate.h"
|
|
#include "sysemu/runstate-action.h"
|
|
#include "sysemu/cpu-throttle.h"
|
|
#include "qapi/error.h"
|
|
#include "qapi/qapi-commands-block.h"
|
|
#include "qapi/qapi-commands-machine.h"
|
|
#include "qapi/qapi-commands-misc.h"
|
|
#include "sysemu/blockdev.h"
|
|
#include "qemu-version.h"
|
|
#include "qemu/cutils.h"
|
|
#include "qemu/main-loop.h"
|
|
#include "qemu/module.h"
|
|
#include <Carbon/Carbon.h>
|
|
#include "hw/core/cpu.h"
|
|
|
|
#ifndef MAC_OS_X_VERSION_10_13
|
|
#define MAC_OS_X_VERSION_10_13 101300
|
|
#endif
|
|
|
|
/* 10.14 deprecates NSOnState and NSOffState in favor of
|
|
* NSControlStateValueOn/Off, which were introduced in 10.13.
|
|
* Define for older versions
|
|
*/
|
|
#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13
|
|
#define NSControlStateValueOn NSOnState
|
|
#define NSControlStateValueOff NSOffState
|
|
#endif
|
|
|
|
//#define DEBUG
|
|
|
|
#ifdef DEBUG
|
|
#define COCOA_DEBUG(...) { (void) fprintf (stdout, __VA_ARGS__); }
|
|
#else
|
|
#define COCOA_DEBUG(...) ((void) 0)
|
|
#endif
|
|
|
|
#define cgrect(nsrect) (*(CGRect *)&(nsrect))
|
|
|
|
typedef struct {
|
|
int width;
|
|
int height;
|
|
} QEMUScreen;
|
|
|
|
static void cocoa_update(DisplayChangeListener *dcl,
|
|
int x, int y, int w, int h);
|
|
|
|
static void cocoa_switch(DisplayChangeListener *dcl,
|
|
DisplaySurface *surface);
|
|
|
|
static void cocoa_refresh(DisplayChangeListener *dcl);
|
|
|
|
static NSWindow *normalWindow;
|
|
static const DisplayChangeListenerOps dcl_ops = {
|
|
.dpy_name = "cocoa",
|
|
.dpy_gfx_update = cocoa_update,
|
|
.dpy_gfx_switch = cocoa_switch,
|
|
.dpy_refresh = cocoa_refresh,
|
|
};
|
|
static DisplayChangeListener dcl = {
|
|
.ops = &dcl_ops,
|
|
};
|
|
static int last_buttons;
|
|
static int cursor_hide = 1;
|
|
static int left_command_key_enabled = 1;
|
|
static bool swap_opt_cmd;
|
|
|
|
static int gArgc;
|
|
static char **gArgv;
|
|
static bool stretch_video;
|
|
static NSTextField *pauseLabel;
|
|
|
|
static QemuSemaphore display_init_sem;
|
|
static QemuSemaphore app_started_sem;
|
|
static bool allow_events;
|
|
|
|
static NSInteger cbchangecount = -1;
|
|
static QemuClipboardInfo *cbinfo;
|
|
static QemuEvent cbevent;
|
|
|
|
// Utility functions to run specified code block with iothread lock held
|
|
typedef void (^CodeBlock)(void);
|
|
typedef bool (^BoolCodeBlock)(void);
|
|
|
|
static void with_iothread_lock(CodeBlock block)
|
|
{
|
|
bool locked = qemu_mutex_iothread_locked();
|
|
if (!locked) {
|
|
qemu_mutex_lock_iothread();
|
|
}
|
|
block();
|
|
if (!locked) {
|
|
qemu_mutex_unlock_iothread();
|
|
}
|
|
}
|
|
|
|
static bool bool_with_iothread_lock(BoolCodeBlock block)
|
|
{
|
|
bool locked = qemu_mutex_iothread_locked();
|
|
bool val;
|
|
|
|
if (!locked) {
|
|
qemu_mutex_lock_iothread();
|
|
}
|
|
val = block();
|
|
if (!locked) {
|
|
qemu_mutex_unlock_iothread();
|
|
}
|
|
return val;
|
|
}
|
|
|
|
// Mac to QKeyCode conversion
|
|
static const int mac_to_qkeycode_map[] = {
|
|
[kVK_ANSI_A] = Q_KEY_CODE_A,
|
|
[kVK_ANSI_B] = Q_KEY_CODE_B,
|
|
[kVK_ANSI_C] = Q_KEY_CODE_C,
|
|
[kVK_ANSI_D] = Q_KEY_CODE_D,
|
|
[kVK_ANSI_E] = Q_KEY_CODE_E,
|
|
[kVK_ANSI_F] = Q_KEY_CODE_F,
|
|
[kVK_ANSI_G] = Q_KEY_CODE_G,
|
|
[kVK_ANSI_H] = Q_KEY_CODE_H,
|
|
[kVK_ANSI_I] = Q_KEY_CODE_I,
|
|
[kVK_ANSI_J] = Q_KEY_CODE_J,
|
|
[kVK_ANSI_K] = Q_KEY_CODE_K,
|
|
[kVK_ANSI_L] = Q_KEY_CODE_L,
|
|
[kVK_ANSI_M] = Q_KEY_CODE_M,
|
|
[kVK_ANSI_N] = Q_KEY_CODE_N,
|
|
[kVK_ANSI_O] = Q_KEY_CODE_O,
|
|
[kVK_ANSI_P] = Q_KEY_CODE_P,
|
|
[kVK_ANSI_Q] = Q_KEY_CODE_Q,
|
|
[kVK_ANSI_R] = Q_KEY_CODE_R,
|
|
[kVK_ANSI_S] = Q_KEY_CODE_S,
|
|
[kVK_ANSI_T] = Q_KEY_CODE_T,
|
|
[kVK_ANSI_U] = Q_KEY_CODE_U,
|
|
[kVK_ANSI_V] = Q_KEY_CODE_V,
|
|
[kVK_ANSI_W] = Q_KEY_CODE_W,
|
|
[kVK_ANSI_X] = Q_KEY_CODE_X,
|
|
[kVK_ANSI_Y] = Q_KEY_CODE_Y,
|
|
[kVK_ANSI_Z] = Q_KEY_CODE_Z,
|
|
|
|
[kVK_ANSI_0] = Q_KEY_CODE_0,
|
|
[kVK_ANSI_1] = Q_KEY_CODE_1,
|
|
[kVK_ANSI_2] = Q_KEY_CODE_2,
|
|
[kVK_ANSI_3] = Q_KEY_CODE_3,
|
|
[kVK_ANSI_4] = Q_KEY_CODE_4,
|
|
[kVK_ANSI_5] = Q_KEY_CODE_5,
|
|
[kVK_ANSI_6] = Q_KEY_CODE_6,
|
|
[kVK_ANSI_7] = Q_KEY_CODE_7,
|
|
[kVK_ANSI_8] = Q_KEY_CODE_8,
|
|
[kVK_ANSI_9] = Q_KEY_CODE_9,
|
|
|
|
[kVK_ANSI_Grave] = Q_KEY_CODE_GRAVE_ACCENT,
|
|
[kVK_ANSI_Minus] = Q_KEY_CODE_MINUS,
|
|
[kVK_ANSI_Equal] = Q_KEY_CODE_EQUAL,
|
|
[kVK_Delete] = Q_KEY_CODE_BACKSPACE,
|
|
[kVK_CapsLock] = Q_KEY_CODE_CAPS_LOCK,
|
|
[kVK_Tab] = Q_KEY_CODE_TAB,
|
|
[kVK_Return] = Q_KEY_CODE_RET,
|
|
[kVK_ANSI_LeftBracket] = Q_KEY_CODE_BRACKET_LEFT,
|
|
[kVK_ANSI_RightBracket] = Q_KEY_CODE_BRACKET_RIGHT,
|
|
[kVK_ANSI_Backslash] = Q_KEY_CODE_BACKSLASH,
|
|
[kVK_ANSI_Semicolon] = Q_KEY_CODE_SEMICOLON,
|
|
[kVK_ANSI_Quote] = Q_KEY_CODE_APOSTROPHE,
|
|
[kVK_ANSI_Comma] = Q_KEY_CODE_COMMA,
|
|
[kVK_ANSI_Period] = Q_KEY_CODE_DOT,
|
|
[kVK_ANSI_Slash] = Q_KEY_CODE_SLASH,
|
|
[kVK_Space] = Q_KEY_CODE_SPC,
|
|
|
|
[kVK_ANSI_Keypad0] = Q_KEY_CODE_KP_0,
|
|
[kVK_ANSI_Keypad1] = Q_KEY_CODE_KP_1,
|
|
[kVK_ANSI_Keypad2] = Q_KEY_CODE_KP_2,
|
|
[kVK_ANSI_Keypad3] = Q_KEY_CODE_KP_3,
|
|
[kVK_ANSI_Keypad4] = Q_KEY_CODE_KP_4,
|
|
[kVK_ANSI_Keypad5] = Q_KEY_CODE_KP_5,
|
|
[kVK_ANSI_Keypad6] = Q_KEY_CODE_KP_6,
|
|
[kVK_ANSI_Keypad7] = Q_KEY_CODE_KP_7,
|
|
[kVK_ANSI_Keypad8] = Q_KEY_CODE_KP_8,
|
|
[kVK_ANSI_Keypad9] = Q_KEY_CODE_KP_9,
|
|
[kVK_ANSI_KeypadDecimal] = Q_KEY_CODE_KP_DECIMAL,
|
|
[kVK_ANSI_KeypadEnter] = Q_KEY_CODE_KP_ENTER,
|
|
[kVK_ANSI_KeypadPlus] = Q_KEY_CODE_KP_ADD,
|
|
[kVK_ANSI_KeypadMinus] = Q_KEY_CODE_KP_SUBTRACT,
|
|
[kVK_ANSI_KeypadMultiply] = Q_KEY_CODE_KP_MULTIPLY,
|
|
[kVK_ANSI_KeypadDivide] = Q_KEY_CODE_KP_DIVIDE,
|
|
[kVK_ANSI_KeypadEquals] = Q_KEY_CODE_KP_EQUALS,
|
|
[kVK_ANSI_KeypadClear] = Q_KEY_CODE_NUM_LOCK,
|
|
|
|
[kVK_UpArrow] = Q_KEY_CODE_UP,
|
|
[kVK_DownArrow] = Q_KEY_CODE_DOWN,
|
|
[kVK_LeftArrow] = Q_KEY_CODE_LEFT,
|
|
[kVK_RightArrow] = Q_KEY_CODE_RIGHT,
|
|
|
|
[kVK_Help] = Q_KEY_CODE_INSERT,
|
|
[kVK_Home] = Q_KEY_CODE_HOME,
|
|
[kVK_PageUp] = Q_KEY_CODE_PGUP,
|
|
[kVK_PageDown] = Q_KEY_CODE_PGDN,
|
|
[kVK_End] = Q_KEY_CODE_END,
|
|
[kVK_ForwardDelete] = Q_KEY_CODE_DELETE,
|
|
|
|
[kVK_Escape] = Q_KEY_CODE_ESC,
|
|
|
|
/* The Power key can't be used directly because the operating system uses
|
|
* it. This key can be emulated by using it in place of another key such as
|
|
* F1. Don't forget to disable the real key binding.
|
|
*/
|
|
/* [kVK_F1] = Q_KEY_CODE_POWER, */
|
|
|
|
[kVK_F1] = Q_KEY_CODE_F1,
|
|
[kVK_F2] = Q_KEY_CODE_F2,
|
|
[kVK_F3] = Q_KEY_CODE_F3,
|
|
[kVK_F4] = Q_KEY_CODE_F4,
|
|
[kVK_F5] = Q_KEY_CODE_F5,
|
|
[kVK_F6] = Q_KEY_CODE_F6,
|
|
[kVK_F7] = Q_KEY_CODE_F7,
|
|
[kVK_F8] = Q_KEY_CODE_F8,
|
|
[kVK_F9] = Q_KEY_CODE_F9,
|
|
[kVK_F10] = Q_KEY_CODE_F10,
|
|
[kVK_F11] = Q_KEY_CODE_F11,
|
|
[kVK_F12] = Q_KEY_CODE_F12,
|
|
[kVK_F13] = Q_KEY_CODE_PRINT,
|
|
[kVK_F14] = Q_KEY_CODE_SCROLL_LOCK,
|
|
[kVK_F15] = Q_KEY_CODE_PAUSE,
|
|
|
|
// JIS keyboards only
|
|
[kVK_JIS_Yen] = Q_KEY_CODE_YEN,
|
|
[kVK_JIS_Underscore] = Q_KEY_CODE_RO,
|
|
[kVK_JIS_KeypadComma] = Q_KEY_CODE_KP_COMMA,
|
|
[kVK_JIS_Eisu] = Q_KEY_CODE_MUHENKAN,
|
|
[kVK_JIS_Kana] = Q_KEY_CODE_HENKAN,
|
|
|
|
/*
|
|
* The eject and volume keys can't be used here because they are handled at
|
|
* a lower level than what an Application can see.
|
|
*/
|
|
};
|
|
|
|
static int cocoa_keycode_to_qemu(int keycode)
|
|
{
|
|
if (ARRAY_SIZE(mac_to_qkeycode_map) <= keycode) {
|
|
error_report("(cocoa) warning unknown keycode 0x%x", keycode);
|
|
return 0;
|
|
}
|
|
return mac_to_qkeycode_map[keycode];
|
|
}
|
|
|
|
/* Displays an alert dialog box with the specified message */
|
|
static void QEMU_Alert(NSString *message)
|
|
{
|
|
NSAlert *alert;
|
|
alert = [NSAlert new];
|
|
[alert setMessageText: message];
|
|
[alert runModal];
|
|
}
|
|
|
|
/* Handles any errors that happen with a device transaction */
|
|
static void handleAnyDeviceErrors(Error * err)
|
|
{
|
|
if (err) {
|
|
QEMU_Alert([NSString stringWithCString: error_get_pretty(err)
|
|
encoding: NSASCIIStringEncoding]);
|
|
error_free(err);
|
|
}
|
|
}
|
|
|
|
/*
|
|
------------------------------------------------------
|
|
QemuCocoaView
|
|
------------------------------------------------------
|
|
*/
|
|
@interface QemuCocoaView : NSView
|
|
{
|
|
QEMUScreen screen;
|
|
NSWindow *fullScreenWindow;
|
|
float cx,cy,cw,ch,cdx,cdy;
|
|
pixman_image_t *pixman_image;
|
|
QKbdState *kbd;
|
|
BOOL isMouseGrabbed;
|
|
BOOL isFullscreen;
|
|
BOOL isAbsoluteEnabled;
|
|
CFMachPortRef eventsTap;
|
|
}
|
|
- (void) switchSurface:(pixman_image_t *)image;
|
|
- (void) grabMouse;
|
|
- (void) ungrabMouse;
|
|
- (void) toggleFullScreen:(id)sender;
|
|
- (void) setFullGrab:(id)sender;
|
|
- (void) handleMonitorInput:(NSEvent *)event;
|
|
- (bool) handleEvent:(NSEvent *)event;
|
|
- (bool) handleEventLocked:(NSEvent *)event;
|
|
- (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled;
|
|
/* The state surrounding mouse grabbing is potentially confusing.
|
|
* isAbsoluteEnabled tracks qemu_input_is_absolute() [ie "is the emulated
|
|
* pointing device an absolute-position one?"], but is only updated on
|
|
* next refresh.
|
|
* isMouseGrabbed tracks whether GUI events are directed to the guest;
|
|
* it controls whether special keys like Cmd get sent to the guest,
|
|
* and whether we capture the mouse when in non-absolute mode.
|
|
*/
|
|
- (BOOL) isMouseGrabbed;
|
|
- (BOOL) isAbsoluteEnabled;
|
|
- (float) cdx;
|
|
- (float) cdy;
|
|
- (QEMUScreen) gscreen;
|
|
- (void) raiseAllKeys;
|
|
@end
|
|
|
|
QemuCocoaView *cocoaView;
|
|
|
|
static CGEventRef handleTapEvent(CGEventTapProxy proxy, CGEventType type, CGEventRef cgEvent, void *userInfo)
|
|
{
|
|
QemuCocoaView *cocoaView = userInfo;
|
|
NSEvent *event = [NSEvent eventWithCGEvent:cgEvent];
|
|
if ([cocoaView isMouseGrabbed] && [cocoaView handleEvent:event]) {
|
|
COCOA_DEBUG("Global events tap: qemu handled the event, capturing!\n");
|
|
return NULL;
|
|
}
|
|
COCOA_DEBUG("Global events tap: qemu did not handle the event, letting it through...\n");
|
|
|
|
return cgEvent;
|
|
}
|
|
|
|
@implementation QemuCocoaView
|
|
- (id)initWithFrame:(NSRect)frameRect
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: initWithFrame\n");
|
|
|
|
self = [super initWithFrame:frameRect];
|
|
if (self) {
|
|
|
|
screen.width = frameRect.size.width;
|
|
screen.height = frameRect.size.height;
|
|
kbd = qkbd_state_init(dcl.con);
|
|
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: dealloc\n");
|
|
|
|
if (pixman_image) {
|
|
pixman_image_unref(pixman_image);
|
|
}
|
|
|
|
qkbd_state_free(kbd);
|
|
|
|
if (eventsTap) {
|
|
CFRelease(eventsTap);
|
|
}
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
- (BOOL) isOpaque
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL) screenContainsPoint:(NSPoint) p
|
|
{
|
|
return (p.x > -1 && p.x < screen.width && p.y > -1 && p.y < screen.height);
|
|
}
|
|
|
|
/* Get location of event and convert to virtual screen coordinate */
|
|
- (CGPoint) screenLocationOfEvent:(NSEvent *)ev
|
|
{
|
|
NSWindow *eventWindow = [ev window];
|
|
// XXX: Use CGRect and -convertRectFromScreen: to support macOS 10.10
|
|
CGRect r = CGRectZero;
|
|
r.origin = [ev locationInWindow];
|
|
if (!eventWindow) {
|
|
if (!isFullscreen) {
|
|
return [[self window] convertRectFromScreen:r].origin;
|
|
} else {
|
|
CGPoint locationInSelfWindow = [[self window] convertRectFromScreen:r].origin;
|
|
CGPoint loc = [self convertPoint:locationInSelfWindow fromView:nil];
|
|
if (stretch_video) {
|
|
loc.x /= cdx;
|
|
loc.y /= cdy;
|
|
}
|
|
return loc;
|
|
}
|
|
} else if ([[self window] isEqual:eventWindow]) {
|
|
if (!isFullscreen) {
|
|
return r.origin;
|
|
} else {
|
|
CGPoint loc = [self convertPoint:r.origin fromView:nil];
|
|
if (stretch_video) {
|
|
loc.x /= cdx;
|
|
loc.y /= cdy;
|
|
}
|
|
return loc;
|
|
}
|
|
} else {
|
|
return [[self window] convertRectFromScreen:[eventWindow convertRectToScreen:r]].origin;
|
|
}
|
|
}
|
|
|
|
- (void) hideCursor
|
|
{
|
|
if (!cursor_hide) {
|
|
return;
|
|
}
|
|
[NSCursor hide];
|
|
}
|
|
|
|
- (void) unhideCursor
|
|
{
|
|
if (!cursor_hide) {
|
|
return;
|
|
}
|
|
[NSCursor unhide];
|
|
}
|
|
|
|
- (void) drawRect:(NSRect) rect
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: drawRect\n");
|
|
|
|
// get CoreGraphic context
|
|
CGContextRef viewContextRef = [[NSGraphicsContext currentContext] CGContext];
|
|
|
|
CGContextSetInterpolationQuality (viewContextRef, kCGInterpolationNone);
|
|
CGContextSetShouldAntialias (viewContextRef, NO);
|
|
|
|
// draw screen bitmap directly to Core Graphics context
|
|
if (!pixman_image) {
|
|
// Draw request before any guest device has set up a framebuffer:
|
|
// just draw an opaque black rectangle
|
|
CGContextSetRGBFillColor(viewContextRef, 0, 0, 0, 1.0);
|
|
CGContextFillRect(viewContextRef, NSRectToCGRect(rect));
|
|
} else {
|
|
int w = pixman_image_get_width(pixman_image);
|
|
int h = pixman_image_get_height(pixman_image);
|
|
int bitsPerPixel = PIXMAN_FORMAT_BPP(pixman_image_get_format(pixman_image));
|
|
int stride = pixman_image_get_stride(pixman_image);
|
|
CGDataProviderRef dataProviderRef = CGDataProviderCreateWithData(
|
|
NULL,
|
|
pixman_image_get_data(pixman_image),
|
|
stride * h,
|
|
NULL
|
|
);
|
|
CGImageRef imageRef = CGImageCreate(
|
|
w, //width
|
|
h, //height
|
|
DIV_ROUND_UP(bitsPerPixel, 8) * 2, //bitsPerComponent
|
|
bitsPerPixel, //bitsPerPixel
|
|
stride, //bytesPerRow
|
|
CGColorSpaceCreateWithName(kCGColorSpaceSRGB), //colorspace
|
|
kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst, //bitmapInfo
|
|
dataProviderRef, //provider
|
|
NULL, //decode
|
|
0, //interpolate
|
|
kCGRenderingIntentDefault //intent
|
|
);
|
|
// selective drawing code (draws only dirty rectangles) (OS X >= 10.4)
|
|
const NSRect *rectList;
|
|
NSInteger rectCount;
|
|
int i;
|
|
CGImageRef clipImageRef;
|
|
CGRect clipRect;
|
|
|
|
[self getRectsBeingDrawn:&rectList count:&rectCount];
|
|
for (i = 0; i < rectCount; i++) {
|
|
clipRect.origin.x = rectList[i].origin.x / cdx;
|
|
clipRect.origin.y = (float)h - (rectList[i].origin.y + rectList[i].size.height) / cdy;
|
|
clipRect.size.width = rectList[i].size.width / cdx;
|
|
clipRect.size.height = rectList[i].size.height / cdy;
|
|
clipImageRef = CGImageCreateWithImageInRect(
|
|
imageRef,
|
|
clipRect
|
|
);
|
|
CGContextDrawImage (viewContextRef, cgrect(rectList[i]), clipImageRef);
|
|
CGImageRelease (clipImageRef);
|
|
}
|
|
CGImageRelease (imageRef);
|
|
CGDataProviderRelease(dataProviderRef);
|
|
}
|
|
}
|
|
|
|
- (void) setContentDimensions
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: setContentDimensions\n");
|
|
|
|
if (isFullscreen) {
|
|
cdx = [[NSScreen mainScreen] frame].size.width / (float)screen.width;
|
|
cdy = [[NSScreen mainScreen] frame].size.height / (float)screen.height;
|
|
|
|
/* stretches video, but keeps same aspect ratio */
|
|
if (stretch_video == true) {
|
|
/* use smallest stretch value - prevents clipping on sides */
|
|
if (MIN(cdx, cdy) == cdx) {
|
|
cdy = cdx;
|
|
} else {
|
|
cdx = cdy;
|
|
}
|
|
} else { /* No stretching */
|
|
cdx = cdy = 1;
|
|
}
|
|
cw = screen.width * cdx;
|
|
ch = screen.height * cdy;
|
|
cx = ([[NSScreen mainScreen] frame].size.width - cw) / 2.0;
|
|
cy = ([[NSScreen mainScreen] frame].size.height - ch) / 2.0;
|
|
} else {
|
|
cx = 0;
|
|
cy = 0;
|
|
cw = screen.width;
|
|
ch = screen.height;
|
|
cdx = 1.0;
|
|
cdy = 1.0;
|
|
}
|
|
}
|
|
|
|
- (void) updateUIInfoLocked
|
|
{
|
|
/* Must be called with the iothread lock, i.e. via updateUIInfo */
|
|
NSSize frameSize;
|
|
QemuUIInfo info;
|
|
|
|
if (!qemu_console_is_graphic(dcl.con)) {
|
|
return;
|
|
}
|
|
|
|
if ([self window]) {
|
|
NSDictionary *description = [[[self window] screen] deviceDescription];
|
|
CGDirectDisplayID display = [[description objectForKey:@"NSScreenNumber"] unsignedIntValue];
|
|
NSSize screenSize = [[[self window] screen] frame].size;
|
|
CGSize screenPhysicalSize = CGDisplayScreenSize(display);
|
|
CVDisplayLinkRef displayLink;
|
|
|
|
frameSize = isFullscreen ? screenSize : [self frame].size;
|
|
|
|
if (!CVDisplayLinkCreateWithCGDisplay(display, &displayLink)) {
|
|
CVTime period = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLink);
|
|
CVDisplayLinkRelease(displayLink);
|
|
if (!(period.flags & kCVTimeIsIndefinite)) {
|
|
update_displaychangelistener(&dcl,
|
|
1000 * period.timeValue / period.timeScale);
|
|
info.refresh_rate = (int64_t)1000 * period.timeScale / period.timeValue;
|
|
}
|
|
}
|
|
|
|
info.width_mm = frameSize.width / screenSize.width * screenPhysicalSize.width;
|
|
info.height_mm = frameSize.height / screenSize.height * screenPhysicalSize.height;
|
|
} else {
|
|
frameSize = [self frame].size;
|
|
info.width_mm = 0;
|
|
info.height_mm = 0;
|
|
}
|
|
|
|
info.xoff = 0;
|
|
info.yoff = 0;
|
|
info.width = frameSize.width;
|
|
info.height = frameSize.height;
|
|
|
|
dpy_set_ui_info(dcl.con, &info, TRUE);
|
|
}
|
|
|
|
- (void) updateUIInfo
|
|
{
|
|
if (!allow_events) {
|
|
/*
|
|
* Don't try to tell QEMU about UI information in the application
|
|
* startup phase -- we haven't yet registered dcl with the QEMU UI
|
|
* layer, and also trying to take the iothread lock would deadlock.
|
|
* When cocoa_display_init() does register the dcl, the UI layer
|
|
* will call cocoa_switch(), which will call updateUIInfo, so
|
|
* we don't lose any information here.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
with_iothread_lock(^{
|
|
[self updateUIInfoLocked];
|
|
});
|
|
}
|
|
|
|
- (void)viewDidMoveToWindow
|
|
{
|
|
[self updateUIInfo];
|
|
}
|
|
|
|
- (void) switchSurface:(pixman_image_t *)image
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: switchSurface\n");
|
|
|
|
int w = pixman_image_get_width(image);
|
|
int h = pixman_image_get_height(image);
|
|
/* cdx == 0 means this is our very first surface, in which case we need
|
|
* to recalculate the content dimensions even if it happens to be the size
|
|
* of the initial empty window.
|
|
*/
|
|
bool isResize = (w != screen.width || h != screen.height || cdx == 0.0);
|
|
|
|
int oldh = screen.height;
|
|
if (isResize) {
|
|
// Resize before we trigger the redraw, or we'll redraw at the wrong size
|
|
COCOA_DEBUG("switchSurface: new size %d x %d\n", w, h);
|
|
screen.width = w;
|
|
screen.height = h;
|
|
[self setContentDimensions];
|
|
[self setFrame:NSMakeRect(cx, cy, cw, ch)];
|
|
}
|
|
|
|
// update screenBuffer
|
|
if (pixman_image) {
|
|
pixman_image_unref(pixman_image);
|
|
}
|
|
|
|
pixman_image = image;
|
|
|
|
// update windows
|
|
if (isFullscreen) {
|
|
[[fullScreenWindow contentView] setFrame:[[NSScreen mainScreen] frame]];
|
|
[normalWindow setFrame:NSMakeRect([normalWindow frame].origin.x, [normalWindow frame].origin.y - h + oldh, w, h + [normalWindow frame].size.height - oldh) display:NO animate:NO];
|
|
} else {
|
|
if (qemu_name)
|
|
[normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]];
|
|
[normalWindow setFrame:NSMakeRect([normalWindow frame].origin.x, [normalWindow frame].origin.y - h + oldh, w, h + [normalWindow frame].size.height - oldh) display:YES animate:NO];
|
|
}
|
|
|
|
if (isResize) {
|
|
[normalWindow center];
|
|
}
|
|
}
|
|
|
|
- (void) toggleFullScreen:(id)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: toggleFullScreen\n");
|
|
|
|
if (isFullscreen) { // switch from fullscreen to desktop
|
|
isFullscreen = FALSE;
|
|
[self ungrabMouse];
|
|
[self setContentDimensions];
|
|
[fullScreenWindow close];
|
|
[normalWindow setContentView: self];
|
|
[normalWindow makeKeyAndOrderFront: self];
|
|
[NSMenu setMenuBarVisible:YES];
|
|
} else { // switch from desktop to fullscreen
|
|
isFullscreen = TRUE;
|
|
[normalWindow orderOut: nil]; /* Hide the window */
|
|
[self grabMouse];
|
|
[self setContentDimensions];
|
|
[NSMenu setMenuBarVisible:NO];
|
|
fullScreenWindow = [[NSWindow alloc] initWithContentRect:[[NSScreen mainScreen] frame]
|
|
styleMask:NSWindowStyleMaskBorderless
|
|
backing:NSBackingStoreBuffered
|
|
defer:NO];
|
|
[fullScreenWindow setAcceptsMouseMovedEvents: YES];
|
|
[fullScreenWindow setHasShadow:NO];
|
|
[fullScreenWindow setBackgroundColor: [NSColor blackColor]];
|
|
[self setFrame:NSMakeRect(cx, cy, cw, ch)];
|
|
[[fullScreenWindow contentView] addSubview: self];
|
|
[fullScreenWindow makeKeyAndOrderFront:self];
|
|
}
|
|
}
|
|
|
|
- (void) setFullGrab:(id)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: setFullGrab\n");
|
|
|
|
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged);
|
|
eventsTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
|
|
mask, handleTapEvent, self);
|
|
if (!eventsTap) {
|
|
warn_report("Could not create event tap, system key combos will not be captured.\n");
|
|
return;
|
|
} else {
|
|
COCOA_DEBUG("Global events tap created! Will capture system key combos.\n");
|
|
}
|
|
|
|
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
|
if (!runLoop) {
|
|
warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n");
|
|
return;
|
|
}
|
|
|
|
CFRunLoopSourceRef tapEventsSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventsTap, 0);
|
|
if (!tapEventsSrc ) {
|
|
warn_report("Could not obtain current CF RunLoop, system key combos will not be captured.\n");
|
|
return;
|
|
}
|
|
|
|
CFRunLoopAddSource(runLoop, tapEventsSrc, kCFRunLoopDefaultMode);
|
|
CFRelease(tapEventsSrc);
|
|
}
|
|
|
|
- (void) toggleKey: (int)keycode {
|
|
qkbd_state_key_event(kbd, keycode, !qkbd_state_key_get(kbd, keycode));
|
|
}
|
|
|
|
// Does the work of sending input to the monitor
|
|
- (void) handleMonitorInput:(NSEvent *)event
|
|
{
|
|
int keysym = 0;
|
|
int control_key = 0;
|
|
|
|
// if the control key is down
|
|
if ([event modifierFlags] & NSEventModifierFlagControl) {
|
|
control_key = 1;
|
|
}
|
|
|
|
/* translates Macintosh keycodes to QEMU's keysym */
|
|
|
|
static const int without_control_translation[] = {
|
|
[0 ... 0xff] = 0, // invalid key
|
|
|
|
[kVK_UpArrow] = QEMU_KEY_UP,
|
|
[kVK_DownArrow] = QEMU_KEY_DOWN,
|
|
[kVK_RightArrow] = QEMU_KEY_RIGHT,
|
|
[kVK_LeftArrow] = QEMU_KEY_LEFT,
|
|
[kVK_Home] = QEMU_KEY_HOME,
|
|
[kVK_End] = QEMU_KEY_END,
|
|
[kVK_PageUp] = QEMU_KEY_PAGEUP,
|
|
[kVK_PageDown] = QEMU_KEY_PAGEDOWN,
|
|
[kVK_ForwardDelete] = QEMU_KEY_DELETE,
|
|
[kVK_Delete] = QEMU_KEY_BACKSPACE,
|
|
};
|
|
|
|
static const int with_control_translation[] = {
|
|
[0 ... 0xff] = 0, // invalid key
|
|
|
|
[kVK_UpArrow] = QEMU_KEY_CTRL_UP,
|
|
[kVK_DownArrow] = QEMU_KEY_CTRL_DOWN,
|
|
[kVK_RightArrow] = QEMU_KEY_CTRL_RIGHT,
|
|
[kVK_LeftArrow] = QEMU_KEY_CTRL_LEFT,
|
|
[kVK_Home] = QEMU_KEY_CTRL_HOME,
|
|
[kVK_End] = QEMU_KEY_CTRL_END,
|
|
[kVK_PageUp] = QEMU_KEY_CTRL_PAGEUP,
|
|
[kVK_PageDown] = QEMU_KEY_CTRL_PAGEDOWN,
|
|
};
|
|
|
|
if (control_key != 0) { /* If the control key is being used */
|
|
if ([event keyCode] < ARRAY_SIZE(with_control_translation)) {
|
|
keysym = with_control_translation[[event keyCode]];
|
|
}
|
|
} else {
|
|
if ([event keyCode] < ARRAY_SIZE(without_control_translation)) {
|
|
keysym = without_control_translation[[event keyCode]];
|
|
}
|
|
}
|
|
|
|
// if not a key that needs translating
|
|
if (keysym == 0) {
|
|
NSString *ks = [event characters];
|
|
if ([ks length] > 0) {
|
|
keysym = [ks characterAtIndex:0];
|
|
}
|
|
}
|
|
|
|
if (keysym) {
|
|
kbd_put_keysym(keysym);
|
|
}
|
|
}
|
|
|
|
- (bool) handleEvent:(NSEvent *)event
|
|
{
|
|
if(!allow_events) {
|
|
/*
|
|
* Just let OSX have all events that arrive before
|
|
* applicationDidFinishLaunching.
|
|
* This avoids a deadlock on the iothread lock, which cocoa_display_init()
|
|
* will not drop until after the app_started_sem is posted. (In theory
|
|
* there should not be any such events, but OSX Catalina now emits some.)
|
|
*/
|
|
return false;
|
|
}
|
|
return bool_with_iothread_lock(^{
|
|
return [self handleEventLocked:event];
|
|
});
|
|
}
|
|
|
|
- (bool) handleEventLocked:(NSEvent *)event
|
|
{
|
|
/* Return true if we handled the event, false if it should be given to OSX */
|
|
COCOA_DEBUG("QemuCocoaView: handleEvent\n");
|
|
int buttons = 0;
|
|
int keycode = 0;
|
|
bool mouse_event = false;
|
|
// Location of event in virtual screen coordinates
|
|
NSPoint p = [self screenLocationOfEvent:event];
|
|
NSUInteger modifiers = [event modifierFlags];
|
|
|
|
/*
|
|
* Check -[NSEvent modifierFlags] here.
|
|
*
|
|
* There is a NSEventType for an event notifying the change of
|
|
* -[NSEvent modifierFlags], NSEventTypeFlagsChanged but these operations
|
|
* are performed for any events because a modifier state may change while
|
|
* the application is inactive (i.e. no events fire) and we don't want to
|
|
* wait for another modifier state change to detect such a change.
|
|
*
|
|
* NSEventModifierFlagCapsLock requires a special treatment. The other flags
|
|
* are handled in similar manners.
|
|
*
|
|
* NSEventModifierFlagCapsLock
|
|
* ---------------------------
|
|
*
|
|
* If CapsLock state is changed, "up" and "down" events will be fired in
|
|
* sequence, effectively updates CapsLock state on the guest.
|
|
*
|
|
* The other flags
|
|
* ---------------
|
|
*
|
|
* If a flag is not set, fire "up" events for all keys which correspond to
|
|
* the flag. Note that "down" events are not fired here because the flags
|
|
* checked here do not tell what exact keys are down.
|
|
*
|
|
* If one of the keys corresponding to a flag is down, we rely on
|
|
* -[NSEvent keyCode] of an event whose -[NSEvent type] is
|
|
* NSEventTypeFlagsChanged to know the exact key which is down, which has
|
|
* the following two downsides:
|
|
* - It does not work when the application is inactive as described above.
|
|
* - It malfactions *after* the modifier state is changed while the
|
|
* application is inactive. It is because -[NSEvent keyCode] does not tell
|
|
* if the key is up or down, and requires to infer the current state from
|
|
* the previous state. It is still possible to fix such a malfanction by
|
|
* completely leaving your hands from the keyboard, which hopefully makes
|
|
* this implementation usable enough.
|
|
*/
|
|
if (!!(modifiers & NSEventModifierFlagCapsLock) !=
|
|
qkbd_state_modifier_get(kbd, QKBD_MOD_CAPSLOCK)) {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, true);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_CAPS_LOCK, false);
|
|
}
|
|
|
|
if (!(modifiers & NSEventModifierFlagShift)) {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT, false);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_SHIFT_R, false);
|
|
}
|
|
if (!(modifiers & NSEventModifierFlagControl)) {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL, false);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_CTRL_R, false);
|
|
}
|
|
if (!(modifiers & NSEventModifierFlagOption)) {
|
|
if (swap_opt_cmd) {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false);
|
|
} else {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false);
|
|
}
|
|
}
|
|
if (!(modifiers & NSEventModifierFlagCommand)) {
|
|
if (swap_opt_cmd) {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_ALT, false);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_ALT_R, false);
|
|
} else {
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_META_L, false);
|
|
qkbd_state_key_event(kbd, Q_KEY_CODE_META_R, false);
|
|
}
|
|
}
|
|
|
|
switch ([event type]) {
|
|
case NSEventTypeFlagsChanged:
|
|
switch ([event keyCode]) {
|
|
case kVK_Shift:
|
|
if (!!(modifiers & NSEventModifierFlagShift)) {
|
|
[self toggleKey:Q_KEY_CODE_SHIFT];
|
|
}
|
|
break;
|
|
|
|
case kVK_RightShift:
|
|
if (!!(modifiers & NSEventModifierFlagShift)) {
|
|
[self toggleKey:Q_KEY_CODE_SHIFT_R];
|
|
}
|
|
break;
|
|
|
|
case kVK_Control:
|
|
if (!!(modifiers & NSEventModifierFlagControl)) {
|
|
[self toggleKey:Q_KEY_CODE_CTRL];
|
|
}
|
|
break;
|
|
|
|
case kVK_RightControl:
|
|
if (!!(modifiers & NSEventModifierFlagControl)) {
|
|
[self toggleKey:Q_KEY_CODE_CTRL_R];
|
|
}
|
|
break;
|
|
|
|
case kVK_Option:
|
|
if (!!(modifiers & NSEventModifierFlagOption)) {
|
|
if (swap_opt_cmd) {
|
|
[self toggleKey:Q_KEY_CODE_META_L];
|
|
} else {
|
|
[self toggleKey:Q_KEY_CODE_ALT];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kVK_RightOption:
|
|
if (!!(modifiers & NSEventModifierFlagOption)) {
|
|
if (swap_opt_cmd) {
|
|
[self toggleKey:Q_KEY_CODE_META_R];
|
|
} else {
|
|
[self toggleKey:Q_KEY_CODE_ALT_R];
|
|
}
|
|
}
|
|
break;
|
|
|
|
/* Don't pass command key changes to guest unless mouse is grabbed */
|
|
case kVK_Command:
|
|
if (isMouseGrabbed &&
|
|
!!(modifiers & NSEventModifierFlagCommand) &&
|
|
left_command_key_enabled) {
|
|
if (swap_opt_cmd) {
|
|
[self toggleKey:Q_KEY_CODE_ALT];
|
|
} else {
|
|
[self toggleKey:Q_KEY_CODE_META_L];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kVK_RightCommand:
|
|
if (isMouseGrabbed &&
|
|
!!(modifiers & NSEventModifierFlagCommand)) {
|
|
if (swap_opt_cmd) {
|
|
[self toggleKey:Q_KEY_CODE_ALT_R];
|
|
} else {
|
|
[self toggleKey:Q_KEY_CODE_META_R];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case NSEventTypeKeyDown:
|
|
keycode = cocoa_keycode_to_qemu([event keyCode]);
|
|
|
|
// forward command key combos to the host UI unless the mouse is grabbed
|
|
if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) {
|
|
return false;
|
|
}
|
|
|
|
// default
|
|
|
|
// handle control + alt Key Combos (ctrl+alt+[1..9,g] is reserved for QEMU)
|
|
if (([event modifierFlags] & NSEventModifierFlagControl) && ([event modifierFlags] & NSEventModifierFlagOption)) {
|
|
NSString *keychar = [event charactersIgnoringModifiers];
|
|
if ([keychar length] == 1) {
|
|
char key = [keychar characterAtIndex:0];
|
|
switch (key) {
|
|
|
|
// enable graphic console
|
|
case '1' ... '9':
|
|
console_select(key - '0' - 1); /* ascii math */
|
|
return true;
|
|
|
|
// release the mouse grab
|
|
case 'g':
|
|
[self ungrabMouse];
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (qemu_console_is_graphic(NULL)) {
|
|
qkbd_state_key_event(kbd, keycode, true);
|
|
} else {
|
|
[self handleMonitorInput: event];
|
|
}
|
|
break;
|
|
case NSEventTypeKeyUp:
|
|
keycode = cocoa_keycode_to_qemu([event keyCode]);
|
|
|
|
// don't pass the guest a spurious key-up if we treated this
|
|
// command-key combo as a host UI action
|
|
if (!isMouseGrabbed && ([event modifierFlags] & NSEventModifierFlagCommand)) {
|
|
return true;
|
|
}
|
|
|
|
if (qemu_console_is_graphic(NULL)) {
|
|
qkbd_state_key_event(kbd, keycode, false);
|
|
}
|
|
break;
|
|
case NSEventTypeMouseMoved:
|
|
if (isAbsoluteEnabled) {
|
|
// Cursor re-entered into a window might generate events bound to screen coordinates
|
|
// and `nil` window property, and in full screen mode, current window might not be
|
|
// key window, where event location alone should suffice.
|
|
if (![self screenContainsPoint:p] || !([[self window] isKeyWindow] || isFullscreen)) {
|
|
if (isMouseGrabbed) {
|
|
[self ungrabMouse];
|
|
}
|
|
} else {
|
|
if (!isMouseGrabbed) {
|
|
[self grabMouse];
|
|
}
|
|
}
|
|
}
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeLeftMouseDown:
|
|
buttons |= MOUSE_EVENT_LBUTTON;
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeRightMouseDown:
|
|
buttons |= MOUSE_EVENT_RBUTTON;
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeOtherMouseDown:
|
|
buttons |= MOUSE_EVENT_MBUTTON;
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeLeftMouseDragged:
|
|
buttons |= MOUSE_EVENT_LBUTTON;
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeRightMouseDragged:
|
|
buttons |= MOUSE_EVENT_RBUTTON;
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeOtherMouseDragged:
|
|
buttons |= MOUSE_EVENT_MBUTTON;
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeLeftMouseUp:
|
|
mouse_event = true;
|
|
if (!isMouseGrabbed && [self screenContainsPoint:p]) {
|
|
/*
|
|
* In fullscreen mode, the window of cocoaView may not be the
|
|
* key window, therefore the position relative to the virtual
|
|
* screen alone will be sufficient.
|
|
*/
|
|
if(isFullscreen || [[self window] isKeyWindow]) {
|
|
[self grabMouse];
|
|
}
|
|
}
|
|
break;
|
|
case NSEventTypeRightMouseUp:
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeOtherMouseUp:
|
|
mouse_event = true;
|
|
break;
|
|
case NSEventTypeScrollWheel:
|
|
/*
|
|
* Send wheel events to the guest regardless of window focus.
|
|
* This is in-line with standard Mac OS X UI behaviour.
|
|
*/
|
|
|
|
/*
|
|
* We shouldn't have got a scroll event when deltaY and delta Y
|
|
* are zero, hence no harm in dropping the event
|
|
*/
|
|
if ([event deltaY] != 0 || [event deltaX] != 0) {
|
|
/* Determine if this is a scroll up or scroll down event */
|
|
if ([event deltaY] != 0) {
|
|
buttons = ([event deltaY] > 0) ?
|
|
INPUT_BUTTON_WHEEL_UP : INPUT_BUTTON_WHEEL_DOWN;
|
|
} else if ([event deltaX] != 0) {
|
|
buttons = ([event deltaX] > 0) ?
|
|
INPUT_BUTTON_WHEEL_LEFT : INPUT_BUTTON_WHEEL_RIGHT;
|
|
}
|
|
|
|
qemu_input_queue_btn(dcl.con, buttons, true);
|
|
qemu_input_event_sync();
|
|
qemu_input_queue_btn(dcl.con, buttons, false);
|
|
qemu_input_event_sync();
|
|
}
|
|
|
|
/*
|
|
* Since deltaX/deltaY also report scroll wheel events we prevent mouse
|
|
* movement code from executing.
|
|
*/
|
|
mouse_event = false;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (mouse_event) {
|
|
/* Don't send button events to the guest unless we've got a
|
|
* mouse grab or window focus. If we have neither then this event
|
|
* is the user clicking on the background window to activate and
|
|
* bring us to the front, which will be done by the sendEvent
|
|
* call below. We definitely don't want to pass that click through
|
|
* to the guest.
|
|
*/
|
|
if ((isMouseGrabbed || [[self window] isKeyWindow]) &&
|
|
(last_buttons != buttons)) {
|
|
static uint32_t bmap[INPUT_BUTTON__MAX] = {
|
|
[INPUT_BUTTON_LEFT] = MOUSE_EVENT_LBUTTON,
|
|
[INPUT_BUTTON_MIDDLE] = MOUSE_EVENT_MBUTTON,
|
|
[INPUT_BUTTON_RIGHT] = MOUSE_EVENT_RBUTTON
|
|
};
|
|
qemu_input_update_buttons(dcl.con, bmap, last_buttons, buttons);
|
|
last_buttons = buttons;
|
|
}
|
|
if (isMouseGrabbed) {
|
|
if (isAbsoluteEnabled) {
|
|
/* Note that the origin for Cocoa mouse coords is bottom left, not top left.
|
|
* The check on screenContainsPoint is to avoid sending out of range values for
|
|
* clicks in the titlebar.
|
|
*/
|
|
if ([self screenContainsPoint:p]) {
|
|
qemu_input_queue_abs(dcl.con, INPUT_AXIS_X, p.x, 0, screen.width);
|
|
qemu_input_queue_abs(dcl.con, INPUT_AXIS_Y, screen.height - p.y, 0, screen.height);
|
|
}
|
|
} else {
|
|
qemu_input_queue_rel(dcl.con, INPUT_AXIS_X, (int)[event deltaX]);
|
|
qemu_input_queue_rel(dcl.con, INPUT_AXIS_Y, (int)[event deltaY]);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
qemu_input_event_sync();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
- (void) grabMouse
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: grabMouse\n");
|
|
|
|
if (!isFullscreen) {
|
|
if (qemu_name)
|
|
[normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s - (Press ctrl + alt + g to release Mouse)", qemu_name]];
|
|
else
|
|
[normalWindow setTitle:@"QEMU - (Press ctrl + alt + g to release Mouse)"];
|
|
}
|
|
[self hideCursor];
|
|
CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled);
|
|
isMouseGrabbed = TRUE; // while isMouseGrabbed = TRUE, QemuCocoaApp sends all events to [cocoaView handleEvent:]
|
|
}
|
|
|
|
- (void) ungrabMouse
|
|
{
|
|
COCOA_DEBUG("QemuCocoaView: ungrabMouse\n");
|
|
|
|
if (!isFullscreen) {
|
|
if (qemu_name)
|
|
[normalWindow setTitle:[NSString stringWithFormat:@"QEMU %s", qemu_name]];
|
|
else
|
|
[normalWindow setTitle:@"QEMU"];
|
|
}
|
|
[self unhideCursor];
|
|
CGAssociateMouseAndMouseCursorPosition(TRUE);
|
|
isMouseGrabbed = FALSE;
|
|
}
|
|
|
|
- (void) setAbsoluteEnabled:(BOOL)tIsAbsoluteEnabled {
|
|
isAbsoluteEnabled = tIsAbsoluteEnabled;
|
|
if (isMouseGrabbed) {
|
|
CGAssociateMouseAndMouseCursorPosition(isAbsoluteEnabled);
|
|
}
|
|
}
|
|
- (BOOL) isMouseGrabbed {return isMouseGrabbed;}
|
|
- (BOOL) isAbsoluteEnabled {return isAbsoluteEnabled;}
|
|
- (float) cdx {return cdx;}
|
|
- (float) cdy {return cdy;}
|
|
- (QEMUScreen) gscreen {return screen;}
|
|
|
|
/*
|
|
* Makes the target think all down keys are being released.
|
|
* This prevents a stuck key problem, since we will not see
|
|
* key up events for those keys after we have lost focus.
|
|
*/
|
|
- (void) raiseAllKeys
|
|
{
|
|
with_iothread_lock(^{
|
|
qkbd_state_lift_all_keys(kbd);
|
|
});
|
|
}
|
|
@end
|
|
|
|
|
|
|
|
/*
|
|
------------------------------------------------------
|
|
QemuCocoaAppController
|
|
------------------------------------------------------
|
|
*/
|
|
@interface QemuCocoaAppController : NSObject
|
|
<NSWindowDelegate, NSApplicationDelegate>
|
|
{
|
|
}
|
|
- (void)doToggleFullScreen:(id)sender;
|
|
- (void)toggleFullScreen:(id)sender;
|
|
- (void)showQEMUDoc:(id)sender;
|
|
- (void)zoomToFit:(id) sender;
|
|
- (void)displayConsole:(id)sender;
|
|
- (void)pauseQEMU:(id)sender;
|
|
- (void)resumeQEMU:(id)sender;
|
|
- (void)displayPause;
|
|
- (void)removePause;
|
|
- (void)restartQEMU:(id)sender;
|
|
- (void)powerDownQEMU:(id)sender;
|
|
- (void)ejectDeviceMedia:(id)sender;
|
|
- (void)changeDeviceMedia:(id)sender;
|
|
- (BOOL)verifyQuit;
|
|
- (void)openDocumentation:(NSString *)filename;
|
|
- (IBAction) do_about_menu_item: (id) sender;
|
|
- (void)adjustSpeed:(id)sender;
|
|
@end
|
|
|
|
@implementation QemuCocoaAppController
|
|
- (id) init
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: init\n");
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
|
|
// create a view and add it to the window
|
|
cocoaView = [[QemuCocoaView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 640.0, 480.0)];
|
|
if(!cocoaView) {
|
|
error_report("(cocoa) can't create a view");
|
|
exit(1);
|
|
}
|
|
|
|
// create a window
|
|
normalWindow = [[NSWindow alloc] initWithContentRect:[cocoaView frame]
|
|
styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskClosable
|
|
backing:NSBackingStoreBuffered defer:NO];
|
|
if(!normalWindow) {
|
|
error_report("(cocoa) can't create window");
|
|
exit(1);
|
|
}
|
|
[normalWindow setAcceptsMouseMovedEvents:YES];
|
|
[normalWindow setTitle:@"QEMU"];
|
|
[normalWindow setContentView:cocoaView];
|
|
[normalWindow makeKeyAndOrderFront:self];
|
|
[normalWindow center];
|
|
[normalWindow setDelegate: self];
|
|
stretch_video = false;
|
|
|
|
/* Used for displaying pause on the screen */
|
|
pauseLabel = [NSTextField new];
|
|
[pauseLabel setBezeled:YES];
|
|
[pauseLabel setDrawsBackground:YES];
|
|
[pauseLabel setBackgroundColor: [NSColor whiteColor]];
|
|
[pauseLabel setEditable:NO];
|
|
[pauseLabel setSelectable:NO];
|
|
[pauseLabel setStringValue: @"Paused"];
|
|
[pauseLabel setFont: [NSFont fontWithName: @"Helvetica" size: 90]];
|
|
[pauseLabel setTextColor: [NSColor blackColor]];
|
|
[pauseLabel sizeToFit];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: dealloc\n");
|
|
|
|
if (cocoaView)
|
|
[cocoaView release];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void)applicationDidFinishLaunching: (NSNotification *) note
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: applicationDidFinishLaunching\n");
|
|
allow_events = true;
|
|
/* Tell cocoa_display_init to proceed */
|
|
qemu_sem_post(&app_started_sem);
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification *)aNotification
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: applicationWillTerminate\n");
|
|
|
|
with_iothread_lock(^{
|
|
shutdown_action = SHUTDOWN_ACTION_POWEROFF;
|
|
qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI);
|
|
});
|
|
|
|
/*
|
|
* Sleep here, because returning will cause OSX to kill us
|
|
* immediately; the QEMU main loop will handle the shutdown
|
|
* request and terminate the process.
|
|
*/
|
|
[NSThread sleepForTimeInterval:INFINITY];
|
|
}
|
|
|
|
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate:
|
|
(NSApplication *)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: applicationShouldTerminate\n");
|
|
return [self verifyQuit];
|
|
}
|
|
|
|
- (void)windowDidChangeScreen:(NSNotification *)notification
|
|
{
|
|
[cocoaView updateUIInfo];
|
|
}
|
|
|
|
- (void)windowDidResize:(NSNotification *)notification
|
|
{
|
|
[cocoaView updateUIInfo];
|
|
}
|
|
|
|
/* Called when the user clicks on a window's close button */
|
|
- (BOOL)windowShouldClose:(id)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: windowShouldClose\n");
|
|
[NSApp terminate: sender];
|
|
/* If the user allows the application to quit then the call to
|
|
* NSApp terminate will never return. If we get here then the user
|
|
* cancelled the quit, so we should return NO to not permit the
|
|
* closing of this window.
|
|
*/
|
|
return NO;
|
|
}
|
|
|
|
/* Called when QEMU goes into the background */
|
|
- (void) applicationWillResignActive: (NSNotification *)aNotification
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: applicationWillResignActive\n");
|
|
[cocoaView ungrabMouse];
|
|
[cocoaView raiseAllKeys];
|
|
}
|
|
|
|
/* We abstract the method called by the Enter Fullscreen menu item
|
|
* because Mac OS 10.7 and higher disables it. This is because of the
|
|
* menu item's old selector's name toggleFullScreen:
|
|
*/
|
|
- (void) doToggleFullScreen:(id)sender
|
|
{
|
|
[self toggleFullScreen:(id)sender];
|
|
}
|
|
|
|
- (void)toggleFullScreen:(id)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: toggleFullScreen\n");
|
|
|
|
[cocoaView toggleFullScreen:sender];
|
|
}
|
|
|
|
- (void) setFullGrab:(id)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: setFullGrab\n");
|
|
|
|
[cocoaView setFullGrab:sender];
|
|
}
|
|
|
|
/* Tries to find then open the specified filename */
|
|
- (void) openDocumentation: (NSString *) filename
|
|
{
|
|
/* Where to look for local files */
|
|
NSString *path_array[] = {@"../share/doc/qemu/", @"../doc/qemu/", @"docs/"};
|
|
NSString *full_file_path;
|
|
NSURL *full_file_url;
|
|
|
|
/* iterate thru the possible paths until the file is found */
|
|
int index;
|
|
for (index = 0; index < ARRAY_SIZE(path_array); index++) {
|
|
full_file_path = [[NSBundle mainBundle] executablePath];
|
|
full_file_path = [full_file_path stringByDeletingLastPathComponent];
|
|
full_file_path = [NSString stringWithFormat: @"%@/%@%@", full_file_path,
|
|
path_array[index], filename];
|
|
full_file_url = [NSURL fileURLWithPath: full_file_path
|
|
isDirectory: false];
|
|
if ([[NSWorkspace sharedWorkspace] openURL: full_file_url] == YES) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* If none of the paths opened a file */
|
|
NSBeep();
|
|
QEMU_Alert(@"Failed to open file");
|
|
}
|
|
|
|
- (void)showQEMUDoc:(id)sender
|
|
{
|
|
COCOA_DEBUG("QemuCocoaAppController: showQEMUDoc\n");
|
|
|
|
[self openDocumentation: @"index.html"];
|
|
}
|
|
|
|
/* Stretches video to fit host monitor size */
|
|
- (void)zoomToFit:(id) sender
|
|
{
|
|
stretch_video = !stretch_video;
|
|
if (stretch_video == true) {
|
|
[sender setState: NSControlStateValueOn];
|
|
} else {
|
|
[sender setState: NSControlStateValueOff];
|
|
}
|
|
}
|
|
|
|
/* Displays the console on the screen */
|
|
- (void)displayConsole:(id)sender
|
|
{
|
|
console_select([sender tag]);
|
|
}
|
|
|
|
/* Pause the guest */
|
|
- (void)pauseQEMU:(id)sender
|
|
{
|
|
with_iothread_lock(^{
|
|
qmp_stop(NULL);
|
|
});
|
|
[sender setEnabled: NO];
|
|
[[[sender menu] itemWithTitle: @"Resume"] setEnabled: YES];
|
|
[self displayPause];
|
|
}
|
|
|
|
/* Resume running the guest operating system */
|
|
- (void)resumeQEMU:(id) sender
|
|
{
|
|
with_iothread_lock(^{
|
|
qmp_cont(NULL);
|
|
});
|
|
[sender setEnabled: NO];
|
|
[[[sender menu] itemWithTitle: @"Pause"] setEnabled: YES];
|
|
[self removePause];
|
|
}
|
|
|
|
/* Displays the word pause on the screen */
|
|
- (void)displayPause
|
|
{
|
|
/* Coordinates have to be calculated each time because the window can change its size */
|
|
int xCoord, yCoord, width, height;
|
|
xCoord = ([normalWindow frame].size.width - [pauseLabel frame].size.width)/2;
|
|
yCoord = [normalWindow frame].size.height - [pauseLabel frame].size.height - ([pauseLabel frame].size.height * .5);
|
|
width = [pauseLabel frame].size.width;
|
|
height = [pauseLabel frame].size.height;
|
|
[pauseLabel setFrame: NSMakeRect(xCoord, yCoord, width, height)];
|
|
[cocoaView addSubview: pauseLabel];
|
|
}
|
|
|
|
/* Removes the word pause from the screen */
|
|
- (void)removePause
|
|
{
|
|
[pauseLabel removeFromSuperview];
|
|
}
|
|
|
|
/* Restarts QEMU */
|
|
- (void)restartQEMU:(id)sender
|
|
{
|
|
with_iothread_lock(^{
|
|
qmp_system_reset(NULL);
|
|
});
|
|
}
|
|
|
|
/* Powers down QEMU */
|
|
- (void)powerDownQEMU:(id)sender
|
|
{
|
|
with_iothread_lock(^{
|
|
qmp_system_powerdown(NULL);
|
|
});
|
|
}
|
|
|
|
/* Ejects the media.
|
|
* Uses sender's tag to figure out the device to eject.
|
|
*/
|
|
- (void)ejectDeviceMedia:(id)sender
|
|
{
|
|
NSString * drive;
|
|
drive = [sender representedObject];
|
|
if(drive == nil) {
|
|
NSBeep();
|
|
QEMU_Alert(@"Failed to find drive to eject!");
|
|
return;
|
|
}
|
|
|
|
__block Error *err = NULL;
|
|
with_iothread_lock(^{
|
|
qmp_eject(true, [drive cStringUsingEncoding: NSASCIIStringEncoding],
|
|
false, NULL, false, false, &err);
|
|
});
|
|
handleAnyDeviceErrors(err);
|
|
}
|
|
|
|
/* Displays a dialog box asking the user to select an image file to load.
|
|
* Uses sender's represented object value to figure out which drive to use.
|
|
*/
|
|
- (void)changeDeviceMedia:(id)sender
|
|
{
|
|
/* Find the drive name */
|
|
NSString * drive;
|
|
drive = [sender representedObject];
|
|
if(drive == nil) {
|
|
NSBeep();
|
|
QEMU_Alert(@"Could not find drive!");
|
|
return;
|
|
}
|
|
|
|
/* Display the file open dialog */
|
|
NSOpenPanel * openPanel;
|
|
openPanel = [NSOpenPanel openPanel];
|
|
[openPanel setCanChooseFiles: YES];
|
|
[openPanel setAllowsMultipleSelection: NO];
|
|
if([openPanel runModal] == NSModalResponseOK) {
|
|
NSString * file = [[[openPanel URLs] objectAtIndex: 0] path];
|
|
if(file == nil) {
|
|
NSBeep();
|
|
QEMU_Alert(@"Failed to convert URL to file path!");
|
|
return;
|
|
}
|
|
|
|
__block Error *err = NULL;
|
|
with_iothread_lock(^{
|
|
qmp_blockdev_change_medium(true,
|
|
[drive cStringUsingEncoding:
|
|
NSASCIIStringEncoding],
|
|
false, NULL,
|
|
[file cStringUsingEncoding:
|
|
NSASCIIStringEncoding],
|
|
true, "raw",
|
|
true, false,
|
|
false, 0,
|
|
&err);
|
|
});
|
|
handleAnyDeviceErrors(err);
|
|
}
|
|
}
|
|
|
|
/* Verifies if the user really wants to quit */
|
|
- (BOOL)verifyQuit
|
|
{
|
|
NSAlert *alert = [NSAlert new];
|
|
[alert autorelease];
|
|
[alert setMessageText: @"Are you sure you want to quit QEMU?"];
|
|
[alert addButtonWithTitle: @"Cancel"];
|
|
[alert addButtonWithTitle: @"Quit"];
|
|
if([alert runModal] == NSAlertSecondButtonReturn) {
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
/* The action method for the About menu item */
|
|
- (IBAction) do_about_menu_item: (id) sender
|
|
{
|
|
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
|
char *icon_path_c = get_relocated_path(CONFIG_QEMU_ICONDIR "/hicolor/512x512/apps/qemu.png");
|
|
NSString *icon_path = [NSString stringWithUTF8String:icon_path_c];
|
|
g_free(icon_path_c);
|
|
NSImage *icon = [[NSImage alloc] initWithContentsOfFile:icon_path];
|
|
NSString *version = @"QEMU emulator version " QEMU_FULL_VERSION;
|
|
NSString *copyright = @QEMU_COPYRIGHT;
|
|
NSDictionary *options;
|
|
if (icon) {
|
|
options = @{
|
|
NSAboutPanelOptionApplicationIcon : icon,
|
|
NSAboutPanelOptionApplicationVersion : version,
|
|
@"Copyright" : copyright,
|
|
};
|
|
[icon release];
|
|
} else {
|
|
options = @{
|
|
NSAboutPanelOptionApplicationVersion : version,
|
|
@"Copyright" : copyright,
|
|
};
|
|
}
|
|
[NSApp orderFrontStandardAboutPanelWithOptions:options];
|
|
[pool release];
|
|
}
|
|
|
|
/* Used by the Speed menu items */
|
|
- (void)adjustSpeed:(id)sender
|
|
{
|
|
int throttle_pct; /* throttle percentage */
|
|
NSMenu *menu;
|
|
|
|
menu = [sender menu];
|
|
if (menu != nil)
|
|
{
|
|
/* Unselect the currently selected item */
|
|
for (NSMenuItem *item in [menu itemArray]) {
|
|
if (item.state == NSControlStateValueOn) {
|
|
[item setState: NSControlStateValueOff];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check the menu item
|
|
[sender setState: NSControlStateValueOn];
|
|
|
|
// get the throttle percentage
|
|
throttle_pct = [sender tag];
|
|
|
|
with_iothread_lock(^{
|
|
cpu_throttle_set(throttle_pct);
|
|
});
|
|
COCOA_DEBUG("cpu throttling at %d%c\n", cpu_throttle_get_percentage(), '%');
|
|
}
|
|
|
|
@end
|
|
|
|
@interface QemuApplication : NSApplication
|
|
@end
|
|
|
|
@implementation QemuApplication
|
|
- (void)sendEvent:(NSEvent *)event
|
|
{
|
|
COCOA_DEBUG("QemuApplication: sendEvent\n");
|
|
if (![cocoaView handleEvent:event]) {
|
|
[super sendEvent: event];
|
|
}
|
|
}
|
|
@end
|
|
|
|
static void create_initial_menus(void)
|
|
{
|
|
// Add menus
|
|
NSMenu *menu;
|
|
NSMenuItem *menuItem;
|
|
|
|
[NSApp setMainMenu:[[NSMenu alloc] init]];
|
|
[NSApp setServicesMenu:[[NSMenu alloc] initWithTitle:@"Services"]];
|
|
|
|
// Application menu
|
|
menu = [[NSMenu alloc] initWithTitle:@""];
|
|
[menu addItemWithTitle:@"About QEMU" action:@selector(do_about_menu_item:) keyEquivalent:@""]; // About QEMU
|
|
[menu addItem:[NSMenuItem separatorItem]]; //Separator
|
|
menuItem = [menu addItemWithTitle:@"Services" action:nil keyEquivalent:@""];
|
|
[menuItem setSubmenu:[NSApp servicesMenu]];
|
|
[menu addItem:[NSMenuItem separatorItem]];
|
|
[menu addItemWithTitle:@"Hide QEMU" action:@selector(hide:) keyEquivalent:@"h"]; //Hide QEMU
|
|
menuItem = (NSMenuItem *)[menu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; // Hide Others
|
|
[menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption|NSEventModifierFlagCommand)];
|
|
[menu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; // Show All
|
|
[menu addItem:[NSMenuItem separatorItem]]; //Separator
|
|
[menu addItemWithTitle:@"Quit QEMU" action:@selector(terminate:) keyEquivalent:@"q"];
|
|
menuItem = [[NSMenuItem alloc] initWithTitle:@"Apple" action:nil keyEquivalent:@""];
|
|
[menuItem setSubmenu:menu];
|
|
[[NSApp mainMenu] addItem:menuItem];
|
|
[NSApp performSelector:@selector(setAppleMenu:) withObject:menu]; // Workaround (this method is private since 10.4+)
|
|
|
|
// Machine menu
|
|
menu = [[NSMenu alloc] initWithTitle: @"Machine"];
|
|
[menu setAutoenablesItems: NO];
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Pause" action: @selector(pauseQEMU:) keyEquivalent: @""] autorelease]];
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle: @"Resume" action: @selector(resumeQEMU:) keyEquivalent: @""] autorelease];
|
|
[menu addItem: menuItem];
|
|
[menuItem setEnabled: NO];
|
|
[menu addItem: [NSMenuItem separatorItem]];
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Reset" action: @selector(restartQEMU:) keyEquivalent: @""] autorelease]];
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle: @"Power Down" action: @selector(powerDownQEMU:) keyEquivalent: @""] autorelease]];
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle: @"Machine" action:nil keyEquivalent:@""] autorelease];
|
|
[menuItem setSubmenu:menu];
|
|
[[NSApp mainMenu] addItem:menuItem];
|
|
|
|
// View menu
|
|
menu = [[NSMenu alloc] initWithTitle:@"View"];
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Enter Fullscreen" action:@selector(doToggleFullScreen:) keyEquivalent:@"f"] autorelease]]; // Fullscreen
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Zoom To Fit" action:@selector(zoomToFit:) keyEquivalent:@""] autorelease]];
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""] autorelease];
|
|
[menuItem setSubmenu:menu];
|
|
[[NSApp mainMenu] addItem:menuItem];
|
|
|
|
// Speed menu
|
|
menu = [[NSMenu alloc] initWithTitle:@"Speed"];
|
|
|
|
// Add the rest of the Speed menu items
|
|
int p, percentage, throttle_pct;
|
|
for (p = 10; p >= 0; p--)
|
|
{
|
|
percentage = p * 10 > 1 ? p * 10 : 1; // prevent a 0% menu item
|
|
|
|
menuItem = [[[NSMenuItem alloc]
|
|
initWithTitle: [NSString stringWithFormat: @"%d%%", percentage] action:@selector(adjustSpeed:) keyEquivalent:@""] autorelease];
|
|
|
|
if (percentage == 100) {
|
|
[menuItem setState: NSControlStateValueOn];
|
|
}
|
|
|
|
/* Calculate the throttle percentage */
|
|
throttle_pct = -1 * percentage + 100;
|
|
|
|
[menuItem setTag: throttle_pct];
|
|
[menu addItem: menuItem];
|
|
}
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle:@"Speed" action:nil keyEquivalent:@""] autorelease];
|
|
[menuItem setSubmenu:menu];
|
|
[[NSApp mainMenu] addItem:menuItem];
|
|
|
|
// Window menu
|
|
menu = [[NSMenu alloc] initWithTitle:@"Window"];
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease]]; // Miniaturize
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease];
|
|
[menuItem setSubmenu:menu];
|
|
[[NSApp mainMenu] addItem:menuItem];
|
|
[NSApp setWindowsMenu:menu];
|
|
|
|
// Help menu
|
|
menu = [[NSMenu alloc] initWithTitle:@"Help"];
|
|
[menu addItem: [[[NSMenuItem alloc] initWithTitle:@"QEMU Documentation" action:@selector(showQEMUDoc:) keyEquivalent:@"?"] autorelease]]; // QEMU Help
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""] autorelease];
|
|
[menuItem setSubmenu:menu];
|
|
[[NSApp mainMenu] addItem:menuItem];
|
|
}
|
|
|
|
/* Returns a name for a given console */
|
|
static NSString * getConsoleName(QemuConsole * console)
|
|
{
|
|
g_autofree char *label = qemu_console_get_label(console);
|
|
|
|
return [NSString stringWithUTF8String:label];
|
|
}
|
|
|
|
/* Add an entry to the View menu for each console */
|
|
static void add_console_menu_entries(void)
|
|
{
|
|
NSMenu *menu;
|
|
NSMenuItem *menuItem;
|
|
int index = 0;
|
|
|
|
menu = [[[NSApp mainMenu] itemWithTitle:@"View"] submenu];
|
|
|
|
[menu addItem:[NSMenuItem separatorItem]];
|
|
|
|
while (qemu_console_lookup_by_index(index) != NULL) {
|
|
menuItem = [[[NSMenuItem alloc] initWithTitle: getConsoleName(qemu_console_lookup_by_index(index))
|
|
action: @selector(displayConsole:) keyEquivalent: @""] autorelease];
|
|
[menuItem setTag: index];
|
|
[menu addItem: menuItem];
|
|
index++;
|
|
}
|
|
}
|
|
|
|
/* Make menu items for all removable devices.
|
|
* Each device is given an 'Eject' and 'Change' menu item.
|
|
*/
|
|
static void addRemovableDevicesMenuItems(void)
|
|
{
|
|
NSMenu *menu;
|
|
NSMenuItem *menuItem;
|
|
BlockInfoList *currentDevice, *pointerToFree;
|
|
NSString *deviceName;
|
|
|
|
currentDevice = qmp_query_block(NULL);
|
|
pointerToFree = currentDevice;
|
|
|
|
menu = [[[NSApp mainMenu] itemWithTitle:@"Machine"] submenu];
|
|
|
|
// Add a separator between related groups of menu items
|
|
[menu addItem:[NSMenuItem separatorItem]];
|
|
|
|
// Set the attributes to the "Removable Media" menu item
|
|
NSString *titleString = @"Removable Media";
|
|
NSMutableAttributedString *attString=[[NSMutableAttributedString alloc] initWithString:titleString];
|
|
NSColor *newColor = [NSColor blackColor];
|
|
NSFontManager *fontManager = [NSFontManager sharedFontManager];
|
|
NSFont *font = [fontManager fontWithFamily:@"Helvetica"
|
|
traits:NSBoldFontMask|NSItalicFontMask
|
|
weight:0
|
|
size:14];
|
|
[attString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [titleString length])];
|
|
[attString addAttribute:NSForegroundColorAttributeName value:newColor range:NSMakeRange(0, [titleString length])];
|
|
[attString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt: 1] range:NSMakeRange(0, [titleString length])];
|
|
|
|
// Add the "Removable Media" menu item
|
|
menuItem = [NSMenuItem new];
|
|
[menuItem setAttributedTitle: attString];
|
|
[menuItem setEnabled: NO];
|
|
[menu addItem: menuItem];
|
|
|
|
/* Loop through all the block devices in the emulator */
|
|
while (currentDevice) {
|
|
deviceName = [[NSString stringWithFormat: @"%s", currentDevice->value->device] retain];
|
|
|
|
if(currentDevice->value->removable) {
|
|
menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Change %s...", currentDevice->value->device]
|
|
action: @selector(changeDeviceMedia:)
|
|
keyEquivalent: @""];
|
|
[menu addItem: menuItem];
|
|
[menuItem setRepresentedObject: deviceName];
|
|
[menuItem autorelease];
|
|
|
|
menuItem = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"Eject %s", currentDevice->value->device]
|
|
action: @selector(ejectDeviceMedia:)
|
|
keyEquivalent: @""];
|
|
[menu addItem: menuItem];
|
|
[menuItem setRepresentedObject: deviceName];
|
|
[menuItem autorelease];
|
|
}
|
|
currentDevice = currentDevice->next;
|
|
}
|
|
qapi_free_BlockInfoList(pointerToFree);
|
|
}
|
|
|
|
@interface QemuCocoaPasteboardTypeOwner : NSObject<NSPasteboardTypeOwner>
|
|
@end
|
|
|
|
@implementation QemuCocoaPasteboardTypeOwner
|
|
|
|
- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSPasteboardType)type
|
|
{
|
|
if (type != NSPasteboardTypeString) {
|
|
return;
|
|
}
|
|
|
|
with_iothread_lock(^{
|
|
QemuClipboardInfo *info = qemu_clipboard_info_ref(cbinfo);
|
|
qemu_event_reset(&cbevent);
|
|
qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT);
|
|
|
|
while (info == cbinfo &&
|
|
info->types[QEMU_CLIPBOARD_TYPE_TEXT].available &&
|
|
info->types[QEMU_CLIPBOARD_TYPE_TEXT].data == NULL) {
|
|
qemu_mutex_unlock_iothread();
|
|
qemu_event_wait(&cbevent);
|
|
qemu_mutex_lock_iothread();
|
|
}
|
|
|
|
if (info == cbinfo) {
|
|
NSData *data = [[NSData alloc] initWithBytes:info->types[QEMU_CLIPBOARD_TYPE_TEXT].data
|
|
length:info->types[QEMU_CLIPBOARD_TYPE_TEXT].size];
|
|
[sender setData:data forType:NSPasteboardTypeString];
|
|
[data release];
|
|
}
|
|
|
|
qemu_clipboard_info_unref(info);
|
|
});
|
|
}
|
|
|
|
@end
|
|
|
|
static QemuCocoaPasteboardTypeOwner *cbowner;
|
|
|
|
static void cocoa_clipboard_notify(Notifier *notifier, void *data);
|
|
static void cocoa_clipboard_request(QemuClipboardInfo *info,
|
|
QemuClipboardType type);
|
|
|
|
static QemuClipboardPeer cbpeer = {
|
|
.name = "cocoa",
|
|
.notifier = { .notify = cocoa_clipboard_notify },
|
|
.request = cocoa_clipboard_request
|
|
};
|
|
|
|
static void cocoa_clipboard_update_info(QemuClipboardInfo *info)
|
|
{
|
|
if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
|
|
return;
|
|
}
|
|
|
|
if (info != cbinfo) {
|
|
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
|
|
qemu_clipboard_info_unref(cbinfo);
|
|
cbinfo = qemu_clipboard_info_ref(info);
|
|
cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner];
|
|
[pool release];
|
|
}
|
|
|
|
qemu_event_set(&cbevent);
|
|
}
|
|
|
|
static void cocoa_clipboard_notify(Notifier *notifier, void *data)
|
|
{
|
|
QemuClipboardNotify *notify = data;
|
|
|
|
switch (notify->type) {
|
|
case QEMU_CLIPBOARD_UPDATE_INFO:
|
|
cocoa_clipboard_update_info(notify->info);
|
|
return;
|
|
case QEMU_CLIPBOARD_RESET_SERIAL:
|
|
/* ignore */
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void cocoa_clipboard_request(QemuClipboardInfo *info,
|
|
QemuClipboardType type)
|
|
{
|
|
NSAutoreleasePool *pool;
|
|
NSData *text;
|
|
|
|
switch (type) {
|
|
case QEMU_CLIPBOARD_TYPE_TEXT:
|
|
pool = [[NSAutoreleasePool alloc] init];
|
|
text = [[NSPasteboard generalPasteboard] dataForType:NSPasteboardTypeString];
|
|
if (text) {
|
|
qemu_clipboard_set_data(&cbpeer, info, type,
|
|
[text length], [text bytes], true);
|
|
}
|
|
[pool release];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The startup process for the OSX/Cocoa UI is complicated, because
|
|
* OSX insists that the UI runs on the initial main thread, and so we
|
|
* need to start a second thread which runs the vl.c qemu_main():
|
|
*
|
|
* Initial thread: 2nd thread:
|
|
* in main():
|
|
* create qemu-main thread
|
|
* wait on display_init semaphore
|
|
* call qemu_main()
|
|
* ...
|
|
* in cocoa_display_init():
|
|
* post the display_init semaphore
|
|
* wait on app_started semaphore
|
|
* create application, menus, etc
|
|
* enter OSX run loop
|
|
* in applicationDidFinishLaunching:
|
|
* post app_started semaphore
|
|
* tell main thread to fullscreen if needed
|
|
* [...]
|
|
* run qemu main-loop
|
|
*
|
|
* We do this in two stages so that we don't do the creation of the
|
|
* GUI application menus and so on for command line options like --help
|
|
* where we want to just print text to stdout and exit immediately.
|
|
*/
|
|
|
|
static void *call_qemu_main(void *opaque)
|
|
{
|
|
int status;
|
|
|
|
COCOA_DEBUG("Second thread: calling qemu_main()\n");
|
|
status = qemu_main(gArgc, gArgv, *_NSGetEnviron());
|
|
COCOA_DEBUG("Second thread: qemu_main() returned, exiting\n");
|
|
[cbowner release];
|
|
exit(status);
|
|
}
|
|
|
|
int main (int argc, char **argv) {
|
|
QemuThread thread;
|
|
|
|
COCOA_DEBUG("Entered main()\n");
|
|
gArgc = argc;
|
|
gArgv = argv;
|
|
|
|
qemu_sem_init(&display_init_sem, 0);
|
|
qemu_sem_init(&app_started_sem, 0);
|
|
|
|
qemu_thread_create(&thread, "qemu_main", call_qemu_main,
|
|
NULL, QEMU_THREAD_DETACHED);
|
|
|
|
COCOA_DEBUG("Main thread: waiting for display_init_sem\n");
|
|
qemu_sem_wait(&display_init_sem);
|
|
COCOA_DEBUG("Main thread: initializing app\n");
|
|
|
|
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
|
|
|
|
// Pull this console process up to being a fully-fledged graphical
|
|
// app with a menubar and Dock icon
|
|
ProcessSerialNumber psn = { 0, kCurrentProcess };
|
|
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
|
|
|
|
[QemuApplication sharedApplication];
|
|
|
|
create_initial_menus();
|
|
|
|
/*
|
|
* Create the menu entries which depend on QEMU state (for consoles
|
|
* and removeable devices). These make calls back into QEMU functions,
|
|
* which is OK because at this point we know that the second thread
|
|
* holds the iothread lock and is synchronously waiting for us to
|
|
* finish.
|
|
*/
|
|
add_console_menu_entries();
|
|
addRemovableDevicesMenuItems();
|
|
|
|
// Create an Application controller
|
|
QemuCocoaAppController *appController = [[QemuCocoaAppController alloc] init];
|
|
[NSApp setDelegate:appController];
|
|
|
|
// Start the main event loop
|
|
COCOA_DEBUG("Main thread: entering OSX run loop\n");
|
|
[NSApp run];
|
|
COCOA_DEBUG("Main thread: left OSX run loop, exiting\n");
|
|
|
|
[appController release];
|
|
[pool release];
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
#pragma mark qemu
|
|
static void cocoa_update(DisplayChangeListener *dcl,
|
|
int x, int y, int w, int h)
|
|
{
|
|
COCOA_DEBUG("qemu_cocoa: cocoa_update\n");
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSRect rect;
|
|
if ([cocoaView cdx] == 1.0) {
|
|
rect = NSMakeRect(x, [cocoaView gscreen].height - y - h, w, h);
|
|
} else {
|
|
rect = NSMakeRect(
|
|
x * [cocoaView cdx],
|
|
([cocoaView gscreen].height - y - h) * [cocoaView cdy],
|
|
w * [cocoaView cdx],
|
|
h * [cocoaView cdy]);
|
|
}
|
|
[cocoaView setNeedsDisplayInRect:rect];
|
|
});
|
|
}
|
|
|
|
static void cocoa_switch(DisplayChangeListener *dcl,
|
|
DisplaySurface *surface)
|
|
{
|
|
pixman_image_t *image = surface->image;
|
|
|
|
COCOA_DEBUG("qemu_cocoa: cocoa_switch\n");
|
|
|
|
// The DisplaySurface will be freed as soon as this callback returns.
|
|
// We take a reference to the underlying pixman image here so it does
|
|
// not disappear from under our feet; the switchSurface method will
|
|
// deref the old image when it is done with it.
|
|
pixman_image_ref(image);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[cocoaView updateUIInfo];
|
|
[cocoaView switchSurface:image];
|
|
});
|
|
}
|
|
|
|
static void cocoa_refresh(DisplayChangeListener *dcl)
|
|
{
|
|
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
|
|
|
|
COCOA_DEBUG("qemu_cocoa: cocoa_refresh\n");
|
|
graphic_hw_update(NULL);
|
|
|
|
if (qemu_input_is_absolute()) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (![cocoaView isAbsoluteEnabled]) {
|
|
if ([cocoaView isMouseGrabbed]) {
|
|
[cocoaView ungrabMouse];
|
|
}
|
|
}
|
|
[cocoaView setAbsoluteEnabled:YES];
|
|
});
|
|
}
|
|
|
|
if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) {
|
|
qemu_clipboard_info_unref(cbinfo);
|
|
cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
|
|
if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) {
|
|
cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
|
|
}
|
|
qemu_clipboard_update(cbinfo);
|
|
cbchangecount = [[NSPasteboard generalPasteboard] changeCount];
|
|
qemu_event_set(&cbevent);
|
|
}
|
|
|
|
[pool release];
|
|
}
|
|
|
|
static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts)
|
|
{
|
|
COCOA_DEBUG("qemu_cocoa: cocoa_display_init\n");
|
|
|
|
/* Tell main thread to go ahead and create the app and enter the run loop */
|
|
qemu_sem_post(&display_init_sem);
|
|
qemu_sem_wait(&app_started_sem);
|
|
COCOA_DEBUG("cocoa_display_init: app start completed\n");
|
|
|
|
QemuCocoaAppController *controller = (QemuCocoaAppController *)[[NSApplication sharedApplication] delegate];
|
|
/* if fullscreen mode is to be used */
|
|
if (opts->has_full_screen && opts->full_screen) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[NSApp activateIgnoringOtherApps: YES];
|
|
[controller toggleFullScreen: nil];
|
|
});
|
|
}
|
|
if (opts->u.cocoa.has_full_grab && opts->u.cocoa.full_grab) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[controller setFullGrab: nil];
|
|
});
|
|
}
|
|
|
|
if (opts->has_show_cursor && opts->show_cursor) {
|
|
cursor_hide = 0;
|
|
}
|
|
if (opts->u.cocoa.has_swap_opt_cmd) {
|
|
swap_opt_cmd = opts->u.cocoa.swap_opt_cmd;
|
|
}
|
|
|
|
if (opts->u.cocoa.has_left_command_key && !opts->u.cocoa.left_command_key) {
|
|
left_command_key_enabled = 0;
|
|
}
|
|
|
|
// register vga output callbacks
|
|
register_displaychangelistener(&dcl);
|
|
|
|
qemu_event_init(&cbevent, false);
|
|
cbowner = [[QemuCocoaPasteboardTypeOwner alloc] init];
|
|
qemu_clipboard_peer_register(&cbpeer);
|
|
}
|
|
|
|
static QemuDisplay qemu_display_cocoa = {
|
|
.type = DISPLAY_TYPE_COCOA,
|
|
.init = cocoa_display_init,
|
|
};
|
|
|
|
static void register_cocoa(void)
|
|
{
|
|
qemu_display_register(&qemu_display_cocoa);
|
|
}
|
|
|
|
type_init(register_cocoa);
|