460 lines
13 KiB
Objective-C
460 lines
13 KiB
Objective-C
// Copyright (c) Akop Karapetyan
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// FIXME: on catalina, it still starts in white
|
|
// FIXME: don't add bogus paths to recently used!
|
|
#import "FBEmulatorController.h"
|
|
|
|
#import "AppDelegate.h"
|
|
#import "NSWindowController+Core.h"
|
|
|
|
@interface FBEmulatorController ()
|
|
|
|
- (void) resizeFrame:(NSSize) newSize
|
|
animate:(BOOL) animate;
|
|
- (void) lockCursor;
|
|
- (void) unlockCursor;
|
|
- (void) hideCursor;
|
|
- (void) unhideCursor:(BOOL) force;
|
|
- (NSPoint) convertPointToScreen:(NSPoint) point;
|
|
|
|
@end
|
|
|
|
@implementation FBEmulatorController
|
|
{
|
|
BOOL isCursorLocked;
|
|
BOOL isCursorVisible;
|
|
BOOL isAutoPaused;
|
|
NSArray *defaultsToObserve;
|
|
BOOL driverLoadError;
|
|
}
|
|
|
|
- (id) init
|
|
{
|
|
if (self = [super initWithWindowNibName:@"Emulator"]) {
|
|
isCursorLocked = NO;
|
|
isCursorVisible = YES;
|
|
isAutoPaused = NO;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
screen.delegate = nil;
|
|
self.video.delegate = nil;
|
|
[self.runloop removeObserver:self];
|
|
}
|
|
|
|
- (void) awakeFromNib
|
|
{
|
|
defaultsToObserve = @[
|
|
@"pauseWhenInactive",
|
|
@"suppressScreenSaver",
|
|
@"hideLockOptions",
|
|
@"masterVolume",
|
|
];
|
|
|
|
NSTitlebarAccessoryViewController *tba = [NSTitlebarAccessoryViewController new];
|
|
tba.view = accView;
|
|
tba.layoutAttribute = NSLayoutAttributeRight;
|
|
lockText.stringValue = NSLocalizedString(@"⌘+click", nil);
|
|
lockText.hidden = YES;
|
|
lockIcon.image = [NSImage imageNamed:@"NSLockUnlockedTemplate"];
|
|
lockIcon.enabled = NO;
|
|
lockIcon.hidden = [NSUserDefaults.standardUserDefaults boolForKey:@"hideLockOptions"];
|
|
[self.window addTitlebarAccessoryViewController:tba];
|
|
self.window.backgroundColor = NSColor.blackColor;
|
|
|
|
screen.delegate = self;
|
|
self.video.delegate = screen;
|
|
[self.runloop addObserver:self];
|
|
|
|
[self.window registerForDraggedTypes:@[NSFilenamesPboardType]];
|
|
for (NSString *key in defaultsToObserve)
|
|
[NSUserDefaults.standardUserDefaults addObserver:self
|
|
forKeyPath:key
|
|
options:NSKeyValueObservingOptionNew
|
|
context:NULL];
|
|
}
|
|
|
|
#pragma mark - NSWindowDelegate
|
|
|
|
- (void) windowDidBecomeKey:(NSNotification *) notification
|
|
{
|
|
[self.input setFocus:YES];
|
|
if (isAutoPaused && self.runloop.isPaused)
|
|
self.runloop.paused = NO;
|
|
isAutoPaused = NO;
|
|
|
|
if ([NSUserDefaults.standardUserDefaults boolForKey:@"suppressScreenSaver"])
|
|
[self.appDelegate suppressScreenSaver];
|
|
}
|
|
|
|
- (void) windowDidResignKey:(NSNotification *) notification
|
|
{
|
|
[self.input setFocus:NO];
|
|
[self unlockCursor];
|
|
|
|
if ([NSUserDefaults.standardUserDefaults boolForKey:@"pauseWhenInactive"]
|
|
&& !self.runloop.isPaused) {
|
|
self.runloop.paused = YES;
|
|
isAutoPaused = YES;
|
|
}
|
|
|
|
[self.appDelegate restoreScreenSaver];
|
|
}
|
|
|
|
- (NSSize) windowWillResize:(NSWindow *) sender
|
|
toSize:(NSSize) frameSize
|
|
{
|
|
NSSize screenSize = [self.video gameScreenSize];
|
|
if (screenSize.width != 0 && screenSize.height != 0) {
|
|
NSRect windowFrame = [[self window] frame];
|
|
NSView *contentView = [[self window] contentView];
|
|
NSRect viewRect = [contentView convertRect:[contentView bounds]
|
|
toView:nil];
|
|
NSRect contentRect = [[self window] contentRectForFrameRect:windowFrame];
|
|
|
|
CGFloat screenRatio = screenSize.width / screenSize.height;
|
|
|
|
float marginY = viewRect.origin.y + windowFrame.size.height - contentRect.size.height;
|
|
float marginX = contentRect.size.width - viewRect.size.width;
|
|
|
|
// Clamp the minimum height
|
|
if ((frameSize.height - marginY) < screenSize.height) {
|
|
frameSize.height = screenSize.height + marginY;
|
|
}
|
|
|
|
// Set the screen width as a percentage of the screen height
|
|
frameSize.width = (frameSize.height - marginY) * screenRatio + marginX;
|
|
}
|
|
|
|
return frameSize;
|
|
}
|
|
|
|
#pragma mark - Notifications
|
|
|
|
- (void) observeValueForKeyPath:(NSString *) keyPath
|
|
ofObject:(id) object
|
|
change:(NSDictionary *) change
|
|
context:(void *) context
|
|
{
|
|
if ([keyPath isEqualToString:@"pauseWhenInactive"]) {
|
|
BOOL newValue = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
|
|
if (!self.window.isKeyWindow && newValue && !self.runloop.isPaused) {
|
|
self.runloop.paused = YES;
|
|
isAutoPaused = YES;
|
|
}
|
|
} else if ([keyPath isEqualToString:@"hideLockOptions"]) {
|
|
BOOL newValue = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
|
|
if (!isCursorLocked)
|
|
lockIcon.hidden = !self.input.usesMouse && newValue;
|
|
} else if ([keyPath isEqualToString:@"masterVolume"]) {
|
|
BOOL newValue = [[change objectForKey:NSKeyValueChangeNewKey] intValue];
|
|
self.audio.volume = newValue / 100.0f;
|
|
}
|
|
}
|
|
|
|
#pragma mark - FBScreenViewDelegate
|
|
|
|
- (void) mouseDidIdle
|
|
{
|
|
[self hideCursor];
|
|
}
|
|
|
|
- (void) mouseStateDidChange
|
|
{
|
|
if (!isCursorLocked)
|
|
[self unhideCursor:NO];
|
|
}
|
|
|
|
- (void) mouseDidMove:(NSPoint) point
|
|
{
|
|
self.input.mouseCoords = point;
|
|
}
|
|
|
|
- (void) mouseButtonStateDidChange:(NSEvent *) event
|
|
{
|
|
if (event.type == NSEventTypeLeftMouseDown
|
|
&& (event.modifierFlags & NSEventModifierFlagCommand) != 0) {
|
|
if (isCursorLocked)
|
|
[self unlockCursor];
|
|
else
|
|
[self lockCursor];
|
|
} else if (event.type == NSEventTypeLeftMouseDown
|
|
&& !isCursorLocked
|
|
&& self.input.usesMouse) {
|
|
[self lockCursor];
|
|
} else switch (event.type) {
|
|
case NSEventTypeLeftMouseDown:
|
|
self.input.mouseButtonStates |= FBN_LMB;
|
|
break;
|
|
case NSEventTypeLeftMouseUp:
|
|
self.input.mouseButtonStates &= ~FBN_LMB;
|
|
break;
|
|
case NSEventTypeRightMouseDown:
|
|
self.input.mouseButtonStates |= FBN_RMB;
|
|
break;
|
|
case NSEventTypeRightMouseUp:
|
|
self.input.mouseButtonStates &= ~FBN_RMB;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Drag & Drop
|
|
|
|
- (BOOL) performDragOperation:(id<NSDraggingInfo>) sender
|
|
{
|
|
NSLog(@"performDragOperation");
|
|
|
|
NSPasteboard *pboard = sender.draggingPasteboard;
|
|
NSString *path = [[pboard propertyListForType:NSFilenamesPboardType] firstObject];
|
|
|
|
[self.appDelegate loadPath:path];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSDragOperation) draggingEntered:(id <NSDraggingInfo>) sender
|
|
{
|
|
NSLog(@"draggingEntered");
|
|
|
|
NSDragOperation dragOp = NSDragOperationNone;
|
|
if (sender.draggingSourceOperationMask & NSDragOperationCopy) {
|
|
NSPasteboard *pboard = sender.draggingPasteboard;
|
|
NSString *path = [[pboard propertyListForType:NSFilenamesPboardType] firstObject];
|
|
|
|
if ([self.appDelegate.supportedFormats containsObject:path.pathExtension])
|
|
dragOp = NSDragOperationCopy;
|
|
}
|
|
|
|
return dragOp;
|
|
}
|
|
|
|
#pragma mark - FBMainThreadDelegate
|
|
|
|
- (void) gameSessionDidStart:(NSString *) name
|
|
{
|
|
NSLog(@"gameSessionDidStart: %@", name);
|
|
NSSize screenSize = self.video.gameScreenSize;
|
|
if (screenSize.width != 0 && screenSize.height != 0)
|
|
[self resizeFrame:NSMakeSize(screenSize.width * 2, screenSize.height * 2)
|
|
animate:NO];
|
|
|
|
lockIcon.enabled = YES;
|
|
lockIcon.hidden = [NSUserDefaults.standardUserDefaults boolForKey:@"hideLockOptions"] && !self.input.usesMouse;
|
|
|
|
[self.window makeFirstResponder:screen];
|
|
}
|
|
|
|
- (void) gameSessionDidEnd
|
|
{
|
|
NSLog(@"gameSessionDidEnd");
|
|
lockIcon.enabled = NO;
|
|
[self unlockCursor];
|
|
|
|
self.window.title = NSBundle.mainBundle.infoDictionary[@"CFBundleName"];
|
|
lockIcon.hidden = [NSUserDefaults.standardUserDefaults boolForKey:@"hideLockOptions"];
|
|
}
|
|
|
|
- (void) driverInitDidStart
|
|
{
|
|
[progressPanelBar startAnimation:self];
|
|
[self.window beginSheet:progressPanel
|
|
completionHandler:^(NSModalResponse returnCode) { }];
|
|
|
|
self.window.title = NSLocalizedString(@"Loading...", nil);
|
|
driverLoadError = NO;
|
|
}
|
|
|
|
- (void) driverInitDidEnd:(NSString *) name
|
|
success:(BOOL) success
|
|
{
|
|
[self.window endSheet:progressPanel];
|
|
|
|
if (success) {
|
|
if ([NSUserDefaults.standardUserDefaults boolForKey:@"pauseWhenInactive"]
|
|
&& !self.window.isKeyWindow) {
|
|
self.runloop.paused = YES;
|
|
isAutoPaused = YES;
|
|
}
|
|
self.window.title = self.runloop.title;
|
|
if (driverLoadError)
|
|
[self.appDelegate displayLogViewer:self];
|
|
} else {
|
|
self.window.title = NSBundle.mainBundle.infoDictionary[(NSString *)kCFBundleNameKey];
|
|
[self.appDelegate displayLogViewer:self];
|
|
}
|
|
}
|
|
|
|
- (void) logDidUpdate:(NSString *) message
|
|
{
|
|
if ([message hasPrefix:@"!"])
|
|
driverLoadError = YES;
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (BOOL) validateMenuItem:(NSMenuItem *) menuItem
|
|
{
|
|
if (menuItem.action == @selector(resetEmulation:))
|
|
return self.runloop.isRunning;
|
|
else if (menuItem.action == @selector(togglePause:)) {
|
|
if (self.runloop.isRunning && self.runloop.isPaused)
|
|
menuItem.title = NSLocalizedString(@"Resume", nil);
|
|
else
|
|
menuItem.title = NSLocalizedString(@"Pause", nil);
|
|
return self.runloop.isRunning;
|
|
}
|
|
|
|
return menuItem.isEnabled;
|
|
}
|
|
|
|
- (void) resetEmulation:(id) sender
|
|
{
|
|
[self.input simReset];
|
|
}
|
|
|
|
- (void) togglePause:(id) sender
|
|
{
|
|
self.runloop.paused = !self.runloop.isPaused;
|
|
}
|
|
|
|
- (void) resizeNormalSize:(id) sender
|
|
{
|
|
NSSize screenSize = self.video.gameScreenSize;
|
|
if (screenSize.width != 0 && screenSize.height != 0)
|
|
[self resizeFrame:screenSize
|
|
animate:YES];
|
|
}
|
|
|
|
- (void) resizeDoubleSize:(id) sender
|
|
{
|
|
NSSize screenSize = self.video.gameScreenSize;
|
|
if (screenSize.width != 0 && screenSize.height != 0)
|
|
[self resizeFrame:NSMakeSize(screenSize.width * 2, screenSize.height * 2)
|
|
animate:YES];
|
|
}
|
|
|
|
- (void) activateCursorLock:(id) sender
|
|
{
|
|
if (!isCursorLocked)
|
|
[self lockCursor];
|
|
}
|
|
|
|
#pragma mark - Private
|
|
|
|
- (void) resizeFrame:(NSSize) newSize
|
|
animate:(BOOL) animate
|
|
{
|
|
// Turn off full screen if active
|
|
if ((NSApplication.sharedApplication.presentationOptions & NSApplicationPresentationFullScreen) != 0) {
|
|
[self.window toggleFullScreen:nil];
|
|
}
|
|
|
|
NSRect windowRect = self.window.frame;
|
|
NSSize windowSize = windowRect.size;
|
|
NSSize glViewSize = self.window.contentView.bounds.size;
|
|
|
|
CGFloat newWidth = newSize.width + (windowSize.width - glViewSize.width);
|
|
CGFloat newHeight = newSize.height + (windowSize.height - glViewSize.height);
|
|
|
|
NSRect newRect = NSMakeRect(windowRect.origin.x, windowRect.origin.y,
|
|
newWidth, newHeight);
|
|
|
|
[self.window setFrame:newRect
|
|
display:YES
|
|
animate:animate];
|
|
}
|
|
|
|
- (void) hideCursor
|
|
{
|
|
if (isCursorVisible) {
|
|
isCursorVisible = NO;
|
|
[NSCursor hide];
|
|
#ifdef DEBUG
|
|
NSLog(@"hideCursor");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
- (void) unhideCursor:(BOOL) force
|
|
{
|
|
if (!isCursorVisible || force) {
|
|
isCursorVisible = YES;
|
|
[NSCursor unhide];
|
|
#ifdef DEBUG
|
|
NSLog(@"unhideCursor");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
- (void) lockCursor
|
|
{
|
|
if (!isCursorLocked) {
|
|
CGAssociateMouseAndMouseCursorPosition(false);
|
|
|
|
CGFloat offset = self.window.screen.frame.size.height + self.window.screen.frame.origin.y;
|
|
NSPoint centerScreen = [self convertPointToScreen:NSMakePoint(NSMidX(screen.bounds),
|
|
NSMidY(screen.bounds))];
|
|
NSPoint screenCoords = NSMakePoint(centerScreen.x, offset - centerScreen.y);
|
|
CGWarpMouseCursorPosition(NSPointToCGPoint(screenCoords));
|
|
|
|
isCursorLocked = YES;
|
|
lockIcon.image = [NSImage imageNamed:@"NSLockLockedTemplate"];
|
|
lockIcon.hidden = NO;
|
|
lockText.hidden = NO;
|
|
#ifdef DEBUG
|
|
NSLog(@"lockCursor");
|
|
#endif
|
|
}
|
|
|
|
[self hideCursor];
|
|
}
|
|
|
|
- (void) unlockCursor
|
|
{
|
|
if (isCursorLocked) {
|
|
CGAssociateMouseAndMouseCursorPosition(true);
|
|
isCursorLocked = NO;
|
|
lockIcon.image = [NSImage imageNamed:@"NSLockUnlockedTemplate"];
|
|
lockIcon.hidden = [NSUserDefaults.standardUserDefaults boolForKey:@"hideLockOptions"] && !self.input.usesMouse;
|
|
lockText.hidden = YES;
|
|
#ifdef DEBUG
|
|
NSLog(@"unlockCursor");
|
|
#endif
|
|
}
|
|
|
|
[self unhideCursor:YES];
|
|
}
|
|
|
|
- (NSPoint) convertPointToScreen:(NSPoint) point
|
|
{
|
|
NSWindow *window = self.window;
|
|
if (@available(macOS 10.12, *))
|
|
return [window convertPointToScreen:point];
|
|
|
|
NSRect frame = window.frame;
|
|
point.x += frame.origin.x;
|
|
point.y += frame.origin.y;
|
|
|
|
return point;
|
|
}
|
|
|
|
@end
|