// 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) sender { NSLog(@"performDragOperation"); NSPasteboard *pboard = sender.draggingPasteboard; NSString *path = [[pboard propertyListForType:NSFilenamesPboardType] firstObject]; [self.appDelegate loadPath:path]; return YES; } - (NSDragOperation) draggingEntered:(id ) 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