diff options
Diffstat (limited to 'xbmc/windowing/osx')
25 files changed, 5003 insertions, 0 deletions
diff --git a/xbmc/windowing/osx/CMakeLists.txt b/xbmc/windowing/osx/CMakeLists.txt new file mode 100644 index 0000000..433f5cf --- /dev/null +++ b/xbmc/windowing/osx/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES CocoaDPMSSupport.cpp + OSScreenSaverOSX.cpp + VideoSyncOsx.mm) +set(HEADERS CocoaDPMSSupport.h + OSScreenSaverOSX.h + VideoSyncOsx.h) + +if(NOT SDL_FOUND) + list(APPEND SOURCES WinEventsOSX.mm + WinEventsOSXImpl.mm + WinSystemOSX.mm) + list(APPEND HEADERS WinEventsOSX.h + WinEventsOSXImpl.h + WinSystemOSX.h) +endif() + +core_add_library(windowing_osx) diff --git a/xbmc/windowing/osx/CocoaDPMSSupport.cpp b/xbmc/windowing/osx/CocoaDPMSSupport.cpp new file mode 100644 index 0000000..bc86220 --- /dev/null +++ b/xbmc/windowing/osx/CocoaDPMSSupport.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CocoaDPMSSupport.h" + +#include "ServiceBroker.h" +#include "utils/log.h" + +#include <CoreFoundation/CFNumber.h> +#include <IOKit/IOKitLib.h> + +CCocoaDPMSSupport::CCocoaDPMSSupport() +{ + m_supportedModes.push_back(OFF); + m_supportedModes.push_back(STANDBY); +} + +bool CCocoaDPMSSupport::EnablePowerSaving(PowerSavingMode mode) +{ + // http://lists.apple.com/archives/Cocoa-dev/2007/Nov/msg00267.html + // This is an unsupported system call that might kernel panic on PPC boxes + // The reported OSX-PPC panic is unverified so we are going to enable this until + // we find out which OSX-PPC boxes have problems, then add detect/disable for those boxes. + io_registry_entry_t r = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler"); + if (!r) + return false; + + switch (mode) + { + case OFF: + case STANDBY: + return (IORegistryEntrySetCFProperty(r, CFSTR("IORequestIdle"), kCFBooleanTrue) == 0); + default: + return false; + } +} + +bool CCocoaDPMSSupport::DisablePowerSaving() +{ + // http://lists.apple.com/archives/Cocoa-dev/2007/Nov/msg00267.html + // This is an unsupported system call that might kernel panic on PPC boxes + // The reported OSX-PPC panic is unverified so we are going to enable this until + // we find out which OSX-PPC boxes have problems, then add detect/disable for those boxes. + io_registry_entry_t r = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler"); + if (!r) + return false; + + // Turn display on + return (IORegistryEntrySetCFProperty(r, CFSTR("IORequestIdle"), kCFBooleanFalse) == 0); +} diff --git a/xbmc/windowing/osx/CocoaDPMSSupport.h b/xbmc/windowing/osx/CocoaDPMSSupport.h new file mode 100644 index 0000000..6a54f42 --- /dev/null +++ b/xbmc/windowing/osx/CocoaDPMSSupport.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "xbmc/powermanagement/DPMSSupport.h" + +class CCocoaDPMSSupport : public CDPMSSupport +{ +public: + CCocoaDPMSSupport(); + ~CCocoaDPMSSupport() override = default; + +protected: + bool EnablePowerSaving(PowerSavingMode mode) override; + bool DisablePowerSaving() override; +}; diff --git a/xbmc/windowing/osx/OSScreenSaverOSX.cpp b/xbmc/windowing/osx/OSScreenSaverOSX.cpp new file mode 100644 index 0000000..019f86c --- /dev/null +++ b/xbmc/windowing/osx/OSScreenSaverOSX.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "OSScreenSaverOSX.h" + +#include <CoreFoundation/CoreFoundation.h> + +void COSScreenSaverOSX::Inhibit() +{ + // see Technical Q&A QA1340 + if (m_assertionID == 0) + { + CFStringRef reasonForActivity= CFSTR("XBMC requested disable system screen saver"); + IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, reasonForActivity, &m_assertionID); + } +} + +void COSScreenSaverOSX::Uninhibit() +{ + if (m_assertionID != 0) + { + IOPMAssertionRelease(m_assertionID); + m_assertionID = 0; + } +}
\ No newline at end of file diff --git a/xbmc/windowing/osx/OSScreenSaverOSX.h b/xbmc/windowing/osx/OSScreenSaverOSX.h new file mode 100644 index 0000000..c79367a --- /dev/null +++ b/xbmc/windowing/osx/OSScreenSaverOSX.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "../OSScreenSaver.h" + +#import <IOKit/pwr_mgt/IOPMLib.h> + +class COSScreenSaverOSX : public KODI::WINDOWING::IOSScreenSaver +{ +public: + COSScreenSaverOSX() = default; + void Inhibit() override; + void Uninhibit() override; + +private: + IOPMAssertionID m_assertionID = 0; +}; diff --git a/xbmc/windowing/osx/OpenGL/CMakeLists.txt b/xbmc/windowing/osx/OpenGL/CMakeLists.txt new file mode 100644 index 0000000..fd250d7 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/CMakeLists.txt @@ -0,0 +1,14 @@ +if(OPENGL_FOUND) + set(SOURCES WinSystemOSXGL.mm) + set(HEADERS WinSystemOSXGL.h) + + if(NOT SDL_FOUND) + list(APPEND SOURCES OSXGLView.mm + OSXGLWindow.mm) + list(APPEND HEADERS OSXGLView.h + OSXGLWindow.h) + endif() + + core_add_library(windowing_osx_opengl) + +endif() diff --git a/xbmc/windowing/osx/OpenGL/OSXGLView.h b/xbmc/windowing/osx/OpenGL/OSXGLView.h new file mode 100644 index 0000000..577642d --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLView.h @@ -0,0 +1,18 @@ +#pragma once + +/* + * Copyright (C) 2021- Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#import <Cocoa/Cocoa.h> + +@interface OSXGLView : NSOpenGLView + +- (id)initWithFrame:(NSRect)frameRect; +- (NSOpenGLContext*)getGLContext; + +@end diff --git a/xbmc/windowing/osx/OpenGL/OSXGLView.mm b/xbmc/windowing/osx/OpenGL/OSXGLView.mm new file mode 100644 index 0000000..6c0a8b5 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLView.mm @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021- Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#import "OSXGLView.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/AppParamParser.h" +#include "application/Application.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/log.h" + +#include "system_gl.h" + +@implementation OSXGLView +{ + NSOpenGLContext* m_glcontext; + NSOpenGLPixelFormat* m_pixFmt; + NSTrackingArea* m_trackingArea; + BOOL pause; +} + +- (id)initWithFrame:(NSRect)frameRect +{ + NSOpenGLPixelFormatAttribute wattrs[] = { + NSOpenGLPFANoRecovery, NSOpenGLPFAAccelerated, + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, (NSOpenGLPixelFormatAttribute)32, + NSOpenGLPFAAlphaSize, (NSOpenGLPixelFormatAttribute)8, + NSOpenGLPFADepthSize, (NSOpenGLPixelFormatAttribute)24, + NSOpenGLPFADoubleBuffer, (NSOpenGLPixelFormatAttribute)0}; + + self = [super initWithFrame:frameRect]; + if (self) + { + m_pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs]; + m_glcontext = [[NSOpenGLContext alloc] initWithFormat:m_pixFmt shareContext:nil]; + } + + [self updateTrackingAreas]; + + GLint swapInterval = 1; + [m_glcontext setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval]; + [m_glcontext makeCurrentContext]; + + return self; +} + +- (void)dealloc +{ + [NSOpenGLContext clearCurrentContext]; + [m_glcontext clearDrawable]; +} + +- (void)drawRect:(NSRect)rect +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [m_glcontext setView:self]; + + // clear screen on first render + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0, 0, 0, 0); + + [m_glcontext update]; + }); +} + +- (void)updateTrackingAreas +{ + if (m_trackingArea != nil) + { + [self removeTrackingArea:m_trackingArea]; + } + + const int opts = + (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways); + m_trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:opts + owner:self + userInfo:nil]; + [self addTrackingArea:m_trackingArea]; +} + +- (void)mouseEntered:(NSEvent*)theEvent +{ + [NSCursor hide]; +} + +- (void)mouseMoved:(NSEvent*)theEvent +{ +} + +- (void)mouseExited:(NSEvent*)theEvent +{ + [NSCursor unhide]; +} + +- (NSOpenGLContext*)getGLContext +{ + return m_glcontext; +} +@end diff --git a/xbmc/windowing/osx/OpenGL/OSXGLWindow.h b/xbmc/windowing/osx/OpenGL/OSXGLWindow.h new file mode 100644 index 0000000..a722804 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLWindow.h @@ -0,0 +1,19 @@ +#pragma once + +/* + * Copyright (C) 2021- Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#import <Cocoa/Cocoa.h> + +@interface OSXGLWindow : NSWindow <NSWindowDelegate> + +@property(atomic) bool resizeState; + +- (id)initWithContentRect:(NSRect)box styleMask:(uint)style; + +@end diff --git a/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm b/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm new file mode 100644 index 0000000..8ee26fc --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2021- Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#import "OSXGLWindow.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/AppParamParser.h" +#include "application/Application.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#import "windowing/osx/OpenGL/OSXGLView.h" +#include "windowing/osx/WinEventsOSX.h" +#import "windowing/osx/WinSystemOSX.h" + +#include "platform/darwin/osx/CocoaInterface.h" + +//------------------------------------------------------------------------------------------ +@implementation OSXGLWindow + +@synthesize resizeState = m_resizeState; + +- (id)initWithContentRect:(NSRect)box styleMask:(uint)style +{ + self = [super initWithContentRect:box styleMask:style backing:NSBackingStoreBuffered defer:YES]; + [self setDelegate:self]; + [self setAcceptsMouseMovedEvents:YES]; + // autosave the window position/size + // Tell the controller to not cascade its windows. + [self.windowController setShouldCascadeWindows:NO]; + [self setFrameAutosaveName:@"OSXGLWindowPositionHeightWidth"]; + + g_application.m_AppFocused = true; + + return self; +} + +- (void)dealloc +{ + [self setDelegate:nil]; +} + +- (BOOL)windowShouldClose:(id)sender +{ + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + + return NO; +} + +- (void)windowDidExpose:(NSNotification*)aNotification +{ + g_application.m_AppFocused = true; +} + +- (void)windowDidMove:(NSNotification*)aNotification +{ + NSOpenGLContext* context = NSOpenGLContext.currentContext; + if (context) + { + if (context.view) + { + NSPoint window_origin = [[[context view] window] frame].origin; + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEOMOVE; + newEvent.move.x = window_origin.x; + newEvent.move.y = window_origin.y; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } + } +} + +- (void)windowWillStartLiveResize:(NSNotification*)notification +{ + m_resizeState = true; +} + +- (void)windowDidEndLiveResize:(NSNotification*)notification +{ + m_resizeState = false; + [self windowDidResize:notification]; +} + +- (void)windowDidResize:(NSNotification*)aNotification +{ + if (!m_resizeState) + { + NSRect rect = [self contentRectForFrameRect:self.frame]; + int width = static_cast<int>(rect.size.width); + int height = static_cast<int>(rect.size.height); + + XBMC_Event newEvent = {}; + + if (!CServiceBroker::GetWinSystem()->IsFullScreen()) + { + RESOLUTION res_index = RES_DESKTOP; + if ((width == CDisplaySettings::GetInstance().GetResolutionInfo(res_index).iWidth) && + (height == CDisplaySettings::GetInstance().GetResolutionInfo(res_index).iHeight)) + return; + + newEvent.type = XBMC_VIDEORESIZE; + } + else + { + // macos may trigger a resize/rescale event just after Kodi has entered fullscreen + // (from windowDidEndLiveResize). Kodi needs to rescale the UI - use a different event + // type since XBMC_VIDEORESIZE is supposed to only be used in windowed mode + newEvent.type = XBMC_FULLSCREEN_UPDATE; + newEvent.move.x = -1; + newEvent.move.y = -1; + } + + newEvent.resize.w = width; + newEvent.resize.h = height; + + // check for valid sizes cause in some cases + // we are hit during fullscreen transition from macos + // and might be technically "zero" sized + if (newEvent.resize.w != 0 && newEvent.resize.h != 0) + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } + } +} + +- (void)windowDidChangeScreen:(NSNotification*)notification +{ + // user has moved the window to a + // different screen + // if (CServiceBroker::GetWinSystem()->IsFullScreen()) + // CServiceBroker::GetWinSystem()->SetMovedToOtherScreen(true); +} + +- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)frameSize +{ + return frameSize; +} + +- (void)windowWillEnterFullScreen:(NSNotification*)pNotification +{ + CWinSystemOSX* winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return; + + // if osx is the issuer of the toggle + // call Kodi's toggle function + if (!winSystem->GetFullscreenWillToggle()) + { + // indicate that we are toggling + // flag will be reset in SetFullscreen once its + // called from Kodi's gui thread + winSystem->SetFullscreenWillToggle(true); + + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + } + else + { + // in this case we are just called because + // of Kodi did a toggle - just reset the flag + // we don't need to do anything else + winSystem->SetFullscreenWillToggle(false); + } +} + +- (void)windowDidExitFullScreen:(NSNotification*)pNotification +{ + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return; + + // if osx is the issuer of the toggle + // call Kodi's toggle function + if (!winSystem->GetFullscreenWillToggle()) + { + // indicate that we are toggling + // flag will be reset in SetFullscreen once its + // called from Kodi's gui thread + winSystem->SetFullscreenWillToggle(true); + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + } + else + { + // in this case we are just called because + // of Kodi did a toggle - just reset the flag + // we don't need to do anything else + winSystem->SetFullscreenWillToggle(false); + } +} + +- (NSApplicationPresentationOptions)window:(NSWindow*)window + willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions +{ + // customize our appearance when entering full screen: + // we don't want the dock to appear but we want the menubar to hide/show automatically + // + return (NSApplicationPresentationFullScreen | // support full screen for this window (required) + NSApplicationPresentationHideDock | // completely hide the dock + NSApplicationPresentationAutoHideMenuBar); // yes we want the menu bar to show/hide +} + +- (void)windowDidMiniaturize:(NSNotification*)aNotification +{ + g_application.m_AppFocused = false; +} + +- (void)windowDidDeminiaturize:(NSNotification*)aNotification +{ + g_application.m_AppFocused = true; +} + +- (void)windowDidBecomeKey:(NSNotification*)aNotification +{ + g_application.m_AppFocused = true; + + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (winSystem) + { + winSystem->enableInputEvents(); + } +} + +- (void)windowDidResignKey:(NSNotification*)aNotification +{ + g_application.m_AppFocused = false; + + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (winSystem) + { + winSystem->disableInputEvents(); + } +} +@end diff --git a/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h new file mode 100644 index 0000000..c38e5e3 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#if defined(HAS_SDL) +#include "windowing/osx/SDL/WinSystemOSXSDL.h" +#else +#include "windowing/osx/WinSystemOSX.h" +#endif +#include "rendering/gl/RenderSystemGL.h" + +class CWinSystemOSXGL : public CWinSystemOSX, public CRenderSystemGL +{ +public: + CWinSystemOSXGL() = default; + ~CWinSystemOSXGL() override = default; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemOSX + CRenderSystemBase *GetRenderSystem() override { return this; } + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + +protected: + void PresentRenderImpl(bool rendered) override; + void SetVSyncImpl(bool enable) override; +}; diff --git a/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm new file mode 100644 index 0000000..30e44f2 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WinSystemOSXGL.h" + +#include "guilib/Texture.h" +#include "rendering/gl/RenderSystemGL.h" +#include "windowing/WindowSystemFactory.h" + +#include <unistd.h> + +void CWinSystemOSXGL::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemOSXGL::CreateWinSystem() +{ + return std::make_unique<CWinSystemOSXGL>(); +} + +void CWinSystemOSXGL::PresentRenderImpl(bool rendered) +{ + if (rendered) + FlushBuffer(); + + // FlushBuffer does not block if window is obscured + // in this case we need to throttle the render loop + if (IsObscured()) + usleep(10000); + + if (m_delayDispReset && m_dispResetTimer.IsTimePast()) + { + m_delayDispReset = false; + AnnounceOnResetDevice(); + } +} + +void CWinSystemOSXGL::SetVSyncImpl(bool enable) +{ + EnableVSync(false); + + if (enable) + { + EnableVSync(true); + } +} + +bool CWinSystemOSXGL::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + CWinSystemOSX::ResizeWindow(newWidth, newHeight, newLeft, newTop); + CRenderSystemGL::ResetRenderSystem(newWidth, newHeight); + + if (m_bVSync) + { + EnableVSync(m_bVSync); + } + + return true; +} + +bool CWinSystemOSXGL::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + CWinSystemOSX::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight); + + if (m_bVSync) + { + EnableVSync(m_bVSync); + } + + return true; +} + diff --git a/xbmc/windowing/osx/SDL/CMakeLists.txt b/xbmc/windowing/osx/SDL/CMakeLists.txt new file mode 100644 index 0000000..33e25d8 --- /dev/null +++ b/xbmc/windowing/osx/SDL/CMakeLists.txt @@ -0,0 +1,10 @@ +if(SDL_FOUND) + set(SOURCES WinEventsSDL.cpp + WinSystemOSXSDL.mm) + set(HEADERS WinEventsSDL.h + WinSystemOSXSDL.h) +endif() + +if(SOURCES) + core_add_library(windowing_osx_SDL) +endif() diff --git a/xbmc/windowing/osx/SDL/WinEventsSDL.cpp b/xbmc/windowing/osx/SDL/WinEventsSDL.cpp new file mode 100644 index 0000000..31b2506 --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinEventsSDL.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WinEventsSDL.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/Key.h" +#include "input/mouse/MouseStat.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "windowing/WinSystem.h" + +#include "platform/darwin/osx/CocoaInterface.h" + +bool CWinEventsOSX::MessagePump() +{ + SDL_Event event; + bool ret = false; + + while (SDL_PollEvent(&event)) + { + switch(event.type) + { + case SDL_QUIT: + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + break; + + case SDL_ACTIVEEVENT: + //If the window was inconified or restored + if( event.active.state & SDL_APPACTIVE ) + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->SetRenderGUI(event.active.gain != 0); + CServiceBroker::GetWinSystem()->NotifyAppActiveChange(g_application.GetRenderGUI()); + } + else if (event.active.state & SDL_APPINPUTFOCUS) + { + g_application.m_AppFocused = event.active.gain != 0; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort && g_application.m_AppFocused) + appPort->SetRenderGUI(g_application.m_AppFocused); + CServiceBroker::GetWinSystem()->NotifyAppFocusChange(g_application.m_AppFocused); + } + break; + + case SDL_KEYDOWN: + { + // process any platform specific shortcuts before handing off to XBMC + if (ProcessOSXShortcuts(event)) + { + ret = true; + break; + } + + XBMC_Event newEvent = {}; + newEvent.type = XBMC_KEYDOWN; + newEvent.key.keysym.scancode = event.key.keysym.scancode; + newEvent.key.keysym.sym = (XBMCKey) event.key.keysym.sym; + newEvent.key.keysym.unicode = event.key.keysym.unicode; + + // Check if the Windows keys are down because SDL doesn't flag this. + uint16_t mod = event.key.keysym.mod; + uint8_t* keystate = SDL_GetKeyState(NULL); + if (keystate[SDLK_LSUPER] || keystate[SDLK_RSUPER]) + mod |= XBMCKMOD_LSUPER; + newEvent.key.keysym.mod = (XBMCMod) mod; + + // don't handle any more messages in the queue until we've handled keydown, + // if a keyup is in the queue it will reset the keypress before it is handled. + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_KEYUP: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_KEYUP; + newEvent.key.keysym.scancode = event.key.keysym.scancode; + newEvent.key.keysym.sym = (XBMCKey) event.key.keysym.sym; + newEvent.key.keysym.mod =(XBMCMod) event.key.keysym.mod; + newEvent.key.keysym.unicode = event.key.keysym.unicode; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_MOUSEBUTTONDOWN: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = event.button.button; + newEvent.button.x = event.button.x; + newEvent.button.y = event.button.y; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_MOUSEBUTTONUP: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = event.button.button; + newEvent.button.x = event.button.x; + newEvent.button.y = event.button.y; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_MOUSEMOTION: + { + if (0 == (SDL_GetAppState() & SDL_APPMOUSEFOCUS)) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + // See CApplication::ProcessSlow() for a description as to why we call Cocoa_HideMouse. + // this is here to restore the pointer when toggling back to window mode from fullscreen. + Cocoa_ShowMouse(); + break; + } + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = event.motion.x; + newEvent.motion.y = event.motion.y; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + case SDL_VIDEORESIZE: + { + // Under newer osx versions sdl is so fucked up that it even fires resize events + // that exceed the screen size (maybe some HiDP incompatibility in old SDL?) + // ensure to ignore those events because it will mess with windowed size + if((event.resize.w > CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth) || + (event.resize.h > CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight)) + { + break; + } + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEORESIZE; + newEvent.resize.w = event.resize.w; + newEvent.resize.h = event.resize.h; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(); + break; + } + case SDL_USEREVENT: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_USEREVENT; + newEvent.user.code = event.user.code; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + case SDL_VIDEOEXPOSE: + CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(); + break; + } + memset(&event, 0, sizeof(SDL_Event)); + } + + return ret; +} + +bool CWinEventsOSX::ProcessOSXShortcuts(SDL_Event& event) +{ + static bool shift = false, cmd = false; + + cmd = !!(SDL_GetModState() & (KMOD_LMETA | KMOD_RMETA )); + shift = !!(SDL_GetModState() & (KMOD_LSHIFT | KMOD_RSHIFT)); + + if (cmd && event.key.type == SDL_KEYDOWN) + { + char keysymbol = event.key.keysym.sym; + + // if the unicode is in the ascii range + // use this instead for getting the real + // character based on the used keyboard layout + // see http://lists.libsdl.org/pipermail/sdl-libsdl.org/2004-May/043716.html + bool isControl = (event.key.keysym.mod & KMOD_CTRL) != 0; + if (!isControl && !(event.key.keysym.unicode & 0xff80)) + keysymbol = event.key.keysym.unicode; + + switch(keysymbol) + { + case SDLK_q: // CMD-q to quit + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + return true; + + case SDLK_f: // CMD-Ctrl-f to toggle fullscreen + if (!isControl) + return false; + g_application.OnAction(CAction(ACTION_TOGGLE_FULLSCREEN)); + return true; + + case SDLK_s: // CMD-3 to take a screenshot + g_application.OnAction(CAction(ACTION_TAKE_SCREENSHOT)); + return true; + + case SDLK_h: // CMD-h to hide + CServiceBroker::GetWinSystem()->Hide(); + return true; + + case SDLK_m: // CMD-m to minimize + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE); + return true; + + default: + return false; + } + } + + return false; +} diff --git a/xbmc/windowing/osx/SDL/WinEventsSDL.h b/xbmc/windowing/osx/SDL/WinEventsSDL.h new file mode 100644 index 0000000..06e6325 --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinEventsSDL.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "windowing/WinEvents.h" + +#include <SDL/SDL_events.h> + +class CWinEventsOSX : public IWinEvents +{ +public: + CWinEventsOSX() = default; + ~CWinEventsOSX() override = default; + bool MessagePump() override; + +private: + static bool ProcessOSXShortcuts(SDL_Event& event); +}; diff --git a/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h new file mode 100644 index 0000000..ab19301 --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "threads/Timer.h" +#include "windowing/WinSystem.h" + +#include <chrono> +#include <string> +#include <vector> + +typedef struct SDL_Surface SDL_Surface; + +class IDispResource; +class CWinEventsOSX; +class CWinSystemOSXImpl; +#ifdef __OBJC__ +@class NSOpenGLContext; +#endif + +class CWinSystemOSX : public CWinSystemBase, public ITimerCallback +{ +public: + + CWinSystemOSX(); + ~CWinSystemOSX() override; + + // ITimerCallback interface + void OnTimeout() override; + + // CWinSystemBase + bool InitWindowSystem() override; + bool DestroyWindowSystem() override; + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override; + bool DestroyWindow() override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void UpdateResolutions() override; + void NotifyAppFocusChange(bool bGaining) override; + void ShowOSMouse(bool show) override; + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + void OnMove(int x, int y) override; + + std::string GetClipboardText() override; + + void Register(IDispResource *resource) override; + void Unregister(IDispResource *resource) override; + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + std::vector<std::string> GetConnectedOutputs() override; + + void WindowChangedScreen(); + + void AnnounceOnLostDevice(); + void AnnounceOnResetDevice(); + void HandleOnResetDevice(); + void StartLostDeviceTimer(); + void StopLostDeviceTimer(); + + void* GetCGLContextObj(); +#ifdef __OBJC__ + NSOpenGLContext* GetNSOpenGLContext(); +#else + void* GetNSOpenGLContext(); +#endif + + // winevents override + bool MessagePump() override; + +protected: + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + + void HandlePossibleRefreshrateChange(); + void GetScreenResolution(int* w, int* h, double* fps, int screenIdx); + void EnableVSync(bool enable); + bool SwitchToVideoMode(int width, int height, double refreshrate); + void FillInVideoModes(); + bool FlushBuffer(void); + bool IsObscured(void); + void StartTextInput(); + void StopTextInput(); + + std::unique_ptr<CWinSystemOSXImpl> m_impl; + SDL_Surface* m_SDLSurface; + CWinEventsOSX *m_osx_events; + bool m_obscured; + std::chrono::time_point<std::chrono::steady_clock> m_obscured_timecheck; + + bool m_movedToOtherScreen; + int m_lastDisplayNr; + double m_refreshRate; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + CTimer m_lostDeviceTimer; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + int m_updateGLContext = 0; +}; diff --git a/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm new file mode 100644 index 0000000..7a1b46b --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm @@ -0,0 +1,1852 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WinSystemOSXSDL.h" + +#include "CompileInfo.h" +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "cores/AudioEngine/AESinkFactory.h" +#include "cores/AudioEngine/Sinks/AESinkDARWINOSX.h" +#include "cores/RetroPlayer/process/osx/RPProcessInfoOSX.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h" +#include "cores/VideoPlayer/Process/osx/ProcessInfoOSX.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "guilib/DispResource.h" +#include "guilib/GUIWindowManager.h" +#include "input/KeyboardStat.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/gl/ScreenshotSurfaceGL.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/log.h" +#include "windowing/osx/CocoaDPMSSupport.h" +#include "windowing/osx/OSScreenSaverOSX.h" +#include "windowing/osx/SDL/WinEventsSDL.h" +#include "windowing/osx/VideoSyncOsx.h" + +#include "platform/darwin/DarwinUtils.h" +#include "platform/darwin/DictionaryUtils.h" +#include "platform/darwin/osx/CocoaInterface.h" +#import "platform/darwin/osx/SDL/OSXTextInputResponder.h" +#include "platform/darwin/osx/XBMCHelper.h" + +#include <cstdlib> +#include <mutex> +#include <signal.h> + +#import <Cocoa/Cocoa.h> +#import <IOKit/graphics/IOGraphicsLib.h> +#import <IOKit/pwr_mgt/IOPMLib.h> +#import <QuartzCore/QuartzCore.h> +#import <SDL/SDL.h> + +// turn off deprecated warning spew. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +using namespace KODI; +using namespace WINDOWING; +using namespace std::chrono_literals; + +//------------------------------------------------------------------------------------------ +// special object-c class for handling the NSWindowDidMoveNotification callback. +@interface windowDidMoveNoteClass : NSObject +{ + void *m_userdata; +} ++ (windowDidMoveNoteClass*) initWith: (void*) userdata; +- (void) windowDidMoveNotification:(NSNotification*) note; +@end + +@implementation windowDidMoveNoteClass ++ (windowDidMoveNoteClass*) initWith: (void*) userdata +{ + windowDidMoveNoteClass *windowDidMove = [windowDidMoveNoteClass new]; + windowDidMove->m_userdata = userdata; + return windowDidMove; +} +- (void) windowDidMoveNotification:(NSNotification*) note +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata; + if (!winsys) + return; + + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + if (context) + { + if ([context view]) + { + NSPoint window_origin = [[[context view] window] frame].origin; + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEOMOVE; + newEvent.move.x = window_origin.x; + newEvent.move.y = window_origin.y; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } + } +} +@end +//------------------------------------------------------------------------------------------ +// special object-c class for handling the NSWindowDidReSizeNotification callback. +@interface windowDidReSizeNoteClass : NSObject +{ + void *m_userdata; +} ++ (windowDidReSizeNoteClass*) initWith: (void*) userdata; +- (void) windowDidReSizeNotification:(NSNotification*) note; +@end +@implementation windowDidReSizeNoteClass ++ (windowDidReSizeNoteClass*) initWith: (void*) userdata +{ + windowDidReSizeNoteClass *windowDidReSize = [windowDidReSizeNoteClass new]; + windowDidReSize->m_userdata = userdata; + return windowDidReSize; +} +- (void) windowDidReSizeNotification:(NSNotification*) note +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata; + if (!winsys) + return; + +} +@end + +//------------------------------------------------------------------------------------------ +// special object-c class for handling the NSWindowDidChangeScreenNotification callback. +@interface windowDidChangeScreenNoteClass : NSObject +{ + void *m_userdata; +} ++ (windowDidChangeScreenNoteClass*) initWith: (void*) userdata; +- (void) windowDidChangeScreenNotification:(NSNotification*) note; +@end +@implementation windowDidChangeScreenNoteClass ++ (windowDidChangeScreenNoteClass*) initWith: (void*) userdata +{ + windowDidChangeScreenNoteClass *windowDidChangeScreen = [windowDidChangeScreenNoteClass new]; + windowDidChangeScreen->m_userdata = userdata; + return windowDidChangeScreen; +} +- (void) windowDidChangeScreenNotification:(NSNotification*) note +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata; + if (!winsys) + return; + winsys->WindowChangedScreen(); +} +@end +//------------------------------------------------------------------------------------------ + +class CWinSystemOSXImpl +{ +public: + NSOpenGLContext* m_glContext; + static NSOpenGLContext* m_lastOwnedContext; + + windowDidMoveNoteClass* m_windowDidMove; + windowDidReSizeNoteClass* m_windowDidReSize; + windowDidChangeScreenNoteClass* m_windowChangedScreen; +}; + +NSOpenGLContext* CWinSystemOSXImpl::m_lastOwnedContext = nil; + + +#define MAX_DISPLAYS 32 +// if there was a devicelost callback +// but no device reset for 3 secs +// a timeout fires the reset callback +// (for ensuring that e.x. AE isn't stuck) +constexpr auto LOST_DEVICE_TIMEOUT_MS = 3000ms; +static NSWindow* blankingWindows[MAX_DISPLAYS]; + +//------------------------------------------------------------------------------------------ +CRect CGRectToCRect(CGRect cgrect) +{ + CRect crect = CRect( + cgrect.origin.x, + cgrect.origin.y, + cgrect.origin.x + cgrect.size.width, + cgrect.origin.y + cgrect.size.height); + return crect; +} +//--------------------------------------------------------------------------------- +void SetMenuBarVisible(bool visible) +{ + if(visible) + { + [[NSApplication sharedApplication] + setPresentationOptions: NSApplicationPresentationDefault]; + } + else + { + [[NSApplication sharedApplication] + setPresentationOptions: NSApplicationPresentationHideMenuBar | + NSApplicationPresentationHideDock]; + } +} +//--------------------------------------------------------------------------------- +CGDirectDisplayID GetDisplayID(int screen_index) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + if (screen_index >= 0 && screen_index < static_cast<int>(numDisplays)) + return(displayArray[screen_index]); + else + return(displayArray[0]); +} + +size_t DisplayBitsPerPixelForMode(CGDisplayModeRef mode) +{ + size_t bitsPerPixel = 0; + + CFStringRef pixEnc = CGDisplayModeCopyPixelEncoding(mode); + if(CFStringCompare(pixEnc, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) + { + bitsPerPixel = 32; + } + else if(CFStringCompare(pixEnc, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) + { + bitsPerPixel = 16; + } + else if(CFStringCompare(pixEnc, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) + { + bitsPerPixel = 8; + } + + CFRelease(pixEnc); + + return bitsPerPixel; +} + +CFArrayRef GetAllDisplayModes(CGDirectDisplayID display) +{ + int value = 1; + + CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value); + if (!number) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Number!"); + return NULL; + } + + CFStringRef key = kCGDisplayShowDuplicateLowResolutionModes; + CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&key, (const void **)&number, 1, NULL, NULL); + CFRelease(number); + + if (!options) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Dictionary!"); + return NULL; + } + + CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(display, options); + CFRelease(options); + + if (!displayModes) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - no displaymodes found!"); + return NULL; + } + + return displayModes; +} + +// mimic former behavior of deprecated CGDisplayBestModeForParameters +CGDisplayModeRef BestMatchForMode(CGDirectDisplayID display, size_t bitsPerPixel, size_t width, size_t height, boolean_t &match) +{ + + // Get a copy of the current display mode + CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode(display); + + // Loop through all display modes to determine the closest match. + // CGDisplayBestModeForParameters is deprecated on 10.6 so we will emulate it's behavior + // Try to find a mode with the requested depth and equal or greater dimensions first. + // If no match is found, try to find a mode with greater depth and same or greater dimensions. + // If still no match is found, just use the current mode. + CFArrayRef allModes = GetAllDisplayModes(display); + + for(int i = 0; i < CFArrayGetCount(allModes); i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + + if(DisplayBitsPerPixelForMode(mode) != bitsPerPixel) + continue; + + if((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + CGDisplayModeRelease(displayMode); // release the copy we got before ... + displayMode = mode; + match = true; + break; + } + } + + // No depth match was found + if(!match) + { + for(int i = 0; i < CFArrayGetCount(allModes); i++) + { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if(DisplayBitsPerPixelForMode(mode) >= bitsPerPixel) + continue; + + if((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + displayMode = mode; + match = true; + break; + } + } + } + + CFRelease(allModes); + + return displayMode; +} + +CGDirectDisplayID GetDisplayIDFromScreen(NSScreen *screen) +{ + NSDictionary* screenInfo = [screen deviceDescription]; + NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"]; + + return (CGDirectDisplayID)[screenID longValue]; +} + +int GetDisplayIndex(CGDirectDisplayID display) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + while (numDisplays > 0) + { + if (display == displayArray[--numDisplays]) + return numDisplays; + } + return -1; +} + +void BlankOtherDisplays(int screen_index) +{ + int i; + int numDisplays = [[NSScreen screens] count]; + + // zero out blankingWindows for debugging + for (i=0; i<MAX_DISPLAYS; i++) + { + blankingWindows[i] = 0; + } + + // Blank. + for (i=0; i<numDisplays; i++) + { + if (i != screen_index) + { + // Get the size. + NSScreen* pScreen = [[NSScreen screens] objectAtIndex:i]; + NSRect screenRect = [pScreen frame]; + + // Build a blanking window. + screenRect.origin = NSZeroPoint; + blankingWindows[i] = [[NSWindow alloc] initWithContentRect:screenRect + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO + screen:pScreen]; + + [blankingWindows[i] setBackgroundColor:[NSColor blackColor]]; + [blankingWindows[i] setLevel:CGShieldingWindowLevel()]; + [blankingWindows[i] makeKeyAndOrderFront:nil]; + } + } +} + +void UnblankDisplays(void) +{ + int numDisplays = [[NSScreen screens] count]; + int i = 0; + + for (i=0; i<numDisplays; i++) + { + if (blankingWindows[i] != 0) + { + // Get rid of the blanking windows we created. + [blankingWindows[i] close]; + blankingWindows[i] = 0; + } + } +} + +CGDisplayFadeReservationToken DisplayFadeToBlack(bool fade) +{ + // Fade to black to hide resolution-switching flicker and garbage. + CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken; + if (CGAcquireDisplayFadeReservation (5, &fade_token) == kCGErrorSuccess && fade) + CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE); + + return(fade_token); +} + +void DisplayFadeFromBlack(CGDisplayFadeReservationToken fade_token, bool fade) +{ + if (fade_token != kCGDisplayFadeReservationInvalidToken) + { + if (fade) + CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE); + CGReleaseDisplayFadeReservation(fade_token); + } +} + +NSString* screenNameForDisplay(CGDirectDisplayID displayID) +{ + NSString* screenName; + @autoreleasepool + { + NSDictionary* deviceInfo = (__bridge_transfer NSDictionary*)IODisplayCreateInfoDictionary( + CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName); + NSDictionary* localizedNames = + [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]]; + + if ([localizedNames count] > 0) + { + screenName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]]; + } + } + + if (screenName == nil) + { + screenName = [[NSString alloc] initWithFormat:@"%i", displayID]; + } + else + { + // ensure screen name is unique by appending displayid + screenName = [screenName stringByAppendingFormat:@" (%@)", [@(displayID) stringValue]]; + } + + return screenName; +} + +int GetDisplayIndex(const std::string& dispName) +{ + int ret = 0; + + // Add full screen settings for additional monitors + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString *name = screenNameForDisplay(GetDisplayID(disp)); + if ([name UTF8String] == dispName) + { + ret = disp; + break; + } + } + + return ret; +} + +void ShowHideNSWindow(NSWindow *wind, bool show) +{ + if (show) + [wind orderFront:nil]; + else + [wind orderOut:nil]; +} + +static NSWindow *curtainWindow; +void fadeInDisplay(NSScreen *theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double) fadeSteps); + + if (curtainWindow != nil) + { + for (int step = 0; step < fadeSteps; step++) + { + double fade = 1.0 - (step * fadeInterval); + [curtainWindow setAlphaValue:fade]; + + NSDate *nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:nextDate]; + } + } + [curtainWindow close]; + curtainWindow = nil; + + [NSCursor unhide]; +} + +void fadeOutDisplay(NSScreen *theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double) fadeSteps); + + [NSCursor hide]; + + curtainWindow = [[NSWindow alloc] + initWithContentRect:[theScreen frame] + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:YES + screen:theScreen]; + + [curtainWindow setAlphaValue:0.0]; + [curtainWindow setBackgroundColor:[NSColor blackColor]]; + [curtainWindow setLevel:NSScreenSaverWindowLevel]; + + [curtainWindow makeKeyAndOrderFront:nil]; + [curtainWindow setFrame:[curtainWindow + frameRectForContentRect:[theScreen frame]] + display:YES + animate:NO]; + + for (int step = 0; step < fadeSteps; step++) + { + double fade = step * fadeInterval; + [curtainWindow setAlphaValue:fade]; + + NSDate *nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:nextDate]; + } +} + +// try to find mode that matches the desired size, refreshrate +// non interlaced, nonstretched, safe for hardware +CGDisplayModeRef GetMode(int width, int height, double refreshrate, int screenIdx) +{ + if ( screenIdx >= (signed)[[NSScreen screens] count]) + return NULL; + + Boolean stretched; + Boolean interlaced; + Boolean safeForHardware; + Boolean televisionoutput; + int w, h, bitsperpixel; + double rate; + RESOLUTION_INFO res; + + CLog::Log(LOGDEBUG, "GetMode looking for suitable mode with {} x {} @ {:f} Hz on display {}", + width, height, refreshrate, screenIdx); + + CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(screenIdx)); + + if (!displayModes) + return NULL; + + for (int i=0; i < CFArrayGetCount(displayModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i); + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = flags & kDisplayModeStretchedFlag ? true : false; + interlaced = flags & kDisplayModeInterlacedFlag ? true : false; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = flags & kDisplayModeSafetyFlags ? true : false; + televisionoutput = flags & kDisplayModeTelevisionFlag ? true : false; + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + rate = CGDisplayModeGetRefreshRate(displayMode); + + + if ((bitsperpixel == 32) && + (safeForHardware == YES) && + (stretched == NO) && + (interlaced == NO) && + (w == width) && + (h == height) && + (rate == refreshrate || rate == 0)) + { + CLog::Log(LOGDEBUG, "GetMode found a match!"); + return displayMode; + } + } + + CFRelease(displayModes); + CLog::Log(LOGERROR, "GetMode - no match found!"); + return NULL; +} + +//--------------------------------------------------------------------------------- +static void DisplayReconfigured(CGDirectDisplayID display, + CGDisplayChangeSummaryFlags flags, void* userData) +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)userData; + if (!winsys) + return; + + CLog::Log(LOGDEBUG, "CWinSystemOSX::DisplayReconfigured with flags {}", flags); + + // we fire the callbacks on start of configuration + // or when the mode set was finished + // or when we are called with flags == 0 (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + + // first check if we need to call OnLostDevice + if (flags & kCGDisplayBeginConfigurationFlag) + { + // pre/post-reconfiguration changes + RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + if (res == RES_INVALID) + return; + + NSScreen* pScreen = nil; + unsigned int screenIdx = 0; + + if ( screenIdx < [[NSScreen screens] count] ) + { + pScreen = [[NSScreen screens] objectAtIndex:screenIdx]; + } + + // kCGDisplayBeginConfigurationFlag is only fired while the screen is still + // valid + if (pScreen) + { + CGDirectDisplayID xbmc_display = GetDisplayIDFromScreen(pScreen); + if (xbmc_display == display) + { + // we only respond to changes on the display we are running on. + winsys->AnnounceOnLostDevice(); + winsys->StartLostDeviceTimer(); + } + } + } + else // the else case checks if we need to call OnResetDevice + { + // we fire if kCGDisplaySetModeFlag is set or if flags == 0 + // (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + // we also don't check the screen here as we might not even have + // one anymore (e.x. when tv is turned off) + if (flags & kCGDisplaySetModeFlag || flags == 0) + { + winsys->StopLostDeviceTimer(); // no need to timeout - we've got the callback + winsys->HandleOnResetDevice(); + } + } + + if ((flags & kCGDisplayAddFlag) || (flags & kCGDisplayRemoveFlag)) + winsys->UpdateResolutions(); +} + +//------------------------------------------------------------------------------ +NSOpenGLContext* CreateWindowedContext(NSOpenGLContext* shareCtx); +void ResizeWindowInternal(int newWidth, int newHeight, int newLeft, int newTop, NSView* last_view); + +//------------------------------------------------------------------------------ +CWinSystemOSX::CWinSystemOSX() + : CWinSystemBase() + , m_impl{new CWinSystemOSXImpl} + , m_lostDeviceTimer(this) +{ + m_SDLSurface = NULL; + m_osx_events = NULL; + m_obscured = false; + m_obscured_timecheck = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000); + m_lastDisplayNr = -1; + m_movedToOtherScreen = false; + m_refreshRate = 0.0; + m_delayDispReset = false; + + m_winEvents.reset(new CWinEventsOSX()); + + AE::CAESinkFactory::ClearSinks(); + CAESinkDARWINOSX::Register(); + m_dpms = std::make_shared<CCocoaDPMSSupport>(); +} + +CWinSystemOSX::~CWinSystemOSX() = default; + +void CWinSystemOSX::StartLostDeviceTimer() +{ + if (m_lostDeviceTimer.IsRunning()) + m_lostDeviceTimer.Restart(); + else + m_lostDeviceTimer.Start(LOST_DEVICE_TIMEOUT_MS, false); +} + +void CWinSystemOSX::StopLostDeviceTimer() +{ + m_lostDeviceTimer.Stop(); +} + +void CWinSystemOSX::OnTimeout() +{ + HandleOnResetDevice(); +} + +bool CWinSystemOSX::InitWindowSystem() +{ + CLog::LogF(LOGINFO, "Setup SDL"); + + /* Clean up on exit, exit on window close and interrupt */ + std::atexit(SDL_Quit); + + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + CLog::LogF(LOGFATAL, "Unable to initialize SDL: {}", SDL_GetError()); + return false; + } + // SDL_Init will install a handler for segfaults, restore the default handler. + signal(SIGSEGV, SIG_DFL); + + SDL_EnableUNICODE(1); + + // set repeat to 10ms to ensure repeat time < frame time + // so that hold times can be reliably detected + SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, 10); + + if (!CWinSystemBase::InitWindowSystem()) + return false; + + m_osx_events = new CWinEventsOSX(); + + CGDisplayRegisterReconfigurationCallback(DisplayReconfigured, (void*)this); + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + auto windowDidMove = [windowDidMoveNoteClass initWith:this]; + [center addObserver:windowDidMove + selector:@selector(windowDidMoveNotification:) + name:NSWindowDidMoveNotification object:nil]; + m_impl->m_windowDidMove = windowDidMove; + + auto windowDidReSize = [windowDidReSizeNoteClass initWith:this]; + [center addObserver:windowDidReSize + selector:@selector(windowDidReSizeNotification:) + name:NSWindowDidResizeNotification object:nil]; + m_impl->m_windowDidReSize = windowDidReSize; + + auto windowDidChangeScreen = [windowDidChangeScreenNoteClass initWith:this]; + [center addObserver:windowDidChangeScreen + selector:@selector(windowDidChangeScreenNotification:) + name:NSWindowDidChangeScreenNotification object:nil]; + m_impl->m_windowChangedScreen = windowDidChangeScreen; + + return true; +} + +bool CWinSystemOSX::DestroyWindowSystem() +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:m_impl->m_windowDidMove name:NSWindowDidMoveNotification object:nil]; + [center removeObserver:m_impl->m_windowDidReSize name:NSWindowDidResizeNotification object:nil]; + [center removeObserver:m_impl->m_windowChangedScreen + name:NSWindowDidChangeScreenNotification + object:nil]; + + CGDisplayRemoveReconfigurationCallback(DisplayReconfigured, (void*)this); + + delete m_osx_events; + m_osx_events = NULL; + + UnblankDisplays(); + m_impl->m_glContext = nil; + return true; +} + +bool CWinSystemOSX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + // force initial window creation to be windowed, if fullscreen, it will switch to it below + // fixes the white screen of death if starting fullscreen and switching to windowed. + RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + m_nWidth = resInfo.iWidth; + m_nHeight = resInfo.iHeight; + m_bFullScreen = false; + + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + + // Enable vertical sync to avoid any tearing. + SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1); + + m_SDLSurface = SDL_SetVideoMode(m_nWidth, m_nHeight, 0, SDL_OPENGL | SDL_RESIZABLE); + if (!m_SDLSurface) + return false; + + // the context SDL creates isn't full screen compatible, so we create new one + // first, find the current contect and make sure a view is attached + NSOpenGLContext* cur_context = [NSOpenGLContext currentContext]; + NSView* view = [cur_context view]; + if (!view) + return false; + + if (CDisplaySettings::GetInstance().GetCurrentResolution() != RES_WINDOW) + { + // If we are not starting up windowed, then hide the initial SDL window + // so we do not see it flash before the fade-out and switch to fullscreen. + ShowHideNSWindow([view window], false); + } + + // disassociate view from context + [cur_context clearDrawable]; + + // release the context + if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context) + { + [ NSOpenGLContext clearCurrentContext ]; + [ cur_context clearDrawable ]; + cur_context = nil; + } + + // create a new context + auto new_context = CreateWindowedContext(nil); + if (!new_context) + return false; + + // associate with current view + [new_context setView:view]; + [new_context makeCurrentContext]; + + // set the window title + [[[new_context view] window] + setTitle:[NSString stringWithFormat:@"%s Media Center", CCompileInfo::GetAppName()]]; + + m_impl->m_glContext = new_context; + CWinSystemOSXImpl::m_lastOwnedContext = new_context; + m_bWindowCreated = true; + + // get screen refreshrate - this is needed + // when we startup in windowed mode and don't run through SetFullScreen + int dummy; + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + // register platform dependent objects + CDVDFactoryCodec::ClearHWAccels(); + VTB::CDecoder::Register(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGL::Register(); + CRendererVTB::Register(); + VIDEOPLAYER::CProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL); + CScreenshotSurfaceGL::Register(); + + return true; +} + +bool CWinSystemOSX::DestroyWindow() +{ + return true; +} + +void ResizeWindowInternal(int newWidth, int newHeight, int newLeft, int newTop, NSView* last_view) +{ + if (last_view && [last_view window]) + { + auto size = NSMakeSize(newWidth, newHeight); + NSWindow* lastWindow = [last_view window]; + [lastWindow setContentSize:size]; + [lastWindow update]; + [last_view setFrameSize:size]; + } +} +bool CWinSystemOSX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + if (!m_impl->m_glContext) + return false; + + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + NSView* view; + NSWindow* window; + + view = [context view]; + + if (view) + { + // It seems, that in macOS 10.15 this defaults to YES, but we currently do not support + // Retina resolutions properly. Ensure that the view uses a 1 pixel per point framebuffer. + view.wantsBestResolutionOpenGLSurface = NO; + } + + if (view && (newWidth > 0) && (newHeight > 0)) + { + window = [view window]; + if (window) + { + [window setContentSize:NSMakeSize(newWidth, newHeight)]; + [window update]; + [view setFrameSize:NSMakeSize(newWidth, newHeight)]; + [context update]; + // this is needed in case we traverse from fullscreen screen 2 + // to windowed on screen 1 directly where in ScreenChangedNotification + // we don't have a window to get the current screen on + // in that case ResizeWindow is called at a later stage from SetFullScreen(false) + // and we can grab the correct display number here then + m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen( [window screen] )); + } + } + + // HACK: resize SDL's view manually so that mouse bounds are correctly updated. + // there are two parts to this, the internal SDL (current_video->screen) and + // the cocoa view ( handled in SetFullScreen). + SDL_SetWidthHeight(newWidth, newHeight); + + [context makeCurrentContext]; + + m_nWidth = newWidth; + m_nHeight = newHeight; + m_impl->m_glContext = context; + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(m_refreshRate); + + return true; +} + +static bool needtoshowme = true; + +bool CWinSystemOSX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + static NSWindow* windowedFullScreenwindow = nil; + static NSPoint last_window_origin; + static NSView* last_view = nil; + static NSSize last_view_size; + static NSPoint last_view_origin; + static NSInteger last_window_level = NSNormalWindowLevel; + bool was_fullscreen = m_bFullScreen; + NSOpenGLContext* cur_context; + + // Fade to black to hide resolution-switching flicker and garbage. + CGDisplayFadeReservationToken fade_token = DisplayFadeToBlack(needtoshowme); + + // If we're already fullscreen then we must be moving to a different display. + // or if we are still on the same display - it might be only a refreshrate/resolution + // change request. + // Recurse to reset fullscreen mode and then continue. + if (was_fullscreen && fullScreen) + { + needtoshowme = false; + ShowHideNSWindow([last_view window], needtoshowme); + RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + CWinSystemOSX::SetFullScreen(false, window, blankOtherDisplays); + needtoshowme = true; + } + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + m_lastDisplayNr = GetDisplayIndex(settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + + cur_context = [NSOpenGLContext currentContext]; + + //handle resolution/refreshrate switching early here + if (m_bFullScreen) + { + // switch videomode + SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate)); + } + + //no context? done. + if (!cur_context) + { + DisplayFadeFromBlack(fade_token, needtoshowme); + return false; + } + + if (windowedFullScreenwindow != nil) + { + [windowedFullScreenwindow close]; + windowedFullScreenwindow = nil; + } + + if (m_bFullScreen) + { + // FullScreen Mode + NSOpenGLContext* newContext = nil; + + // Save info about the windowed context so we can restore it when returning to windowed. + last_view = [cur_context view]; + last_view_size = [last_view frame].size; + last_view_origin = [last_view frame].origin; + last_window_origin = [[last_view window] frame].origin; + last_window_level = [[last_view window] level]; + + // This is Cocoa Windowed FullScreen Mode + // Get the screen rect of our current display + NSScreen* pScreen = [[NSScreen screens] objectAtIndex:m_lastDisplayNr]; + NSRect screenRect = [pScreen frame]; + + // remove frame origin offset of original display + screenRect.origin = NSZeroPoint; + + // make a new window to act as the windowedFullScreen + windowedFullScreenwindow = [[NSWindow alloc] initWithContentRect:screenRect + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO + screen:pScreen]; + windowedFullScreenwindow.releasedWhenClosed = NO; + + [windowedFullScreenwindow setBackgroundColor:[NSColor blackColor]]; + [windowedFullScreenwindow makeKeyAndOrderFront:nil]; + + // make our window the same level as the rest to enable cmd+tab switching + [windowedFullScreenwindow setLevel:NSNormalWindowLevel]; + // this will make our window topmost and hide all system messages + //[windowedFullScreenwindow setLevel:CGShieldingWindowLevel()]; + + // ...and the original one beneath it and on the same screen. + [[last_view window] setLevel:NSNormalWindowLevel-1]; + [[last_view window] setFrameOrigin:[pScreen frame].origin]; + // expand the mouse bounds in SDL view to fullscreen + [ last_view setFrameOrigin:NSMakePoint(0.0, 0.0)]; + [ last_view setFrameSize:NSMakeSize(m_nWidth, m_nHeight) ]; + + NSView* blankView = [[NSView alloc] init]; + [windowedFullScreenwindow setContentView:blankView]; + [windowedFullScreenwindow setContentSize:NSMakeSize(m_nWidth, m_nHeight)]; + [windowedFullScreenwindow update]; + [blankView setFrameSize:NSMakeSize(m_nWidth, m_nHeight)]; + + // force SDL's window origin to be at zero: + // on Monterey, X coordinate becomes non-zero somewhere after this method returns + const auto twoSeconds = dispatch_time(DISPATCH_TIME_NOW, 2LL * NSEC_PER_SEC); + dispatch_after(twoSeconds, dispatch_get_main_queue(), ^{ + for (NSWindow* w in NSApp.windows) + { + if (w == windowedFullScreenwindow) + continue; + [w setFrameOrigin:w.screen.frame.origin]; + break; + } + }); + + // Obtain windowed pixel format and create a new context. + newContext = CreateWindowedContext(cur_context); + [newContext setView:blankView]; + + // Hide the menu bar. + SetMenuBarVisible(false); + + // Blank other displays if requested. + if (blankOtherDisplays) + BlankOtherDisplays(m_lastDisplayNr); + + // Hide the mouse. + [NSCursor hide]; + + // Release old context if we created it. + if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context) + { + [NSOpenGLContext clearCurrentContext]; + [cur_context clearDrawable]; + } + + // activate context + [newContext makeCurrentContext]; + CWinSystemOSXImpl::m_lastOwnedContext = newContext; + } + else + { + // Windowed Mode + // exit fullscreen + [cur_context clearDrawable]; + + [NSCursor unhide]; + + // Show menubar. + SetMenuBarVisible(true); + + // restore the windowed window level + [[last_view window] setLevel:last_window_level]; + + // Unblank. + // Force the unblank when returning from fullscreen, we get called with blankOtherDisplays set false. + //if (blankOtherDisplays) + UnblankDisplays(); + + // create our new context (sharing with the current one) + auto newContext = CreateWindowedContext(cur_context); + if (!newContext) + return false; + + // Assign view from old context, move back to original screen. + [newContext setView:last_view]; + [[last_view window] setFrameOrigin:last_window_origin]; + // return the mouse bounds in SDL view to previous size + [last_view setFrameSize:last_view_size]; + [last_view setFrameOrigin:last_view_origin]; + + // Release the fullscreen context. + if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context) + { + [NSOpenGLContext clearCurrentContext]; + [cur_context clearDrawable]; + } + + // Activate context. + [newContext makeCurrentContext]; + CWinSystemOSXImpl::m_lastOwnedContext = newContext; + } + + DisplayFadeFromBlack(fade_token, needtoshowme); + + ShowHideNSWindow([last_view window], needtoshowme); + // need to make sure SDL tracks any window size changes + ResizeWindow(m_nWidth, m_nHeight, -1, -1); + ResizeWindowInternal(m_nWidth, m_nHeight, -1, -1, last_view); + // restore origin once again when going to windowed mode + if (!fullScreen) + { + [[last_view window] setFrameOrigin:last_window_origin]; + } + HandlePossibleRefreshrateChange(); + + m_updateGLContext = 0; + return true; +} + +void CWinSystemOSX::UpdateResolutions() +{ + CWinSystemBase::UpdateResolutions(); + + // Add desktop resolution + int w, h; + double fps; + + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + GetScreenResolution(&w, &h, &fps, dispIdx); + NSString* dispName = screenNameForDisplay(GetDisplayID(dispIdx)); + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), [dispName UTF8String], w, h, fps, 0); + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + // now just fill in the possible resolutions for the attached screens + // and push to the resolution info vector + FillInVideoModes(); + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +/* +void* Cocoa_GL_CreateContext(void* pixFmt, void* shareCtx) +{ + if (!pixFmt) + return nil; + + NSOpenGLContext* newContext = [[NSOpenGLContext alloc] initWithFormat:(NSOpenGLPixelFormat*)pixFmt + shareContext:(NSOpenGLContext*)shareCtx]; + + // snipit from SDL_cocoaopengl.m + // + // Wisdom from Apple engineer in reference to UT2003's OpenGL performance: + // "You are blowing a couple of the internal OpenGL function caches. This + // appears to be happening in the VAO case. You can tell OpenGL to up + // the cache size by issuing the following calls right after you create + // the OpenGL context. The default cache size is 16." --ryan. + // + + #ifndef GLI_ARRAY_FUNC_CACHE_MAX + #define GLI_ARRAY_FUNC_CACHE_MAX 284 + #endif + + #ifndef GLI_SUBMIT_FUNC_CACHE_MAX + #define GLI_SUBMIT_FUNC_CACHE_MAX 280 + #endif + + { + long cache_max = 64; + CGLContextObj ctx = (CGLContextObj)[newContext CGLContextObj]; + CGLSetParameter(ctx, (CGLContextParameter)GLI_SUBMIT_FUNC_CACHE_MAX, &cache_max); + CGLSetParameter(ctx, (CGLContextParameter)GLI_ARRAY_FUNC_CACHE_MAX, &cache_max); + } + + // End Wisdom from Apple Engineer section. --ryan. + return newContext; +} +*/ + +NSOpenGLContext* CreateWindowedContext(NSOpenGLContext* shareCtx) +{ + NSOpenGLPixelFormat* pixFmt; + if (getenv("KODI_GL_PROFILE_LEGACY")) + { + NSOpenGLPixelFormatAttribute wattrs[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(8), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs]; + } + else + { + NSOpenGLPixelFormatAttribute wattrs_gl3[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFAOpenGLProfile, + NSOpenGLProfileVersion3_2Core, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(24), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs_gl3]; + } + + auto newContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:shareCtx]; + + if (!newContext) + { + // bah, try again for non-accelerated renderer + NSOpenGLPixelFormatAttribute wattrs2[] = + { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFADepthSize, (NSOpenGLPixelFormatAttribute)8, + (NSOpenGLPixelFormatAttribute)0 + }; + newContext = [[NSOpenGLContext alloc] + initWithFormat:[[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs2] + shareContext:shareCtx]; + } + + return newContext; +} + +NSOpenGLContext* CreateFullScreenContext(int screen_index, NSOpenGLContext* shareCtx) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + CGDirectDisplayID displayID; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + displayID = displayArray[screen_index]; + + NSOpenGLPixelFormat* pixFmt; + if (getenv("KODI_GL_PROFILE_LEGACY")) + { + NSOpenGLPixelFormatAttribute fsattrs[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(8), + NSOpenGLPFAScreenMask, + static_cast<NSOpenGLPixelFormatAttribute>(CGDisplayIDToOpenGLDisplayMask(displayID)), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:fsattrs]; + } + else + { + NSOpenGLPixelFormatAttribute fsattrs_gl3[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(24), + NSOpenGLPFAOpenGLProfile, + NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAScreenMask, + static_cast<NSOpenGLPixelFormatAttribute>(CGDisplayIDToOpenGLDisplayMask(displayID)), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:fsattrs_gl3]; + } + + if (!pixFmt) + return nil; + + auto newContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:shareCtx]; + + return newContext; +} + +void CWinSystemOSX::GetScreenResolution(int* w, int* h, double* fps, int screenIdx) +{ + CGDirectDisplayID display_id = (CGDirectDisplayID)GetDisplayID(screenIdx); + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id); + *w = CGDisplayModeGetWidth(mode); + *h = CGDisplayModeGetHeight(mode); + *fps = CGDisplayModeGetRefreshRate(mode); + CGDisplayModeRelease(mode); + if ((int)*fps == 0) + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + *fps = 60.0; + } +} + +void CWinSystemOSX::EnableVSync(bool enable) +{ + // OpenGL Flush synchronised with vertical retrace + GLint swapInterval = enable ? 1 : 0; + [[NSOpenGLContext currentContext] setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval]; +} + +bool CWinSystemOSX::SwitchToVideoMode(int width, int height, double refreshrate) +{ + boolean_t match = false; + CGDisplayModeRef dispMode = NULL; + + int screenIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + // Figure out the screen size. (default to main screen) + CGDirectDisplayID display_id = GetDisplayID(screenIdx); + + // find mode that matches the desired size, refreshrate + // non interlaced, nonstretched, safe for hardware + dispMode = GetMode(width, height, refreshrate, screenIdx); + + //not found - fallback to bestemdeforparameters + if (!dispMode) + { + dispMode = BestMatchForMode(display_id, 32, width, height, match); + + if (!match) + dispMode = BestMatchForMode(display_id, 16, width, height, match); + + // still no match? fallback to current resolution of the display which HAS to work [tm] + if (!match) + { + int tmpWidth; + int tmpHeight; + double tmpRefresh; + + GetScreenResolution(&tmpWidth, &tmpHeight, &tmpRefresh, screenIdx); + dispMode = GetMode(tmpWidth, tmpHeight, tmpRefresh, screenIdx); + + // no way to get a resolution set + if (!dispMode) + return false; + } + + if (!match) + return false; + } + + // switch mode and return success + CGDisplayCapture(display_id); + CGDisplayConfigRef cfg; + CGBeginDisplayConfiguration(&cfg); + CGConfigureDisplayWithDisplayMode(cfg, display_id, dispMode, nullptr); + CGError err = CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly); + CGDisplayRelease(display_id); + + m_refreshRate = CGDisplayModeGetRefreshRate(dispMode); + + Cocoa_CVDisplayLinkUpdate(); + + return (err == kCGErrorSuccess); +} + +void CWinSystemOSX::FillInVideoModes() +{ + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + // Add full screen settings for additional monitors + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + Boolean stretched; + Boolean interlaced; + Boolean safeForHardware; + Boolean televisionoutput; + int w, h, bitsperpixel; + double refreshrate; + RESOLUTION_INFO res; + + CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(disp)); + NSString *dispName = screenNameForDisplay(GetDisplayID(disp)); + + CLog::Log(LOGINFO, "Display {} has name {}", disp, [dispName UTF8String]); + + if (NULL == displayModes) + continue; + + for (int i=0; i < CFArrayGetCount(displayModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i); + + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = flags & kDisplayModeStretchedFlag ? true : false; + interlaced = flags & kDisplayModeInterlacedFlag ? true : false; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = flags & kDisplayModeSafetyFlags ? true : false; + televisionoutput = flags & kDisplayModeTelevisionFlag ? true : false; + + if ((bitsperpixel == 32) && + (safeForHardware == YES) && + (stretched == NO) && + (interlaced == NO)) + { + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + refreshrate = CGDisplayModeGetRefreshRate(displayMode); + if ((int)refreshrate == 0) // LCD display? + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + refreshrate = 60.0; + } + CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} @ {:f} Hz", disp, + w, h, refreshrate); + + // only add the resolution if it belongs to "our" screen + // all others are only logged above... + if (disp == dispIdx) + { + UpdateDesktopResolution(res, (dispName != nil) ? [dispName UTF8String] : "Unknown", w, h, refreshrate, 0); + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } + } + } + CFRelease(displayModes); + } +} + +bool CWinSystemOSX::FlushBuffer(void) +{ + if (m_updateGLContext < 5) + { + [m_impl->m_glContext update]; + m_updateGLContext++; + } + + [m_impl->m_glContext flushBuffer]; + + return true; +} + +bool CWinSystemOSX::IsObscured(void) +{ + // check once a second if we are obscured. + auto now_time = std::chrono::steady_clock::now(); + if (m_obscured_timecheck > now_time) + return m_obscured; + else + m_obscured_timecheck = now_time + std::chrono::milliseconds(1000); + + NSOpenGLContext* cur_context = [NSOpenGLContext currentContext]; + NSView* view = [cur_context view]; + if (!view) + { + // sanity check, we should always have a view + m_obscured = true; + return m_obscured; + } + + NSWindow *window = [view window]; + if (!window) + { + // sanity check, we should always have a window + m_obscured = true; + return m_obscured; + } + + if ([window isVisible] == NO) + { + // not visible means the window is not showing. + // this should never really happen as we are always visible + // even when minimized in dock. + m_obscured = true; + return m_obscured; + } + + // check if we are minimized (to an icon in the Dock). + if ([window isMiniaturized] == YES) + { + m_obscured = true; + return m_obscured; + } + + // check if we are showing on the active workspace. + if ([window isOnActiveSpace] == NO) + { + m_obscured = true; + return m_obscured; + } + + // default to false before we start parsing though the windows. + // if we are are obscured by any windows, then set true. + m_obscured = false; + static bool obscureLogged = false; + + CGWindowListOption opts; + opts = kCGWindowListOptionOnScreenAboveWindow | kCGWindowListExcludeDesktopElements; + CFArrayRef windowIDs =CGWindowListCreate(opts, (CGWindowID)[window windowNumber]); + + if (!windowIDs) + return m_obscured; + + CFArrayRef windowDescs = CGWindowListCreateDescriptionFromArray(windowIDs); + if (!windowDescs) + { + CFRelease(windowIDs); + return m_obscured; + } + + CGRect bounds = NSRectToCGRect([window frame]); + // kCGWindowBounds measures the origin as the top-left corner of the rectangle + // relative to the top-left corner of the screen. + // NSWindow’s frame property measures the origin as the bottom-left corner + // of the rectangle relative to the bottom-left corner of the screen. + // convert bounds from NSWindow to CGWindowBounds here. + bounds.origin.y = [[window screen] frame].size.height - bounds.origin.y - bounds.size.height; + + std::vector<CRect> partialOverlaps; + CRect ourBounds = CGRectToCRect(bounds); + + for (CFIndex idx=0; idx < CFArrayGetCount(windowDescs); idx++) + { + // walk the window list of windows that are above us and are not desktop elements + CFDictionaryRef windowDictionary = (CFDictionaryRef)CFArrayGetValueAtIndex(windowDescs, idx); + + // skip the Dock window, it actually covers the entire screen. + CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(windowDictionary, kCGWindowOwnerName); + if (CFStringCompare(ownerName, CFSTR("Dock"), 0) == kCFCompareEqualTo) + continue; + + // Ignore known brightness tools for dimming the screen. They claim to cover + // the whole XBMC window and therefore would make the framerate limiter + // kicking in. Unfortunately even the alpha of these windows is 1.0 so + // we have to check the ownerName. + if (CFStringCompare(ownerName, CFSTR("Shades"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("SmartSaver"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Brightness Slider"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Displaperture"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Dreamweaver"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Window Server"), 0) == kCFCompareEqualTo) + continue; + + CFDictionaryRef rectDictionary = (CFDictionaryRef)CFDictionaryGetValue(windowDictionary, kCGWindowBounds); + if (!rectDictionary) + continue; + + CGRect windowBounds; + if (CGRectMakeWithDictionaryRepresentation(rectDictionary, &windowBounds)) + { + if (CGRectContainsRect(windowBounds, bounds)) + { + // if the windowBounds completely encloses our bounds, we are obscured. + if (!obscureLogged) + { + std::string appName; + if (CDarwinUtils::CFStringRefToUTF8String(ownerName, appName)) + CLog::Log(LOGDEBUG, "WinSystemOSX: Fullscreen window {} obscures Kodi!", appName); + obscureLogged = true; + } + m_obscured = true; + break; + } + + // handle overlapping windows above us that combine + // to obscure by collecting any partial overlaps, + // then subtract them from our bounds and check + // for any remaining area. + CRect intersection = CGRectToCRect(windowBounds); + intersection.Intersect(ourBounds); + if (!intersection.IsEmpty()) + partialOverlaps.push_back(intersection); + } + } + + if (!m_obscured) + { + // if we are here we are not obscured by any fullscreen window - reset flag + // for allowing the logmessage above to show again if this changes. + if (obscureLogged) + obscureLogged = false; + std::vector<CRect> rects = ourBounds.SubtractRects(partialOverlaps); + // they got us covered + if (rects.empty()) + m_obscured = true; + } + + CFRelease(windowDescs); + CFRelease(windowIDs); + + return m_obscured; +} + +void CWinSystemOSX::NotifyAppFocusChange(bool bGaining) +{ + if (!(m_bFullScreen && bGaining)) + return; + @autoreleasepool + { + // find the window + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + if (context) + { + NSView* view; + + view = [context view]; + if (view) + { + NSWindow* window; + window = [view window]; + if (window) + { + SetMenuBarVisible(false); + [window orderFront:nil]; + } + } + } + } +} + +void CWinSystemOSX::ShowOSMouse(bool show) +{ + SDL_ShowCursor(show ? 1 : 0); +} + +bool CWinSystemOSX::Minimize() +{ + @autoreleasepool + { + [[NSApplication sharedApplication] miniaturizeAll:nil]; + } + return true; +} + +bool CWinSystemOSX::Restore() +{ + @autoreleasepool + { + [[NSApplication sharedApplication] unhide:nil]; + } + return true; +} + +bool CWinSystemOSX::Hide() +{ + @autoreleasepool + { + [[NSApplication sharedApplication] hide:nil]; + } + return true; +} + +void CWinSystemOSX::HandlePossibleRefreshrateChange() +{ + static double oldRefreshRate = m_refreshRate; + Cocoa_CVDisplayLinkUpdate(); + int dummy = 0; + + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + if (oldRefreshRate != m_refreshRate) + { + oldRefreshRate = m_refreshRate; + // send a message so that videoresolution (and refreshrate) + // is changed + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VIDEORESIZE, m_SDLSurface->w, m_SDLSurface->h); + } +} + +void CWinSystemOSX::OnMove(int x, int y) +{ + HandlePossibleRefreshrateChange(); +} + +std::unique_ptr<IOSScreenSaver> CWinSystemOSX::GetOSScreenSaverImpl() +{ + return std::unique_ptr<IOSScreenSaver> (new COSScreenSaverOSX); +} + +OSXTextInputResponder *g_textInputResponder = nil; + +void CWinSystemOSX::StartTextInput() +{ + NSView *parentView = [[NSApp keyWindow] contentView]; + + /* We only keep one field editor per process, since only the front most + * window can receive text input events, so it make no sense to keep more + * than one copy. When we switched to another window and requesting for + * text input, simply remove the field editor from its superview then add + * it to the front most window's content view */ + if (!g_textInputResponder) { + g_textInputResponder = + [[OSXTextInputResponder alloc] initWithFrame: NSMakeRect(0.0, 0.0, 0.0, 0.0)]; + } + + if (![[g_textInputResponder superview] isEqual: parentView]) + { +// DLOG(@"add fieldEdit to window contentView"); + [g_textInputResponder removeFromSuperview]; + [parentView addSubview: g_textInputResponder]; + [[NSApp keyWindow] makeFirstResponder: g_textInputResponder]; + } +} +void CWinSystemOSX::StopTextInput() +{ + if (g_textInputResponder) + { + [g_textInputResponder removeFromSuperview]; + g_textInputResponder = nil; + } +} + +void CWinSystemOSX::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemOSX::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +bool CWinSystemOSX::Show(bool raise) +{ + @autoreleasepool + { + auto app = [NSApplication sharedApplication]; + if (raise) + { + [app unhide:nil]; + [app activateIgnoringOtherApps:YES]; + [app arrangeInFront:nil]; + } + else + { + [app unhideWithoutActivation]; + } + } + return true; +} + +void CWinSystemOSX::WindowChangedScreen() +{ + // user has moved the window to a + // different screen + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + m_lastDisplayNr = -1; + + // if we are here the user dragged the window to a different + // screen and we return the screen of the window + if (context) + { + NSView* view; + + view = [context view]; + if (view) + { + NSWindow* window; + window = [view window]; + if (window) + { + m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen([window screen])); + } + } + } + if (m_lastDisplayNr == -1) + m_lastDisplayNr = 0;// default to main screen +} + +void CWinSystemOSX::AnnounceOnLostDevice() +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::Log(LOGDEBUG, "CWinSystemOSX::AnnounceOnLostDevice"); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); +} + +void CWinSystemOSX::HandleOnResetDevice() +{ + + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + else + { + AnnounceOnResetDevice(); + } +} + +void CWinSystemOSX::AnnounceOnResetDevice() +{ + double currentFps = m_refreshRate; + int w = 0; + int h = 0; + int currentScreenIdx = m_lastDisplayNr; + // ensure that graphics context knows about the current refreshrate before + // doing the callbacks + GetScreenResolution(&w, &h, ¤tFps, currentScreenIdx); + + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(currentFps); + + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::Log(LOGDEBUG, "CWinSystemOSX::AnnounceOnResetDevice"); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); +} + +void* CWinSystemOSX::GetCGLContextObj() +{ + return [m_impl->m_glContext CGLContextObj]; +} + +NSOpenGLContext* CWinSystemOSX::GetNSOpenGLContext() +{ + return m_impl->m_glContext; +} + +std::string CWinSystemOSX::GetClipboardText(void) +{ + std::string utf8_text; + + const char *szStr = Cocoa_Paste(); + if (szStr) + utf8_text = szStr; + + return utf8_text; +} + +std::unique_ptr<CVideoSync> CWinSystemOSX::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync(new CVideoSyncOsx(clock)); + return pVSync; +} + +bool CWinSystemOSX::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +std::vector<std::string> CWinSystemOSX::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + outputs.emplace_back("Default"); + + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString *dispName = screenNameForDisplay(GetDisplayID(disp)); + outputs.emplace_back([dispName UTF8String]); + } + + return outputs; +} diff --git a/xbmc/windowing/osx/VideoSyncOsx.h b/xbmc/windowing/osx/VideoSyncOsx.h new file mode 100644 index 0000000..b3ba145 --- /dev/null +++ b/xbmc/windowing/osx/VideoSyncOsx.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/DispResource.h" +#include "threads/Event.h" +#include "windowing/VideoSync.h" + +class CVideoSyncOsx : public CVideoSync, IDispResource +{ +public: + CVideoSyncOsx(void* clock) + : CVideoSync(clock), m_LastVBlankTime(0), m_displayLost(false), m_displayReset(false) + { + } + + // CVideoSync interface + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + void RefreshChanged() override; + + // IDispResource interface + void OnLostDisplay() override; + void OnResetDisplay() override; + + // used in the displaylink callback + void VblankHandler(int64_t nowtime, uint32_t timebase); + +private: + virtual bool InitDisplayLink(); + virtual void DeinitDisplayLink(); + + int64_t m_LastVBlankTime; //timestamp of the last vblank, used for calculating how many vblanks happened + volatile bool m_displayLost; + volatile bool m_displayReset; + CEvent m_lostEvent; +}; + diff --git a/xbmc/windowing/osx/VideoSyncOsx.mm b/xbmc/windowing/osx/VideoSyncOsx.mm new file mode 100644 index 0000000..d19e724 --- /dev/null +++ b/xbmc/windowing/osx/VideoSyncOsx.mm @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "VideoSyncOsx.h" + +#include "ServiceBroker.h" +#include "utils/MathUtils.h" +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WinSystem.h" + +#include "platform/darwin/osx/CocoaInterface.h" + +#include <CoreVideo/CVHostTime.h> +#include <QuartzCore/CVDisplayLink.h> +#include <unistd.h> + +using namespace std::chrono_literals; + +bool CVideoSyncOsx::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} setting up OSX", __FUNCTION__); + + //init the vblank timestamp + m_LastVBlankTime = 0; + UpdateClock = func; + m_displayLost = false; + m_displayReset = false; + m_lostEvent.Reset(); + + CServiceBroker::GetWinSystem()->Register(this); + + return true; +} + +void CVideoSyncOsx::Run(CEvent& stopEvent) +{ + InitDisplayLink(); + + //because cocoa has a vblank callback, we just keep sleeping until we're asked to stop the thread + while(!stopEvent.Signaled() && !m_displayLost && !m_displayReset) + { + usleep(100000); + } + + m_lostEvent.Set(); + + while(!stopEvent.Signaled() && m_displayLost && !m_displayReset) + { + usleep(10000); + } + + DeinitDisplayLink(); +} + +void CVideoSyncOsx::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} cleaning up OSX", __FUNCTION__); + m_lostEvent.Set(); + m_LastVBlankTime = 0; + CServiceBroker::GetWinSystem()->Unregister(this); +} + +float CVideoSyncOsx::GetFps() +{ + m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS(); + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} Detected refreshrate: {:f} hertz", __FUNCTION__, m_fps); + return m_fps; +} + +void CVideoSyncOsx::RefreshChanged() +{ + m_displayReset = true; +} + +void CVideoSyncOsx::OnLostDisplay() +{ + if (!m_displayLost) + { + m_displayLost = true; + m_lostEvent.Wait(1000ms); + } +} + +void CVideoSyncOsx::OnResetDisplay() +{ + m_displayReset = true; +} + +void CVideoSyncOsx::VblankHandler(int64_t nowtime, uint32_t timebase) +{ + int NrVBlanks; + double VBlankTime; + int64_t Now = CurrentHostCounter(); + + if (m_LastVBlankTime != 0) + { + VBlankTime = (double)(nowtime - m_LastVBlankTime) / (double)timebase; + NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps)); + + //update the vblank timestamp, update the clock and send a signal that we got a vblank + UpdateClock(NrVBlanks, Now, m_refClock); + } + + //save the timestamp of this vblank so we can calculate how many happened next time + m_LastVBlankTime = nowtime; +} + +// Called by the Core Video Display Link whenever it's appropriate to render a frame. +static CVReturn DisplayLinkCallBack(CVDisplayLinkRef displayLink, const CVTimeStamp* inNow, const CVTimeStamp* inOutputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) +{ + @autoreleasepool + { + CVideoSyncOsx* VideoSyncOsx = reinterpret_cast<CVideoSyncOsx*>(displayLinkContext); + + if (inOutputTime->flags & kCVTimeStampHostTimeValid) + VideoSyncOsx->VblankHandler(inOutputTime->hostTime, CVGetHostClockFrequency()); + else + VideoSyncOsx->VblankHandler(CVGetCurrentHostTime(), CVGetHostClockFrequency()); + } + + return kCVReturnSuccess; +} + +bool CVideoSyncOsx::InitDisplayLink() +{ + bool ret = true; + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} setting up displaylink", __FUNCTION__); + + if (!Cocoa_CVDisplayLinkCreate((void*)DisplayLinkCallBack, reinterpret_cast<void*>(this))) + { + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} Cocoa_CVDisplayLinkCreate failed", __FUNCTION__); + ret = false; + } + return ret; +} + +void CVideoSyncOsx::DeinitDisplayLink() +{ + Cocoa_CVDisplayLinkRelease(); +} + diff --git a/xbmc/windowing/osx/WinEventsOSX.h b/xbmc/windowing/osx/WinEventsOSX.h new file mode 100644 index 0000000..7c4644f --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSX.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Thread.h" +#include "windowing/WinEvents.h" +#include "windowing/XBMC_events.h" + +#include <memory> + +struct CWinEventsOSXImplWrapper; + +class CWinEventsOSX : public IWinEvents, public CThread +{ +public: + CWinEventsOSX(); + ~CWinEventsOSX(); + + void MessagePush(XBMC_Event* newEvent); + bool MessagePump(); + size_t GetQueueSize(); + + void enableInputEvents(); + void disableInputEvents(); + +private: + std::unique_ptr<CWinEventsOSXImplWrapper> m_eventsImplWrapper; +}; diff --git a/xbmc/windowing/osx/WinEventsOSX.mm b/xbmc/windowing/osx/WinEventsOSX.mm new file mode 100644 index 0000000..b2d07db --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSX.mm @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WinEventsOSX.h" + +#import "WinEventsOSXImpl.h" + +struct CWinEventsOSXImplWrapper +{ + CWinEventsOSXImpl* callbackClass; +}; + +CWinEventsOSX::CWinEventsOSX() : CThread("CWinEventsOSX") +{ + m_eventsImplWrapper = std::make_unique<CWinEventsOSXImplWrapper>(); + m_eventsImplWrapper->callbackClass = [CWinEventsOSXImpl new]; + Create(); +} + +CWinEventsOSX::~CWinEventsOSX() +{ + m_bStop = true; + StopThread(true); +} + +void CWinEventsOSX::MessagePush(XBMC_Event* newEvent) +{ + [m_eventsImplWrapper->callbackClass MessagePush:newEvent]; +} + +size_t CWinEventsOSX::GetQueueSize() +{ + return [m_eventsImplWrapper->callbackClass GetQueueSize]; +} + +bool CWinEventsOSX::MessagePump() +{ + return [m_eventsImplWrapper->callbackClass MessagePump]; +} + +void CWinEventsOSX::enableInputEvents() +{ + return [m_eventsImplWrapper->callbackClass enableInputEvents]; +} + +void CWinEventsOSX::disableInputEvents() +{ + return [m_eventsImplWrapper->callbackClass disableInputEvents]; +} diff --git a/xbmc/windowing/osx/WinEventsOSXImpl.h b/xbmc/windowing/osx/WinEventsOSXImpl.h new file mode 100644 index 0000000..2dcacd4 --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSXImpl.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "windowing/osx/WinEventsOSX.h" + +#import <CoreGraphics/CoreGraphics.h> +#import <Foundation/Foundation.h> + +@interface CWinEventsOSXImpl : NSObject + +- (void)MessagePush:(XBMC_Event*)newEvent; +- (size_t)GetQueueSize; +- (bool)MessagePump; +- (void)enableInputEvents; +- (void)disableInputEvents; +- (XBMC_Event)keyPressEvent:(CGEventRef*)event; + +@end diff --git a/xbmc/windowing/osx/WinEventsOSXImpl.mm b/xbmc/windowing/osx/WinEventsOSXImpl.mm new file mode 100644 index 0000000..48c4e37 --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSXImpl.mm @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WinEventsOSXImpl.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "input/XBMC_keysym.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "input/mouse/MouseStat.h" +#include "messaging/ApplicationMessenger.h" +#include "threads/CriticalSection.h" +#include "utils/log.h" +#include "windowing/osx/WinSystemOSX.h" + +#include <memory> +#include <mutex> +#include <queue> + +#import <AppKit/AppKit.h> +#import <Carbon/Carbon.h> // kvk_ANSI_ keycodes +#import <CoreGraphics/CoreGraphics.h> +#import <Foundation/Foundation.h> + +#pragma mark - objc implementation + +@implementation CWinEventsOSXImpl +{ + std::queue<XBMC_Event> events; + CCriticalSection m_inputlock; + id mLocalMonitorId; +} + +#pragma mark - init + +- (instancetype)init +{ + self = [super init]; + + [self enableInputEvents]; + + return self; +} + +- (void)MessagePush:(XBMC_Event*)newEvent +{ + std::unique_lock<CCriticalSection> lock(m_inputlock); + events.emplace(*newEvent); +} + +- (bool)MessagePump +{ + + bool ret = false; + + // Do not always loop, only pump the initial queued count events. else if ui keep pushing + // events the loop won't finish then it will block xbmc main message loop. + for (size_t pumpEventCount = [self GetQueueSize]; pumpEventCount > 0; --pumpEventCount) + { + // Pop up only one event per time since in App::OnEvent it may init modal dialog which init + // deeper message loop and call the deeper MessagePump from there. + XBMC_Event pumpEvent = {}; + { + std::unique_lock<CCriticalSection> lock(m_inputlock); + if (events.size() == 0) + return ret; + pumpEvent = events.front(); + events.pop(); + } + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(pumpEvent); + } + return ret; +} + +- (size_t)GetQueueSize +{ + std::unique_lock<CCriticalSection> lock(m_inputlock); + return events.size(); +} + +- (unichar)OsxKey2XbmcKey:(unichar)character +{ + switch (character) + { + case kVK_ANSI_8: + case NSLeftArrowFunctionKey: + return XBMCK_LEFT; + case kVK_ANSI_0: + case NSRightArrowFunctionKey: + return XBMCK_RIGHT; + case kVK_ANSI_RightBracket: + case NSUpArrowFunctionKey: + return XBMCK_UP; + case kVK_ANSI_O: + case NSDownArrowFunctionKey: + return XBMCK_DOWN; + case NSDeleteCharacter: + return XBMCK_BACKSPACE; + default: + return character; + } +} + +- (XBMCMod)OsxMod2XbmcMod:(CGEventFlags)appleModifier +{ + unsigned int xbmcModifier = XBMCKMOD_NONE; + // shift left + if (appleModifier & kCGEventFlagMaskAlphaShift) + xbmcModifier |= XBMCKMOD_LSHIFT; + // shift right + if (appleModifier & kCGEventFlagMaskShift) + xbmcModifier |= XBMCKMOD_RSHIFT; + // left ctrl + if (appleModifier & kCGEventFlagMaskControl) + xbmcModifier |= XBMCKMOD_LCTRL; + // left alt/option + if (appleModifier & kCGEventFlagMaskAlternate) + xbmcModifier |= XBMCKMOD_LALT; + // left command + if (appleModifier & kCGEventFlagMaskCommand) + xbmcModifier |= XBMCKMOD_LMETA; + + return static_cast<XBMCMod>(xbmcModifier); +} + +- (bool)ProcessOSXShortcuts:(XBMC_Event&)event +{ + const auto cmd = (event.key.keysym.mod & (XBMCKMOD_LMETA | XBMCKMOD_RMETA)) != 0; + if (cmd && event.type == XBMC_KEYDOWN) + { + switch (event.key.keysym.sym) + { + case XBMCK_q: // CMD-q to quit + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + return true; + + case XBMCK_CTRLF: // CMD-CTRL-f to toggle fullscreen + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + return true; + + case XBMCK_s: // CMD-s to take a screenshot + { + CAction* action = new CAction(ACTION_TAKE_SCREENSHOT); + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(action)); + return true; + } + case XBMCK_h: // CMD-h to hide (but we minimize for now) + case XBMCK_m: // CMD-m to minimize + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE); + return true; + + default: + return false; + } + } + + return false; +} + +- (void)enableInputEvents +{ + [self disableInputEvents]; // allow only one registration at a time + + // clang-format off + // Create an event tap. We are interested in mouse and keyboard events. + NSEventMask eventMask = + NSEventMaskKeyDown | NSEventMaskKeyUp | + NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | + NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | + NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp | + NSEventMaskScrollWheel | + NSEventMaskLeftMouseDragged | + NSEventMaskRightMouseDragged | + NSEventMaskOtherMouseDragged | + NSEventMaskMouseMoved; + // clang-format on + + mLocalMonitorId = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask + handler:^(NSEvent* event) { + return [self InputEventHandler:event]; + }]; +} + +- (void)disableInputEvents +{ + // Disable the local Monitor + if (mLocalMonitorId != nil) + [NSEvent removeMonitor:mLocalMonitorId]; + mLocalMonitorId = nil; +} + +- (NSEvent*)InputEventHandler:(NSEvent*)nsevent +{ + bool passEvent = true; + CGEventRef event = nsevent.CGEvent; + CGEventType type = CGEventGetType(event); + + // The incoming mouse position. + NSPoint location = nsevent.locationInWindow; + if (location.x < 0 || location.y < 0) + return nsevent; + + // cocoa world is upside down ... + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return nsevent; + + NSRect frame = winSystem->GetWindowDimensions(); + location.y = frame.size.height - location.y; + + XBMC_Event newEvent = {}; + + switch (type) + { + // handle mouse events and transform them into the xbmc event world + case kCGEventLeftMouseUp: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = XBMC_BUTTON_LEFT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventLeftMouseDown: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = XBMC_BUTTON_LEFT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventRightMouseUp: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = XBMC_BUTTON_RIGHT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventRightMouseDown: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = XBMC_BUTTON_RIGHT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventOtherMouseUp: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = XBMC_BUTTON_MIDDLE; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventOtherMouseDown: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = XBMC_BUTTON_MIDDLE; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventMouseMoved: + case kCGEventLeftMouseDragged: + case kCGEventRightMouseDragged: + case kCGEventOtherMouseDragged: + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = location.x; + newEvent.motion.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventScrollWheel: + // very strange, real scrolls have non-zero deltaY followed by same number of events + // with a zero deltaY. This reverses our scroll which is WTF? anoying. Trap them out here. + if (nsevent.deltaY != 0.0) + { + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + newEvent.button.button = + CGEventGetIntegerValueField(event, kCGScrollWheelEventDeltaAxis1) > 0 + ? XBMC_BUTTON_WHEELUP + : XBMC_BUTTON_WHEELDOWN; + [self MessagePush:&newEvent]; + + newEvent.type = XBMC_MOUSEBUTTONUP; + [self MessagePush:&newEvent]; + } + break; + + // handle keyboard events and transform them into the xbmc event world + case kCGEventKeyUp: + newEvent = [self keyPressEvent:&event]; + newEvent.type = XBMC_KEYUP; + + [self MessagePush:&newEvent]; + passEvent = false; + break; + case kCGEventKeyDown: + newEvent = [self keyPressEvent:&event]; + newEvent.type = XBMC_KEYDOWN; + + if (![self ProcessOSXShortcuts:newEvent]) + [self MessagePush:&newEvent]; + passEvent = false; + + break; + default: + return nsevent; + } + // We must return the event for it to be useful if not already handled + if (passEvent) + return nsevent; + else + return nullptr; +} + +- (XBMC_Event)keyPressEvent:(CGEventRef*)event +{ + UniCharCount actualStringLength = 0; + // Get stringlength of event + CGEventKeyboardGetUnicodeString(*event, 0, &actualStringLength, nullptr); + + // Create array with size of event string + UniChar unicodeString[actualStringLength]; + memset(unicodeString, 0, sizeof(unicodeString)); + + auto keycode = + static_cast<CGKeyCode>(CGEventGetIntegerValueField(*event, kCGKeyboardEventKeycode)); + CGEventKeyboardGetUnicodeString(*event, sizeof(unicodeString) / sizeof(*unicodeString), + &actualStringLength, unicodeString); + + XBMC_Event newEvent = {}; + + // May be possible for actualStringLength > 1. Havent been able to replicate anything + // larger than 1, but keep in mind for any regressions + if (actualStringLength == 0) + { + return newEvent; + } + else if (actualStringLength > 1) + { + CLog::Log(LOGERROR, "CWinEventsOSXImpl::keyPressEvent - event string > 1 - size: {}", + static_cast<int>(actualStringLength)); + return newEvent; + } + + unicodeString[0] = [self OsxKey2XbmcKey:unicodeString[0]]; + + newEvent.key.keysym.scancode = keycode; + newEvent.key.keysym.sym = static_cast<XBMCKey>(unicodeString[0]); + newEvent.key.keysym.unicode = unicodeString[0]; + newEvent.key.keysym.mod = [self OsxMod2XbmcMod:CGEventGetFlags(*event)]; + + return newEvent; +} + +@end diff --git a/xbmc/windowing/osx/WinSystemOSX.h b/xbmc/windowing/osx/WinSystemOSX.h new file mode 100644 index 0000000..0ac08d4 --- /dev/null +++ b/xbmc/windowing/osx/WinSystemOSX.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "threads/Timer.h" +#include "windowing/WinSystem.h" + +#include <memory> +#include <string> +#include <vector> + +typedef struct _CGLContextObject* CGLContextObj; +typedef struct CGRect NSRect; + +class IDispResource; +class CWinEventsOSX; +#ifdef __OBJC__ +@class NSWindow; +@class OSXGLView; +#else +struct NSWindow; +struct OSXGLView; +#endif + +class CWinSystemOSX : public CWinSystemBase, public ITimerCallback +{ +public: + CWinSystemOSX(); + ~CWinSystemOSX() override; + + // ITimerCallback interface + void OnTimeout() override; + + // CWinSystemBase + bool InitWindowSystem() override; + bool DestroyWindowSystem() override; + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override; + bool DestroyWindow() override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void UpdateResolutions() override; + void NotifyAppFocusChange(bool bGaining) override; + void ShowOSMouse(bool show) override; + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + void OnMove(int x, int y) override; + + void SetOcclusionState(bool occluded); + + std::string GetClipboardText() override; + + void Register(IDispResource* resource) override; + void Unregister(IDispResource* resource) override; + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + void WindowChangedScreen(); + + void AnnounceOnLostDevice(); + void AnnounceOnResetDevice(); + void HandleOnResetDevice(); + void StartLostDeviceTimer(); + void StopLostDeviceTimer(); + + void SetMovedToOtherScreen(bool moved) { m_movedToOtherScreen = moved; } + int CheckDisplayChanging(uint32_t flags); + void SetFullscreenWillToggle(bool toggle) { m_fullscreenWillToggle = toggle; } + bool GetFullscreenWillToggle() { return m_fullscreenWillToggle; } + + CGLContextObj GetCGLContextObj(); + + std::vector<std::string> GetConnectedOutputs() override; + + // winevents override + bool MessagePump() override; + + NSRect GetWindowDimensions(); + void enableInputEvents(); + void disableInputEvents(); + +protected: + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + + void GetScreenResolution(int* w, int* h, double* fps, int screenIdx); + void EnableVSync(bool enable); + bool SwitchToVideoMode(int width, int height, double refreshrate); + void FillInVideoModes(); + bool FlushBuffer(); + bool IsObscured(); + + bool DestroyWindowInternal(); + + std::unique_ptr<CWinEventsOSX> m_winEvents; + + std::string m_name; + bool m_obscured; + NSWindow* m_appWindow; + OSXGLView* m_glView; + bool m_movedToOtherScreen; + int m_lastDisplayNr; + double m_refreshRate; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + CTimer m_lostDeviceTimer; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + bool m_fullscreenWillToggle; + CCriticalSection m_critSection; +}; diff --git a/xbmc/windowing/osx/WinSystemOSX.mm b/xbmc/windowing/osx/WinSystemOSX.mm new file mode 100644 index 0000000..85dd4b7 --- /dev/null +++ b/xbmc/windowing/osx/WinSystemOSX.mm @@ -0,0 +1,1305 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WinSystemOSX.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "cores/AudioEngine/AESinkFactory.h" +#include "cores/AudioEngine/Sinks/AESinkDARWINOSX.h" +#include "cores/RetroPlayer/process/osx/RPProcessInfoOSX.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h" +#include "cores/VideoPlayer/Process/osx/ProcessInfoOSX.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "guilib/DispResource.h" +#include "guilib/GUIWindowManager.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/gl/ScreenshotSurfaceGL.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/CriticalSection.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/osx/CocoaDPMSSupport.h" +#include "windowing/osx/OSScreenSaverOSX.h" +#import "windowing/osx/OpenGL/OSXGLView.h" +#import "windowing/osx/OpenGL/OSXGLWindow.h" +#include "windowing/osx/VideoSyncOsx.h" +#include "windowing/osx/WinEventsOSX.h" + +#include "platform/darwin/DarwinUtils.h" +#include "platform/darwin/DictionaryUtils.h" +#include "platform/darwin/osx/CocoaInterface.h" +#include "platform/darwin/osx/powermanagement/CocoaPowerSyscall.h" + +#include <chrono> +#include <cstdlib> +#include <mutex> +#include <signal.h> + +#import <Cocoa/Cocoa.h> +#import <Foundation/Foundation.h> +#import <IOKit/graphics/IOGraphicsLib.h> +#import <IOKit/pwr_mgt/IOPMLib.h> +#import <QuartzCore/QuartzCore.h> + +using namespace KODI; +using namespace MESSAGING; +using namespace WINDOWING; +using namespace std::chrono_literals; + +#define MAX_DISPLAYS 32 +static NSWindow* blankingWindows[MAX_DISPLAYS]; + +size_t DisplayBitsPerPixelForMode(CGDisplayModeRef mode) +{ + size_t bitsPerPixel = 0; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // No replacement for CGDisplayModeCopyPixelEncoding + // Disable warning for now + CFStringRef pixEnc = CGDisplayModeCopyPixelEncoding(mode); +#pragma GCC diagnostic pop + + if (CFStringCompare(pixEnc, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == + kCFCompareEqualTo) + { + bitsPerPixel = 32; + } + else if (CFStringCompare(pixEnc, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == + kCFCompareEqualTo) + { + bitsPerPixel = 16; + } + else if (CFStringCompare(pixEnc, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == + kCFCompareEqualTo) + { + bitsPerPixel = 8; + } + + CFRelease(pixEnc); + + return bitsPerPixel; +} + +#pragma mark - GetScreenName + +NSString* screenNameForDisplay(CGDirectDisplayID displayID) +{ + NSString* screenName; + @autoreleasepool + { + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // No real replacement of CGDisplayIOServicePort + // Stackoverflow links to https://github.com/glfw/glfw/pull/192 as a possible replacement + // disable warning for now + NSDictionary* deviceInfo = (__bridge_transfer NSDictionary*)IODisplayCreateInfoDictionary( + CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName); + +#pragma GCC diagnostic pop + + NSDictionary* localizedNames = + [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]]; + + if ([localizedNames count] > 0) + { + screenName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]]; + } + } + + if (screenName == nil) + { + screenName = [[NSString alloc] initWithFormat:@"%i", displayID]; + } + else + { + // ensure screen name is unique by appending displayid + screenName = [screenName stringByAppendingFormat:@" (%@)", [@(displayID) stringValue]]; + } + + return screenName; +} + +#pragma mark - GetDisplay + +CGDirectDisplayID GetDisplayID(int screen_index) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + uint32_t numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + if (screen_index >= 0 && screen_index < static_cast<int>(numDisplays)) + return (displayArray[screen_index]); + else + return (displayArray[0]); +} + +CGDirectDisplayID GetDisplayIDFromScreen(NSScreen* screen) +{ + NSDictionary* screenInfo = screen.deviceDescription; + NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"]; + + return (CGDirectDisplayID)[screenID longValue]; +} + +int GetDisplayIndex(CGDirectDisplayID display) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + while (numDisplays > 0) + { + if (display == displayArray[--numDisplays]) + return numDisplays; + } + return -1; +} + +int GetDisplayIndex(const std::string& dispName) +{ + int ret = 0; + + // Add full screen settings for additional monitors + int numDisplays = NSScreen.screens.count; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString* name = screenNameForDisplay(GetDisplayID(disp)); + if (name.UTF8String == dispName) + { + ret = disp; + break; + } + } + + return ret; +} + +#pragma mark - Display Modes + +CFArrayRef GetAllDisplayModes(CGDirectDisplayID display) +{ + int value = 1; + + CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value); + if (!number) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Number!"); + return nullptr; + } + + CFStringRef key = kCGDisplayShowDuplicateLowResolutionModes; + CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void**)&key, + (const void**)&number, 1, nullptr, nullptr); + CFRelease(number); + + if (!options) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Dictionary!"); + return nullptr; + } + + CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(display, options); + CFRelease(options); + + if (!displayModes) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - no displaymodes found!"); + return nullptr; + } + + return displayModes; +} + +// try to find mode that matches the desired size, refreshrate +// non interlaced, nonstretched, safe for hardware +CGDisplayModeRef GetMode(int width, int height, double refreshrate, int screenIdx) +{ + if (screenIdx >= (signed)[[NSScreen screens] count]) + return nullptr; + + bool stretched; + bool interlaced; + bool safeForHardware; + int w, h, bitsperpixel; + double rate; + RESOLUTION_INFO res; + + CLog::Log(LOGDEBUG, "GetMode looking for suitable mode with {} x {} @ {} Hz on display {}", width, + height, refreshrate, screenIdx); + + CFArrayRef allModes = GetAllDisplayModes(GetDisplayID(screenIdx)); + + if (!allModes) + return nullptr; + + for (int i = 0; i < CFArrayGetCount(allModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = (flags & kDisplayModeStretchedFlag) != 0; + interlaced = (flags & kDisplayModeInterlacedFlag) != 0; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = (flags & kDisplayModeSafetyFlags) != 0; + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + rate = CGDisplayModeGetRefreshRate(displayMode); + + if ((bitsperpixel == 32) && (safeForHardware == true) && (stretched == false) && + (interlaced == false) && (w == width) && (h == height) && + (rate == refreshrate || rate == 0)) + { + CFRelease(allModes); + CLog::Log(LOGDEBUG, "GetMode found a match!"); + return CGDisplayModeRetain(displayMode); + } + } + + CFRelease(allModes); + CLog::Log(LOGERROR, "GetMode - no match found!"); + return nullptr; +} + +// mimic former behavior of deprecated CGDisplayBestModeForParameters +CGDisplayModeRef BestMatchForMode(CGDirectDisplayID display, + size_t bitsPerPixel, + size_t width, + size_t height) +{ + // Loop through all display modes to determine the closest match. + // CGDisplayBestModeForParameters is deprecated on 10.6 so we will emulate it's behavior + // Try to find a mode with the requested depth and equal or greater dimensions first. + // If no match is found, try to find a mode with greater depth and same or greater dimensions. + // If still no match is found, just use the current mode. + CFArrayRef allModes = GetAllDisplayModes(display); + + if (!allModes) + return nullptr; + + CGDisplayModeRef displayMode = nullptr; + + for (int i = 0; i < CFArrayGetCount(allModes); i++) + { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + + if (!mode) + continue; + + if (DisplayBitsPerPixelForMode(mode) != bitsPerPixel) + continue; + + if ((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + displayMode = mode; + break; + } + } + + // No depth match was found + if (!displayMode) + { + for (int i = 0; i < CFArrayGetCount(allModes); i++) + { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + + if (!mode) + continue; + + if (DisplayBitsPerPixelForMode(mode) >= bitsPerPixel) + continue; + + if ((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + displayMode = mode; + break; + } + } + } + + CFRelease(allModes); + + return displayMode; +} + +#pragma mark - Blank Displays + +void BlankOtherDisplays(int screen_index) +{ + int i; + int numDisplays = [[NSScreen screens] count]; + + // zero out blankingWindows for debugging + for (i = 0; i < MAX_DISPLAYS; i++) + { + blankingWindows[i] = 0; + } + + // Blank. + for (i = 0; i < numDisplays; i++) + { + if (i != screen_index) + { + // Get the size. + NSScreen* pScreen = [NSScreen.screens objectAtIndex:i]; + NSRect screenRect = pScreen.frame; + + // Build a blanking window. + screenRect.origin = NSZeroPoint; + blankingWindows[i] = [[NSWindow alloc] initWithContentRect:screenRect + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:NO + screen:pScreen]; + + [blankingWindows[i] setBackgroundColor:NSColor.blackColor]; + [blankingWindows[i] setLevel:CGShieldingWindowLevel()]; + [blankingWindows[i] makeKeyAndOrderFront:nil]; + } + } +} + +void UnblankDisplays(void) +{ + for (auto i = 0; i < static_cast<int>(NSScreen.screens.count); i++) + { + if (blankingWindows[i] != 0) + { + // Get rid of the blanking windows we created. + [blankingWindows[i] close]; + blankingWindows[i] = 0; + } + } +} + +#pragma mark - Fade Display +//! @Todo Look to replace Fade with CABasicAnimation +static NSWindow* curtainWindow; +void fadeInDisplay(NSScreen* theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double)fadeSteps); + + if (curtainWindow != nil) + { + for (int step = 0; step < fadeSteps; step++) + { + double fade = 1.0 - (step * fadeInterval); + [curtainWindow setAlphaValue:fade]; + + NSDate* nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [NSRunLoop.currentRunLoop runUntilDate:nextDate]; + } + } + [curtainWindow close]; + curtainWindow = nil; +} + +void fadeOutDisplay(NSScreen* theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double)fadeSteps); + + [NSCursor hide]; + + curtainWindow = [[NSWindow alloc] initWithContentRect:[theScreen frame] + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES + screen:theScreen]; + + [curtainWindow setAlphaValue:0.0]; + [curtainWindow setBackgroundColor:NSColor.blackColor]; + [curtainWindow setLevel:NSScreenSaverWindowLevel]; + + [curtainWindow makeKeyAndOrderFront:nil]; + [curtainWindow setFrame:[curtainWindow frameRectForContentRect:[theScreen frame]] + display:YES + animate:NO]; + + for (int step = 0; step < fadeSteps; step++) + { + double fade = step * fadeInterval; + [curtainWindow setAlphaValue:fade]; + + NSDate* nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [NSRunLoop.currentRunLoop runUntilDate:nextDate]; + } +} + +//--------------------------------------------------------------------------------- +static void DisplayReconfigured(CGDirectDisplayID display, + CGDisplayChangeSummaryFlags flags, + void* userData) +{ + CWinSystemOSX* winsys = (CWinSystemOSX*)userData; + if (!winsys) + return; + + CLog::Log(LOGDEBUG, "CWinSystemOSX::DisplayReconfigured with flags {}", flags); + + // we fire the callbacks on start of configuration + // or when the mode set was finished + // or when we are called with flags == 0 (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + + // first check if we need to call OnLostDevice + if (flags & kCGDisplayBeginConfigurationFlag) + { + // pre/post-reconfiguration changes + RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + if (res == RES_INVALID) + return; + + NSScreen* pScreen = nil; + unsigned int screenIdx = 0; + + if (screenIdx < NSScreen.screens.count) + { + pScreen = [NSScreen.screens objectAtIndex:screenIdx]; + } + + // kCGDisplayBeginConfigurationFlag is only fired while the screen is still + // valid + if (pScreen) + { + CGDirectDisplayID xbmc_display = GetDisplayIDFromScreen(pScreen); + if (xbmc_display == display) + { + // we only respond to changes on the display we are running on. + winsys->AnnounceOnLostDevice(); + winsys->StartLostDeviceTimer(); + } + } + } + else // the else case checks if we need to call OnResetDevice + { + // we fire if kCGDisplaySetModeFlag is set or if flags == 0 + // (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + // we also don't check the screen here as we might not even have + // one anymore (e.x. when tv is turned off) + if (flags & kCGDisplaySetModeFlag || flags == 0) + { + winsys->StopLostDeviceTimer(); // no need to timeout - we've got the callback + winsys->HandleOnResetDevice(); + } + } +} + +#pragma mark - CWinSystemOSX +//------------------------------------------------------------------------------ +CWinSystemOSX::CWinSystemOSX() : CWinSystemBase(), m_lostDeviceTimer(this) +{ + m_appWindow = nullptr; + m_glView = nullptr; + m_obscured = false; + m_lastDisplayNr = -1; + m_movedToOtherScreen = false; + m_refreshRate = 0.0; + m_delayDispReset = false; + + m_winEvents.reset(new CWinEventsOSX()); + + AE::CAESinkFactory::ClearSinks(); + CAESinkDARWINOSX::Register(); + CCocoaPowerSyscall::Register(); + m_dpms = std::make_shared<CCocoaDPMSSupport>(); +} + +CWinSystemOSX::~CWinSystemOSX() = default; + +void CWinSystemOSX::Register(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemOSX::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemOSX::AnnounceOnLostDevice() +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::LogF(LOGDEBUG, "Lost Device Announce"); + for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); +} + +void CWinSystemOSX::HandleOnResetDevice() +{ + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + else + { + AnnounceOnResetDevice(); + } +} + +void CWinSystemOSX::AnnounceOnResetDevice() +{ + double currentFps = m_refreshRate; + int w = 0; + int h = 0; + int currentScreenIdx = m_lastDisplayNr; + // ensure that graphics context knows about the current refreshrate before + // doing the callbacks + GetScreenResolution(&w, &h, ¤tFps, currentScreenIdx); + + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(currentFps); + + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::LogF(LOGDEBUG, "Reset Device Announce"); + for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); +} + +#pragma mark - Timers + +void CWinSystemOSX::StartLostDeviceTimer() +{ + if (m_lostDeviceTimer.IsRunning()) + m_lostDeviceTimer.Restart(); + else + m_lostDeviceTimer.Start(3000ms, false); +} + +void CWinSystemOSX::StopLostDeviceTimer() +{ + m_lostDeviceTimer.Stop(); +} + +void CWinSystemOSX::OnTimeout() +{ + HandleOnResetDevice(); +} + +#pragma mark - WindowSystem + +bool CWinSystemOSX::InitWindowSystem() +{ + if (!CWinSystemBase::InitWindowSystem()) + return false; + + CGDisplayRegisterReconfigurationCallback(DisplayReconfigured, (void*)this); + + return true; +} + +bool CWinSystemOSX::DestroyWindowSystem() +{ + CGDisplayRemoveReconfigurationCallback(DisplayReconfigured, (void*)this); + + DestroyWindowInternal(); + + if (m_glView) + { + m_glView = nullptr; + } + + UnblankDisplays(); + return true; +} + +bool CWinSystemOSX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + // force initial window creation to be windowed, if fullscreen, it will switch to it below + // fixes the white screen of death if starting fullscreen and switching to windowed. + RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + m_nWidth = resInfo.iWidth; + m_nHeight = resInfo.iHeight; + m_bFullScreen = false; + m_name = name; + + __block NSWindow* appWindow; + // because we are not main thread, delay any updates + // and only become keyWindow after it finishes. + [NSAnimationContext beginGrouping]; + [NSAnimationContext.currentContext setCompletionHandler:^{ + [appWindow makeKeyWindow]; + }]; + + const NSUInteger windowStyleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + + if (m_appWindow == nullptr) + { + // create new content view + NSRect rect = [appWindow contentRectForFrameRect:appWindow.frame]; + + // create new view if we don't have one + if (!m_glView) + m_glView = [[OSXGLView alloc] initWithFrame:rect]; + + OSXGLView* view = (OSXGLView*)m_glView; + + dispatch_sync(dispatch_get_main_queue(), ^{ + appWindow = [[OSXGLWindow alloc] initWithContentRect:NSMakeRect(0, 0, m_nWidth, m_nHeight) + styleMask:windowStyleMask]; + NSString* title = [NSString stringWithUTF8String:m_name.c_str()]; + appWindow.backgroundColor = NSColor.blackColor; + appWindow.title = title; + + NSWindowCollectionBehavior behavior = appWindow.collectionBehavior; + behavior |= NSWindowCollectionBehaviorFullScreenPrimary; + [appWindow setCollectionBehavior:behavior]; + + // associate with current window + [appWindow setContentView:view]; + }); + + [view.getGLContext makeCurrentContext]; + [view.getGLContext update]; + + m_appWindow = appWindow; + m_bWindowCreated = true; + } + + // warning, we can order front but not become + // key window or risk starting up with bad flicker + // becoming key window must happen in completion block. + [(NSWindow*)m_appWindow performSelectorOnMainThread:@selector(orderFront:) + withObject:nil + waitUntilDone:YES]; + + [NSAnimationContext endGrouping]; + + // get screen refreshrate - this is needed + // when we startup in windowed mode and don't run through SetFullScreen + int dummy; + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + // register platform dependent objects + CDVDFactoryCodec::ClearHWAccels(); + VTB::CDecoder::Register(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGL::Register(); + CRendererVTB::Register(); + VIDEOPLAYER::CProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL); + CScreenshotSurfaceGL::Register(); + + return true; +} + +bool CWinSystemOSX::DestroyWindowInternal() +{ + // set this 1st, we should really mutex protext m_appWindow in this class + m_bWindowCreated = false; + if (m_appWindow) + { + NSWindow* oldAppWindow = m_appWindow; + m_appWindow = nullptr; + dispatch_sync(dispatch_get_main_queue(), ^{ + [oldAppWindow setContentView:nil]; + }); + } + + return true; +} + +bool CWinSystemOSX::DestroyWindow() +{ + return true; +} + +bool CWinSystemOSX::Minimize() +{ + @autoreleasepool + { + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication miniaturizeAll:nil]; + }); + } + return true; +} + +bool CWinSystemOSX::Restore() +{ + @autoreleasepool + { + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication unhide:nil]; + }); + } + return true; +} + +bool CWinSystemOSX::Show(bool raise) +{ + @autoreleasepool + { + auto app = NSApplication.sharedApplication; + if (raise) + { + [app unhide:nil]; + [app activateIgnoringOtherApps:YES]; + [app arrangeInFront:nil]; + } + else + { + [app unhideWithoutActivation]; + } + } + return true; +} + +bool CWinSystemOSX::Hide() +{ + @autoreleasepool + { + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication hide:nil]; + }); + } + return true; +} + +NSRect CWinSystemOSX::GetWindowDimensions() +{ + if (m_appWindow) + { + NSWindow* win = (NSWindow*)m_appWindow; + NSRect frame = win.contentView.frame; + return frame; + } +} + +#pragma mark - Resize Window + +bool CWinSystemOSX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + if (!m_appWindow) + return false; + + [(OSXGLWindow*)m_appWindow setResizeState:true]; + + __block OSXGLView* view; + dispatch_sync(dispatch_get_main_queue(), ^{ + view = m_appWindow.contentView; + }); + + if (view) + { + // It seems, that in macOS 10.15 this defaults to YES, but we currently do not support + // Retina resolutions properly. Ensure that the view uses a 1 pixel per point framebuffer. + dispatch_sync(dispatch_get_main_queue(), ^{ + view.wantsBestResolutionOpenGLSurface = NO; + }); + } + + if (newWidth < 0) + { + newWidth = [(NSWindow*)m_appWindow minSize].width; + } + + if (newHeight < 0) + { + newHeight = [(NSWindow*)m_appWindow minSize].height; + } + + if (view) + { + dispatch_sync(dispatch_get_main_queue(), ^{ + NSOpenGLContext* context = [view getGLContext]; + NSWindow* window = m_appWindow; + + NSRect pos = window.frame; + + NSRect myNewContentFrame = NSMakeRect(pos.origin.x, pos.origin.y, newWidth, newHeight); + NSRect myNewWindowRect = [window frameRectForContentRect:myNewContentFrame]; + [window setFrame:myNewWindowRect display:TRUE]; + + [context update]; + }); + } + + m_nWidth = newWidth; + m_nHeight = newHeight; + + [(OSXGLWindow*)m_appWindow setResizeState:false]; + + return true; +} + +bool CWinSystemOSX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // if (m_lastDisplayNr == -1) + // m_lastDisplayNr = res.iScreen; + + __block NSWindow* window = m_appWindow; + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + m_lastDisplayNr = GetDisplayIndex(settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + + //handle resolution/refreshrate switching early here + if (m_bFullScreen) + { + // switch videomode + SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate)); + // hide the OS mouse + [NSCursor hide]; + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + [window setAllowsConcurrentViewDrawing:NO]; + }); + + if (m_fullscreenWillToggle) + { + ResizeWindow(m_nWidth, m_nHeight, -1, -1); + m_fullscreenWillToggle = false; + return true; + } + + if (m_bFullScreen) + { + // This is Cocoa Windowed FullScreen Mode + // Get the screen rect of our current display + NSScreen* pScreen = [NSScreen.screens objectAtIndex:m_lastDisplayNr]; + + // remove frame origin offset of original display + pScreen.frame.origin = NSZeroPoint; + + dispatch_sync(dispatch_get_main_queue(), ^{ + [window.contentView setFrameSize:NSMakeSize(m_nWidth, m_nHeight)]; + window.title = @""; + [window setAllowsConcurrentViewDrawing:YES]; + }); + + // Blank other displays if requested. + if (blankOtherDisplays) + BlankOtherDisplays(m_lastDisplayNr); + } + else + { + // Show menubar. + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication setPresentationOptions:NSApplicationPresentationDefault]; + }); + + // Unblank. + // Force the unblank when returning from fullscreen, we get called with blankOtherDisplays set false. + //if (blankOtherDisplays) + UnblankDisplays(); + } + + //DisplayFadeFromBlack(fade_token, needtoshowme); + + m_fullscreenWillToggle = true; + // toggle cocoa fullscreen mode + if ([m_appWindow respondsToSelector:@selector(toggleFullScreen:)]) + [m_appWindow performSelectorOnMainThread:@selector(toggleFullScreen:) + withObject:nil + waitUntilDone:YES]; + + ResizeWindow(m_nWidth, m_nHeight, -1, -1); + + return true; +} + +#pragma mark - Resolution + +void CWinSystemOSX::UpdateResolutions() +{ + CWinSystemBase::UpdateResolutions(); + + // Add desktop resolution + int w; + int h; + double fps; + + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOSCREEN_MONITOR)); + GetScreenResolution(&w, &h, &fps, dispIdx); + NSString* dispName = screenNameForDisplay(GetDisplayID(dispIdx)); + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), + dispName.UTF8String, w, h, fps, 0); + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + // now just fill in the possible resolutions for the attached screens + // and push to the resolution info vector + FillInVideoModes(); + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +void CWinSystemOSX::GetScreenResolution(int* w, int* h, double* fps, int screenIdx) +{ + CGDirectDisplayID display_id = (CGDirectDisplayID)GetDisplayID(screenIdx); + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id); + *w = CGDisplayModeGetWidth(mode); + *h = CGDisplayModeGetHeight(mode); + *fps = CGDisplayModeGetRefreshRate(mode); + CGDisplayModeRelease(mode); + if (static_cast<int>(*fps) == 0) + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + *fps = 60.0; + } +} + +#pragma mark - Video Modes + +bool CWinSystemOSX::SwitchToVideoMode(int width, int height, double refreshrate) +{ + CGDisplayModeRef dispMode = nullptr; + + int screenIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + // Figure out the screen size. (default to main screen) + CGDirectDisplayID display_id = GetDisplayID(screenIdx); + + // find mode that matches the desired size, refreshrate + // non interlaced, nonstretched, safe for hardware + dispMode = GetMode(width, height, refreshrate, screenIdx); + + //not found - fallback to bestemdeforparameters + if (!dispMode) + { + dispMode = BestMatchForMode(display_id, 32, width, height); + + if (!dispMode) + { + dispMode = BestMatchForMode(display_id, 16, width, height); + + // still no match? fallback to current resolution of the display which HAS to work [tm] + if (!dispMode) + { + int currentWidth; + int currentHeight; + double currentRefresh; + + GetScreenResolution(¤tWidth, ¤tHeight, ¤tRefresh, screenIdx); + dispMode = GetMode(currentWidth, currentHeight, currentRefresh, screenIdx); + + // no way to get a resolution set + if (!dispMode) + return false; + } + } + } + // switch mode and return success + CGDisplayCapture(display_id); + CGDisplayConfigRef cfg; + CGBeginDisplayConfiguration(&cfg); + CGConfigureDisplayWithDisplayMode(cfg, display_id, dispMode, nullptr); + CGError err = CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly); + CGDisplayRelease(display_id); + + m_refreshRate = CGDisplayModeGetRefreshRate(dispMode); + + Cocoa_CVDisplayLinkUpdate(); + + return (err == kCGErrorSuccess); +} + +void CWinSystemOSX::FillInVideoModes() +{ + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + for (int disp = 0; disp < static_cast<int>(NSScreen.screens.count); disp++) + { + bool stretched; + bool interlaced; + bool safeForHardware; + int w, h, bitsperpixel; + double refreshrate; + RESOLUTION_INFO res; + + CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(disp)); + NSString* dispName = screenNameForDisplay(GetDisplayID(disp)); + + CLog::LogF(LOGINFO, "Display {} has name {}", disp, [dispName UTF8String]); + + if (!displayModes) + continue; + + for (int i = 0; i < CFArrayGetCount(displayModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i); + + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = (flags & kDisplayModeStretchedFlag) != 0; + interlaced = (flags & kDisplayModeInterlacedFlag) != 0; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = (flags & kDisplayModeSafetyFlags) != 0; + + if ((bitsperpixel == 32) && (safeForHardware == true) && (stretched == false) && + (interlaced == false)) + { + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + refreshrate = CGDisplayModeGetRefreshRate(displayMode); + if (static_cast<int>(refreshrate) == 0) // LCD display? + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + refreshrate = 60.0; + } + CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} @ {} Hz", disp, w, + h, refreshrate); + + // only add the resolution if it belongs to "our" screen + // all others are only logged above... + if (disp == dispIdx) + { + UpdateDesktopResolution(res, (dispName != nil) ? [dispName UTF8String] : "Unknown", w, h, + refreshrate, 0); + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } + } + } + CFRelease(displayModes); + } +} + +#pragma mark - Occlusion + +bool CWinSystemOSX::IsObscured() +{ + if (m_obscured) + CLog::LogF(LOGDEBUG, "Obscured"); + return m_obscured; +} + +void CWinSystemOSX::SetOcclusionState(bool occluded) +{ + // m_obscured = occluded; + // CLog::LogF(LOGDEBUG, "{}", occluded ? "true":"false"); +} + +void CWinSystemOSX::NotifyAppFocusChange(bool bGaining) +{ + if (!(m_bFullScreen && bGaining)) + return; + @autoreleasepool + { + // find the window + NSOpenGLContext* context = NSOpenGLContext.currentContext; + if (context) + { + NSView* view; + + view = context.view; + if (view) + { + NSWindow* window; + window = view.window; + if (window) + { + [window orderFront:nil]; + } + } + } + } +} + +#pragma mark - Window Move + +void CWinSystemOSX::OnMove(int x, int y) +{ + static double oldRefreshRate = m_refreshRate; + Cocoa_CVDisplayLinkUpdate(); + + int dummy = 0; + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + if (oldRefreshRate != m_refreshRate) + { + oldRefreshRate = m_refreshRate; + + // send a message so that videoresolution (and refreshrate) is changed + NSWindow* win = m_appWindow; + NSRect frame = win.contentView.frame; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VIDEORESIZE, frame.size.width, + frame.size.height); + } +} + +void CWinSystemOSX::WindowChangedScreen() +{ + // user has moved the window to a + // different screen + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + m_lastDisplayNr = -1; + + // if we are here the user dragged the window to a different + // screen and we return the screen of the window + if (context) + { + NSView* view; + + view = context.view; + if (view) + { + NSWindow* window; + window = view.window; + if (window) + { + m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen(window.screen)); + } + } + } + if (m_lastDisplayNr == -1) + m_lastDisplayNr = 0; // default to main screen +} + +CGLContextObj CWinSystemOSX::GetCGLContextObj() +{ + __block CGLContextObj cglcontex = nullptr; + if (m_appWindow) + { + dispatch_sync(dispatch_get_main_queue(), ^{ + OSXGLView* contentView = m_appWindow.contentView; + cglcontex = contentView.getGLContext.CGLContextObj; + }); + } + + return cglcontex; +} + +bool CWinSystemOSX::FlushBuffer() +{ + if (m_appWindow) + { + dispatch_sync(dispatch_get_main_queue(), ^{ + OSXGLView* contentView = m_appWindow.contentView; + NSOpenGLContext* glcontex = contentView.getGLContext; + [glcontex flushBuffer]; + }); + } + + return true; +} + +#pragma mark - Vsync + +void CWinSystemOSX::EnableVSync(bool enable) +{ + // OpenGL Flush synchronised with vertical retrace + GLint swapInterval = enable ? 1 : 0; + [NSOpenGLContext.currentContext setValues:&swapInterval + forParameter:NSOpenGLContextParameterSwapInterval]; +} + +std::unique_ptr<CVideoSync> CWinSystemOSX::GetVideoSync(void* clock) +{ + return std::make_unique<CVideoSyncOsx>(clock); +} + +std::vector<std::string> CWinSystemOSX::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + outputs.push_back("Default"); + + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString* dispName = screenNameForDisplay(GetDisplayID(disp)); + outputs.push_back(dispName.UTF8String); + } + + return outputs; +} + +#pragma mark - OSScreenSaver + +std::unique_ptr<IOSScreenSaver> CWinSystemOSX::GetOSScreenSaverImpl() +{ + return std::make_unique<COSScreenSaverOSX>(); +} + +#pragma mark - Input + +bool CWinSystemOSX::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +void CWinSystemOSX::enableInputEvents() +{ + m_winEvents->enableInputEvents(); +} + +void CWinSystemOSX::disableInputEvents() +{ + m_winEvents->disableInputEvents(); +} + +std::string CWinSystemOSX::GetClipboardText() +{ + std::string utf8_text; + + const char* szStr = Cocoa_Paste(); + if (szStr) + utf8_text = szStr; + + return utf8_text; +} + +void CWinSystemOSX::ShowOSMouse(bool show) +{ +} + +#pragma mark - Unused + +CGDisplayFadeReservationToken DisplayFadeToBlack(bool fade) +{ + // Fade to black to hide resolution-switching flicker and garbage. + CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken; + if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess && fade) + CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, + TRUE); + + return (fade_token); +} + +void DisplayFadeFromBlack(CGDisplayFadeReservationToken fade_token, bool fade) +{ + if (fade_token != kCGDisplayFadeReservationInvalidToken) + { + if (fade) + CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, + 0.0, FALSE); + CGReleaseDisplayFadeReservation(fade_token); + } +} |