From 5da0f5c7fac8de09cee6233963ba2b2e7dd07d70 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sun, 5 Jan 2020 21:40:18 +0100 Subject: [PATCH] - added the Posix backend code. Nothing is hooked up yet and adjusted - next thing to do. --- source/platform/macos/SDLMain.mm | 373 - source/platform/macos/osxbits.h | 16 - source/platform/macos/osxbits.mm | 179 - source/platform/macos/osxmain.h | 11 - source/platform/posix/cocoa/gl_sysfb.h | 102 + source/platform/posix/cocoa/i_common.h | 61 + source/platform/posix/cocoa/i_input.mm | 804 ++ source/platform/posix/cocoa/i_joystick.cpp | 1244 +++ source/platform/posix/cocoa/i_main.mm | 519 + source/platform/posix/cocoa/i_system.mm | 304 + source/platform/posix/cocoa/i_video.mm | 1072 ++ source/platform/posix/cocoa/st_console.h | 96 + source/platform/posix/cocoa/st_console.mm | 532 + source/platform/posix/cocoa/st_start.mm | 176 + source/platform/posix/dikeys.h | 155 + source/platform/posix/hardware.h | 40 + source/platform/posix/i_steam.cpp | 227 + source/platform/posix/i_system.h | 116 + .../{macos => posix/osx}/i_specialpaths.mm | 0 source/platform/posix/osx/iwadpicker_cocoa.mm | 470 + source/platform/posix/osx/zdoom-info.plist | 52 + source/platform/posix/osx/zdoom.icns | Bin 0 -> 120245 bytes source/platform/posix/readme.md | 6 + source/platform/posix/sdl/crashcatcher.c | 425 + source/platform/posix/sdl/gl_sysfb.h | 49 + source/platform/posix/sdl/hardware.cpp | 85 + source/platform/posix/sdl/i_gui.cpp | 87 + source/platform/posix/sdl/i_input.cpp | 568 ++ .../{unix => posix/sdl}/i_joystick.cpp | 2 +- source/platform/posix/sdl/i_main.cpp | 211 + source/platform/posix/sdl/i_system.cpp | 418 + source/platform/posix/sdl/i_system.mm | 19 + source/platform/posix/sdl/sdlglvideo.cpp | 760 ++ source/platform/posix/sdl/st_start.cpp | 331 + source/platform/posix/unix/gtk_dialogs.cpp | 444 + .../{ => posix}/unix/i_specialpaths.cpp | 0 source/platform/posix/zdoom.xpm | 8613 +++++++++++++++++ source/platform/unix/i_input.cpp | 723 -- 38 files changed, 17987 insertions(+), 1303 deletions(-) delete mode 100644 source/platform/macos/SDLMain.mm delete mode 100644 source/platform/macos/osxbits.h delete mode 100644 source/platform/macos/osxbits.mm delete mode 100644 source/platform/macos/osxmain.h create mode 100644 source/platform/posix/cocoa/gl_sysfb.h create mode 100644 source/platform/posix/cocoa/i_common.h create mode 100644 source/platform/posix/cocoa/i_input.mm create mode 100644 source/platform/posix/cocoa/i_joystick.cpp create mode 100644 source/platform/posix/cocoa/i_main.mm create mode 100644 source/platform/posix/cocoa/i_system.mm create mode 100644 source/platform/posix/cocoa/i_video.mm create mode 100644 source/platform/posix/cocoa/st_console.h create mode 100644 source/platform/posix/cocoa/st_console.mm create mode 100644 source/platform/posix/cocoa/st_start.mm create mode 100644 source/platform/posix/dikeys.h create mode 100644 source/platform/posix/hardware.h create mode 100644 source/platform/posix/i_steam.cpp create mode 100644 source/platform/posix/i_system.h rename source/platform/{macos => posix/osx}/i_specialpaths.mm (100%) create mode 100644 source/platform/posix/osx/iwadpicker_cocoa.mm create mode 100644 source/platform/posix/osx/zdoom-info.plist create mode 100644 source/platform/posix/osx/zdoom.icns create mode 100644 source/platform/posix/readme.md create mode 100644 source/platform/posix/sdl/crashcatcher.c create mode 100644 source/platform/posix/sdl/gl_sysfb.h create mode 100644 source/platform/posix/sdl/hardware.cpp create mode 100644 source/platform/posix/sdl/i_gui.cpp create mode 100644 source/platform/posix/sdl/i_input.cpp rename source/platform/{unix => posix/sdl}/i_joystick.cpp (99%) create mode 100644 source/platform/posix/sdl/i_main.cpp create mode 100644 source/platform/posix/sdl/i_system.cpp create mode 100644 source/platform/posix/sdl/i_system.mm create mode 100644 source/platform/posix/sdl/sdlglvideo.cpp create mode 100644 source/platform/posix/sdl/st_start.cpp create mode 100644 source/platform/posix/unix/gtk_dialogs.cpp rename source/platform/{ => posix}/unix/i_specialpaths.cpp (100%) create mode 100644 source/platform/posix/zdoom.xpm delete mode 100644 source/platform/unix/i_input.cpp diff --git a/source/platform/macos/SDLMain.mm b/source/platform/macos/SDLMain.mm deleted file mode 100644 index 280b1a869..000000000 --- a/source/platform/macos/SDLMain.mm +++ /dev/null @@ -1,373 +0,0 @@ -/* SDLMain.m - main entry point for our Cocoa-ized SDL app - Initial Version: Darrell Walisser - Non-NIB-Code & other changes: Max Horn - - Feel free to customize this file to suit your needs - - Modified for EDuke32 -*/ - -#import -#include - -#if TARGET_OS_MAC - -#include "compat.h" - -#import - -#include "sdl_inc.h" - -#import - -#include /* for MAXPATHLEN */ - -/* For some reason, Apple removed setAppleMenu from the headers in 10.4, - but the method still is there and works. To avoid warnings, we declare - it ourselves here. */ -@interface NSApplication(SDL_Missing_Methods) -- (void)setAppleMenu:(NSMenu *)menu; -@end - - -/* Use this flag to determine whether we use CPS (docking) or not */ -#define SDL_USE_CPS -#ifdef SDL_USE_CPS - -#ifdef __cplusplus -extern "C" { -#endif - -/* Portions of CPS.h */ -typedef struct CPSProcessSerNum -{ - UInt32 lo; - UInt32 hi; -} CPSProcessSerNum; - -extern OSErr CPSGetCurrentProcess( CPSProcessSerNum *psn); -extern OSErr CPSEnableForegroundOperation( CPSProcessSerNum *psn, UInt32 _arg2, UInt32 _arg3, UInt32 _arg4, UInt32 _arg5); -extern OSErr CPSSetFrontProcess( CPSProcessSerNum *psn); - -#ifdef __cplusplus -} -#endif - -#endif /* SDL_USE_CPS */ - -static int gArgc; -static char **gArgv; -static BOOL gFinderLaunch; -static BOOL gCalledAppMainline = FALSE; - -static id nsapp; - -static NSString *getApplicationName(void) -{ - const NSDictionary *dict; - NSString *appName = 0; - - /* Determine the application name */ - dict = (const NSDictionary *)CFBundleGetInfoDictionary(CFBundleGetMainBundle()); - if (dict) - appName = [dict objectForKey: @"CFBundleName"]; - - if (![appName length]) - appName = [[NSProcessInfo processInfo] processName]; - - return appName; -} - -@interface NSApplication (SDLApplication) -@end - -@implementation NSApplication (SDLApplication) -/* Invoked from the Quit menu item */ -- (void)terminateCall:(id)sender -{ - /* Post a SDL_QUIT event */ - SDL_Event event; - event.type = SDL_QUIT; - SDL_PushEvent(&event); - - UNREFERENCED_PARAMETER(sender); -} -@end - -@interface SDLMain : NSObject -@end - -/* The main class of the application, the application's delegate */ -@implementation SDLMain - -/* Set the working directory to the .app's parent directory */ -- (void) setupWorkingDirectory:(BOOL)shouldChdir -{ - if (shouldChdir) - { - char parentdir[MAXPATHLEN]; - CFURLRef url = CFBundleCopyBundleURL(CFBundleGetMainBundle()); - CFURLRef url2 = CFURLCreateCopyDeletingLastPathComponent(0, url); - if (CFURLGetFileSystemRepresentation(url2, 1, (UInt8 *)parentdir, MAXPATHLEN)) { - chdir(parentdir); /* chdir to the binary app's parent */ - } - CFRelease(url); - CFRelease(url2); - } -} - -static void setApplicationMenu(void) -{ - /* warning: this code is very odd */ - NSMenu *appleMenu; - NSMenuItem *menuItem; - NSString *title; - NSString *appName; - - appName = getApplicationName(); - appleMenu = [[NSMenu alloc] initWithTitle:@""]; - - /* Add menu items */ - title = [@"About " stringByAppendingString:appName]; - [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; - - [appleMenu addItem:[NSMenuItem separatorItem]]; - - title = [@"Hide " stringByAppendingString:appName]; - [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; - - menuItem = (NSMenuItem *)[appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; - [menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask|NSCommandKeyMask)]; - - [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; - - [appleMenu addItem:[NSMenuItem separatorItem]]; - - title = [@"Quit " stringByAppendingString:appName]; - [appleMenu addItemWithTitle:title action:@selector(terminateCall:) keyEquivalent:@"q"]; - - - /* Put menu into the menubar */ - menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; - [menuItem setSubmenu:appleMenu]; - [[nsapp mainMenu] addItem:menuItem]; - - /* Tell the application object that this is now the application menu */ - [nsapp setAppleMenu:appleMenu]; - - /* Finally give up our references to the objects */ - [appleMenu release]; - [menuItem release]; -} - -/* Create a window menu */ -static void setupWindowMenu(void) -{ - NSMenu *windowMenu; - NSMenuItem *windowMenuItem; - NSMenuItem *menuItem; - - windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; - - /* "Minimize" item */ - menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; - [windowMenu addItem:menuItem]; - [menuItem release]; - - /* Put menu into the menubar */ - windowMenuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; - [windowMenuItem setSubmenu:windowMenu]; - [[nsapp mainMenu] addItem:windowMenuItem]; - - /* Tell the application object that this is now the window menu */ - [nsapp setWindowsMenu:windowMenu]; - - /* Finally give up our references to the objects */ - [windowMenu release]; - [windowMenuItem release]; -} - -/* Replacement for NSApplicationMain */ -static void CustomApplicationMain (int argc, char **argv) -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - SDLMain *sdlMain; - - /* Ensure the application object is initialised */ - nsapp = [NSApplication sharedApplication]; - -#ifdef SDL_USE_CPS - { - CPSProcessSerNum PSN; - /* Tell the dock about us */ - if (!CPSGetCurrentProcess(&PSN)) - if (!CPSEnableForegroundOperation(&PSN,0x03,0x3C,0x2C,0x1103)) - if (!CPSSetFrontProcess(&PSN)) - [NSApplication sharedApplication]; - } -#endif /* SDL_USE_CPS */ - - /* Set up the menubar */ - [nsapp setMainMenu:[[NSMenu alloc] init]]; - setApplicationMenu(); - setupWindowMenu(); - - /* Create SDLMain and make it the app delegate */ - sdlMain = [[SDLMain alloc] init]; - [nsapp setDelegate:sdlMain]; - - /* Start the main event loop */ - [nsapp run]; - - [sdlMain release]; - [pool release]; - - UNREFERENCED_PARAMETER(argc); - UNREFERENCED_PARAMETER(argv); -} - - -/* - * Catch document open requests...this lets us notice files when the app - * was launched by double-clicking a document, or when a document was - * dragged/dropped on the app's icon. You need to have a - * CFBundleDocumentsType section in your Info.plist to get this message, - * apparently. - * - * Files are added to gArgv, so to the app, they'll look like command line - * arguments. Previously, apps launched from the finder had nothing but - * an argv[0]. - * - * This message may be received multiple times to open several docs on launch. - * - * This message is ignored once the app's mainline has been called. - */ -- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename -{ - const char *temparg; - size_t arglen; - char *arg; - char **newargv; - - if (!gFinderLaunch) /* MacOS is passing command line args. */ - return FALSE; - - if (gCalledAppMainline) /* app has started, ignore this document. */ - return FALSE; - - temparg = [filename UTF8String]; - arglen = SDL_strlen(temparg) + 1; - arg = (char *) SDL_malloc(arglen); - if (arg == NULL) - return FALSE; - - newargv = (char **) realloc(gArgv, sizeof (char *) * (gArgc + 2)); - if (newargv == NULL) - { - SDL_free(arg); - return FALSE; - } - gArgv = newargv; - - SDL_strlcpy(arg, temparg, arglen); - gArgv[gArgc++] = arg; - gArgv[gArgc] = NULL; - - UNREFERENCED_PARAMETER(theApplication); - - return TRUE; -} - - -/* Called when the internal event loop has just started running */ -- (void) applicationDidFinishLaunching: (NSNotification *) note -{ - int status; - - /* Set the working directory to the .app's parent directory */ - [self setupWorkingDirectory:gFinderLaunch]; - - /* Hand off to main application code */ - gCalledAppMainline = TRUE; - status = SDL_main (gArgc, gArgv); - - UNREFERENCED_PARAMETER(note); - - /* We're done, thank you for playing */ - exit(status); -} -@end - - -@implementation NSString (ReplaceSubString) - -- (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString -{ - unsigned int bufferSize; - unsigned int selfLen = [self length]; - unsigned int aStringLen = [aString length]; - unichar *buffer; - NSRange localRange; - NSString *result; - - bufferSize = selfLen + aStringLen - aRange.length; - buffer = (unichar *)NSAllocateMemoryPages(bufferSize*sizeof(unichar)); - - /* Get first part into buffer */ - localRange.location = 0; - localRange.length = aRange.location; - [self getCharacters:buffer range:localRange]; - - /* Get middle part into buffer */ - localRange.location = 0; - localRange.length = aStringLen; - [aString getCharacters:(buffer+aRange.location) range:localRange]; - - /* Get last part into buffer */ - localRange.location = aRange.location + aRange.length; - localRange.length = selfLen - localRange.location; - [self getCharacters:(buffer+aRange.location+aStringLen) range:localRange]; - - /* Build output string */ - result = [NSString stringWithCharacters:buffer length:bufferSize]; - - NSDeallocateMemoryPages(buffer, bufferSize); - - return result; -} - -@end - - - -#ifdef main -# undef main -#endif - - -/* Main entry point to executable - should *not* be SDL_main! */ -int main (int argc, char **argv) -{ - /* Copy the arguments into a global variable */ - /* This is passed if we are launched by double-clicking */ - if ( argc >= 2 && strncmp (argv[1], "-psn", 4) == 0 ) { - gArgv = (char **) SDL_malloc(sizeof (char *) * 2); - gArgv[0] = argv[0]; - gArgv[1] = NULL; - gArgc = 1; - gFinderLaunch = YES; - } else { - int i; - gArgc = argc; - gArgv = (char **) SDL_malloc(sizeof (char *) * (argc+1)); - for (i = 0; i <= argc; i++) - gArgv[i] = argv[i]; - gFinderLaunch = NO; - } - - CustomApplicationMain (argc, argv); - - return 0; -} - -#endif diff --git a/source/platform/macos/osxbits.h b/source/platform/macos/osxbits.h deleted file mode 100644 index 639e48869..000000000 --- a/source/platform/macos/osxbits.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef osxbits_h_ -#define osxbits_h_ -#include - -void osx_preopen(void); -void osx_postopen(void); - -int osx_msgbox(const char *name, const char *msg); -int osx_ynbox(const char *name, const char *msg); - -char *osx_gethomedir(void); -char *osx_getsupportdir(int32_t local); -char *osx_getappdir(void); -char *osx_getapplicationsdir(int32_t local); - -#endif diff --git a/source/platform/macos/osxbits.mm b/source/platform/macos/osxbits.mm deleted file mode 100644 index 16f04ec01..000000000 --- a/source/platform/macos/osxbits.mm +++ /dev/null @@ -1,179 +0,0 @@ -#include "compat.h" -#include "osxbits.h" -#import -#import - -#ifndef MAC_OS_X_VERSION_10_5 -# define NSImageScaleNone NSScaleNone -#endif - -#ifndef MAC_OS_X_VERSION_10_12 -# define NSEventModifierFlagOption NSAlternateKeyMask -# define NSEventModifierFlagCommand NSCommandKeyMask -# define NSEventMaskAny NSAnyEventMask -# define NSWindowStyleMaskTitled NSTitledWindowMask -# define NSWindowStyleMaskClosable NSClosableWindowMask -# define NSWindowStyleMaskMiniaturizable NSMiniaturizableWindowMask -# define NSWindowStyleMaskResizable NSResizableWindowMask -# define NSAlertStyleInformational NSInformationalAlertStyle -# define NSControlSizeSmall NSSmallControlSize -#endif - -#ifndef MAC_OS_VERSION_10_3 -# define MAC_OS_VERSION_10_3 1030 -#endif - -id nsapp; - -void osx_preopen(void) -{ - // fix for "ld: absolute address to symbol _NSApp in a different linkage unit not supported" - // (OS X 10.6) when building for PPC - nsapp = [NSApplication sharedApplication]; -} - -void osx_postopen(void) -{ - [nsapp finishLaunching]; -} - -int osx_msgbox(const char *name, const char *msg) -{ - NSString *mmsg = [[NSString alloc] initWithUTF8String:msg]; - -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_3 - NSAlert *alert = [[NSAlert alloc] init]; - [alert addButtonWithTitle: @"OK"]; - [alert setInformativeText: mmsg]; - [alert setAlertStyle: NSAlertStyleInformational]; - - [alert runModal]; - - [alert release]; - -#else - NSRunAlertPanel(nil, mmsg, @"OK", nil, nil); -#endif - - [mmsg release]; - - UNREFERENCED_PARAMETER(name); - - return 0; -} - -int osx_ynbox(const char *name, const char *msg) -{ - NSString *mmsg = [[NSString alloc] initWithUTF8String:msg]; - int r; - -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_3 - NSAlert *alert = [[NSAlert alloc] init]; - - [alert addButtonWithTitle:@"Yes"]; - [alert addButtonWithTitle:@"No"]; - [alert setInformativeText: mmsg]; - [alert setAlertStyle: NSAlertStyleInformational]; - - r = ([alert runModal] == NSAlertFirstButtonReturn); - - [alert release]; -#else - r = (NSRunAlertPanel(nil, mmsg, @"Yes", @"No", nil) == NSAlertDefaultReturn); -#endif - - [mmsg release]; - - UNREFERENCED_PARAMETER(name); - - return r; -} - -char *osx_gethomedir(void) -{ - NSString *path = NSHomeDirectory(); - const char *Cpath = [path UTF8String]; - char *returnpath = NULL; - - if (Cpath) - returnpath = Bstrdup(Cpath); - - [path release]; - - return returnpath; -} - -char *osx_getsupportdir(int32_t local) -{ - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, local ? NSUserDomainMask : NSLocalDomainMask, YES); - char *returnpath = NULL; - - if ([paths count] > 0) - { - const char *Cpath = [[paths objectAtIndex:0] UTF8String]; - - if (Cpath) - returnpath = Bstrdup(Cpath); - } - - [paths release]; - - return returnpath; -} - -char *osx_getappdir(void) -{ - CFBundleRef mainBundle; - CFURLRef resUrl, fullUrl; - CFStringRef str; - const char *s; - char *dir = NULL; - - mainBundle = CFBundleGetMainBundle(); - if (!mainBundle) { - return NULL; - } - - resUrl = CFBundleCopyResourcesDirectoryURL(mainBundle); - CFRelease(mainBundle); - if (!resUrl) { - return NULL; - } - fullUrl = CFURLCopyAbsoluteURL(resUrl); - if (fullUrl) { - CFRelease(resUrl); - resUrl = fullUrl; - } - - str = CFURLCopyFileSystemPath(resUrl, kCFURLPOSIXPathStyle); - CFRelease(resUrl); - if (!str) { - return NULL; - } - - s = CFStringGetCStringPtr(str, CFStringGetSystemEncoding()); - if (s) { - dir = strdup(s); - } - CFRelease(str); - - return dir; -} - -char *osx_getapplicationsdir(int32_t local) -{ - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, local ? NSUserDomainMask : NSLocalDomainMask, YES); - char *returnpath = NULL; - - if ([paths count] > 0) - { - const char *Cpath = [[paths objectAtIndex:0] UTF8String]; - - if (Cpath) - returnpath = Bstrdup(Cpath); - } - - [paths release]; - - return returnpath; -} diff --git a/source/platform/macos/osxmain.h b/source/platform/macos/osxmain.h deleted file mode 100644 index 4683df57a..000000000 --- a/source/platform/macos/osxmain.h +++ /dev/null @@ -1,11 +0,0 @@ -/* SDLMain.m - main entry point for our Cocoa-ized SDL app - Initial Version: Darrell Walisser - Non-NIB-Code & other changes: Max Horn - - Feel free to customize this file to suit your needs -*/ - -#import - -@interface SDLMain : NSObject -@end diff --git a/source/platform/posix/cocoa/gl_sysfb.h b/source/platform/posix/cocoa/gl_sysfb.h new file mode 100644 index 000000000..144c3cb44 --- /dev/null +++ b/source/platform/posix/cocoa/gl_sysfb.h @@ -0,0 +1,102 @@ +/* + ** gl_sysfb.h + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2018 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#ifndef COCOA_GL_SYSFB_H_INCLUDED +#define COCOA_GL_SYSFB_H_INCLUDED + +#include "v_video.h" + +#ifdef __OBJC__ +@class NSCursor; +@class CocoaWindow; +#else +typedef struct objc_object NSCursor; +typedef struct objc_object CocoaWindow; +#endif + +class SystemBaseFrameBuffer : public DFrameBuffer +{ +public: + // This must have the same parameters as the Windows version, even if they are not used! + SystemBaseFrameBuffer(void *hMonitor, bool fullscreen); + ~SystemBaseFrameBuffer(); + + bool IsFullscreen() override; + + int GetClientWidth() override; + int GetClientHeight() override; + void ToggleFullscreen(bool yes) override; + void SetWindowSize(int width, int height) override; + + virtual void SetMode(bool fullscreen, bool hiDPI); + + static void UseHiDPI(bool hiDPI); + static void SetCursor(NSCursor* cursor); + static void SetWindowVisible(bool visible); + static void SetWindowTitle(const char* title); + + void SetWindow(CocoaWindow* window) { m_window = window; } + +protected: + SystemBaseFrameBuffer() {} + + void SetFullscreenMode(); + void SetWindowedMode(); + + bool m_fullscreen; + bool m_hiDPI; + + CocoaWindow* m_window; + + int GetTitleBarHeight() const; + +}; + +class SystemGLFrameBuffer : public SystemBaseFrameBuffer +{ + typedef SystemBaseFrameBuffer Super; + +public: + SystemGLFrameBuffer(void *hMonitor, bool fullscreen); + + void SetVSync(bool vsync) override; + + void SetMode(bool fullscreen, bool hiDPI) override; + +protected: + void SwapBuffers(); + + SystemGLFrameBuffer() {} +}; + +#endif // COCOA_GL_SYSFB_H_INCLUDED diff --git a/source/platform/posix/cocoa/i_common.h b/source/platform/posix/cocoa/i_common.h new file mode 100644 index 000000000..f60d82ced --- /dev/null +++ b/source/platform/posix/cocoa/i_common.h @@ -0,0 +1,61 @@ +/* + ** i_common.h + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2015 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#ifndef COCOA_I_COMMON_INCLUDED +#define COCOA_I_COMMON_INCLUDED + +#import + + +// Version of AppKit framework we are interested in +// The following values are needed to build with earlier SDKs + +#define AppKit10_7 1138 +#define AppKit10_8 1187 +#define AppKit10_9 1265 + + +@interface NSWindow(ExitAppOnClose) +- (void)exitAppOnClose; +@end + + +void I_ProcessEvent(NSEvent* event); + +void I_ProcessJoysticks(); + +NSSize I_GetContentViewSize(const NSWindow* window); +void I_SetMainWindowVisible(bool visible); +void I_SetNativeMouse(bool wantNative); + +#endif // COCOA_I_COMMON_INCLUDED diff --git a/source/platform/posix/cocoa/i_input.mm b/source/platform/posix/cocoa/i_input.mm new file mode 100644 index 000000000..261f5e98b --- /dev/null +++ b/source/platform/posix/cocoa/i_input.mm @@ -0,0 +1,804 @@ +/* + ** i_input.mm + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2015 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include "i_common.h" + +#import + +#include "c_console.h" +#include "c_cvars.h" +#include "c_dispatch.h" +#include "d_event.h" +#include "d_gui.h" +#include "dikeys.h" +#include "doomdef.h" +#include "doomstat.h" +#include "v_video.h" +#include "events.h" +#include "g_game.h" +#include "g_levellocals.h" + + +EXTERN_CVAR(Int, m_use_mouse) + +CVAR(Bool, use_mouse, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CVAR(Bool, m_noprescale, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CVAR(Bool, m_filter, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) + +CVAR(Bool, k_allowfullscreentoggle, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) + +CUSTOM_CVAR(Int, mouse_capturemode, 1, CVAR_GLOBALCONFIG | CVAR_ARCHIVE) +{ + if (self < 0) + { + self = 0; + } + else if (self > 2) + { + self = 2; + } +} + + +extern int paused, chatmodeon; +extern constate_e ConsoleState; + +bool GUICapture; + + +namespace +{ + +// TODO: remove this magic! +size_t s_skipMouseMoves; + + +// --------------------------------------------------------------------------- + + +void CheckGUICapture() +{ + bool wantCapture = (MENU_Off == menuactive) + ? (c_down == ConsoleState || c_falling == ConsoleState || chatmodeon) + : (MENU_On == menuactive || MENU_OnNoPause == menuactive); + + // [ZZ] check active event handlers that want the UI processing + if (!wantCapture && primaryLevel->localEventManager->CheckUiProcessors()) + { + wantCapture = true; + } + + if (wantCapture != GUICapture) + { + GUICapture = wantCapture; + + ResetButtonStates(); + } +} + +void SetCursorPosition(const NSPoint position) +{ + NSWindow* window = [NSApp keyWindow]; + if (nil == window) + { + return; + } + + const NSRect displayRect = [[window screen] frame]; + const CGPoint eventPoint = CGPointMake(position.x, displayRect.size.height - position.y); + + CGEventSourceRef eventSource = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState); + + if (NULL != eventSource) + { + CGEventRef mouseMoveEvent = CGEventCreateMouseEvent(eventSource, + kCGEventMouseMoved, eventPoint, kCGMouseButtonLeft); + + if (NULL != mouseMoveEvent) + { + CGEventPost(kCGHIDEventTap, mouseMoveEvent); + CFRelease(mouseMoveEvent); + } + + CFRelease(eventSource); + } + + // TODO: remove this magic! + s_skipMouseMoves = 2; +} + +void CenterCursor() +{ + NSWindow* window = [NSApp keyWindow]; + if (nil == window) + { + return; + } + + const NSRect displayRect = [[window screen] frame]; + const NSRect windowRect = [window frame]; + const NSPoint centerPoint = { NSMidX(windowRect), NSMidY(windowRect) }; + + SetCursorPosition(centerPoint); +} + +bool IsInGame() +{ + switch (mouse_capturemode) + { + default: + case 0: + return gamestate == GS_LEVEL; + + case 1: + return gamestate == GS_LEVEL + || gamestate == GS_INTERMISSION + || gamestate == GS_FINALE; + + case 2: + return true; + } +} + +void CheckNativeMouse() +{ + const bool windowed = (NULL == screen) || !screen->IsFullscreen(); + bool wantNative; + + if (windowed) + { + if (![NSApp isActive] || !use_mouse) + { + wantNative = true; + } + else if (MENU_WaitKey == menuactive) + { + wantNative = false; + } + else + { + wantNative = (!m_use_mouse || MENU_WaitKey != menuactive) + && (!IsInGame() || GUICapture || paused || demoplayback); + } + } + else + { + // ungrab mouse when in the menu with mouse control on. + wantNative = m_use_mouse + && (MENU_On == menuactive || MENU_OnNoPause == menuactive); + } + + if (!wantNative && primaryLevel->localEventManager->CheckRequireMouse()) + wantNative = true; + + I_SetNativeMouse(wantNative); +} + +} // unnamed namespace + + +void I_GetEvent() +{ + [[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode]; +} + +void I_StartTic() +{ + CheckGUICapture(); + CheckNativeMouse(); + + I_ProcessJoysticks(); + I_GetEvent(); +} + +void I_StartFrame() +{ +} + + +void I_SetMouseCapture() +{ +} + +void I_ReleaseMouseCapture() +{ +} + +void I_SetNativeMouse(bool wantNative) +{ + static bool nativeMouse = true; + static NSPoint mouseLocation; + + if (wantNative != nativeMouse) + { + nativeMouse = wantNative; + + if (!wantNative) + { + mouseLocation = [NSEvent mouseLocation]; + CenterCursor(); + } + + CGAssociateMouseAndMouseCursorPosition(wantNative); + + if (wantNative) + { + SetCursorPosition(mouseLocation); + + [NSCursor unhide]; + } + else + { + [NSCursor hide]; + } + } +} + + +// --------------------------------------------------------------------------- + + +namespace +{ + +const size_t KEY_COUNT = 128; + + +// See Carbon -> HIToolbox -> Events.h for kVK_ constants + +const uint8_t KEYCODE_TO_DIK[KEY_COUNT] = +{ + DIK_A, DIK_S, DIK_D, DIK_F, DIK_H, DIK_G, DIK_Z, DIK_X, // 0x00 - 0x07 + DIK_C, DIK_V, 0, DIK_B, DIK_Q, DIK_W, DIK_E, DIK_R, // 0x08 - 0x0F + DIK_Y, DIK_T, DIK_1, DIK_2, DIK_3, DIK_4, DIK_6, DIK_5, // 0x10 - 0x17 + DIK_EQUALS, DIK_9, DIK_7, DIK_MINUS, DIK_8, DIK_0, DIK_RBRACKET, DIK_O, // 0x18 - 0x1F + DIK_U, DIK_LBRACKET, DIK_I, DIK_P, DIK_RETURN, DIK_L, DIK_J, DIK_APOSTROPHE, // 0x20 - 0x27 + DIK_K, DIK_SEMICOLON, DIK_BACKSLASH, DIK_COMMA, DIK_SLASH, DIK_N, DIK_M, DIK_PERIOD, // 0x28 - 0x2F + DIK_TAB, DIK_SPACE, DIK_GRAVE, DIK_BACK, 0, DIK_ESCAPE, 0, DIK_LWIN, // 0x30 - 0x37 + DIK_LSHIFT, DIK_CAPITAL, DIK_LMENU, DIK_LCONTROL, DIK_RSHIFT, DIK_RMENU, DIK_RCONTROL, 0, // 0x38 - 0x3F + 0, DIK_DECIMAL, 0, DIK_MULTIPLY, 0, DIK_ADD, 0, 0, // 0x40 - 0x47 + DIK_VOLUMEUP, DIK_VOLUMEDOWN, DIK_MUTE, DIK_SLASH, DIK_NUMPADENTER, 0, DIK_SUBTRACT, 0, // 0x48 - 0x4F + 0, DIK_NUMPAD_EQUALS, DIK_NUMPAD0, DIK_NUMPAD1, DIK_NUMPAD2, DIK_NUMPAD3, DIK_NUMPAD4, DIK_NUMPAD5, // 0x50 - 0x57 + DIK_NUMPAD6, DIK_NUMPAD7, 0, DIK_NUMPAD8, DIK_NUMPAD9, 0, 0, 0, // 0x58 - 0x5F + DIK_F5, DIK_F6, DIK_F7, DIK_F3, DIK_F8, DIK_F9, 0, DIK_F11, // 0x60 - 0x67 + 0, DIK_F13, 0, DIK_F14, 0, DIK_F10, 0, DIK_F12, // 0x68 - 0x6F + 0, DIK_F15, 0, DIK_HOME, 0, DIK_DELETE, DIK_F4, DIK_END, // 0x70 - 0x77 + DIK_F2, 0, DIK_F1, DIK_LEFT, DIK_RIGHT, DIK_DOWN, DIK_UP, 0, // 0x78 - 0x7F +}; + +const uint8_t KEYCODE_TO_ASCII[KEY_COUNT] = +{ + 'a', 's', 'd', 'f', 'h', 'g', 'z', 'x', // 0x00 - 0x07 + 'c', 'v', 0, 'b', 'q', 'w', 'e', 'r', // 0x08 - 0x0F + 'y', 't', '1', '2', '3', '4', '6', '5', // 0x10 - 0x17 + '=', '9', '7', '-', '8', '0', ']', 'o', // 0x18 - 0x1F + 'u', '[', 'i', 'p', 13, 'l', 'j', '\'', // 0x20 - 0x27 + 'k', ';', '\\', ',', '/', 'n', 'm', '.', // 0x28 - 0x2F + 9, ' ', '`', 12, 0, 27, 0, 0, // 0x30 - 0x37 + 0, 0, 0, 0, 0, 0, 0, 0, // 0x38 - 0x3F + 0, 0, 0, 0, 0, 0, 0, 0, // 0x40 - 0x47 + 0, 0, 0, 0, 0, 0, 0, 0, // 0x48 - 0x4F + 0, 0, 0, 0, 0, 0, 0, 0, // 0x50 - 0x57 + 0, 0, 0, 0, 0, 0, 0, 0, // 0x58 - 0x5F + 0, 0, 0, 0, 0, 0, 0, 0, // 0x60 - 0x67 + 0, 0, 0, 0, 0, 0, 0, 0, // 0x68 - 0x6F + 0, 0, 0, 0, 0, 0, 0, 0, // 0x70 - 0x77 + 0, 0, 0, 0, 0, 0, 0, 0, // 0x78 - 0x7F +}; + + +uint8_t ModifierToDIK(const uint32_t modifier) +{ + switch (modifier) + { + case NSAlphaShiftKeyMask: return DIK_CAPITAL; + case NSShiftKeyMask: return DIK_LSHIFT; + case NSControlKeyMask: return DIK_LCONTROL; + case NSAlternateKeyMask: return DIK_LMENU; + case NSCommandKeyMask: return DIK_LWIN; + } + + return 0; +} + +int16_t ModifierFlagsToGUIKeyModifiers(NSEvent* theEvent) +{ + const NSUInteger modifiers([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask); + return ((modifiers & NSShiftKeyMask ) ? GKM_SHIFT : 0) + | ((modifiers & NSControlKeyMask ) ? GKM_CTRL : 0) + | ((modifiers & NSAlternateKeyMask) ? GKM_ALT : 0) + | ((modifiers & NSCommandKeyMask ) ? GKM_META : 0); +} + +bool ShouldGenerateGUICharEvent(NSEvent* theEvent) +{ + const NSUInteger modifiers([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask); + return !(modifiers & NSControlKeyMask) + && !(modifiers & NSAlternateKeyMask) + && !(modifiers & NSCommandKeyMask) + && !(modifiers & NSFunctionKeyMask); +} + + +NSStringEncoding GetEncodingForUnicodeCharacter(const unichar character) +{ + if (character >= L'\u0100' && character <= L'\u024F') + { + return NSWindowsCP1250StringEncoding; // Central and Eastern Europe + } + else if (character >= L'\u0370' && character <= L'\u03FF') + { + return NSWindowsCP1253StringEncoding; // Greek + } + else if (character >= L'\u0400' && character <= L'\u04FF') + { + return NSWindowsCP1251StringEncoding; // Cyrillic + } + + // TODO: add handling for other characters + // TODO: Turkish should use NSWindowsCP1254StringEncoding + + return NSWindowsCP1252StringEncoding; +} + +unsigned char GetCharacterFromNSEvent(NSEvent* theEvent, unichar *realchar) +{ + const NSString* unicodeCharacters = [theEvent characters]; + + if (0 == [unicodeCharacters length]) + { + return '\0'; + } + + const unichar unicodeCharacter = [unicodeCharacters characterAtIndex:0]; + const NSStringEncoding encoding = GetEncodingForUnicodeCharacter(unicodeCharacter); + + unsigned char character = '\0'; + + if (NSWindowsCP1252StringEncoding == encoding) + { + // TODO: make sure that the following is always correct + character = unicodeCharacter & 0xFF; + } + else + { + const NSData* const characters = + [[theEvent characters] dataUsingEncoding:encoding]; + + character = [characters length] > 0 + ? *static_cast([characters bytes]) + : '\0'; + } + + *realchar = unicodeCharacter; + return character; +} + +void ProcessKeyboardEventInMenu(NSEvent* theEvent) +{ + event_t event = {}; + + unichar realchar; + event.type = EV_GUI_Event; + event.subtype = NSKeyDown == [theEvent type] ? EV_GUI_KeyDown : EV_GUI_KeyUp; + event.data2 = GetCharacterFromNSEvent(theEvent, &realchar); + event.data3 = ModifierFlagsToGUIKeyModifiers(theEvent); + + if (EV_GUI_KeyDown == event.subtype && [theEvent isARepeat]) + { + event.subtype = EV_GUI_KeyRepeat; + } + + const unsigned short keyCode = [theEvent keyCode]; + + switch (keyCode) + { + case kVK_Return: event.data1 = GK_RETURN; break; + case kVK_PageUp: event.data1 = GK_PGUP; break; + case kVK_PageDown: event.data1 = GK_PGDN; break; + case kVK_End: event.data1 = GK_END; break; + case kVK_Home: event.data1 = GK_HOME; break; + case kVK_LeftArrow: event.data1 = GK_LEFT; break; + case kVK_RightArrow: event.data1 = GK_RIGHT; break; + case kVK_UpArrow: event.data1 = GK_UP; break; + case kVK_DownArrow: event.data1 = GK_DOWN; break; + case kVK_Delete: event.data1 = GK_BACKSPACE; break; + case kVK_ForwardDelete: event.data1 = GK_DEL; break; + case kVK_Escape: event.data1 = GK_ESCAPE; break; + case kVK_F1: event.data1 = GK_F1; break; + case kVK_F2: event.data1 = GK_F2; break; + case kVK_F3: event.data1 = GK_F3; break; + case kVK_F4: event.data1 = GK_F4; break; + case kVK_F5: event.data1 = GK_F5; break; + case kVK_F6: event.data1 = GK_F6; break; + case kVK_F7: event.data1 = GK_F7; break; + case kVK_F8: event.data1 = GK_F8; break; + case kVK_F9: event.data1 = GK_F9; break; + case kVK_F10: event.data1 = GK_F10; break; + case kVK_F11: event.data1 = GK_F11; break; + case kVK_F12: event.data1 = GK_F12; break; + default: + event.data1 = KEYCODE_TO_ASCII[keyCode]; + break; + } + + if (event.data1 < 128) + { + event.data1 = toupper(event.data1); + + D_PostEvent(&event); + } + + if (!iscntrl(event.data2) + && EV_GUI_KeyUp != event.subtype + && ShouldGenerateGUICharEvent(theEvent)) + { + event.subtype = EV_GUI_Char; + event.data1 = realchar; + event.data2 = event.data3 & GKM_ALT; + + D_PostEvent(&event); + } +} + + +void NSEventToGameMousePosition(NSEvent* inEvent, event_t* outEvent) +{ + const NSWindow* window = [inEvent window]; + const NSView* view = [window contentView]; + + const NSPoint screenPos = [NSEvent mouseLocation]; + const NSRect screenRect = NSMakeRect(screenPos.x, screenPos.y, 0, 0); + const NSRect windowRect = [window convertRectFromScreen:screenRect]; + + NSPoint viewPos; + NSSize viewSize; + CGFloat scale; + + if (view.layer == nil) + { + viewPos = [view convertPointToBacking:windowRect.origin]; + viewSize = [view convertSizeToBacking:view.frame.size]; + scale = 1.0; + } + else + { + viewPos = windowRect.origin; + viewSize = view.frame.size; + scale = view.layer.contentsScale; + } + + const CGFloat posX = viewPos.x * scale; + const CGFloat posY = (viewSize.height - viewPos.y) * scale; + + outEvent->data1 = static_cast(posX); + outEvent->data2 = static_cast(posY); + + screen->ScaleCoordsFromWindow(outEvent->data1, outEvent->data2); +} + +void ProcessMouseMoveInMenu(NSEvent* theEvent) +{ + event_t event = {}; + + event.type = EV_GUI_Event; + event.subtype = EV_GUI_MouseMove; + event.data3 = ModifierFlagsToGUIKeyModifiers(theEvent); + + NSEventToGameMousePosition(theEvent, &event); + + D_PostEvent(&event); +} + +void ProcessMouseMoveInGame(NSEvent* theEvent) +{ + int x([theEvent deltaX]); + int y(-[theEvent deltaY]); + + if (0 == x && 0 == y) + { + return; + } + + if (!m_noprescale) + { + x *= 3; + y *= 2; + } + + event_t event = {}; + + static int lastX = 0, lastY = 0; + + if (m_filter) + { + event.x = (x + lastX) / 2; + event.y = (y + lastY) / 2; + } + else + { + event.x = x; + event.y = y; + } + + lastX = x; + lastY = y; + + if (0 != event.x || 0 != event.y) + { + event.type = EV_Mouse; + + D_PostEvent(&event); + } +} + + +void ProcessKeyboardEvent(NSEvent* theEvent) +{ + const unsigned short keyCode = [theEvent keyCode]; + if (keyCode >= KEY_COUNT) + { + assert(!"Unknown keycode"); + return; + } + + const bool isARepeat = [theEvent isARepeat]; + + if (k_allowfullscreentoggle + && (kVK_ANSI_F == keyCode) + && (NSCommandKeyMask & [theEvent modifierFlags]) + && (NSKeyDown == [theEvent type]) + && !isARepeat) + { + ToggleFullscreen = !ToggleFullscreen; + return; + } + + if (GUICapture) + { + ProcessKeyboardEventInMenu(theEvent); + } + else if (!isARepeat) + { + event_t event = {}; + + event.type = NSKeyDown == [theEvent type] ? EV_KeyDown : EV_KeyUp; + event.data1 = KEYCODE_TO_DIK[ keyCode ]; + + if (0 != event.data1) + { + event.data2 = KEYCODE_TO_ASCII[ keyCode ]; + + D_PostEvent(&event); + } + } +} + +void ProcessKeyboardFlagsEvent(NSEvent* theEvent) +{ + if (GUICapture) + { + // Ignore events from modifier keys in menu/console/chat + return; + } + + static const uint32_t FLAGS_MASK = + NSDeviceIndependentModifierFlagsMask & ~NSNumericPadKeyMask; + + const uint32_t modifiers = [theEvent modifierFlags] & FLAGS_MASK; + static uint32_t oldModifiers = 0; + const uint32_t deltaModifiers = modifiers ^ oldModifiers; + + if (0 == deltaModifiers) + { + return; + } + + event_t event = {}; + event.type = modifiers > oldModifiers ? EV_KeyDown : EV_KeyUp; + event.data1 = ModifierToDIK(deltaModifiers); + + oldModifiers = modifiers; + + if (DIK_CAPITAL == event.data1) + { + // Caps Lock is a modifier key which generates one event per state change + // but not per actual key press or release. So treat any event as key down + event.type = EV_KeyDown; + } + + D_PostEvent(&event); +} + + +void ProcessMouseMoveEvent(NSEvent* theEvent) +{ + if (!use_mouse) + { + return; + } + + if (s_skipMouseMoves > 0) + { + --s_skipMouseMoves; + return; + } + + if (GUICapture) + { + ProcessMouseMoveInMenu(theEvent); + } + else + { + ProcessMouseMoveInGame(theEvent); + } +} + +void ProcessMouseButtonEvent(NSEvent* theEvent) +{ + if (!use_mouse) + { + return; + } + + event_t event = {}; + + const NSEventType cocoaEventType = [theEvent type]; + + if (GUICapture) + { + event.type = EV_GUI_Event; + event.data3 = ModifierFlagsToGUIKeyModifiers(theEvent); + + switch (cocoaEventType) + { + case NSLeftMouseDown: event.subtype = EV_GUI_LButtonDown; break; + case NSRightMouseDown: event.subtype = EV_GUI_RButtonDown; break; + case NSOtherMouseDown: event.subtype = EV_GUI_MButtonDown; break; + case NSLeftMouseUp: event.subtype = EV_GUI_LButtonUp; break; + case NSRightMouseUp: event.subtype = EV_GUI_RButtonUp; break; + case NSOtherMouseUp: event.subtype = EV_GUI_MButtonUp; break; + default: break; + } + + NSEventToGameMousePosition(theEvent, &event); + + D_PostEvent(&event); + } + else + { + switch (cocoaEventType) + { + case NSLeftMouseDown: + case NSRightMouseDown: + case NSOtherMouseDown: + event.type = EV_KeyDown; + break; + + case NSLeftMouseUp: + case NSRightMouseUp: + case NSOtherMouseUp: + event.type = EV_KeyUp; + break; + + default: + break; + } + + event.data1 = MIN(KEY_MOUSE1 + [theEvent buttonNumber], NSInteger(KEY_MOUSE8)); + + D_PostEvent(&event); + } +} + +void ProcessMouseWheelEvent(NSEvent* theEvent) +{ + if (!use_mouse) + { + return; + } + + const int16_t modifiers = ModifierFlagsToGUIKeyModifiers(theEvent); + const CGFloat delta = (modifiers & GKM_SHIFT) + ? [theEvent deltaX] + : [theEvent deltaY]; + const bool isZeroDelta = fabs(delta) < 1.0E-5; + + if (isZeroDelta && GUICapture) + { + return; + } + + event_t event = {}; + + if (GUICapture) + { + event.type = EV_GUI_Event; + event.subtype = delta > 0.0f ? EV_GUI_WheelUp : EV_GUI_WheelDown; + event.data3 = modifiers; + } + else + { + event.type = isZeroDelta ? EV_KeyUp : EV_KeyDown; + event.data1 = delta > 0.0f ? KEY_MWHEELUP : KEY_MWHEELDOWN; + } + + D_PostEvent(&event); +} + +} // unnamed namespace + + +void I_ProcessEvent(NSEvent* event) +{ + const NSEventType eventType = [event type]; + + switch (eventType) + { + case NSMouseMoved: + ProcessMouseMoveEvent(event); + break; + + case NSLeftMouseDown: + case NSLeftMouseUp: + case NSRightMouseDown: + case NSRightMouseUp: + case NSOtherMouseDown: + case NSOtherMouseUp: + ProcessMouseButtonEvent(event); + break; + + case NSLeftMouseDragged: + case NSRightMouseDragged: + case NSOtherMouseDragged: + ProcessMouseButtonEvent(event); + ProcessMouseMoveEvent(event); + break; + + case NSScrollWheel: + ProcessMouseWheelEvent(event); + break; + + case NSKeyDown: + case NSKeyUp: + ProcessKeyboardEvent(event); + break; + + case NSFlagsChanged: + ProcessKeyboardFlagsEvent(event); + break; + + default: + break; + } +} diff --git a/source/platform/posix/cocoa/i_joystick.cpp b/source/platform/posix/cocoa/i_joystick.cpp new file mode 100644 index 000000000..bebf43b42 --- /dev/null +++ b/source/platform/posix/cocoa/i_joystick.cpp @@ -0,0 +1,1244 @@ +/* + ** i_joystick.cpp + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2015 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include +#include +#include + +#include "d_event.h" +#include "doomdef.h" +#include "i_system.h" +#include "m_argv.h" +#include "m_joy.h" +#include "templates.h" +#include "v_text.h" + + +EXTERN_CVAR(Bool, joy_axespolling) + + +namespace +{ + +FString ToFString(const CFStringRef string) +{ + if (NULL == string) + { + return FString(); + } + + const CFIndex stringLength = CFStringGetLength(string); + + if (0 == stringLength) + { + return FString(); + } + + const size_t bufferSize = CFStringGetMaximumSizeForEncoding(stringLength, kCFStringEncodingUTF8) + 1; + + char buffer[bufferSize]; + memset(buffer, 0, bufferSize); + + CFStringGetCString(string, buffer, bufferSize, kCFStringEncodingUTF8); + + return FString(buffer); +} + + +// --------------------------------------------------------------------------- + + +class IOKitJoystick : public IJoystickConfig +{ +public: + explicit IOKitJoystick(io_object_t device); + virtual ~IOKitJoystick(); + + virtual FString GetName(); + virtual float GetSensitivity(); + virtual void SetSensitivity(float scale); + + virtual int GetNumAxes(); + virtual float GetAxisDeadZone(int axis); + virtual EJoyAxis GetAxisMap(int axis); + virtual const char* GetAxisName(int axis); + virtual float GetAxisScale(int axis); + + virtual void SetAxisDeadZone(int axis, float deadZone); + virtual void SetAxisMap(int axis, EJoyAxis gameAxis); + virtual void SetAxisScale(int axis, float scale); + + virtual bool IsSensitivityDefault(); + virtual bool IsAxisDeadZoneDefault(int axis); + virtual bool IsAxisMapDefault(int axis); + virtual bool IsAxisScaleDefault(int axis); + + virtual void SetDefaultConfig(); + virtual FString GetIdentifier(); + + void AddAxes(float axes[NUM_JOYAXIS]) const; + + void Update(); + + void UseAxesPolling(bool axesPolling); + + io_object_t* GetNotificationPtr(); + +private: + IOHIDDeviceInterface** m_interface; + IOHIDQueueInterface** m_queue; + + FString m_name; + FString m_identifier; + + float m_sensitivity; + + struct AnalogAxis + { + IOHIDElementCookie cookie; + + char name[64]; + + float value; + + int32_t minValue; + int32_t maxValue; + + float deadZone; + float defaultDeadZone; + float sensitivity; + float defaultSensitivity; + + EJoyAxis gameAxis; + EJoyAxis defaultGameAxis; + + AnalogAxis() + { + memset(this, 0, sizeof *this); + } + }; + + TArray m_axes; + + struct DigitalButton + { + IOHIDElementCookie cookie; + int32_t value; + + explicit DigitalButton(const IOHIDElementCookie cookie) + : cookie(cookie) + , value(0) + { } + }; + + TArray m_buttons; + TArray m_POVs; + + bool m_useAxesPolling; + + io_object_t m_notification; + + + static const float DEFAULT_DEADZONE; + static const float DEFAULT_SENSITIVITY; + + void ProcessAxes(); + bool ProcessAxis (const IOHIDEventStruct& event); + bool ProcessButton(const IOHIDEventStruct& event); + bool ProcessPOV (const IOHIDEventStruct& event); + + void GatherDeviceInfo(io_object_t device, CFDictionaryRef properties); + + static void GatherElementsHandler(const void* value, void* parameter); + void GatherCollectionElements(CFDictionaryRef properties); + + void AddAxis(CFDictionaryRef element); + void AddButton(CFDictionaryRef element); + void AddPOV(CFDictionaryRef element); + + void AddToQueue(IOHIDElementCookie cookie); + void RemoveFromQueue(IOHIDElementCookie cookie); +}; + + +const float IOKitJoystick::DEFAULT_DEADZONE = 0.25f; +const float IOKitJoystick::DEFAULT_SENSITIVITY = 1.0f; + + +IOHIDDeviceInterface** CreateDeviceInterface(const io_object_t device) +{ + IOCFPlugInInterface** plugInInterface = NULL; + SInt32 score = 0; + + const kern_return_t pluginResult = IOCreatePlugInInterfaceForService(device, + kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugInInterface, &score); + + IOHIDDeviceInterface** interface = NULL; + + if (KERN_SUCCESS == pluginResult) + { + // Call a method of the intermediate plug-in to create the device interface + + const HRESULT queryResult = (*plugInInterface)->QueryInterface(plugInInterface, + CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), reinterpret_cast(&interface)); + + IODestroyPlugInInterface(plugInInterface); // [?] or maybe (*plugInInterface)->Release(plugInInterface); + + if (S_OK == queryResult) + { + const IOReturn openResult = (*interface)->open(interface, 0); + + if (kIOReturnSuccess != openResult) + { + (*interface)->Release(interface); + + Printf(TEXTCOLOR_RED "IOHIDDeviceInterface::open() failed with code 0x%08X\n", openResult); + return NULL; + } + } + else + { + Printf(TEXTCOLOR_RED "IOCFPlugInInterface::QueryInterface() failed with code 0x%08X\n", + static_cast(queryResult)); + return NULL; + } + } + else + { + Printf(TEXTCOLOR_RED "IOCreatePlugInInterfaceForService() failed with code %i\n", pluginResult); + return NULL; + } + + return interface; +} + +IOHIDQueueInterface** CreateDeviceQueue(IOHIDDeviceInterface** const interface) +{ + if (NULL == interface) + { + return NULL; + } + + IOHIDQueueInterface** queue = (*interface)->allocQueue(interface); + + if (NULL == queue) + { + Printf(TEXTCOLOR_RED "IOHIDDeviceInterface::allocQueue() failed\n"); + return NULL; + } + + static const uint32_t QUEUE_FLAGS = 0; + static const uint32_t QUEUE_DEPTH = 0; + + const IOReturn queueResult = (*queue)->create(queue, QUEUE_FLAGS, QUEUE_DEPTH); + + if (kIOReturnSuccess != queueResult) + { + (*queue)->Release(queue); + + Printf(TEXTCOLOR_RED "IOHIDQueueInterface::create() failed with code 0x%08X\n", queueResult); + return NULL; + } + + return queue; +} + + +IOKitJoystick::IOKitJoystick(const io_object_t device) +: m_interface(CreateDeviceInterface(device)) +, m_queue(CreateDeviceQueue(m_interface)) +, m_sensitivity(DEFAULT_SENSITIVITY) +, m_useAxesPolling(true) +, m_notification(0) +{ + if (NULL == m_interface || NULL == m_queue) + { + return; + } + + CFMutableDictionaryRef properties = NULL; + const kern_return_t propertiesResult = + IORegistryEntryCreateCFProperties(device, &properties, kCFAllocatorDefault, kNilOptions); + + if (KERN_SUCCESS != propertiesResult || NULL == properties) + { + Printf(TEXTCOLOR_RED "IORegistryEntryCreateCFProperties() failed with code %i\n", propertiesResult); + return; + } + + GatherDeviceInfo(device, properties); + GatherCollectionElements(properties); + + CFRelease(properties); + + UseAxesPolling(joy_axespolling); + + (*m_queue)->start(m_queue); + + SetDefaultConfig(); +} + +IOKitJoystick::~IOKitJoystick() +{ + M_SaveJoystickConfig(this); + + if (0 != m_notification) + { + IOObjectRelease(m_notification); + } + + if (NULL != m_queue) + { + (*m_queue)->stop(m_queue); + (*m_queue)->dispose(m_queue); + (*m_queue)->Release(m_queue); + } + + if (NULL != m_interface) + { + (*m_interface)->close(m_interface); + (*m_interface)->Release(m_interface); + } +} + + +FString IOKitJoystick::GetName() +{ + return m_name; +} + + +float IOKitJoystick::GetSensitivity() +{ + return m_sensitivity; +} + +void IOKitJoystick::SetSensitivity(float scale) +{ + m_sensitivity = scale; +} + + +int IOKitJoystick::GetNumAxes() +{ + return static_cast(m_axes.Size()); +} + +#define IS_AXIS_VALID (static_cast(axis) < m_axes.Size()) + +float IOKitJoystick::GetAxisDeadZone(int axis) +{ + return IS_AXIS_VALID ? m_axes[axis].deadZone : 0.0f; +} + +EJoyAxis IOKitJoystick::GetAxisMap(int axis) +{ + return IS_AXIS_VALID ? m_axes[axis].gameAxis : JOYAXIS_None; +} + +const char* IOKitJoystick::GetAxisName(int axis) +{ + return IS_AXIS_VALID ? m_axes[axis].name : "Invalid"; +} + +float IOKitJoystick::GetAxisScale(int axis) +{ + return IS_AXIS_VALID ? m_axes[axis].sensitivity : 0.0f; +} + +void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone) +{ + if (IS_AXIS_VALID) + { + m_axes[axis].deadZone = clamp(deadZone, 0.0f, 1.0f); + } +} + +void IOKitJoystick::SetAxisMap(int axis, EJoyAxis gameAxis) +{ + if (IS_AXIS_VALID) + { + m_axes[axis].gameAxis = (gameAxis> JOYAXIS_None && gameAxis = 3) + { + m_axes[0].gameAxis = JOYAXIS_Side; + m_axes[1].gameAxis = JOYAXIS_Forward; + m_axes[2].gameAxis = JOYAXIS_Yaw; + + // Four axes? First two are movement, last two are looking around. + + if (axisCount >= 4) + { + m_axes[3].gameAxis = JOYAXIS_Pitch; +// ??? m_axes[3].sensitivity = 0.75f; + + // Five axes? Use the fifth one for moving up and down. + + if (axisCount >= 5) + { + m_axes[4].gameAxis = JOYAXIS_Up; + } + } + } + + // If there is only one axis, then we make no assumptions about how + // the user might want to use it. + + // Preserve defaults for config saving. + + for (size_t i = 0; i < axisCount; ++i) + { + m_axes[i].defaultDeadZone = m_axes[i].deadZone; + m_axes[i].defaultSensitivity = m_axes[i].sensitivity; + m_axes[i].defaultGameAxis = m_axes[i].gameAxis; + } +} + + +FString IOKitJoystick::GetIdentifier() +{ + return m_identifier; +} + + +void IOKitJoystick::AddAxes(float axes[NUM_JOYAXIS]) const +{ + for (size_t i = 0, count = m_axes.Size(); i < count; ++i) + { + const EJoyAxis axis = m_axes[i].gameAxis; + + if (JOYAXIS_None == axis) + { + continue; + } + + axes[axis] -= m_axes[i].value; + } +} + + +void IOKitJoystick::UseAxesPolling(const bool axesPolling) +{ + m_useAxesPolling = axesPolling; + + for (size_t i = 0, count = m_axes.Size(); i < count; ++i) + { + AnalogAxis& axis = m_axes[i]; + + if (m_useAxesPolling) + { + RemoveFromQueue(axis.cookie); + } + else + { + AddToQueue(axis.cookie); + } + } +} + + +void IOKitJoystick::Update() +{ + if (NULL == m_queue) + { + return; + } + + IOHIDEventStruct event = { }; + AbsoluteTime zeroTime = { }; + + const IOReturn eventResult = (*m_queue)->getNextEvent(m_queue, &event, zeroTime, 0); + + if (kIOReturnSuccess == eventResult) + { + if (use_joystick) + { + ProcessAxis(event) || ProcessButton(event) || ProcessPOV(event); + } + } + else if (kIOReturnUnderrun != eventResult) + { + Printf(TEXTCOLOR_RED "IOHIDQueueInterface::getNextEvent() failed with code 0x%08X\n", eventResult); + } + + ProcessAxes(); +} + + +void IOKitJoystick::ProcessAxes() +{ + if (NULL == m_interface || !m_useAxesPolling) + { + return; + } + + for (size_t i = 0, count = m_axes.Size(); i < count; ++i) + { + AnalogAxis& axis = m_axes[i]; + + static const double scaledMin = -1; + static const double scaledMax = 1; + + IOHIDEventStruct event; + + if (kIOReturnSuccess == (*m_interface)->getElementValue(m_interface, axis.cookie, &event)) + { + const double scaledValue = scaledMin + + (event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue); + const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL); + + axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity); + } + else + { + axis.value = 0.0f; + } + } +} + + +bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event) +{ + if (m_useAxesPolling) + { + return false; + } + + for (size_t i = 0, count = m_axes.Size(); i < count; ++i) + { + if (event.elementCookie != m_axes[i].cookie) + { + continue; + } + + AnalogAxis& axis = m_axes[i]; + + static const double scaledMin = -1; + static const double scaledMax = 1; + + const double scaledValue = scaledMin + + (event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue); + const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL); + + axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity); + + return true; + } + + return false; +} + +bool IOKitJoystick::ProcessButton(const IOHIDEventStruct& event) +{ + for (size_t i = 0, count = m_buttons.Size(); i < count; ++i) + { + if (event.elementCookie != m_buttons[i].cookie) + { + continue; + } + + int32_t& current = m_buttons[i].value; + const int32_t previous = current; + current = event.value; + + Joy_GenerateButtonEvents(previous, current, 1, static_cast(KEY_FIRSTJOYBUTTON + i)); + + return true; + } + + return false; +} + +bool IOKitJoystick::ProcessPOV(const IOHIDEventStruct& event) +{ + for (size_t i = 0, count = m_POVs.Size(); i ( + CFDictionaryGetValue(properties, CFSTR(kIOHIDManufacturerKey))); + CFStringRef productRef = static_cast( + CFDictionaryGetValue(properties, CFSTR(kIOHIDProductKey))); + CFNumberRef vendorIDRef = static_cast( + CFDictionaryGetValue(properties, CFSTR(kIOHIDVendorIDKey))); + CFNumberRef productIDRef = static_cast( + CFDictionaryGetValue(properties, CFSTR(kIOHIDProductIDKey))); + + CFMutableDictionaryRef usbProperties = NULL; + + if ( NULL == vendorRef || NULL == productRef + || NULL == vendorIDRef || NULL == productIDRef) + { + // OS X is not mirroring all USB properties to HID page, so need to look at USB device page also + // Step up two levels and get dictionary of USB properties + + io_registry_entry_t parent1; + kern_return_t ioResult = IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent1); + + if (KERN_SUCCESS == ioResult) + { + io_registry_entry_t parent2; + ioResult = IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent2); + + if (KERN_SUCCESS == ioResult) + { + ioResult = IORegistryEntryCreateCFProperties(parent2, &usbProperties, kCFAllocatorDefault, kNilOptions); + + if (KERN_SUCCESS != ioResult) + { + Printf(TEXTCOLOR_RED "IORegistryEntryCreateCFProperties() failed with code %i\n", ioResult); + } + + IOObjectRelease(parent2); + } + else + { + Printf(TEXTCOLOR_RED "IORegistryEntryGetParentEntry(2) failed with code %i\n", ioResult); + } + + IOObjectRelease(parent1); + } + else + { + Printf(TEXTCOLOR_RED "IORegistryEntryGetParentEntry(1) failed with code %i\n", ioResult); + } + } + + if (NULL != usbProperties) + { + if (NULL == vendorRef) + { + vendorRef = static_cast( + CFDictionaryGetValue(usbProperties, CFSTR("USB Vendor Name"))); + } + + if (NULL == productRef) + { + productRef = static_cast( + CFDictionaryGetValue(usbProperties, CFSTR("USB Product Name"))); + } + + if (NULL == vendorIDRef) + { + vendorIDRef = static_cast( + CFDictionaryGetValue(usbProperties, CFSTR("idVendor"))); + } + + if (NULL == productIDRef) + { + productIDRef = static_cast( + CFDictionaryGetValue(usbProperties, CFSTR("idProduct"))); + } + } + + m_name += ToFString(vendorRef); + m_name += " "; + m_name += ToFString(productRef); + + int vendorID = 0, productID = 0; + + if (NULL != vendorIDRef) + { + CFNumberGetValue(vendorIDRef, kCFNumberIntType, &vendorID); + } + + if (NULL != productIDRef) + { + CFNumberGetValue(productIDRef, kCFNumberIntType, &productID); + } + + m_identifier.AppendFormat("VID_%04x_PID_%04x", vendorID, productID); + + if (NULL != usbProperties) + { + CFRelease(usbProperties); + } +} + + +long GetElementValue(const CFDictionaryRef element, const CFStringRef key) +{ + const CFNumberRef number = + static_cast(CFDictionaryGetValue(element, key)); + long result = 0; + + if (NULL != number && CFGetTypeID(number) == CFNumberGetTypeID()) + { + CFNumberGetValue(number, kCFNumberLongType, &result); + } + + return result; +} + +void IOKitJoystick::GatherElementsHandler(const void* value, void* parameter) +{ + assert(NULL != value); + assert(NULL != parameter); + + const CFDictionaryRef element = static_cast(value); + IOKitJoystick* thisPtr = static_cast(parameter); + + if (CFGetTypeID(element) != CFDictionaryGetTypeID()) + { + Printf(TEXTCOLOR_RED "IOKitJoystick: Encountered wrong element type\n"); + return; + } + + const long type = GetElementValue(element, CFSTR(kIOHIDElementTypeKey)); + + if (kIOHIDElementTypeCollection == type) + { + thisPtr->GatherCollectionElements(element); + } + else if (0 != type) + { + const long usagePage = GetElementValue(element, CFSTR(kIOHIDElementUsagePageKey)); + + if (kHIDPage_GenericDesktop == usagePage) + { + const long usage = GetElementValue(element, CFSTR(kIOHIDElementUsageKey)); + + if ( kHIDUsage_GD_Slider == usage + || kHIDUsage_GD_X == usage || kHIDUsage_GD_Y == usage || kHIDUsage_GD_Z == usage + || kHIDUsage_GD_Rx == usage || kHIDUsage_GD_Ry == usage || kHIDUsage_GD_Rz == usage) + { + thisPtr->AddAxis(element); + } + else if (kHIDUsage_GD_Hatswitch == usage && thisPtr->m_POVs.Size() < 4) + { + thisPtr->AddPOV(element); + } + } + else if (kHIDPage_Button == usagePage) + { + thisPtr->AddButton(element); + } + } +} + +void IOKitJoystick::GatherCollectionElements(const CFDictionaryRef properties) +{ + const CFArrayRef topElement = static_cast( + CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey))); + + if (NULL == topElement || CFGetTypeID(topElement) != CFArrayGetTypeID()) + { + Printf(TEXTCOLOR_RED "GatherCollectionElements: invalid properties dictionary\n"); + return; + } + + const CFRange range = { 0, CFArrayGetCount(topElement) }; + + CFArrayApplyFunction(topElement, range, GatherElementsHandler, this); +} + + +IOHIDElementCookie GetElementCookie(const CFDictionaryRef element) +{ + // Use C-style cast to avoid 32/64-bit IOHIDElementCookie type issue + return (IOHIDElementCookie)GetElementValue(element, CFSTR(kIOHIDElementCookieKey)); +} + +void IOKitJoystick::AddAxis(const CFDictionaryRef element) +{ + AnalogAxis axis; + + axis.cookie = GetElementCookie(element); + axis.minValue = GetElementValue(element, CFSTR(kIOHIDElementMinKey)); + axis.maxValue = GetElementValue(element, CFSTR(kIOHIDElementMaxKey)); + + const CFStringRef nameRef = static_cast( + CFDictionaryGetValue(element, CFSTR(kIOHIDElementNameKey))); + + if (NULL != nameRef && CFStringGetTypeID() == CFGetTypeID(nameRef)) + { + CFStringGetCString(nameRef, axis.name, sizeof(axis.name) - 1, kCFStringEncodingUTF8); + } + else + { + snprintf(axis.name, sizeof(axis.name), "Axis %i", m_axes.Size() + 1); + } + + m_axes.Push(axis); +} + +void IOKitJoystick::AddButton(CFDictionaryRef element) +{ + const DigitalButton button(GetElementCookie(element)); + + m_buttons.Push(button); + + AddToQueue(button.cookie); +} + +void IOKitJoystick::AddPOV(CFDictionaryRef element) +{ + const DigitalButton pov(GetElementCookie(element)); + + m_POVs.Push(pov); + + AddToQueue(pov.cookie); +} + + +void IOKitJoystick::AddToQueue(const IOHIDElementCookie cookie) +{ + if (NULL == m_queue) + { + return; + } + + if (!(*m_queue)->hasElement(m_queue, cookie)) + { + (*m_queue)->addElement(m_queue, cookie, 0); + } +} + +void IOKitJoystick::RemoveFromQueue(const IOHIDElementCookie cookie) +{ + if (NULL == m_queue) + { + return; + } + + if ((*m_queue)->hasElement(m_queue, cookie)) + { + (*m_queue)->removeElement(m_queue, cookie); + } +} + + +io_object_t* IOKitJoystick::GetNotificationPtr() +{ + return &m_notification; +} + + +// --------------------------------------------------------------------------- + + +class IOKitJoystickManager +{ +public: + IOKitJoystickManager(); + ~IOKitJoystickManager(); + + void GetJoysticks(TArray& joysticks) const; + + void AddAxes(float axes[NUM_JOYAXIS]) const; + + // Updates axes/buttons states + void Update(); + + void UseAxesPolling(bool axesPolling); + +private: + typedef TDeletingArray JoystickList; + JoystickList m_joysticks; + + static const size_t NOTIFICATION_PORT_COUNT = 2; + + IONotificationPortRef m_notificationPorts[NOTIFICATION_PORT_COUNT]; + io_iterator_t m_notifications [NOTIFICATION_PORT_COUNT]; + + // Rebuilds device list + void Rescan(int usagePage, int usage, size_t notificationPortIndex); + void AddDevices(IONotificationPortRef notificationPort, const io_iterator_t iterator); + + static void OnDeviceAttached(void* refcon, io_iterator_t iterator); + static void OnDeviceRemoved(void* refcon, io_service_t service, + natural_t messageType, void* messageArgument); +}; + + +IOKitJoystickManager* s_joystickManager; + + +IOKitJoystickManager::IOKitJoystickManager() +{ + memset(m_notifications, 0, sizeof m_notifications); + + for (size_t i = 0; i < NOTIFICATION_PORT_COUNT; ++i) + { + m_notificationPorts[i] = IONotificationPortCreate(kIOMasterPortDefault); + + if (NULL == m_notificationPorts[i]) + { + Printf(TEXTCOLOR_RED "IONotificationPortCreate(%zu) failed\n", i); + return; + } + + CFRunLoopAddSource(CFRunLoopGetCurrent(), + IONotificationPortGetRunLoopSource(m_notificationPorts[i]), kCFRunLoopDefaultMode); + } + + Rescan(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, 0); + Rescan(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, 1); +} + +IOKitJoystickManager::~IOKitJoystickManager() +{ + for (size_t i = 0; i < NOTIFICATION_PORT_COUNT; ++i) + { + IONotificationPortRef& port = m_notificationPorts[i]; + + if (NULL != port) + { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), + IONotificationPortGetRunLoopSource(port), kCFRunLoopDefaultMode); + + IONotificationPortDestroy(port); + port = NULL; + } + + io_iterator_t& notification = m_notifications[i]; + + if (0 != notification) + { + IOObjectRelease(notification); + notification = 0; + } + } +} + + +void IOKitJoystickManager::GetJoysticks(TArray& joysticks) const +{ + const size_t joystickCount = m_joysticks.Size(); + + joysticks.Resize(joystickCount); + + for (size_t i = 0; i < joystickCount; ++i) + { + M_LoadJoystickConfig(m_joysticks[i]); + + joysticks[i] = m_joysticks[i]; + } +} + +void IOKitJoystickManager::AddAxes(float axes[NUM_JOYAXIS]) const +{ + for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i) + { + m_joysticks[i]->AddAxes(axes); + } +} + + +void IOKitJoystickManager::Update() +{ + for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i) + { + m_joysticks[i]->Update(); + } +} + + +void IOKitJoystickManager::UseAxesPolling(const bool axesPolling) +{ + for (size_t i = 0, count = m_joysticks.Size(); i < count; ++i) + { + m_joysticks[i]->UseAxesPolling(axesPolling); + } +} + + +void PostDeviceChangeEvent() +{ + const event_t event = { EV_DeviceChange }; + D_PostEvent(&event); +} + + +void IOKitJoystickManager::Rescan(const int usagePage, const int usage, const size_t notificationPortIndex) +{ + CFMutableDictionaryRef deviceMatching = IOServiceMatching(kIOHIDDeviceKey); + + if (NULL == deviceMatching) + { + Printf(TEXTCOLOR_RED "IOServiceMatching() returned NULL\n"); + return; + } + + const CFNumberRef usagePageRef = + CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usagePage); + CFDictionarySetValue(deviceMatching, CFSTR(kIOHIDPrimaryUsagePageKey), usagePageRef); + + const CFNumberRef usageRef = + CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); + CFDictionarySetValue(deviceMatching, CFSTR(kIOHIDPrimaryUsageKey), usageRef); + + assert(notificationPortIndex < NOTIFICATION_PORT_COUNT); + io_iterator_t* iteratorPtr = &m_notifications[notificationPortIndex]; + + const IONotificationPortRef notificationPort = m_notificationPorts[notificationPortIndex]; + assert(NULL != notificationPort); + + const kern_return_t notificationResult = IOServiceAddMatchingNotification(notificationPort, + kIOFirstMatchNotification, deviceMatching, OnDeviceAttached, notificationPort, iteratorPtr); + + // IOServiceAddMatchingNotification() consumes one reference of matching dictionary + // Thus CFRelease(deviceMatching) is not needed + + CFRelease(usageRef); + CFRelease(usagePageRef); + + if (KERN_SUCCESS != notificationResult) + { + Printf(TEXTCOLOR_RED "IOServiceAddMatchingNotification() failed with code %i\n", notificationResult); + } + + AddDevices(notificationPort, *iteratorPtr); +} + +void IOKitJoystickManager::AddDevices(const IONotificationPortRef notificationPort, const io_iterator_t iterator) +{ + while (io_object_t device = IOIteratorNext(iterator)) + { + IOKitJoystick* joystick = new IOKitJoystick(device); + m_joysticks.Push(joystick); + + const kern_return_t notificationResult = IOServiceAddInterestNotification(notificationPort, + device, kIOGeneralInterest, OnDeviceRemoved, joystick, joystick->GetNotificationPtr()); + if (KERN_SUCCESS != notificationResult) + { + Printf(TEXTCOLOR_RED "IOServiceAddInterestNotification() failed with code %i\n", notificationResult); + } + + IOObjectRelease(device); + + PostDeviceChangeEvent(); + } +} + + +void IOKitJoystickManager::OnDeviceAttached(void* const refcon, const io_iterator_t iterator) +{ + assert(NULL != refcon); + const IONotificationPortRef notificationPort = static_cast(refcon); + + assert(NULL != s_joystickManager); + s_joystickManager->AddDevices(notificationPort, iterator); +} + +void IOKitJoystickManager::OnDeviceRemoved(void* const refcon, io_service_t, const natural_t messageType, void*) +{ + if (messageType != kIOMessageServiceIsTerminated) + { + return; + } + + assert(NULL != refcon); + IOKitJoystick* const joystick = static_cast(refcon); + + assert(NULL != s_joystickManager); + JoystickList& joysticks = s_joystickManager->m_joysticks; + + for (unsigned int i = 0, count = joysticks.Size(); i < count; ++i) + { + if (joystick == joysticks[i]) + { + joysticks.Delete(i); + break; + } + } + + delete joystick; + + PostDeviceChangeEvent(); +} + +} // unnamed namespace + + +// --------------------------------------------------------------------------- + + +void I_ShutdownInput() +{ + delete s_joystickManager; + s_joystickManager = NULL; +} + +void I_GetJoysticks(TArray& sticks) +{ + // Instances of IOKitJoystick depend on GameConfig object. + // M_SaveDefaultsFinal() must be called after destruction of IOKitJoystickManager. + // To ensure this, its initialization is moved here. + // As M_LoadDefaults() was already called at this moment, + // the order of atterm's functions will be correct + + if (NULL == s_joystickManager && !Args->CheckParm("-nojoy")) + { + s_joystickManager = new IOKitJoystickManager; + } + + if (NULL != s_joystickManager) + { + s_joystickManager->GetJoysticks(sticks); + } +} + +void I_GetAxes(float axes[NUM_JOYAXIS]) +{ + for (size_t i = 0; i < NUM_JOYAXIS; ++i) + { + axes[i] = 0.0f; + } + + if (use_joystick && NULL != s_joystickManager) + { + s_joystickManager->AddAxes(axes); + } +} + +IJoystickConfig* I_UpdateDeviceList() +{ + // Does nothing, device list is always kept up-to-date + + return NULL; +} + + +// --------------------------------------------------------------------------- + + +void I_ProcessJoysticks() +{ + if (NULL != s_joystickManager) + { + s_joystickManager->Update(); + } +} + + +// --------------------------------------------------------------------------- + + +CUSTOM_CVAR(Bool, joy_axespolling, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_NOINITCALL) +{ + if (NULL != s_joystickManager) + { + s_joystickManager->UseAxesPolling(self); + } +} diff --git a/source/platform/posix/cocoa/i_main.mm b/source/platform/posix/cocoa/i_main.mm new file mode 100644 index 000000000..d0108d2a7 --- /dev/null +++ b/source/platform/posix/cocoa/i_main.mm @@ -0,0 +1,519 @@ +/* + ** i_main.mm + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2018 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include "i_common.h" +#include "s_sound.h" + +#include + +#include "c_console.h" +#include "c_cvars.h" +#include "cmdlib.h" +#include "d_main.h" +#include "i_system.h" +#include "m_argv.h" +#include "st_console.h" +#include "version.h" +#include "doomerrors.h" +#include "s_music.h" + + +#define ZD_UNUSED(VARIABLE) ((void)(VARIABLE)) + + +// --------------------------------------------------------------------------- + + +CVAR (Bool, i_soundinbackground, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) +EXTERN_CVAR(Int, vid_defwidth ) +EXTERN_CVAR(Int, vid_defheight) +EXTERN_CVAR(Bool, vid_vsync ) + + +// --------------------------------------------------------------------------- + + +void Mac_I_FatalError(const char* const message) +{ + I_SetMainWindowVisible(false); + S_StopMusic(true); + + FConsoleWindow::GetInstance().ShowFatalError(message); +} + + +#if MAC_OS_X_VERSION_MAX_ALLOWED < 101000 + +// Available since 10.9 with no public declaration/definition until 10.10 + +struct NSOperatingSystemVersion +{ + NSInteger majorVersion; + NSInteger minorVersion; + NSInteger patchVersion; +}; + +@interface NSProcessInfo(OperatingSystemVersion) +- (NSOperatingSystemVersion)operatingSystemVersion; +@end + +#endif // before 10.10 + +void I_DetectOS() +{ + NSOperatingSystemVersion version = {}; + NSProcessInfo* const processInfo = [NSProcessInfo processInfo]; + + if ([processInfo respondsToSelector:@selector(operatingSystemVersion)]) + { + version = [processInfo operatingSystemVersion]; + } + + const char* name = "Unknown version"; + + if (10 == version.majorVersion) switch (version.minorVersion) + { + case 7: name = "Mac OS X Lion"; break; + case 8: name = "OS X Mountain Lion"; break; + case 9: name = "OS X Mavericks"; break; + case 10: name = "OS X Yosemite"; break; + case 11: name = "OS X El Capitan"; break; + case 12: name = "macOS Sierra"; break; + case 13: name = "macOS High Sierra"; break; + case 14: name = "macOS Mojave"; break; + case 15: name = "macOS Catalina"; break; + } + + char release[16] = "unknown"; + size_t size = sizeof release - 1; + sysctlbyname("kern.osversion", release, &size, nullptr, 0); + + char model[64] = "Unknown Mac model"; + size = sizeof model - 1; + sysctlbyname("hw.model", model, &size, nullptr, 0); + + const char* const architecture = +#ifdef __i386__ + "32-bit Intel"; +#elif defined __x86_64__ + "64-bit Intel"; +#else + "Unknown"; +#endif + + Printf("%s running %s %d.%d.%d (%s) %s\n", model, name, + int(version.majorVersion), int(version.minorVersion), int(version.patchVersion), + release, architecture); +} + + +FArgs* Args; // command line arguments + + +namespace +{ + +TArray s_argv; + +int DoMain(int argc, char** argv) +{ + printf(GAMENAME" %s - %s - Cocoa version\nCompiled on %s\n\n", + GetVersionString(), GetGitTime(), __DATE__); + + seteuid(getuid()); + + // Set LC_NUMERIC environment variable in case some library decides to + // clear the setlocale call at least this will be correct. + // Note that the LANG environment variable is overridden by LC_* + setenv("LC_NUMERIC", "C", 1); + setlocale(LC_ALL, "C"); + + // Set reasonable default values for video settings + + const NSSize screenSize = [[NSScreen mainScreen] frame].size; + vid_defwidth = static_cast(screenSize.width); + vid_defheight = static_cast(screenSize.height); + vid_vsync = true; + + Args = new FArgs(argc, argv); + + NSString* exePath = [[NSBundle mainBundle] executablePath]; + progdir = [[exePath stringByDeletingLastPathComponent] UTF8String]; + progdir += "/"; + + auto ret = D_DoomMain(); + FConsoleWindow::DeleteInstance(); + return ret; +} + +} // unnamed namespace + + +// --------------------------------------------------------------------------- + + +@interface ApplicationController : NSResponder +{ +} + +- (void)keyDown:(NSEvent*)theEvent; +- (void)keyUp:(NSEvent*)theEvent; + +- (void)applicationDidBecomeActive:(NSNotification*)aNotification; +- (void)applicationWillResignActive:(NSNotification*)aNotification; + +- (void)applicationDidFinishLaunching:(NSNotification*)aNotification; + +- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename; + +- (void)processEvents:(NSTimer*)timer; + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; + +- (void)sendExitEvent:(id)sender; + +@end + + +ApplicationController* appCtrl; + + +@implementation ApplicationController + +- (void)keyDown:(NSEvent*)theEvent +{ + // Empty but present to avoid playing of 'beep' alert sound + + ZD_UNUSED(theEvent); +} + +- (void)keyUp:(NSEvent*)theEvent +{ + // Empty but present to avoid playing of 'beep' alert sound + + ZD_UNUSED(theEvent); +} + + +extern bool AppActive; + +- (void)applicationDidBecomeActive:(NSNotification*)aNotification +{ + ZD_UNUSED(aNotification); + + S_SetSoundPaused(1); + + AppActive = true; +} + +- (void)applicationWillResignActive:(NSNotification*)aNotification +{ + ZD_UNUSED(aNotification); + + S_SetSoundPaused(i_soundinbackground); + + AppActive = false; +} + + +- (void)applicationDidFinishLaunching:(NSNotification*)aNotification +{ + // When starting from command line with real executable path, e.g. ZDoom.app/Contents/MacOS/ZDoom + // application remains deactivated for an unknown reason. + // The following call resolves this issue + [NSApp activateIgnoringOtherApps:YES]; + + // Setup timer for custom event loop + + NSTimer* timer = [NSTimer timerWithTimeInterval:0 + target:self + selector:@selector(processEvents:) + userInfo:nil + repeats:YES]; + [[NSRunLoop currentRunLoop] addTimer:timer + forMode:NSDefaultRunLoopMode]; + + FConsoleWindow::CreateInstance(); + + const size_t argc = s_argv.Size(); + TArray argv(argc + 1, true); + + for (size_t i = 0; i < argc; ++i) + { + argv[i] = s_argv[i].LockBuffer(); + } + + argv[argc] = nullptr; + + exit(DoMain(argc, &argv[0])); +} + + +- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename +{ + ZD_UNUSED(theApplication); + + // Some parameters from command line are passed to this function + // These parameters need to be skipped to avoid duplication + // Note: SDL has different approach to fix this issue, see the same method in SDLMain.m + + const char* const charFileName = [filename UTF8String]; + + for (size_t i = 0, count = s_argv.Size(); i < count; ++i) + { + if (0 == strcmp(s_argv[i], charFileName)) + { + return FALSE; + } + } + + bool iwad = false; + + if (const char* const extPos = strrchr(charFileName, '.')) + { + iwad = 0 == stricmp(extPos, ".iwad") + || 0 == stricmp(extPos, ".ipk3") + || 0 == stricmp(extPos, ".ipk7"); + } + + s_argv.Push(iwad ? "-iwad" : "-file"); + s_argv.Push(charFileName); + + return TRUE; +} + + +- (void)processEvents:(NSTimer*)timer +{ + ZD_UNUSED(timer); + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + while (true) + { + NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask + untilDate:[NSDate dateWithTimeIntervalSinceNow:0] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + if (nil == event) + { + break; + } + + I_ProcessEvent(event); + + [NSApp sendEvent:event]; + } + + [NSApp updateWindows]; + + [pool release]; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender +{ + [self sendExitEvent:sender]; + return NSTerminateLater; +} + +- (void)sendExitEvent:(id)sender +{ + throw CExitEvent(0); +} + +@end + + +// --------------------------------------------------------------------------- + + +namespace +{ + +NSMenuItem* CreateApplicationMenu() +{ + NSMenu* menu = [NSMenu new]; + + [menu addItemWithTitle:[@"About " stringByAppendingString:@GAMENAME] + action:@selector(orderFrontStandardAboutPanel:) + keyEquivalent:@""]; + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItemWithTitle:[@"Hide " stringByAppendingString:@GAMENAME] + action:@selector(hide:) + keyEquivalent:@"h"]; + [[menu addItemWithTitle:@"Hide Others" + action:@selector(hideOtherApplications:) + keyEquivalent:@"h"] + setKeyEquivalentModifierMask:NSAlternateKeyMask | NSCommandKeyMask]; + [menu addItemWithTitle:@"Show All" + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItemWithTitle:[@"Quit " stringByAppendingString:@GAMENAME] + action:@selector(sendExitEvent:) + keyEquivalent:@"q"]; + + NSMenuItem* menuItem = [NSMenuItem new]; + [menuItem setSubmenu:menu]; + + if ([NSApp respondsToSelector:@selector(setAppleMenu:)]) + { + [NSApp performSelector:@selector(setAppleMenu:) withObject:menu]; + } + + return menuItem; +} + +NSMenuItem* CreateEditMenu() +{ + NSMenu* menu = [[NSMenu alloc] initWithTitle:@"Edit"]; + + [menu addItemWithTitle:@"Undo" + action:@selector(undo:) + keyEquivalent:@"z"]; + [menu addItemWithTitle:@"Redo" + action:@selector(redo:) + keyEquivalent:@"Z"]; + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItemWithTitle:@"Cut" + action:@selector(cut:) + keyEquivalent:@"x"]; + [menu addItemWithTitle:@"Copy" + action:@selector(copy:) + keyEquivalent:@"c"]; + [menu addItemWithTitle:@"Paste" + action:@selector(paste:) + keyEquivalent:@"v"]; + [menu addItemWithTitle:@"Delete" + action:@selector(delete:) + keyEquivalent:@""]; + [menu addItemWithTitle:@"Select All" + action:@selector(selectAll:) + keyEquivalent:@"a"]; + + NSMenuItem* menuItem = [NSMenuItem new]; + [menuItem setSubmenu:menu]; + + return menuItem; +} + +NSMenuItem* CreateWindowMenu() +{ + NSMenu* menu = [[NSMenu alloc] initWithTitle:@"Window"]; + [NSApp setWindowsMenu:menu]; + + [menu addItemWithTitle:@"Minimize" + action:@selector(performMiniaturize:) + keyEquivalent:@"m"]; + [menu addItemWithTitle:@"Zoom" + action:@selector(performZoom:) + keyEquivalent:@""]; + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItemWithTitle:@"Bring All to Front" + action:@selector(arrangeInFront:) + keyEquivalent:@""]; + + NSMenuItem* menuItem = [NSMenuItem new]; + [menuItem setSubmenu:menu]; + + return menuItem; +} + +void CreateMenu() +{ + NSMenu* menuBar = [NSMenu new]; + [menuBar addItem:CreateApplicationMenu()]; + [menuBar addItem:CreateEditMenu()]; + [menuBar addItem:CreateWindowMenu()]; + + [NSApp setMainMenu:menuBar]; +} + +void ReleaseApplicationController() +{ + if (NULL != appCtrl) + { + [NSApp setDelegate:nil]; + [NSApp deactivate]; + + [appCtrl release]; + appCtrl = NULL; + } +} + +} // unnamed namespace + + +int main(int argc, char** argv) +{ + for (int i = 0; i < argc; ++i) + { + const char* const argument = argv[i]; + +#if _DEBUG + if (0 == strcmp(argument, "-wait_for_debugger")) + { + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@GAMENAME]; + [alert setInformativeText:@"Waiting for debugger..."]; + [alert addButtonWithTitle:@"Continue"]; + [alert runModal]; + } +#endif // _DEBUG + + s_argv.Push(argument); + } + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + [NSApplication sharedApplication]; + + // The following code isn't mandatory, + // but it enables to run the application without a bundle + if ([NSApp respondsToSelector:@selector(setActivationPolicy:)]) + { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + } + + CreateMenu(); + + atexit(ReleaseApplicationController); + + appCtrl = [ApplicationController new]; + [NSApp setDelegate:appCtrl]; + [NSApp run]; + + [pool release]; + + return EXIT_SUCCESS; +} diff --git a/source/platform/posix/cocoa/i_system.mm b/source/platform/posix/cocoa/i_system.mm new file mode 100644 index 000000000..e28443490 --- /dev/null +++ b/source/platform/posix/cocoa/i_system.mm @@ -0,0 +1,304 @@ +/* + ** i_system.mm + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2018 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include "i_common.h" + +#include +#include + +#include "d_protocol.h" +#include "doomdef.h" +#include "doomerrors.h" +#include "doomstat.h" +#include "g_game.h" +#include "gameconfigfile.h" +#include "i_sound.h" +#include "i_system.h" +#include "st_console.h" +#include "v_text.h" +#include "x86.h" +#include "cmdlib.h" + + +void I_Tactile(int /*on*/, int /*off*/, int /*total*/) +{ +} + + +ticcmd_t* I_BaseTiccmd() +{ + static ticcmd_t emptycmd; + return &emptycmd; +} + + + +double PerfToSec, PerfToMillisec; + +static void CalculateCPUSpeed() +{ + long long frequency; + size_t size = sizeof frequency; + + if (0 == sysctlbyname("machdep.tsc.frequency", &frequency, &size, nullptr, 0) && 0 != frequency) + { + PerfToSec = 1.0 / frequency; + PerfToMillisec = 1000.0 / frequency; + + if (!batchrun) + { + Printf("CPU speed: %.0f MHz\n", 0.001 / PerfToMillisec); + } + } +} + +void I_Init(void) +{ + CheckCPUID(&CPU); + CalculateCPUSpeed(); + DumpCPUInfo(&CPU); +} + +void I_SetIWADInfo() +{ +} + + +void I_DebugPrint(const char *cp) +{ + NSLog(@"%s", cp); +} + + +void I_PrintStr(const char* const message) +{ + FConsoleWindow::GetInstance().AddText(message); + + // Strip out any color escape sequences before writing to output + char* const copy = new char[strlen(message) + 1]; + const char* srcp = message; + char* dstp = copy; + + while ('\0' != *srcp) + { + if (TEXTCOLOR_ESCAPE == *srcp) + { + if ('\0' != srcp[1]) + { + srcp += 2; + } + else + { + break; + } + } + else if (0x1d == *srcp || 0x1f == *srcp) // Opening and closing bar character + { + *dstp++ = '-'; + ++srcp; + } + else if (0x1e == *srcp) // Middle bar character + { + *dstp++ = '='; + ++srcp; + } + else + { + *dstp++ = *srcp++; + } + } + + *dstp = '\0'; + + fputs(copy, stdout); + delete[] copy; + fflush(stdout); +} + + +void Mac_I_FatalError(const char* const message); + +void I_ShowFatalError(const char *message) +{ + Mac_I_FatalError(message); +} + + +int I_PickIWad(WadStuff* const wads, const int numwads, const bool showwin, const int defaultiwad) +{ + if (!showwin) + { + return defaultiwad; + } + + I_SetMainWindowVisible(false); + + extern int I_PickIWad_Cocoa(WadStuff*, int, bool, int); + const int result = I_PickIWad_Cocoa(wads, numwads, showwin, defaultiwad); + + I_SetMainWindowVisible(true); + + return result; +} + + +bool I_WriteIniFailed() +{ + printf("The config file %s could not be saved:\n%s\n", GameConfig->GetPathName(), strerror(errno)); + return false; // return true to retry +} + + +static const char *pattern; + +#if MAC_OS_X_VERSION_MAX_ALLOWED < 1080 +static int matchfile(struct dirent *ent) +#else +static int matchfile(const struct dirent *ent) +#endif +{ + return fnmatch(pattern, ent->d_name, FNM_NOESCAPE) == 0; +} + +void* I_FindFirst(const char* const filespec, findstate_t* const fileinfo) +{ + FString dir; + + const char* const slash = strrchr(filespec, '/'); + + if (slash) + { + pattern = slash+1; + dir = FString(filespec, slash - filespec + 1); + } + else + { + pattern = filespec; + dir = "."; + } + + fileinfo->current = 0; + fileinfo->count = scandir(dir.GetChars(), &fileinfo->namelist, matchfile, alphasort); + + if (fileinfo->count > 0) + { + return fileinfo; + } + + return (void*)-1; +} + +int I_FindNext(void* const handle, findstate_t* const fileinfo) +{ + findstate_t* const state = static_cast(handle); + + if (state->current < fileinfo->count) + { + return ++state->current < fileinfo->count ? 0 : -1; + } + + return -1; +} + +int I_FindClose(void* const handle) +{ + findstate_t* const state = static_cast(handle); + + if (handle != (void*)-1 && state->count > 0) + { + for (int i = 0; i < state->count; ++i) + { + free(state->namelist[i]); + } + + free(state->namelist); + state->namelist = NULL; + state->count = 0; + } + + return 0; +} + +int I_FindAttr(findstate_t* const fileinfo) +{ + dirent* const ent = fileinfo->namelist[fileinfo->current]; + bool isdir; + + if (DirEntryExists(ent->d_name, &isdir)) + { + return isdir ? FA_DIREC : 0; + } + + return 0; +} + + +void I_PutInClipboard(const char* const string) +{ + NSPasteboard* const pasteBoard = [NSPasteboard generalPasteboard]; + NSString* const stringType = NSStringPboardType; + NSArray* const types = [NSArray arrayWithObjects:stringType, nil]; + NSString* const content = [NSString stringWithUTF8String:string]; + + [pasteBoard declareTypes:types + owner:nil]; + [pasteBoard setString:content + forType:stringType]; +} + +FString I_GetFromClipboard(bool returnNothing) +{ + if (returnNothing) + { + return FString(); + } + + NSPasteboard* const pasteBoard = [NSPasteboard generalPasteboard]; + NSString* const value = [pasteBoard stringForType:NSStringPboardType]; + + return FString([value UTF8String]); +} + + +unsigned int I_MakeRNGSeed() +{ + return static_cast(arc4random()); +} + + +TArray I_GetGogPaths() +{ + // GOG's Doom games are Windows only at the moment + return TArray(); +} + diff --git a/source/platform/posix/cocoa/i_video.mm b/source/platform/posix/cocoa/i_video.mm new file mode 100644 index 000000000..b06fb0bb8 --- /dev/null +++ b/source/platform/posix/cocoa/i_video.mm @@ -0,0 +1,1072 @@ +/* + ** i_video.mm + ** + **--------------------------------------------------------------------------- + ** Copyright 2012-2018 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include "gl_load/gl_load.h" + +#ifdef HAVE_VULKAN +#define VK_USE_PLATFORM_MACOS_MVK +#define VK_USE_PLATFORM_METAL_EXT +#include "volk/volk.h" +#endif + +#include "i_common.h" + +#include "v_video.h" +#include "bitmap.h" +#include "c_dispatch.h" +#include "doomstat.h" +#include "hardware.h" +#include "i_system.h" +#include "m_argv.h" +#include "m_png.h" +#include "swrenderer/r_swrenderer.h" +#include "st_console.h" +#include "v_text.h" +#include "version.h" +#include "doomerrors.h" + +#include "gl/system/gl_framebuffer.h" +#include "vulkan/system/vk_framebuffer.h" +#include "rendering/polyrenderer/backend/poly_framebuffer.h" + + +@implementation NSWindow(ExitAppOnClose) + +- (void)exitAppOnClose +{ + NSButton* closeButton = [self standardWindowButton:NSWindowCloseButton]; + [closeButton setAction:@selector(sendExitEvent:)]; + [closeButton setTarget:[NSApp delegate]]; +} + +@end + +@interface NSWindow(EnterFullscreenOnZoom) +- (void)enterFullscreenOnZoom; +@end + +@implementation NSWindow(EnterFullscreenOnZoom) + +- (void)enterFullscreen:(id)sender +{ + ToggleFullscreen = true; +} + +- (void)enterFullscreenOnZoom +{ + NSButton* zoomButton = [self standardWindowButton:NSWindowZoomButton]; + [zoomButton setEnabled:YES]; + [zoomButton setAction:@selector(enterFullscreen:)]; + [zoomButton setTarget:self]; +} + +@end + +EXTERN_CVAR(Bool, vid_hidpi) +EXTERN_CVAR(Int, vid_defwidth) +EXTERN_CVAR(Int, vid_defheight) +EXTERN_CVAR(Int, vid_preferbackend) +EXTERN_CVAR(Bool, vk_debug) + +CVAR(Bool, mvk_debug, false, 0) + +CUSTOM_CVAR(Bool, vid_autoswitch, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_NOINITCALL) +{ + Printf("You must restart " GAMENAME " to apply graphics switching mode\n"); +} + + +// --------------------------------------------------------------------------- + + +namespace +{ + const NSInteger LEVEL_FULLSCREEN = NSMainMenuWindowLevel + 1; + const NSInteger LEVEL_WINDOWED = NSNormalWindowLevel; + + const NSUInteger STYLE_MASK_FULLSCREEN = NSBorderlessWindowMask; + const NSUInteger STYLE_MASK_WINDOWED = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask; +} + + +// --------------------------------------------------------------------------- + + +@interface CocoaWindow : NSWindow +{ + NSString* m_title; +} + +- (BOOL)canBecomeKeyWindow; +- (void)setTitle:(NSString*)title; +- (void)updateTitle; + +@end + + +@implementation CocoaWindow + +- (BOOL)canBecomeKeyWindow +{ + return true; +} + +- (void)setTitle:(NSString*)title +{ + m_title = title; + + [self updateTitle]; +} + +- (void)updateTitle +{ + if (nil == m_title) + { + m_title = [NSString stringWithFormat:@"%s %s", GAMESIG, GetVersionString()]; + } + + [super setTitle:m_title]; +} + +- (void)frameDidChange:(NSNotification*)notification +{ + const NSRect frame = [self frame]; + win_x = frame.origin.x; + win_y = frame.origin.y; + win_w = frame.size.width; + win_h = frame.size.height; +} + +@end + + +// --------------------------------------------------------------------------- + + +@interface OpenGLCocoaView : NSOpenGLView +{ + NSCursor* m_cursor; +} + +- (void)setCursor:(NSCursor*)cursor; + +@end + + +@implementation OpenGLCocoaView + +- (void)drawRect:(NSRect)dirtyRect +{ + [NSColor.blackColor setFill]; + NSRectFill(dirtyRect); +} + +- (void)resetCursorRects +{ + [super resetCursorRects]; + + NSCursor* const cursor = nil == m_cursor + ? [NSCursor arrowCursor] + : m_cursor; + + [self addCursorRect:[self bounds] + cursor:cursor]; +} + +- (void)setCursor:(NSCursor*)cursor +{ + m_cursor = cursor; +} + +@end + + +// --------------------------------------------------------------------------- + + +@interface VulkanCocoaView : NSView +{ + NSCursor* m_cursor; +} + +- (void)setCursor:(NSCursor*)cursor; + +@end + + +@implementation VulkanCocoaView + +- (void)resetCursorRects +{ + [super resetCursorRects]; + + NSCursor* const cursor = nil == m_cursor + ? [NSCursor arrowCursor] + : m_cursor; + + [self addCursorRect:[self bounds] + cursor:cursor]; +} + +- (void)setCursor:(NSCursor*)cursor +{ + m_cursor = cursor; +} + ++(Class) layerClass +{ + return NSClassFromString(@"CAMetalLayer"); +} + +-(CALayer*) makeBackingLayer +{ + return [self.class.layerClass layer]; +} + +-(BOOL) isOpaque +{ + return YES; +} + +@end + + +// --------------------------------------------------------------------------- + + +extern id appCtrl; + + +namespace +{ + +CocoaWindow* CreateWindow(const NSUInteger styleMask) +{ + CocoaWindow* const window = [CocoaWindow alloc]; + [window initWithContentRect:NSMakeRect(0, 0, vid_defwidth, vid_defheight) + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + [window setOpaque:YES]; + [window makeFirstResponder:appCtrl]; + [window setAcceptsMouseMovedEvents:YES]; + + NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:window + selector:@selector(frameDidChange:) + name:NSWindowDidEndLiveResizeNotification + object:nil]; + [nc addObserver:window + selector:@selector(frameDidChange:) + name:NSWindowDidMoveNotification + object:nil]; + + return window; +} + +enum class OpenGLProfile +{ + Core, + Legacy +}; + +NSOpenGLPixelFormat* CreatePixelFormat(const OpenGLProfile profile) +{ + NSOpenGLPixelFormatAttribute attributes[16]; + size_t i = 0; + + attributes[i++] = NSOpenGLPFADoubleBuffer; + attributes[i++] = NSOpenGLPFAColorSize; + attributes[i++] = NSOpenGLPixelFormatAttribute(32); + attributes[i++] = NSOpenGLPFADepthSize; + attributes[i++] = NSOpenGLPixelFormatAttribute(24); + attributes[i++] = NSOpenGLPFAStencilSize; + attributes[i++] = NSOpenGLPixelFormatAttribute(8); + + if (profile == OpenGLProfile::Core) + { + attributes[i++] = NSOpenGLPFAOpenGLProfile; + attributes[i++] = NSOpenGLProfileVersion3_2Core; + } + + if (!vid_autoswitch) + { + attributes[i++] = NSOpenGLPFAAllowOfflineRenderers; + } + + attributes[i] = NSOpenGLPixelFormatAttribute(0); + + assert(i < sizeof attributes / sizeof attributes[0]); + + return [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes]; +} + +void SetupOpenGLView(CocoaWindow* const window, const OpenGLProfile profile) +{ + NSOpenGLPixelFormat* pixelFormat = CreatePixelFormat(profile); + + if (nil == pixelFormat) + { + I_FatalError("Cannot create OpenGL pixel format, graphics hardware is not supported"); + } + + // Create OpenGL context and view + + const NSRect contentRect = [window contentRectForFrameRect:[window frame]]; + OpenGLCocoaView* glView = [[OpenGLCocoaView alloc] initWithFrame:contentRect + pixelFormat:pixelFormat]; + [[glView openGLContext] makeCurrentContext]; + + [window setContentView:glView]; +} + +} // unnamed namespace + + +// --------------------------------------------------------------------------- + + +class CocoaVideo : public IVideo +{ +public: + CocoaVideo() + { + ms_isVulkanEnabled = vid_preferbackend == 1 && NSAppKitVersionNumber >= 1404; // NSAppKitVersionNumber10_11 + } + + ~CocoaVideo() + { +#ifdef HAVE_VULKAN + delete m_vulkanDevice; +#endif + ms_window = nil; + } + + virtual DFrameBuffer* CreateFrameBuffer() override + { + assert(ms_window == nil); + ms_window = CreateWindow(STYLE_MASK_WINDOWED); + + const NSRect contentRect = [ms_window contentRectForFrameRect:[ms_window frame]]; + SystemBaseFrameBuffer *fb = nullptr; + +#ifdef HAVE_VULKAN + if (ms_isVulkanEnabled) + { + NSView* vulkanView = [[VulkanCocoaView alloc] initWithFrame:contentRect]; + vulkanView.wantsLayer = YES; + vulkanView.layer.backgroundColor = NSColor.blackColor.CGColor; + + [ms_window setContentView:vulkanView]; + + // See vk_mvk_moltenvk.h for comprehensive explanation of configuration options set below + // https://github.com/KhronosGroup/MoltenVK/blob/master/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h + + if (vk_debug) + { + // Output errors and informational messages + setenv("MVK_CONFIG_LOG_LEVEL", "2", 0); + + if (mvk_debug) + { + // Extensive MoltenVK logging, too spammy even for vk_debug CVAR + setenv("MVK_DEBUG", "1", 0); + } + } + else + { + // Limit MoltenVK logging to errors only + setenv("MVK_CONFIG_LOG_LEVEL", "1", 0); + } + + if (!vid_autoswitch) + { + // CVAR from pre-Vulkan era has a priority over vk_device selection + setenv("MVK_CONFIG_FORCE_LOW_POWER_GPU", "1", 0); + } + + // The following settings improve performance like suggested at + // https://github.com/KhronosGroup/MoltenVK/issues/581#issuecomment-487293665 + setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0); + setenv("MVK_CONFIG_PRESENT_WITH_COMMAND_BUFFER", "0", 0); + + try + { + m_vulkanDevice = new VulkanDevice(); + fb = new VulkanFrameBuffer(nullptr, fullscreen, m_vulkanDevice); + } + catch (std::exception const&) + { + ms_isVulkanEnabled = false; + + SetupOpenGLView(ms_window, OpenGLProfile::Core); + } + } + else +#endif + if (vid_preferbackend == 2) + { + SetupOpenGLView(ms_window, OpenGLProfile::Legacy); + + fb = new PolyFrameBuffer(nullptr, fullscreen); + } + else + { + SetupOpenGLView(ms_window, OpenGLProfile::Core); + } + + if (fb == nullptr) + { + fb = new OpenGLRenderer::OpenGLFrameBuffer(0, fullscreen); + } + + fb->SetWindow(ms_window); + fb->SetMode(fullscreen, vid_hidpi); + fb->SetSize(fb->GetClientWidth(), fb->GetClientHeight()); + + // This lame hack is a temporary workaround for strange performance issues + // with fullscreen window and Core Animation's Metal layer + // It is somehow related to initial window level and flags + // Toggling fullscreen -> window -> fullscreen mysteriously solves the problem + if (ms_isVulkanEnabled && fullscreen) + { + fb->SetMode(false, vid_hidpi); + fb->SetMode(true, vid_hidpi); + } + + return fb; + } + + static CocoaWindow* GetWindow() + { + return ms_window; + } + +private: + VulkanDevice *m_vulkanDevice = nullptr; + + static CocoaWindow* ms_window; + + static bool ms_isVulkanEnabled; +}; + + +CocoaWindow* CocoaVideo::ms_window; + +bool CocoaVideo::ms_isVulkanEnabled; + + +// --------------------------------------------------------------------------- + + +static SystemBaseFrameBuffer* frameBuffer; + + +SystemBaseFrameBuffer::SystemBaseFrameBuffer(void*, const bool fullscreen) +: DFrameBuffer(vid_defwidth, vid_defheight) +, m_fullscreen(false) +, m_hiDPI(false) +, m_window(nullptr) +{ + assert(frameBuffer == nullptr); + frameBuffer = this; + + FConsoleWindow::GetInstance().Show(false); +} + +SystemBaseFrameBuffer::~SystemBaseFrameBuffer() +{ + assert(frameBuffer == this); + frameBuffer = nullptr; + + NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:m_window + name:NSWindowDidMoveNotification + object:nil]; + [nc removeObserver:m_window + name:NSWindowDidEndLiveResizeNotification + object:nil]; +} + +bool SystemBaseFrameBuffer::IsFullscreen() +{ + return m_fullscreen; +} + +void SystemBaseFrameBuffer::ToggleFullscreen(bool yes) +{ + SetMode(yes, m_hiDPI); +} + +void SystemBaseFrameBuffer::SetWindowSize(int width, int height) +{ + if (width < VID_MIN_WIDTH || height < VID_MIN_HEIGHT) + { + return; + } + + if (fullscreen) + { + // Enter windowed mode in order to calculate title bar height + fullscreen = false; + SetMode(false, m_hiDPI); + } + + win_w = width; + win_h = height + GetTitleBarHeight(); + + SetMode(false, m_hiDPI); + + [m_window center]; +} + +int SystemBaseFrameBuffer::GetTitleBarHeight() const +{ + const NSRect windowFrame = [m_window frame]; + const NSRect contentFrame = [m_window contentRectForFrameRect:windowFrame]; + const int titleBarHeight = windowFrame.size.height - contentFrame.size.height; + + return titleBarHeight; +} + + +int SystemBaseFrameBuffer::GetClientWidth() +{ + const int clientWidth = I_GetContentViewSize(m_window).width; + return clientWidth > 0 ? clientWidth : GetWidth(); +} + +int SystemBaseFrameBuffer::GetClientHeight() +{ + const int clientHeight = I_GetContentViewSize(m_window).height; + return clientHeight > 0 ? clientHeight : GetHeight(); +} + + +void SystemBaseFrameBuffer::SetFullscreenMode() +{ + if (!m_fullscreen) + { + [m_window setLevel:LEVEL_FULLSCREEN]; + [m_window setStyleMask:STYLE_MASK_FULLSCREEN]; + + [m_window setHidesOnDeactivate:YES]; + } + + const NSRect screenFrame = [[m_window screen] frame]; + [m_window setFrame:screenFrame display:YES]; +} + +void SystemBaseFrameBuffer::SetWindowedMode() +{ + if (m_fullscreen) + { + [m_window setLevel:LEVEL_WINDOWED]; + [m_window setStyleMask:STYLE_MASK_WINDOWED]; + + [m_window setHidesOnDeactivate:NO]; + } + + const int minimumFrameWidth = VID_MIN_WIDTH; + const int minimumFrameHeight = VID_MIN_HEIGHT + GetTitleBarHeight(); + const NSSize minimumFrameSize = NSMakeSize(minimumFrameWidth, minimumFrameHeight); + [m_window setMinSize:minimumFrameSize]; + + const bool isFrameValid = win_x != -1 && win_y != -1 + && win_w >= minimumFrameWidth && win_h >= minimumFrameHeight; + + if (!isFrameValid) + { + const NSRect screenSize = [[NSScreen mainScreen] frame]; + win_x = screenSize.origin.x + screenSize.size.width / 10; + win_y = screenSize.origin.y + screenSize.size.height / 10; + win_w = screenSize.size.width * 8 / 10; + win_h = screenSize.size.height * 8 / 10 + GetTitleBarHeight(); + } + + const NSRect frameSize = NSMakeRect(win_x, win_y, win_w, win_h); + [m_window setFrame:frameSize display:YES]; + [m_window enterFullscreenOnZoom]; + [m_window exitAppOnClose]; +} + +void SystemBaseFrameBuffer::SetMode(const bool fullscreen, const bool hiDPI) +{ + if ([m_window.contentView isKindOfClass:[OpenGLCocoaView class]]) + { + NSOpenGLView* const glView = [m_window contentView]; + [glView setWantsBestResolutionOpenGLSurface:hiDPI]; + } + else + { + assert(m_window.screen != nil); + assert(m_window.contentView.layer != nil); + [m_window.contentView layer].contentsScale = hiDPI ? m_window.screen.backingScaleFactor : 1.0; + } + + if (fullscreen) + { + SetFullscreenMode(); + } + else + { + SetWindowedMode(); + } + + [m_window updateTitle]; + + if (![m_window isKeyWindow]) + { + [m_window makeKeyAndOrderFront:nil]; + } + + m_fullscreen = fullscreen; + m_hiDPI = hiDPI; +} + + +void SystemBaseFrameBuffer::UseHiDPI(const bool hiDPI) +{ + if (frameBuffer != nullptr) + { + frameBuffer->SetMode(frameBuffer->m_fullscreen, hiDPI); + } +} + +void SystemBaseFrameBuffer::SetCursor(NSCursor* cursor) +{ + if (frameBuffer != nullptr) + { + NSWindow* const window = frameBuffer->m_window; + id view = [window contentView]; + + [view setCursor:cursor]; + [window invalidateCursorRectsForView:view]; + } +} + +void SystemBaseFrameBuffer::SetWindowVisible(bool visible) +{ + if (frameBuffer != nullptr) + { + if (visible) + { + [frameBuffer->m_window orderFront:nil]; + } + else + { + [frameBuffer->m_window orderOut:nil]; + } + + I_SetNativeMouse(!visible); + } +} + +void SystemBaseFrameBuffer::SetWindowTitle(const char* title) +{ + if (frameBuffer != nullptr) + { + NSString* const nsTitle = nullptr == title ? nil : + [NSString stringWithCString:title encoding:NSISOLatin1StringEncoding]; + [frameBuffer->m_window setTitle:nsTitle]; + } +} + + +// --------------------------------------------------------------------------- + + +SystemGLFrameBuffer::SystemGLFrameBuffer(void *hMonitor, bool fullscreen) +: SystemBaseFrameBuffer(hMonitor, fullscreen) +{ +} + + +void SystemGLFrameBuffer::SetVSync(bool vsync) +{ + const GLint value = vsync ? 1 : 0; + + [[NSOpenGLContext currentContext] setValues:&value + forParameter:NSOpenGLCPSwapInterval]; +} + + +void SystemGLFrameBuffer::SetMode(const bool fullscreen, const bool hiDPI) +{ + NSOpenGLView* const glView = [m_window contentView]; + [glView setWantsBestResolutionOpenGLSurface:hiDPI]; + + if (fullscreen) + { + SetFullscreenMode(); + } + else + { + SetWindowedMode(); + } + + [m_window updateTitle]; + + if (![m_window isKeyWindow]) + { + [m_window makeKeyAndOrderFront:nil]; + } + + m_fullscreen = fullscreen; + m_hiDPI = hiDPI; +} + + +void SystemGLFrameBuffer::SwapBuffers() +{ + [[NSOpenGLContext currentContext] flushBuffer]; +} + + +// --------------------------------------------------------------------------- + + +IVideo* Video; + + +// --------------------------------------------------------------------------- + + +void I_ShutdownGraphics() +{ + if (NULL != screen) + { + delete screen; + screen = NULL; + } + + delete Video; + Video = NULL; +} + +void I_InitGraphics() +{ + Video = new CocoaVideo; +} + + +// --------------------------------------------------------------------------- + +CUSTOM_CVAR(Bool, vid_hidpi, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_NOINITCALL) +{ + SystemBaseFrameBuffer::UseHiDPI(self); +} + + +// --------------------------------------------------------------------------- + + +bool I_SetCursor(FTexture *cursorpic) +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSCursor* cursor = nil; + + if (NULL != cursorpic && cursorpic->isValid()) + { + // Create bitmap image representation + + auto sbuffer = cursorpic->CreateTexBuffer(0); + + const NSInteger imageWidth = sbuffer.mWidth; + const NSInteger imageHeight = sbuffer.mHeight; + const NSInteger imagePitch = sbuffer.mWidth * 4; + + NSBitmapImageRep* bitmapImageRep = [NSBitmapImageRep alloc]; + [bitmapImageRep initWithBitmapDataPlanes:NULL + pixelsWide:imageWidth + pixelsHigh:imageHeight + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:imagePitch + bitsPerPixel:0]; + + // Load bitmap data to representation + + uint8_t* buffer = [bitmapImageRep bitmapData]; + memcpy(buffer, sbuffer.mBuffer, imagePitch * imageHeight); + + // Swap red and blue components in each pixel + + for (size_t i = 0; i < size_t(imageWidth * imageHeight); ++i) + { + const size_t offset = i * 4; + std::swap(buffer[offset ], buffer[offset + 2]); + } + + // Create image from representation and set it as cursor + + NSData* imageData = [bitmapImageRep representationUsingType:NSPNGFileType + properties:[NSDictionary dictionary]]; + NSImage* cursorImage = [[NSImage alloc] initWithData:imageData]; + + cursor = [[NSCursor alloc] initWithImage:cursorImage + hotSpot:NSMakePoint(0.0f, 0.0f)]; + } + + SystemBaseFrameBuffer::SetCursor(cursor); + + [pool release]; + + return true; +} + + +NSSize I_GetContentViewSize(const NSWindow* const window) +{ + const NSView* const view = [window contentView]; + const NSSize frameSize = [view frame].size; + + return (vid_hidpi) + ? [view convertSizeToBacking:frameSize] + : frameSize; +} + +void I_SetMainWindowVisible(bool visible) +{ + SystemBaseFrameBuffer::SetWindowVisible(visible); +} + +// each platform has its own specific version of this function. +void I_SetWindowTitle(const char* title) +{ + SystemBaseFrameBuffer::SetWindowTitle(title); +} + + +#ifdef HAVE_VULKAN +void I_GetVulkanDrawableSize(int *width, int *height) +{ + NSWindow* const window = CocoaVideo::GetWindow(); + assert(window != nil); + + const NSSize size = I_GetContentViewSize(window); + + if (width != nullptr) + { + *width = int(size.width); + } + + if (height != nullptr) + { + *height = int(size.height); + } +} + +bool I_GetVulkanPlatformExtensions(unsigned int *count, const char **names) +{ + static std::vector extensions; + + if (extensions.empty()) + { + uint32_t extensionPropertyCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionPropertyCount, nullptr); + + std::vector extensionProperties(extensionPropertyCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionPropertyCount, extensionProperties.data()); + + static const char* const EXTENSION_NAMES[] = + { + VK_KHR_SURFACE_EXTENSION_NAME, // KHR_surface, required + VK_EXT_METAL_SURFACE_EXTENSION_NAME, // EXT_metal_surface, optional, preferred + VK_MVK_MACOS_SURFACE_EXTENSION_NAME, // MVK_macos_surface, optional, deprecated + }; + + for (const VkExtensionProperties ¤tProperties : extensionProperties) + { + for (const char *const extensionName : EXTENSION_NAMES) + { + if (strcmp(currentProperties.extensionName, extensionName) == 0) + { + extensions.push_back(extensionName); + } + } + } + } + + static const unsigned int extensionCount = static_cast(extensions.size()); + assert(extensionCount >= 2); // KHR_surface + at least one of the platform surface extentions + + if (count == nullptr && names == nullptr) + { + return false; + } + else if (names == nullptr) + { + *count = extensionCount; + return true; + } + else + { + const bool result = *count >= extensionCount; + *count = std::min(*count, extensionCount); + + for (unsigned int i = 0; i < *count; ++i) + { + names[i] = extensions[i]; + } + + return result; + } +} + +bool I_CreateVulkanSurface(VkInstance instance, VkSurfaceKHR *surface) +{ + NSView *const view = CocoaVideo::GetWindow().contentView; + CALayer *const layer = view.layer; + + // Set magnification filter for swapchain image when it's copied to a physical display surface + // This is needed for gfx-portability because MoltenVK uses preferred nearest sampling by default + const char *const magFilterEnv = getenv("MVK_CONFIG_SWAPCHAIN_MAG_FILTER_USE_NEAREST"); + const bool useNearestFilter = magFilterEnv == nullptr || strtol(magFilterEnv, nullptr, 0) != 0; + layer.magnificationFilter = useNearestFilter ? kCAFilterNearest : kCAFilterLinear; + + if (vkCreateMetalSurfaceEXT) + { + // Preferred surface creation path + VkMetalSurfaceCreateInfoEXT surfaceCreateInfo; + surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; + surfaceCreateInfo.pNext = nullptr; + surfaceCreateInfo.flags = 0; + surfaceCreateInfo.pLayer = static_cast(layer); + + const VkResult result = vkCreateMetalSurfaceEXT(instance, &surfaceCreateInfo, nullptr, surface); + return result == VK_SUCCESS; + } + + // Deprecated surface creation path + VkMacOSSurfaceCreateInfoMVK windowCreateInfo; + windowCreateInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; + windowCreateInfo.pNext = nullptr; + windowCreateInfo.flags = 0; + windowCreateInfo.pView = view; + + const VkResult result = vkCreateMacOSSurfaceMVK(instance, &windowCreateInfo, nullptr, surface); + return result == VK_SUCCESS; +} +#endif + + +namespace +{ + TArray polyPixelBuffer; + GLuint polyTexture; + + int polyWidth = -1; + int polyHeight = -1; + int polyVSync = -1; +} + +void I_PolyPresentInit() +{ + ogl_LoadFunctions(); + + glGenTextures(1, &polyTexture); + assert(polyTexture != 0); + + glEnable(GL_TEXTURE_RECTANGLE_ARB); + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, polyTexture); + + glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +uint8_t *I_PolyPresentLock(int w, int h, bool vsync, int &pitch) +{ + static const int PIXEL_BYTES = 4; + + if (polyPixelBuffer.Size() == 0 || w != polyWidth || h != polyHeight) + { + polyPixelBuffer.Resize(w * h * PIXEL_BYTES); + + polyWidth = w; + polyHeight = h; + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0.0, w, h, 0.0, -1.0, 1.0); + + glViewport(0, 0, w, h); + } + + if (vsync != polyVSync) + { + const GLint value = vsync ? 1 : 0; + + [[NSOpenGLContext currentContext] setValues:&value + forParameter:NSOpenGLCPSwapInterval]; + } + + pitch = w * PIXEL_BYTES; + + return &polyPixelBuffer[0]; +} + +void I_PolyPresentUnlock(int x, int y, int w, int h) +{ + glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGBA8, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, &polyPixelBuffer[0]); + + glBegin(GL_QUADS); + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + glTexCoord2f(0.0f, 0.0f); + glVertex2f(0.0f, 0.0f); + glTexCoord2f(w, 0.0f); + glVertex2f(w, 0.0f); + glTexCoord2f(w, h); + glVertex2f(w, h); + glTexCoord2f(0.0f, h); + glVertex2f(0.0f, h); + glEnd(); + + glFlush(); + + [[NSOpenGLContext currentContext] flushBuffer]; +} + +void I_PolyPresentDeinit() +{ + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &polyTexture); +} diff --git a/source/platform/posix/cocoa/st_console.h b/source/platform/posix/cocoa/st_console.h new file mode 100644 index 000000000..b2af7bade --- /dev/null +++ b/source/platform/posix/cocoa/st_console.h @@ -0,0 +1,96 @@ +/* + ** st_console.h + ** + **--------------------------------------------------------------------------- + ** Copyright 2015 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#ifndef COCOA_ST_CONSOLE_INCLUDED +#define COCOA_ST_CONSOLE_INCLUDED + +@class NSButton; +@class NSProgressIndicator; +@class NSScrollView; +@class NSTextField; +@class NSTextView; +@class NSView; +@class NSWindow; + +struct PalEntry; + + +class FConsoleWindow +{ +public: + static FConsoleWindow& GetInstance(); + + static void CreateInstance(); + static void DeleteInstance(); + + void Show(bool visible); + void ShowFatalError(const char* message); + + void AddText(const char* message); + + void SetTitleText(); + void SetProgressBar(bool visible); + + // FStartupScreen functionality + void Progress(int current, int maximum); + void NetInit(const char* message, int playerCount); + void NetProgress(int count); + void NetDone(); + +private: + NSWindow* m_window; + NSTextView* m_textView; + NSScrollView* m_scrollView; + NSProgressIndicator* m_progressBar; + + NSView* m_netView; + NSTextField* m_netMessageText; + NSTextField* m_netCountText; + NSProgressIndicator* m_netProgressBar; + NSButton* m_netAbortButton; + + unsigned int m_characterCount; + + int m_netCurPos; + int m_netMaxPos; + + FConsoleWindow(); + + void ExpandTextView(float height); + + void AddText(const PalEntry& color, const char* message); + + void ScrollTextToBottom(); +}; + +#endif // COCOA_ST_CONSOLE_INCLUDED diff --git a/source/platform/posix/cocoa/st_console.mm b/source/platform/posix/cocoa/st_console.mm new file mode 100644 index 000000000..93d0a1320 --- /dev/null +++ b/source/platform/posix/cocoa/st_console.mm @@ -0,0 +1,532 @@ +/* + ** st_console.mm + ** + **--------------------------------------------------------------------------- + ** Copyright 2015 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include "i_common.h" + +#include "d_main.h" +#include "st_console.h" +#include "v_text.h" +#include "version.h" +#include "i_time.h" + + +static NSColor* RGB(const uint8_t red, const uint8_t green, const uint8_t blue) +{ + return [NSColor colorWithCalibratedRed:red / 255.0f + green:green / 255.0f + blue:blue / 255.0f + alpha:1.0f]; +} + +static NSColor* RGB(const PalEntry& color) +{ + return RGB(color.r, color.g, color.b); +} + +static NSColor* RGB(const uint32_t color) +{ + return RGB(PalEntry(color)); +} + + +static const CGFloat PROGRESS_BAR_HEIGHT = 18.0f; +static const CGFloat NET_VIEW_HEIGHT = 88.0f; + + +FConsoleWindow::FConsoleWindow() +: m_window([NSWindow alloc]) +, m_textView([NSTextView alloc]) +, m_scrollView([NSScrollView alloc]) +, m_progressBar(nil) +, m_netView(nil) +, m_netMessageText(nil) +, m_netCountText(nil) +, m_netProgressBar(nil) +, m_netAbortButton(nil) +, m_characterCount(0) +, m_netCurPos(0) +, m_netMaxPos(0) +{ + const CGFloat initialWidth = 512.0f; + const CGFloat initialHeight = 384.0f; + const NSRect initialRect = NSMakeRect(0.0f, 0.0f, initialWidth, initialHeight); + + [m_textView initWithFrame:initialRect]; + [m_textView setEditable:NO]; + [m_textView setBackgroundColor:RGB(70, 70, 70)]; + [m_textView setMinSize:NSMakeSize(0.0f, initialHeight)]; + [m_textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [m_textView setVerticallyResizable:YES]; + [m_textView setHorizontallyResizable:NO]; + [m_textView setAutoresizingMask:NSViewWidthSizable]; + + NSTextContainer* const textContainer = [m_textView textContainer]; + [textContainer setContainerSize:NSMakeSize(initialWidth, FLT_MAX)]; + [textContainer setWidthTracksTextView:YES]; + + [m_scrollView initWithFrame:initialRect]; + [m_scrollView setBorderType:NSNoBorder]; + [m_scrollView setHasVerticalScroller:YES]; + [m_scrollView setHasHorizontalScroller:NO]; + [m_scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [m_scrollView setDocumentView:m_textView]; + + NSString* const title = [NSString stringWithFormat:@"%s %s - Console", GAMESIG, GetVersionString()]; + + [m_window initWithContentRect:initialRect + styleMask:NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask + backing:NSBackingStoreBuffered + defer:NO]; + [m_window setMinSize:[m_window frame].size]; + [m_window setShowsResizeIndicator:NO]; + [m_window setTitle:title]; + [m_window center]; + [m_window exitAppOnClose]; + + // Do not allow fullscreen mode for this window + [m_window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + + [[m_window contentView] addSubview:m_scrollView]; + + [m_window makeKeyAndOrderFront:nil]; +} + + +static FConsoleWindow* s_instance; + + +void FConsoleWindow::CreateInstance() +{ + assert(NULL == s_instance); + s_instance = new FConsoleWindow; +} + +void FConsoleWindow::DeleteInstance() +{ + assert(NULL != s_instance); + delete s_instance; + s_instance = NULL; +} + +FConsoleWindow& FConsoleWindow::GetInstance() +{ + assert(NULL != s_instance); + return *s_instance; +} + + +void FConsoleWindow::Show(const bool visible) +{ + if (visible) + { + [m_window orderFront:nil]; + } + else + { + [m_window orderOut:nil]; + } +} + +void FConsoleWindow::ShowFatalError(const char* const message) +{ + SetProgressBar(false); + NetDone(); + + const CGFloat textViewWidth = [m_scrollView frame].size.width; + + ExpandTextView(-32.0f); + + NSButton* quitButton = [[NSButton alloc] initWithFrame:NSMakeRect(textViewWidth - 76.0f, 0.0f, 72.0f, 30.0f)]; + [quitButton setAutoresizingMask:NSViewMinXMargin]; + [quitButton setBezelStyle:NSRoundedBezelStyle]; + [quitButton setTitle:@"Quit"]; + [quitButton setKeyEquivalent:@"\r"]; + [quitButton setTarget:NSApp]; + [quitButton setAction:@selector(stopModal)]; + + NSView* quitPanel = [[NSView alloc] initWithFrame:NSMakeRect(0.0f, 0.0f, textViewWidth, 32.0f)]; + [quitPanel setAutoresizingMask:NSViewWidthSizable]; + [quitPanel addSubview:quitButton]; + + [[m_window contentView] addSubview:quitPanel]; + [m_window orderFront:nil]; + + AddText(PalEntry(255, 0, 0), "\nExecution could not continue.\n"); + AddText(PalEntry(255, 255, 170), message); + AddText("\n"); + + ScrollTextToBottom(); + + [NSApp runModalForWindow:m_window]; +} + + +static const unsigned int THIRTY_FPS = 33; // milliseconds per update + + +template +struct TimedUpdater +{ + explicit TimedUpdater(const Function& function) + { + const unsigned int currentTime = I_msTime(); + + if (currentTime - m_previousTime > interval) + { + m_previousTime = currentTime; + + function(); + + [[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode]; + } + } + + static unsigned int m_previousTime; +}; + +template +unsigned int TimedUpdater::m_previousTime; + +template +static void UpdateTimed(const Function& function) +{ + TimedUpdater dummy(function); +} + + +void FConsoleWindow::AddText(const char* message) +{ + PalEntry color(223, 223, 223); + + char buffer[1024] = {}; + size_t pos = 0; + bool reset = false; + + while (*message != '\0') + { + if ((TEXTCOLOR_ESCAPE == *message && 0 != pos) + || (pos == sizeof buffer - 1) + || reset) + { + buffer[pos] = '\0'; + pos = 0; + reset = false; + + AddText(color, buffer); + } + + if (TEXTCOLOR_ESCAPE == *message) + { + const uint8_t* colorID = reinterpret_cast(message) + 1; + if ('\0' == *colorID) + { + break; + } + + const EColorRange range = V_ParseFontColor(colorID, CR_UNTRANSLATED, CR_YELLOW); + + if (range != CR_UNDEFINED) + { + color = V_LogColorFromColorRange(range); + } + + message += 2; + } + else if (0x1d == *message || 0x1f == *message) // Opening and closing bar characters + { + buffer[pos++] = '-'; + ++message; + } + else if (0x1e == *message) // Middle bar character + { + buffer[pos++] = '='; + ++message; + } + else + { + buffer[pos++] = *message++; + } + } + + if (0 != pos) + { + buffer[pos] = '\0'; + + AddText(color, buffer); + } + + if ([m_window isVisible]) + { + UpdateTimed([&]() + { + [m_textView scrollRangeToVisible:NSMakeRange(m_characterCount, 0)]; + }); + } +} + +void FConsoleWindow::AddText(const PalEntry& color, const char* const message) +{ + NSString* const text = [NSString stringWithCString:message + encoding:NSISOLatin1StringEncoding]; + + NSDictionary* const attributes = [NSDictionary dictionaryWithObjectsAndKeys: + [NSFont systemFontOfSize:14.0f], NSFontAttributeName, + RGB(color), NSForegroundColorAttributeName, + nil]; + + NSAttributedString* const formattedText = + [[NSAttributedString alloc] initWithString:text + attributes:attributes]; + [[m_textView textStorage] appendAttributedString:formattedText]; + + m_characterCount += [text length]; +} + + +void FConsoleWindow::ScrollTextToBottom() +{ + [m_textView scrollRangeToVisible:NSMakeRange(m_characterCount, 0)]; + + [[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode]; +} + + +void FConsoleWindow::SetTitleText() +{ + static const CGFloat TITLE_TEXT_HEIGHT = 32.0f; + + NSRect textViewFrame = [m_scrollView frame]; + textViewFrame.size.height -= TITLE_TEXT_HEIGHT; + [m_scrollView setFrame:textViewFrame]; + + const NSRect titleTextRect = NSMakeRect( + 0.0f, + textViewFrame.origin.y + textViewFrame.size.height, + textViewFrame.size.width, + TITLE_TEXT_HEIGHT); + + // Temporary solution for the same foreground and background colors + // It's used in graphical startup screen, with Hexen style in particular + // Native OS X backend doesn't implement this yet + + if (DoomStartupInfo.FgColor == DoomStartupInfo.BkColor) + { + DoomStartupInfo.FgColor = ~DoomStartupInfo.FgColor; + } + + NSTextField* titleText = [[NSTextField alloc] initWithFrame:titleTextRect]; + [titleText setStringValue:[NSString stringWithCString:DoomStartupInfo.Name + encoding:NSISOLatin1StringEncoding]]; + [titleText setAlignment:NSCenterTextAlignment]; + [titleText setTextColor:RGB(DoomStartupInfo.FgColor)]; + [titleText setBackgroundColor:RGB(DoomStartupInfo.BkColor)]; + [titleText setFont:[NSFont fontWithName:@"Trebuchet MS Bold" size:18.0f]]; + [titleText setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin]; + [titleText setSelectable:NO]; + [titleText setBordered:NO]; + + [[m_window contentView] addSubview:titleText]; +} + +void FConsoleWindow::SetProgressBar(const bool visible) +{ + if ( (!visible && nil == m_progressBar) + || (visible && nil != m_progressBar)) + { + return; + } + + if (visible) + { + ExpandTextView(-PROGRESS_BAR_HEIGHT); + + static const CGFloat PROGRESS_BAR_X = 2.0f; + const NSRect PROGRESS_BAR_RECT = NSMakeRect( + PROGRESS_BAR_X, 0.0f, + [m_window frame].size.width - PROGRESS_BAR_X * 2, 16.0f); + + m_progressBar = [[NSProgressIndicator alloc] initWithFrame:PROGRESS_BAR_RECT]; + [m_progressBar setIndeterminate:NO]; + [m_progressBar setAutoresizingMask:NSViewWidthSizable]; + + [[m_window contentView] addSubview:m_progressBar]; + } + else + { + ExpandTextView(PROGRESS_BAR_HEIGHT); + + [m_progressBar removeFromSuperview]; + [m_progressBar release]; + m_progressBar = nil; + } +} + + +void FConsoleWindow::ExpandTextView(const float height) +{ + NSRect textFrame = [m_scrollView frame]; + textFrame.origin.y -= height; + textFrame.size.height += height; + [m_scrollView setFrame:textFrame]; +} + + +void FConsoleWindow::Progress(const int current, const int maximum) +{ + if (nil == m_progressBar) + { + return; + } + + UpdateTimed([&]() + { + [m_progressBar setMaxValue:maximum]; + [m_progressBar setDoubleValue:current]; + }); +} + + +void FConsoleWindow::NetInit(const char* const message, const int playerCount) +{ + if (nil == m_netView) + { + SetProgressBar(false); + ExpandTextView(-NET_VIEW_HEIGHT); + + // Message like 'Waiting for players' or 'Contacting host' + m_netMessageText = [[NSTextField alloc] initWithFrame:NSMakeRect(12.0f, 64.0f, 400.0f, 16.0f)]; + [m_netMessageText setAutoresizingMask:NSViewWidthSizable]; + [m_netMessageText setDrawsBackground:NO]; + [m_netMessageText setSelectable:NO]; + [m_netMessageText setBordered:NO]; + + // Text with connected/total players count + m_netCountText = [[NSTextField alloc] initWithFrame:NSMakeRect(428.0f, 64.0f, 72.0f, 16.0f)]; + [m_netCountText setAutoresizingMask:NSViewMinXMargin]; + [m_netCountText setAlignment:NSRightTextAlignment]; + [m_netCountText setDrawsBackground:NO]; + [m_netCountText setSelectable:NO]; + [m_netCountText setBordered:NO]; + + // Connection progress + m_netProgressBar = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(12.0f, 40.0f, 488.0f, 16.0f)]; + [m_netProgressBar setAutoresizingMask:NSViewWidthSizable]; + [m_netProgressBar setMaxValue:playerCount]; + + if (0 == playerCount) + { + // Joining game + [m_netProgressBar setIndeterminate:YES]; + [m_netProgressBar startAnimation:nil]; + } + else + { + // Hosting game + [m_netProgressBar setIndeterminate:NO]; + } + + // Cancel network game button + m_netAbortButton = [[NSButton alloc] initWithFrame:NSMakeRect(432.0f, 8.0f, 72.0f, 28.0f)]; + [m_netAbortButton setAutoresizingMask:NSViewMinXMargin]; + [m_netAbortButton setBezelStyle:NSRoundedBezelStyle]; + [m_netAbortButton setTitle:@"Cancel"]; + [m_netAbortButton setKeyEquivalent:@"\r"]; + [m_netAbortButton setTarget:[NSApp delegate]]; + [m_netAbortButton setAction:@selector(sendExitEvent:)]; + + // Panel for controls above + m_netView = [[NSView alloc] initWithFrame:NSMakeRect(0.0f, 0.0f, 512.0f, NET_VIEW_HEIGHT)]; + [m_netView setAutoresizingMask:NSViewWidthSizable]; + [m_netView addSubview:m_netMessageText]; + [m_netView addSubview:m_netCountText]; + [m_netView addSubview:m_netProgressBar]; + [m_netView addSubview:m_netAbortButton]; + + NSRect windowRect = [m_window frame]; + windowRect.origin.y -= NET_VIEW_HEIGHT; + windowRect.size.height += NET_VIEW_HEIGHT; + + [m_window setFrame:windowRect display:YES]; + [[m_window contentView] addSubview:m_netView]; + + ScrollTextToBottom(); + } + + [m_netMessageText setStringValue:[NSString stringWithUTF8String:message]]; + + m_netCurPos = 0; + m_netMaxPos = playerCount; + + NetProgress(1); // You always know about yourself +} + +void FConsoleWindow::NetProgress(const int count) +{ + if (0 == count) + { + ++m_netCurPos; + } + else + { + m_netCurPos = count; + } + + if (nil == m_netView) + { + return; + } + + if (m_netMaxPos > 1) + { + [m_netCountText setStringValue:[NSString stringWithFormat:@"%d / %d", m_netCurPos, m_netMaxPos]]; + [m_netProgressBar setDoubleValue:MIN(m_netCurPos, m_netMaxPos)]; + } +} + +void FConsoleWindow::NetDone() +{ + if (nil != m_netView) + { + ExpandTextView(NET_VIEW_HEIGHT); + + [m_netView removeFromSuperview]; + [m_netView release]; + m_netView = nil; + + // Released by m_netView + m_netMessageText = nil; + m_netCountText = nil; + m_netProgressBar = nil; + m_netAbortButton = nil; + } +} diff --git a/source/platform/posix/cocoa/st_start.mm b/source/platform/posix/cocoa/st_start.mm new file mode 100644 index 000000000..feaa5412f --- /dev/null +++ b/source/platform/posix/cocoa/st_start.mm @@ -0,0 +1,176 @@ +/* + ** st_start.mm + ** + **--------------------------------------------------------------------------- + ** Copyright 2015 Alexey Lysiuk + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include + +#import + +#include "c_cvars.h" +#include "doomtype.h" +#include "st_console.h" +#include "st_start.h" +#include "doomerrors.h" + + +FStartupScreen *StartScreen; + + +CUSTOM_CVAR(Int, showendoom, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +{ + if (self < 0) + { + self = 0; + } + else if (self > 2) + { + self = 2; + } +} + + +// --------------------------------------------------------------------------- + + +FBasicStartupScreen::FBasicStartupScreen(int maxProgress, bool showBar) +: FStartupScreen(maxProgress) +{ + FConsoleWindow& consoleWindow = FConsoleWindow::GetInstance(); + consoleWindow.SetProgressBar(true); + consoleWindow.SetTitleText(); + +#if 0 + // Testing code, please do not remove + consoleWindow.AddText("----------------------------------------------------------------\n"); + consoleWindow.AddText("1234567890 !@#$%^&*() ,<.>/?;:'\" [{]}\\| `~-_=+ " + "This is very very very long message needed to trigger word wrapping...\n\n"); + consoleWindow.AddText("Multiline...\n\tmessage...\n\t\twith...\n\t\t\ttabs.\n\n"); + + consoleWindow.AddText(TEXTCOLOR_BRICK "TEXTCOLOR_BRICK\n" TEXTCOLOR_TAN "TEXTCOLOR_TAN\n"); + consoleWindow.AddText(TEXTCOLOR_GRAY "TEXTCOLOR_GRAY & TEXTCOLOR_GREY\n"); + consoleWindow.AddText(TEXTCOLOR_GREEN "TEXTCOLOR_GREEN\n" TEXTCOLOR_BROWN "TEXTCOLOR_BROWN\n"); + consoleWindow.AddText(TEXTCOLOR_GOLD "TEXTCOLOR_GOLD\n" TEXTCOLOR_RED "TEXTCOLOR_RED\n"); + consoleWindow.AddText(TEXTCOLOR_BLUE "TEXTCOLOR_BLUE\n" TEXTCOLOR_ORANGE "TEXTCOLOR_ORANGE\n"); + consoleWindow.AddText(TEXTCOLOR_WHITE "TEXTCOLOR_WHITE\n" TEXTCOLOR_YELLOW "TEXTCOLOR_YELLOW\n"); + consoleWindow.AddText(TEXTCOLOR_UNTRANSLATED "TEXTCOLOR_UNTRANSLATED\n"); + consoleWindow.AddText(TEXTCOLOR_BLACK "TEXTCOLOR_BLACK\n" TEXTCOLOR_LIGHTBLUE "TEXTCOLOR_LIGHTBLUE\n"); + consoleWindow.AddText(TEXTCOLOR_CREAM "TEXTCOLOR_CREAM\n" TEXTCOLOR_OLIVE "TEXTCOLOR_OLIVE\n"); + consoleWindow.AddText(TEXTCOLOR_DARKGREEN "TEXTCOLOR_DARKGREEN\n" TEXTCOLOR_DARKRED "TEXTCOLOR_DARKRED\n"); + consoleWindow.AddText(TEXTCOLOR_DARKBROWN "TEXTCOLOR_DARKBROWN\n" TEXTCOLOR_PURPLE "TEXTCOLOR_PURPLE\n"); + consoleWindow.AddText(TEXTCOLOR_DARKGRAY "TEXTCOLOR_DARKGRAY\n" TEXTCOLOR_CYAN "TEXTCOLOR_CYAN\n"); + consoleWindow.AddText(TEXTCOLOR_ICE "TEXTCOLOR_ICE\n" TEXTCOLOR_FIRE "TEXTCOLOR_FIRE\n"); + consoleWindow.AddText(TEXTCOLOR_SAPPHIRE "TEXTCOLOR_SAPPHIRE\n" TEXTCOLOR_TEAL "TEXTCOLOR_TEAL\n"); + consoleWindow.AddText(TEXTCOLOR_NORMAL "TEXTCOLOR_NORMAL\n" TEXTCOLOR_BOLD "TEXTCOLOR_BOLD\n"); + consoleWindow.AddText(TEXTCOLOR_CHAT "TEXTCOLOR_CHAT\n" TEXTCOLOR_TEAMCHAT "TEXTCOLOR_TEAMCHAT\n"); + consoleWindow.AddText("----------------------------------------------------------------\n"); +#endif // _DEBUG +} + +FBasicStartupScreen::~FBasicStartupScreen() +{ + FConsoleWindow::GetInstance().SetProgressBar(false); +} + + +void FBasicStartupScreen::Progress() +{ + if (CurPos < MaxPos) + { + ++CurPos; + } + + FConsoleWindow::GetInstance().Progress(CurPos, MaxPos); +} + + +void FBasicStartupScreen::NetInit(const char* const message, const int playerCount) +{ + FConsoleWindow::GetInstance().NetInit(message, playerCount); +} + +void FBasicStartupScreen::NetProgress(const int count) +{ + FConsoleWindow::GetInstance().NetProgress(count); +} + +void FBasicStartupScreen::NetMessage(const char* const format, ...) +{ + va_list args; + va_start(args, format); + + FString message; + message.VFormat(format, args); + va_end(args); + + Printf("%s\n", message.GetChars()); +} + +void FBasicStartupScreen::NetDone() +{ + FConsoleWindow::GetInstance().NetDone(); +} + +bool FBasicStartupScreen::NetLoop(bool (*timerCallback)(void*), void* const userData) +{ + while (true) + { + if (timerCallback(userData)) + { + break; + } + + [[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode]; + + // Do not poll to often + usleep(50000); + } + + return true; +} + + +// --------------------------------------------------------------------------- + + +FStartupScreen *FStartupScreen::CreateInstance(const int maxProgress) +{ + return new FBasicStartupScreen(maxProgress, true); +} + + +// --------------------------------------------------------------------------- + + +void ST_Endoom() +{ + throw CExitEvent(0); +} diff --git a/source/platform/posix/dikeys.h b/source/platform/posix/dikeys.h new file mode 100644 index 000000000..4541b0ffd --- /dev/null +++ b/source/platform/posix/dikeys.h @@ -0,0 +1,155 @@ +// ZDoom bases its keycodes on DirectInput's scan codes +// Why? Because it was Win32-only before porting to anything else, +// so this made sense. AFAIK, it's primarily used under Win32 now, +// so it still makes sense. +// +// Actually, these key codes may only be used for key bindings now, +// in which case they're not really necessary--if we tweaked c_bind.cpp. + +enum +{ + DIK_ESCAPE = 1, + DIK_1, + DIK_2, + DIK_3, + DIK_4, + DIK_5, + DIK_6, + DIK_7, + DIK_8, + DIK_9, + DIK_0, + DIK_MINUS, /* - on main keyboard */ + DIK_EQUALS, + DIK_BACK, /* backspace */ + DIK_TAB, + DIK_Q, + DIK_W, + DIK_E, + DIK_R, + DIK_T, + DIK_Y, + DIK_U, + DIK_I, + DIK_O, + DIK_P, + DIK_LBRACKET, + DIK_RBRACKET, + DIK_RETURN, /* Enter on main keyboard */ + DIK_LCONTROL, + DIK_A, + DIK_S, + DIK_D, + DIK_F, + DIK_G, + DIK_H, + DIK_J, + DIK_K, + DIK_L, + DIK_SEMICOLON, + DIK_APOSTROPHE, + DIK_GRAVE, /* accent grave */ + DIK_LSHIFT, + DIK_BACKSLASH, + DIK_Z, + DIK_X, + DIK_C, + DIK_V, + DIK_B, + DIK_N, + DIK_M, + DIK_COMMA, + DIK_PERIOD, /* . on main keyboard */ + DIK_SLASH, /* / on main keyboard */ + DIK_RSHIFT, + DIK_MULTIPLY, /* * on numeric keypad */ + DIK_LMENU, /* left Alt */ + DIK_SPACE, + DIK_CAPITAL, + DIK_F1, + DIK_F2, + DIK_F3, + DIK_F4, + DIK_F5, + DIK_F6, + DIK_F7, + DIK_F8, + DIK_F9, + DIK_F10, + DIK_NUMLOCK, + DIK_SCROLL, /* Scroll Lock */ + DIK_NUMPAD7, + DIK_NUMPAD8, + DIK_NUMPAD9, + DIK_SUBTRACT, /* - on numeric keypad */ + DIK_NUMPAD4, + DIK_NUMPAD5, + DIK_NUMPAD6, + DIK_ADD, /* + on numeric keypad */ + DIK_NUMPAD1, + DIK_NUMPAD2, + DIK_NUMPAD3, + DIK_NUMPAD0, + DIK_DECIMAL, /* . on numeric keypad */ + DIK_OEM_102 = 0x56, /* < > | on UK/Germany keyboards */ + DIK_F11, + DIK_F12, + DIK_F13 = 0x64, /* (NEC PC98) */ + DIK_F14, /* (NEC PC98) */ + DIK_F15, /* (NEC PC98) */ + DIK_KANA = 0x70, /* (Japanese keyboard) */ + DIK_ABNT_C1 = 0x73, /* / ? on Portugese (Brazilian) keyboards */ + DIK_CONVERT = 0x79, /* (Japanese keyboard) */ + DIK_NOCONVERT = 0x7B, /* (Japanese keyboard) */ + DIK_YEN = 0x7D, /* (Japanese keyboard) */ + DIK_ABNT_C2 = 0x7E, /* Numpad . on Portugese (Brazilian) keyboards */ + DIK_NUMPAD_EQUALS = 0x8D, /* = on numeric keypad (NEC PC98) */ + DIK_PREVTRACK = 0x90, /* Previous Track (DIK_CIRCUMFLEX on Japanese keyboard) */ + DIK_AT, /* (NEC PC98) */ + DIK_COLON, /* (NEC PC98) */ + DIK_UNDERLINE, /* (NEC PC98) */ + DIK_KANJI, /* (Japanese keyboard) */ + DIK_STOP, /* (NEC PC98) */ + DIK_AX, /* (Japan AX) */ + DIK_UNLABELED, /* (J3100) */ + DIK_NEXTTRACK = 0x99, /* Next Track */ + DIK_NUMPADENTER = 0x9C, /* Enter on numeric keypad */ + DIK_RCONTROL = 0x9D, + DIK_MUTE = 0xA0, /* Mute */ + DIK_CALCULATOR = 0xA1, /* Calculator */ + DIK_PLAYPAUSE = 0xA2, /* Play / Pause */ + DIK_MEDIASTOP = 0xA4, /* Media Stop */ + DIK_VOLUMEDOWN = 0xAE, /* Volume - */ + DIK_VOLUMEUP = 0xB0, /* Volume + */ + DIK_WEBHOME = 0xB2, /* Web home */ + DIK_NUMPADCOMMA = 0xB3, /* , on numeric keypad (NEC PC98) */ + DIK_DIVIDE = 0xB5, /* / on numeric keypad */ + DIK_SYSRQ = 0xB7, + DIK_RMENU = 0xB8, /* right Alt */ + DIK_PAUSE = 0xC5, /* Pause */ + DIK_HOME = 0xC7, /* Home on arrow keypad */ + DIK_UP = 0xC8, /* UpArrow on arrow keypad */ + DIK_PRIOR = 0xC9, /* PgUp on arrow keypad */ + DIK_LEFT = 0xCB, /* LeftArrow on arrow keypad */ + DIK_RIGHT = 0xCD, /* RightArrow on arrow keypad */ + DIK_END = 0xCF, /* End on arrow keypad */ + DIK_DOWN = 0xD0, /* DownArrow on arrow keypad */ + DIK_NEXT = 0xD1, /* PgDn on arrow keypad */ + DIK_INSERT = 0xD2, /* Insert on arrow keypad */ + DIK_DELETE = 0xD3, /* Delete on arrow keypad */ + DIK_LWIN = 0xDB, /* Left Windows key */ + DIK_RWIN = 0xDC, /* Right Windows key */ + DIK_APPS = 0xDD, /* AppMenu key */ + DIK_POWER = 0xDE, /* System Power */ + DIK_SLEEP = 0xDF, /* System Sleep */ + DIK_WAKE = 0xE3, /* System Wake */ + DIK_WEBSEARCH = 0xE5, /* Web Search */ + DIK_WEBFAVORITES = 0xE6, /* Web Favorites */ + DIK_WEBREFRESH = 0xE7, /* Web Refresh */ + DIK_WEBSTOP = 0xE8, /* Web Stop */ + DIK_WEBFORWARD = 0xE9, /* Web Forward */ + DIK_WEBBACK = 0xEA, /* Web Back */ + DIK_MYCOMPUTER = 0xEB, /* My Computer */ + DIK_MAIL = 0xEC, /* Mail */ + DIK_MEDIASELECT = 0xED /* Media Select */ +}; diff --git a/source/platform/posix/hardware.h b/source/platform/posix/hardware.h new file mode 100644 index 000000000..a7918935f --- /dev/null +++ b/source/platform/posix/hardware.h @@ -0,0 +1,40 @@ +/* +** hardware.h +** +**--------------------------------------------------------------------------- +** Copyright 1998-2006 Randy Heit +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#ifndef __HARDWARE_H__ +#define __HARDWARE_H__ + +#include "i_video.h" +#include "v_video.h" + +#endif // __HARDWARE_H__ diff --git a/source/platform/posix/i_steam.cpp b/source/platform/posix/i_steam.cpp new file mode 100644 index 000000000..dccadb021 --- /dev/null +++ b/source/platform/posix/i_steam.cpp @@ -0,0 +1,227 @@ +/* +** i_steam.cpp +** +**--------------------------------------------------------------------------- +** Copyright 2013 Braden Obrzut +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +** +*/ + +#include + +#ifdef __APPLE__ +#include "m_misc.h" +#endif // __APPLE__ + +#include "doomerrors.h" +#include "d_main.h" +#include "sc_man.h" +#include "cmdlib.h" + +static void PSR_FindEndBlock(FScanner &sc) +{ + int depth = 1; + do + { + if(sc.CheckToken('}')) + --depth; + else if(sc.CheckToken('{')) + ++depth; + else + sc.MustGetAnyToken(); + } + while(depth); +} +static void PSR_SkipBlock(FScanner &sc) +{ + sc.MustGetToken('{'); + PSR_FindEndBlock(sc); +} +static bool PSR_FindAndEnterBlock(FScanner &sc, const char* keyword) +{ + // Finds a block with a given keyword and then enter it (opening brace) + // Should be closed with PSR_FindEndBlock + while(sc.GetToken()) + { + if(sc.TokenType == '}') + { + sc.UnGet(); + return false; + } + + sc.TokenMustBe(TK_StringConst); + if(!sc.Compare(keyword)) + { + if(!sc.CheckToken(TK_StringConst)) + PSR_SkipBlock(sc); + } + else + { + sc.MustGetToken('{'); + return true; + } + } + return false; +} +static TArray PSR_ReadBaseInstalls(FScanner &sc) +{ + TArray result; + + // Get a list of possible install directories. + while(sc.GetToken()) + { + if(sc.TokenType == '}') + break; + + sc.TokenMustBe(TK_StringConst); + FString key(sc.String); + if(key.Left(18).CompareNoCase("BaseInstallFolder_") == 0) + { + sc.MustGetToken(TK_StringConst); + result.Push(FString(sc.String) + "/steamapps/common"); + } + else + { + if(sc.CheckToken('{')) + PSR_FindEndBlock(sc); + else + sc.MustGetToken(TK_StringConst); + } + } + + return result; +} +static TArray ParseSteamRegistry(const char* path) +{ + TArray dirs; + + // Read registry data + FScanner sc; + if (sc.OpenFile(path)) + { + sc.SetCMode(true); + + // Find the SteamApps listing + if (PSR_FindAndEnterBlock(sc, "InstallConfigStore")) + { + if (PSR_FindAndEnterBlock(sc, "Software")) + { + if (PSR_FindAndEnterBlock(sc, "Valve")) + { + if (PSR_FindAndEnterBlock(sc, "Steam")) + { + dirs = PSR_ReadBaseInstalls(sc); + } + PSR_FindEndBlock(sc); + } + PSR_FindEndBlock(sc); + } + PSR_FindEndBlock(sc); + } + } + return dirs; +} + +static struct SteamAppInfo +{ + const char* const BasePath; + const int AppID; +} AppInfo[] = +{ + {"Doom 2/base", 2300}, + {"Final Doom/base", 2290}, + {"Heretic Shadow of the Serpent Riders/base", 2390}, + {"Hexen/base", 2360}, + {"Hexen Deathkings of the Dark Citadel/base", 2370}, + {"Ultimate Doom/base", 2280}, + {"DOOM 3 BFG Edition/base/wads", 208200}, + {"Strife", 317040} +}; + +TArray I_GetSteamPath() +{ + TArray result; + TArray SteamInstallFolders; + + // Linux and OS X actually allow the user to install to any location, so + // we need to figure out on an app-by-app basis where the game is installed. + // To do so, we read the virtual registry. +#ifdef __APPLE__ + const FString appSupportPath = M_GetMacAppSupportPath(); + FString regPath = appSupportPath + "/Steam/config/config.vdf"; + try + { + SteamInstallFolders = ParseSteamRegistry(regPath); + } + catch(class CRecoverableError &error) + { + // If we can't parse for some reason just pretend we can't find anything. + return result; + } + + SteamInstallFolders.Push(appSupportPath + "/Steam/SteamApps/common"); +#else + char* home = getenv("HOME"); + if(home != NULL && *home != '\0') + { + FString regPath; + regPath.Format("%s/.steam/config/config.vdf", home); + // [BL] The config seems to have moved from the more modern .local to + // .steam at some point. Not sure if it's just my setup so I guess we + // can fall back on it? + if(!FileExists(regPath)) + regPath.Format("%s/.local/share/Steam/config/config.vdf", home); + + try + { + SteamInstallFolders = ParseSteamRegistry(regPath); + } + catch(class CRecoverableError &error) + { + // If we can't parse for some reason just pretend we can't find anything. + return result; + } + + regPath.Format("%s/.local/share/Steam/SteamApps/common", home); + SteamInstallFolders.Push(regPath); + } +#endif + + for(unsigned int i = 0;i < SteamInstallFolders.Size();++i) + { + for(unsigned int app = 0;app < countof(AppInfo);++app) + { + struct stat st; + FString candidate(SteamInstallFolders[i] + "/" + AppInfo[app].BasePath); + if(DirExists(candidate)) + result.Push(candidate); + } + } + + return result; +} diff --git a/source/platform/posix/i_system.h b/source/platform/posix/i_system.h new file mode 100644 index 000000000..49fcb9977 --- /dev/null +++ b/source/platform/posix/i_system.h @@ -0,0 +1,116 @@ +#ifndef __I_SYSTEM__ +#define __I_SYSTEM__ + +#include +#include + +#if defined(__sun) || defined(__sun__) || defined(__SRV4) || defined(__srv4__) +#define __solaris__ 1 +#endif + +#include "doomtype.h" +#include +#include + +struct ticcmd_t; +struct WadStuff; + +#ifndef SHARE_DIR +#define SHARE_DIR "/usr/local/share/" +#endif + + +// Called by DoomMain. +void I_Init (void); + +// Return a seed value for the RNG. +unsigned int I_MakeRNGSeed(); + + +void I_StartFrame (void); + +void I_StartTic (void); + +// Asynchronous interrupt functions should maintain private queues +// that are read by the synchronous functions +// to be converted into events. + +// Either returns a null ticcmd, +// or calls a loadable driver to build it. +// This ticcmd will then be modified by the gameloop +// for normal input. +ticcmd_t *I_BaseTiccmd (void); + +void I_Tactile (int on, int off, int total); + +void I_DebugPrint (const char *cp); + +// Print a console string +void I_PrintStr (const char *str); + +// Set the title string of the startup window +void I_SetIWADInfo (); + +// Pick from multiple IWADs to use +int I_PickIWad (WadStuff *wads, int numwads, bool queryiwad, int defaultiwad); + +// [RH] Checks the registry for Steam's install path, so we can scan its +// directories for IWADs if the user purchased any through Steam. +TArray I_GetSteamPath(); + +TArray I_GetGogPaths(); + +// The ini could not be saved at exit +bool I_WriteIniFailed (); + +class FTexture; +bool I_SetCursor(FTexture *); + +// Directory searching routines + +struct findstate_t +{ +private: + int count; + struct dirent **namelist; + int current; + + friend void *I_FindFirst(const char *filespec, findstate_t *fileinfo); + friend int I_FindNext(void *handle, findstate_t *fileinfo); + friend const char *I_FindName(findstate_t *fileinfo); + friend int I_FindAttr(findstate_t *fileinfo); + friend int I_FindClose(void *handle); +}; + +void *I_FindFirst (const char *filespec, findstate_t *fileinfo); +int I_FindNext (void *handle, findstate_t *fileinfo); +int I_FindClose (void *handle); +int I_FindAttr (findstate_t *fileinfo); + +inline const char *I_FindName(findstate_t *fileinfo) +{ + return (fileinfo->namelist[fileinfo->current]->d_name); +} + +#define FA_RDONLY 1 +#define FA_HIDDEN 2 +#define FA_SYSTEM 4 +#define FA_DIREC 8 +#define FA_ARCH 16 + +static inline char *strlwr(char *str) +{ + char *ptr = str; + while(*ptr) + { + *ptr = tolower(*ptr); + ++ptr; + } + return str; +} + +inline int I_GetNumaNodeCount() { return 1; } +inline int I_GetNumaNodeThreadCount(int numaNode) { return std::max(std::thread::hardware_concurrency(), 1); } +inline void I_SetThreadNumaNode(std::thread &thread, int numaNode) { } + +#endif diff --git a/source/platform/macos/i_specialpaths.mm b/source/platform/posix/osx/i_specialpaths.mm similarity index 100% rename from source/platform/macos/i_specialpaths.mm rename to source/platform/posix/osx/i_specialpaths.mm diff --git a/source/platform/posix/osx/iwadpicker_cocoa.mm b/source/platform/posix/osx/iwadpicker_cocoa.mm new file mode 100644 index 000000000..fd9c60014 --- /dev/null +++ b/source/platform/posix/osx/iwadpicker_cocoa.mm @@ -0,0 +1,470 @@ +/* + ** iwadpicker_cocoa.mm + ** + ** Implements Mac OS X native IWAD Picker. + ** + **--------------------------------------------------------------------------- + ** Copyright 2010 Braden Obrzut + ** All rights reserved. + ** + ** Redistribution and use in source and binary forms, with or without + ** modification, are permitted provided that the following conditions + ** are met: + ** + ** 1. Redistributions of source code must retain the above copyright + ** notice, this list of conditions and the following disclaimer. + ** 2. Redistributions in binary form must reproduce the above copyright + ** notice, this list of conditions and the following disclaimer in the + ** documentation and/or other materials provided with the distribution. + ** 3. The name of the author may not be used to endorse or promote products + ** derived from this software without specific prior written permission. + ** + ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **--------------------------------------------------------------------------- + ** + */ + +#include "cmdlib.h" +#include "d_main.h" +#include "version.h" +#include "c_cvars.h" +#include "m_argv.h" +#include "m_misc.h" +#include "gameconfigfile.h" +#include "doomerrors.h" + +#include +#include + + +CVAR(String, osx_additional_parameters, "", CVAR_ARCHIVE | CVAR_NOSET | CVAR_GLOBALCONFIG); + +enum +{ + COLUMN_IWAD, + COLUMN_GAME, + + NUM_COLUMNS +}; + +static const char* const tableHeaders[NUM_COLUMNS] = { "IWAD", "Game" }; + +// Class to convert the IWAD data into a form that Cocoa can use. +@interface IWADTableData : NSObject +{ + NSMutableArray *data; +} + +- (void)dealloc; +- (IWADTableData *)init:(WadStuff *) wads num:(int) numwads; + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView; +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex; +@end + +@implementation IWADTableData + +- (void)dealloc +{ + [data release]; + + [super dealloc]; +} + +- (IWADTableData *)init:(WadStuff *) wads num:(int) numwads +{ + data = [[NSMutableArray alloc] initWithCapacity:numwads]; + + for(int i = 0;i < numwads;i++) + { + NSMutableDictionary *record = [[NSMutableDictionary alloc] initWithCapacity:NUM_COLUMNS]; + const char* filename = strrchr(wads[i].Path, '/'); + if(filename == NULL) + filename = wads[i].Path; + else + filename++; + [record setObject:[NSString stringWithUTF8String:filename] forKey:[NSString stringWithUTF8String:tableHeaders[COLUMN_IWAD]]]; + [record setObject:[NSString stringWithUTF8String:wads[i].Name] forKey:[NSString stringWithUTF8String:tableHeaders[COLUMN_GAME]]]; + [data addObject:record]; + [record release]; + } + + return self; +} + +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [data count]; +} + +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +{ + NSParameterAssert(rowIndex >= 0 && (unsigned int) rowIndex < [data count]); + NSMutableDictionary *record = [data objectAtIndex:rowIndex]; + return [record objectForKey:[aTableColumn identifier]]; +} + +@end + +static NSDictionary* GetKnownFileTypes() +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + @"-file" , @"wad", + @"-file" , @"pk3", + @"-file" , @"zip", + @"-file" , @"pk7", + @"-file" , @"7z", + @"-deh" , @"deh", + @"-bex" , @"bex", + @"-exec" , @"cfg", + @"-playdemo", @"lmp", + nil]; +} + +static NSArray* GetKnownExtensions() +{ + return [GetKnownFileTypes() allKeys]; +} + +@interface NSMutableString(AppendKnownFileType) +- (void)appendKnownFileType:(NSString *)filePath; +@end + +@implementation NSMutableString(AppendKnownFileType) +- (void)appendKnownFileType:(NSString *)filePath +{ + NSString* extension = [[filePath pathExtension] lowercaseString]; + NSString* parameter = [GetKnownFileTypes() objectForKey:extension]; + + if (nil == parameter) + { + return; + } + + [self appendFormat:@"%@ \"%@\" ", parameter, filePath]; +} +@end + +// So we can listen for button actions and such we need to have an Obj-C class. +@interface IWADPicker : NSObject +{ + NSApplication *app; + NSWindow *window; + NSButton *okButton; + NSButton *cancelButton; + NSButton *browseButton; + NSTextField *parametersTextField; + bool cancelled; +} + +- (void)buttonPressed:(id) sender; +- (void)browseButtonPressed:(id) sender; +- (void)doubleClicked:(id) sender; +- (void)makeLabel:(NSTextField *)label withString:(const char*) str; +- (int)pickIWad:(WadStuff *)wads num:(int) numwads showWindow:(bool) showwin defaultWad:(int) defaultiwad; +- (NSString*)commandLineParameters; +- (void)menuActionSent:(NSNotification*)notification; +@end + +@implementation IWADPicker + +- (void)buttonPressed:(id) sender +{ + if(sender == cancelButton) + cancelled = true; + + [window orderOut:self]; + [app stopModal]; +} + +- (void)browseButtonPressed:(id) sender +{ + NSOpenPanel* openPanel = [NSOpenPanel openPanel]; + [openPanel setAllowsMultipleSelection:YES]; + [openPanel setCanChooseFiles:YES]; + [openPanel setCanChooseDirectories:YES]; + [openPanel setResolvesAliases:YES]; + [openPanel setAllowedFileTypes:GetKnownExtensions()]; + + if (NSOKButton == [openPanel runModal]) + { + NSArray* files = [openPanel URLs]; + NSMutableString* parameters = [NSMutableString string]; + + for (NSUInteger i = 0, ei = [files count]; i < ei; ++i) + { + NSString* filePath = [[files objectAtIndex:i] path]; + BOOL isDirectory = false; + + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory] && isDirectory) + { + [parameters appendFormat:@"-file \"%@\" ", filePath]; + } + else + { + [parameters appendKnownFileType:filePath]; + } + } + + if ([parameters length] > 0) + { + NSString* newParameters = [parametersTextField stringValue]; + + if ([newParameters length] > 0 + && NO == [newParameters hasSuffix:@" "]) + { + newParameters = [newParameters stringByAppendingString:@" "]; + } + + newParameters = [newParameters stringByAppendingString:parameters]; + + [parametersTextField setStringValue: newParameters]; + } + } +} + +- (void)doubleClicked:(id) sender +{ + if ([sender clickedRow] >= 0) + { + [window orderOut:self]; + [app stopModal]; + } +} + +// Apparently labels in Cocoa are uneditable text fields, so lets make this a +// little more automated. +- (void)makeLabel:(NSTextField *)label withString:(const char*) str +{ + [label setStringValue:[NSString stringWithUTF8String:str]]; + [label setBezeled:NO]; + [label setDrawsBackground:NO]; + [label setEditable:NO]; + [label setSelectable:NO]; +} + +- (int)pickIWad:(WadStuff *)wads num:(int) numwads showWindow:(bool) showwin defaultWad:(int) defaultiwad +{ + cancelled = false; + + app = [NSApplication sharedApplication]; + id windowTitle = [NSString stringWithFormat:@"%s %s", GAMENAME, GetVersionString()]; + + NSRect frame = NSMakeRect(0, 0, 440, 450); + window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]; + [window setTitle:windowTitle]; + + NSTextField *description = [[NSTextField alloc] initWithFrame:NSMakeRect(18, 384, 402, 50)]; + [self makeLabel:description withString:GAMENAME " found more than one IWAD\nSelect from the list below to determine which one to use:"]; + [[window contentView] addSubview:description]; + [description release]; + + NSScrollView *iwadScroller = [[NSScrollView alloc] initWithFrame:NSMakeRect(20, 135, 402, 256)]; + NSTableView *iwadTable = [[NSTableView alloc] initWithFrame:[iwadScroller bounds]]; + IWADTableData *tableData = [[IWADTableData alloc] init:wads num:numwads]; + for(int i = 0;i < NUM_COLUMNS;i++) + { + NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:[NSString stringWithUTF8String:tableHeaders[i]]]; + [[column headerCell] setStringValue:[column identifier]]; + if(i == 0) + [column setMaxWidth:110]; + [column setEditable:NO]; + [column setResizingMask:NSTableColumnAutoresizingMask]; + [iwadTable addTableColumn:column]; + [column release]; + } + [iwadScroller setDocumentView:iwadTable]; + [iwadScroller setHasVerticalScroller:YES]; + [iwadTable setDataSource:tableData]; + [iwadTable sizeToFit]; + [iwadTable setDoubleAction:@selector(doubleClicked:)]; + [iwadTable setTarget:self]; + NSIndexSet *selection = [[NSIndexSet alloc] initWithIndex:defaultiwad]; + [iwadTable selectRowIndexes:selection byExtendingSelection:NO]; + [selection release]; + [iwadTable scrollRowToVisible:defaultiwad]; + [[window contentView] addSubview:iwadScroller]; + [iwadTable release]; + [iwadScroller release]; + + NSTextField *additionalParametersLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(18, 108, 144, 17)]; + [self makeLabel:additionalParametersLabel withString:"Additional Parameters:"]; + [[window contentView] addSubview:additionalParametersLabel]; + parametersTextField = [[NSTextField alloc] initWithFrame:NSMakeRect(20, 48, 402, 54)]; + [parametersTextField setStringValue:[NSString stringWithUTF8String:osx_additional_parameters]]; + [[window contentView] addSubview:parametersTextField]; + + // Doesn't look like the SDL version implements this so lets not show it. + /*NSButton *dontAsk = [[NSButton alloc] initWithFrame:NSMakeRect(18, 18, 178, 18)]; + [dontAsk setTitle:[NSString stringWithCString:"Don't ask me this again"]]; + [dontAsk setButtonType:NSSwitchButton]; + [dontAsk setState:(showwin ? NSOffState : NSOnState)]; + [[window contentView] addSubview:dontAsk];*/ + + okButton = [[NSButton alloc] initWithFrame:NSMakeRect(236, 8, 96, 32)]; + [okButton setTitle:@"OK"]; + [okButton setBezelStyle:NSRoundedBezelStyle]; + [okButton setAction:@selector(buttonPressed:)]; + [okButton setTarget:self]; + [okButton setKeyEquivalent:@"\r"]; + [[window contentView] addSubview:okButton]; + + cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(332, 8, 96, 32)]; + [cancelButton setTitle:@"Cancel"]; + [cancelButton setBezelStyle:NSRoundedBezelStyle]; + [cancelButton setAction:@selector(buttonPressed:)]; + [cancelButton setTarget:self]; + [cancelButton setKeyEquivalent:@"\033"]; + [[window contentView] addSubview:cancelButton]; + + browseButton = [[NSButton alloc] initWithFrame:NSMakeRect(14, 8, 96, 32)]; + [browseButton setTitle:@"Browse..."]; + [browseButton setBezelStyle:NSRoundedBezelStyle]; + [browseButton setAction:@selector(browseButtonPressed:)]; + [browseButton setTarget:self]; + [[window contentView] addSubview:browseButton]; + + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self selector:@selector(menuActionSent:) name:NSMenuDidSendActionNotification object:nil]; + + [window center]; + [app runModalForWindow:window]; + + [center removeObserver:self name:NSMenuDidSendActionNotification object:nil]; + + [window release]; + [okButton release]; + [cancelButton release]; + [browseButton release]; + + return cancelled ? -1 : [iwadTable selectedRow]; +} + +- (NSString*)commandLineParameters +{ + return [parametersTextField stringValue]; +} + +- (void)menuActionSent:(NSNotification*)notification +{ + NSDictionary* userInfo = [notification userInfo]; + NSMenuItem* menuItem = [userInfo valueForKey:@"MenuItem"]; + + if ( @selector(terminate:) == [menuItem action] ) + { + throw CExitEvent(0); + } +} + +@end + + +EXTERN_CVAR(String, defaultiwad) + +static NSString* GetArchitectureString() +{ +#ifdef __i386__ + return @"i386"; +#elif defined __x86_64__ + return @"x86_64"; +#endif +} + +static void RestartWithParameters(const WadStuff& wad, NSString* parameters) +{ + assert(nil != parameters); + + defaultiwad = wad.Name; + + GameConfig->DoGameSetup("Doom"); + M_SaveDefaults(NULL); + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + @try + { + NSString* executablePath = [NSString stringWithUTF8String:Args->GetArg(0)]; + + NSMutableArray* const arguments = [[NSMutableArray alloc] init]; + + // The following value shoud be equal to NSAppKitVersionNumber10_5 + // It's hard-coded in order to build with earlier SDKs + const bool canSelectArchitecture = NSAppKitVersionNumber >= 949; + + if (canSelectArchitecture) + { + [arguments addObject:@"-arch"]; + [arguments addObject:GetArchitectureString()]; + [arguments addObject:executablePath]; + + executablePath = @"/usr/bin/arch"; + } + + [arguments addObject:@"-iwad"]; + [arguments addObject:[NSString stringWithUTF8String:wad.Path]]; + + for (int i = 1, count = Args->NumArgs(); i < count; ++i) + { + NSString* currentParameter = [NSString stringWithUTF8String:Args->GetArg(i)]; + [arguments addObject:currentParameter]; + } + + wordexp_t expansion = {}; + + if (0 == wordexp([parameters UTF8String], &expansion, 0)) + { + for (size_t i = 0; i < expansion.we_wordc; ++i) + { + NSString* argumentString = [NSString stringWithCString:expansion.we_wordv[i] + encoding:NSUTF8StringEncoding]; + [arguments addObject:argumentString]; + } + + wordfree(&expansion); + } + + [NSTask launchedTaskWithLaunchPath:executablePath + arguments:arguments]; + + _exit(0); // to avoid atexit()'s functions + } + @catch (NSException* e) + { + NSLog(@"Cannot restart: %@", [e reason]); + } + + [pool release]; +} + +// Simple wrapper so we can call this from outside. +int I_PickIWad_Cocoa (WadStuff *wads, int numwads, bool showwin, int defaultiwad) +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + IWADPicker *picker = [IWADPicker alloc]; + int ret = [picker pickIWad:wads num:numwads showWindow:showwin defaultWad:defaultiwad]; + + NSString* parametersToAppend = [picker commandLineParameters]; + osx_additional_parameters = [parametersToAppend UTF8String]; + + if (ret >= 0) + { + if (0 != [parametersToAppend length]) + { + RestartWithParameters(wads[ret], parametersToAppend); + } + } + + [pool release]; + + return ret; +} diff --git a/source/platform/posix/osx/zdoom-info.plist b/source/platform/posix/osx/zdoom-info.plist new file mode 100644 index 000000000..f0bbbf3d9 --- /dev/null +++ b/source/platform/posix/osx/zdoom-info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIconFile + zdoom.icns + CFBundleIdentifier + org.drdteam.gzdoom + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + GZDoom + CFBundlePackageType + APPL + CFBundleShortVersionString + Development Version + CFBundleSignature + ???? + LSApplicationCategoryType + public.app-category.action-games + LSMinimumSystemVersion + 10.9 + CFBundleDocumentTypes + + + CFBundleTypeName + Doom Resource File + CFBundleTypeRole + Viewer + CFBundleTypeExtensions + + wad + pk3 + zip + pk7 + 7z + iwad + ipk3 + ipk7 + + + + NSPrincipalClass + NSApplication + NSSupportsAutomaticGraphicsSwitching + + + diff --git a/source/platform/posix/osx/zdoom.icns b/source/platform/posix/osx/zdoom.icns new file mode 100644 index 0000000000000000000000000000000000000000..a0df7f5f7931e269dc82f81a9e4e15548bb5e9a6 GIT binary patch literal 120245 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