Skip to content

Instantly share code, notes, and snippets.

@bibhas
Created November 28, 2020 10:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bibhas/29dacefe7e1d3742f301e825dabd3efa to your computer and use it in GitHub Desktop.
Save bibhas/29dacefe7e1d3742f301e825dabd3efa to your computer and use it in GitHub Desktop.
//
// CoreLib.h
// CoreLib
//
// Created by CoreCode on 12.04.12.
/* Copyright © 2020 CoreCode Limited
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitationthe rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifdef __OBJC__
#ifndef CORELIB
#define CORELIB 1
#ifdef __cplusplus
extern "C"
{
#endif
// include system headers and make sure requrements are met
#if __has_feature(modules)
@import Darwin.TargetConditionals;
@import Darwin.Availability;
#else
#import <TargetConditionals.h>
#import <Availability.h>
#endif
#if ! __has_feature(objc_arc) // this is hit even when including corelib in the PCH and any file in the project - maybe not even using corelib - still uses manual retain count, so its a warning and not an error
#warning CoreLib > 1.13 does not support manual reference counting anymore
#endif
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
#if __has_feature(modules)
@import Cocoa;
#else
#import <Cocoa/Cocoa.h>
#endif
#if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_13
#error CoreLib only deploys back to Mac OS X 10.13
#endif
#endif
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
#if __has_feature(modules)
@import UIKit;
#else
#import <UIKit/UIKit.h>
#endif
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
#error CoreLib only deploys back to iOS 8
#endif
#endif
// !!!: BASIC TYPES
typedef CGPoint CCFloatPoint;
typedef struct { NSInteger x; NSInteger y; } CCIntPoint;
typedef struct { CGFloat min; CGFloat max; CGFloat length; } CCFloatRange1D;
typedef struct { CCFloatPoint min; CCFloatPoint max; CCFloatPoint length; } CCFloatRange2D;
typedef struct { NSInteger min; NSInteger max; NSInteger length; } CCIntRange1D;
typedef struct { CCIntPoint min; CCIntPoint max; CCIntPoint length; } CCIntRange2D;
#ifdef __BLOCKS__
typedef void (^BasicBlock)(void);
typedef void (^DoubleInBlock)(double input);
typedef void (^StringInBlock)(NSString *input);
typedef void (^ObjectInBlock)(id input);
typedef id (^ObjectInOutBlock)(id input);
typedef int (^ObjectInIntOutBlock)(id input);
typedef float (^ObjectInFloatOutBlock)(id input);
typedef CCIntPoint (^ObjectInPointOutBlock)(id input);
typedef int (^IntInOutBlock)(int input);
typedef void (^IntInBlock)(int input);
typedef int (^IntOutBlock)(void);
#endif
#ifdef __cplusplus
#define CC_ENUM(type, name) enum class name : type
#else
#define CC_ENUM(type, name) typedef NS_ENUM(type, name)
#endif
CC_ENUM(uint8_t, openChoice)
{
openSupportRequestMail = 1, // VendorProductPage info.plist key
openBetaSignupMail, // FeedbackEmail info.plist key
openHomepageWebsite, // VendorProductPage info.plist key
openAppStoreWebsite, // StoreProductPage info.plist key
openAppStoreApp, // StoreProductPage info.plist key
openMacupdateWebsite // MacupdateProductPage info.plist key
};
@protocol CoreLibAppDelegate
@optional
- (NSString *)customSupportRequestAppName;
- (NSString *)customSupportRequestLicense;
@end
#define MAKE_MAKER(classname) \
static inline NS ## classname * make ## classname (void) { return (NS ## classname *)[NS ## classname new];}
MAKE_MAKER(MutableArray)
MAKE_MAKER(MutableDictionary)
MAKE_MAKER(MutableIndexSet)
MAKE_MAKER(MutableString)
MAKE_MAKER(MutableSet)
// !!!: CORELIB OBJ INTERFACE
@interface CoreLib : NSObject
// info bundle key convenience
@property (readonly, nonatomic) NSString *appBundleIdentifier;
@property (readonly, nonatomic) int appBuildNumber;
@property (readonly, nonatomic) NSString *appVersionString;
@property (readonly, nonatomic) NSString *appName;
// path convenience
@property (readonly, nonatomic) NSString *prefsPath;
@property (readonly, nonatomic) NSString *resDir;
@property (readonly, nonatomic) NSString *docDir;
@property (readonly, nonatomic) NSString *deskDir;
@property (readonly, nonatomic) NSString *suppDir;
@property (readonly, nonatomic) NSURL *prefsURL;
@property (readonly, nonatomic) NSURL *resURL;
@property (readonly, nonatomic) NSURL *docURL;
@property (readonly, nonatomic) NSURL *deskURL;
@property (readonly, nonatomic) NSURL *suppURL;
@property (readonly, nonatomic) NSURL *homeURLInsideSandbox;
@property (readonly, nonatomic) NSURL *homeURLOutsideSandbox;
// misc
@property (readonly, nonatomic) NSArray <NSString *>*appCrashLogFilenames;
@property (readonly, nonatomic) NSArray <NSString *>*appCrashLogs;
@property (readonly, nonatomic) NSString *appChecksumSHA;
@property (readonly, nonatomic) NSString *appChecksumIncludingFrameworksSHA;
- (void)openURL:(openChoice)choice;
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
- (void)sendSupportRequestMail:(NSString *)text;
#endif
@end
@interface FakeAlertWindow : NSWindow
@end
// !!!: GLOBALS
extern CoreLib *cc; // init CoreLib with: cc = [CoreLib new];
extern NSUserDefaults *userDefaults;
extern NSFileManager *fileManager;
extern NSNotificationCenter *notificationCenter;
extern NSBundle *bundle;
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
extern NSFontManager *fontManager;
extern NSDistributedNotificationCenter *distributedNotificationCenter;
extern NSWorkspace *workspace;
extern NSApplication *application;
extern NSProcessInfo *processInfo;
#endif
// !!!: ALERT FUNCTIONS
NSInteger alert_selection_popup(NSString *prompt, NSArray<NSString *> *choices, NSArray<NSString *> *buttons, NSUInteger *result); // alert with popup button for selection of choice
NSInteger alert_selection_matrix(NSString *prompt, NSArray<NSString *> *choices, NSArray<NSString *> *buttons, NSUInteger *result); // alert with radiobutton matrix for selection of choice
NSInteger alert_input(NSString *title, NSArray *buttons, NSString **result); // alert with text field prompting users
NSInteger alert_inputtext(NSString *title, NSArray *buttons, NSString **result); // alert with large text view prompting users
NSInteger alert_checkbox(NSString *title, NSString *message, NSArray <NSString *>*buttons, NSString *checkboxTitle, NSUInteger *checkboxStatus); // alert with a single checkbox
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
NSInteger alert_colorwell(NSString *prompt, NSArray <NSString *>*buttons, NSColor **selectedColor); // alert with a colorwell for choosing colors
NSInteger alert_customicon(NSString *title, NSString *message, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, NSImage *customIcon);
#endif
NSInteger alert_inputsecure(NSString *prompt, NSArray *buttons, NSString **result);
NSInteger alert_outputtext(NSString *message, NSArray *buttons, NSString *text);
NSInteger alert_apptitled(NSString *message, NSString *defaultButton, NSString *alternateButton, NSString *otherButton);
NSInteger alert(NSString *title, NSString *message, NSString *defaultButton, NSString *alternateButton, NSString *otherButton);
void alert_dontwarnagain_version(NSString *identifier, NSString *title, NSString *message, NSString *defaultButton, NSString *dontwarnButton) __attribute__((nonnull (4, 5)));
void alert_dontwarnagain_ever(NSString *identifier, NSString *title, NSString *message, NSString *defaultButton, NSString *dontwarnButton) __attribute__((nonnull (4, 5)));
void alert_feedback_fatal(NSString *usermsg, NSString *details) __attribute__((noreturn));
void alert_feedback_nonfatal(NSString *usermsg, NSString *details);
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
void alert_nonmodal(NSString *title, NSString *message, NSString *button);
void alert_nonmodal_customicon(NSString *title, NSString *message, NSString *button, NSImage *customIcon);
void alert_nonmodal_customicon_block(NSString *title, NSString *message, NSString *button, NSImage *customIcon, BasicBlock block);
void alert_nonmodal_checkbox(NSString *title, NSString *message, NSString *button, NSString *checkboxTitle, NSInteger checkboxStatusIn, IntInBlock resultBlock);
#endif
// !!!: OBJECT CREATION FUNCTIONS
NSString *makeString(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
NSString *makeLocalizedString(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
NSValue *makeRectValue(CGFloat x, CGFloat y, CGFloat width, CGFloat height);
NSString *makeTempDirectory(void);
NSString *makeTempFilepath(NSString *extension);
NSPredicate *makePredicate(NSString *format, ...);
NSString *makeDescription(NSObject *sender, NSArray *args);
#define makeDictionaryOfVariables(...) _makeDictionaryOfVariables(@"" # __VA_ARGS__, __VA_ARGS__, nil) // like NSDictionaryOfVariableBindings() but safe in case of nil values
NSDictionary<NSString *, id> * _makeDictionaryOfVariables(NSString * commaSeparatedKeysString, id firstValue, ...); // not for direct use
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
NSColor *makeColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a); // params from 0..1
NSColor *makeColor255(CGFloat r, CGFloat g, CGFloat b, CGFloat a); // params from 0..255
#else
UIColor *makeColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a);
UIColor *makeColor255(CGFloat r, CGFloat g, CGFloat b, CGFloat a);
#endif
CGFloat generateRandomFloatBetween(CGFloat a, CGFloat b);
int generateRandomIntBetween(int a, int b);
// !!!: GRAND CENTRAL DISPATCH FUNCTIONS
void dispatch_after_main(float seconds, dispatch_block_t block);
void dispatch_after_back(float seconds, dispatch_block_t block);
void dispatch_async_main(dispatch_block_t block);
void dispatch_async_back(dispatch_block_t block);
void dispatch_sync_main(dispatch_block_t block);
void dispatch_sync_back(dispatch_block_t block);
BOOL dispatch_sync_back_timeout(dispatch_block_t block, float timeoutSeconds); // returns 0 on succ
id dispatch_async_to_sync(BasicBlock block);
// !!!: CONSTANT KEYS
#define CONST_KEY(name) \
static NSString *const k ## name ## Key = @ #name;
#define CONST_KEY_IMPLEMENTATION(name) \
NSString *const k ## name ## Key = @ #name;
#define CONST_KEY_DECLARATION(name) \
extern NSString *const k ## name ## Key;
#define CONST_KEY_ENUM(name, enumname) \
@interface name ## Key : NSString @property (assign, nonatomic) enumname defaultInt; @end \
static name ## Key *const k ## name ## Key = ( name ## Key *) @ #name;
#define CONST_KEY_ENUM_IMPLEMENTATION(name, enumname) \
name ## Key *const k ## name ## Key = ( name ## Key *) @ #name;
#define CONST_KEY_ENUM_DECLARATION(name, enumname) \
@interface name ## Key : NSString @property (assign, nonatomic) enumname defaultInt; @end \
extern name ## Key *const k ## name ## Key;
// !!!: LOGGING
typedef NS_ENUM(uint8_t, cc_log_type)
{
CC_LOG_LEVEL_DEBUG = 7,
CC_LOG_LEVEL_DEFAULT = 5,
CC_LOG_LEVEL_ERROR = 3,
CC_LOG_LEVEL_FAULT = 0,
};
void log_to_prefs(NSString *string);
void cc_log_enablecapturetofile(NSURL *fileURL, unsigned long long sizeLimit, cc_log_type minimumLogType);
void cc_defaults_addtoarray(NSString *key, NSObject *entry, NSUInteger maximumEntries);
void cc_log_level(cc_log_type level, NSString *format, ...) NS_FORMAT_FUNCTION(2,3);
#ifdef FORCE_LOG
#define cc_log_debug(...) cc_log_level(CC_LOG_LEVEL_DEFAULT, __VA_ARGS__)
#elif defined(DEBUG) && !defined(FORCE_NOLOG)
#define cc_log_debug(...) cc_log_level(CC_LOG_LEVEL_DEBUG, __VA_ARGS__)
#else
#define cc_log_debug(...)
#endif
#define cc_log(...) cc_log_level(CC_LOG_LEVEL_DEFAULT, __VA_ARGS__)
#define cc_log_error(...) cc_log_level(CC_LOG_LEVEL_ERROR, __VA_ARGS__)
#define cc_log_emerg(...) cc_log_level(CC_LOG_LEVEL_FAULT, __VA_ARGS__)
#define LOGFUNCA cc_log_debug(@"%@ %@ (%p)", self.undoManager.isUndoing ? @"UNDOACTION" : (self.undoManager.isRedoing ? @"REDOACTION" : @"ACTION"), @(__PRETTY_FUNCTION__), (__bridge void *)self);
#define LOGFUNC cc_log_debug(@"%@ (%p)", @(__PRETTY_FUNCTION__), (__bridge void *)self);
#define LOGFUNCPARAMA(x) cc_log_debug(@"%@ %@ (%p) [%@]", self.undoManager.isUndoing ? @"UNDOACTION" : (self.undoManager.isRedoing ? @"REDOACTION" : @"ACTION"), @(__PRETTY_FUNCTION__), (__bridge void *)self, [(x) description]);
#define LOGFUNCPARAM(x) cc_log_debug(@"%@ (%p) [%@]", @(__PRETTY_FUNCTION__), (__bridge void *)self, [(NSObject *)(x) description]);
#define LOGSUCC cc_log_debug(@"success %@ %d", @(__FILE__), __LINE__);
#define LOGFAIL cc_log_debug(@"failure %@ %d", @(__FILE__), __LINE__);
#define LOG(x) cc_log_debug(@"%@", [(x) description]);
// !!!: ASSERTION MACROS
#ifdef CUSTOM_ASSERT_FUNCTION // allows clients to get more info about failures, just def CUSTOM_ASSERT_FUNCTION to a function that sends the string home
void CUSTOM_ASSERT_FUNCTION(NSString * text);
#define assert_custom(e) (__builtin_expect(!(e), 0) ? CUSTOM_ASSERT_FUNCTION(makeString(@"%@ %@ %i %@", @(__func__), @(__FILE__), __LINE__, @(#e))) : (void)0)
#define assert_custom_info(e, i) (__builtin_expect(!(e), 0) ? CUSTOM_ASSERT_FUNCTION(makeString(@"%@ %@ %i %@ info: %@", @(__func__), @(__FILE__), __LINE__, @(#e), i)) : (void)0)
#else
#define assert_custom(e) assert(e)
#define assert_custom_info(e, i) assert(((e) || ((e) && (i))))
#endif
#define ASSERT_MAINTHREAD assert_custom_info([NSThread currentThread] == [NSThread mainThread], makeString(@"main thread violation: %@", [NSThread.callStackSymbols joined:@"|"]))
#define ASSERT_BACKTHREAD assert_custom_info([NSThread currentThread] != [NSThread mainThread], makeString(@"back thread violation: %@", [NSThread.callStackSymbols joined:@"|"]))
// !!!: CONVENIENCE MACROS
#define PROPERTY_STR(p) NSStringFromSelector(@selector(p))
#define OBJECT_OR(x,y) ((x) ? (x) : (y))
#define STRING_OR(x, y) (((x) && ([x isKindOfClass:[NSString class]]) && ([((NSString *)x) length])) ? (x) : (y))
#define VALID_STR(x) (((x) && ([x isKindOfClass:[NSString class]])) ? (x) : @"")
#define NON_NIL_STR(x) ((x) ? (x) : @"")
#define NON_NIL_ARR(x) ((x) ? (x) : @[])
#define NON_NIL_NUM(x) ((x) ? (x) : @(0))
#define NON_NIL_OBJ(x) ((x) ? (x) : [NSNull null])
#define NON_NSNULL_OBJ(x) (([[NSNull null] isEqual:(x)]) ? nil : (x))
#define IS_FLOAT_EQUAL(x,y) (fabsf((x)-(y)) < 0.0001f)
#define IS_DOUBLE_EQUAL(x,y) (fabs((x)-(y)) < 0.000001)
#define IS_IN_RANGE(v,l,h) (((v) >= (l)) && ((v) <= (h)))
#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
#define ONCE_PER_FUNCTION(b) { static dispatch_once_t onceToken; dispatch_once(&onceToken, b); }
#define ONCE_PER_OBJECT(o,b) @synchronized(o){ static dispatch_once_t onceToken; NSNumber *tokenNumber = [o associatedValueForKey:o.id]; onceToken = tokenNumber.longValue; dispatch_once(&onceToken, b); [o setAssociatedValue:@(onceToken) forKey:o.id]; }
#define ONCE_EVERY_MINUTES(b,m) { static NSDate *time = nil; if (!time || [[NSDate date] timeIntervalSinceDate:time] > (m * 60)) { b(); time = [NSDate date]; }}
#define RANDOM_INIT {srandom((unsigned)time(0));}
#define RANDOM_FLOAT(a,b) ((a) + ((b) - (a)) * (random() / (CGFloat) RAND_MAX))
#define RANDOM_INT(a,b) ((int)((a) + ((b) - (a) + 1) * (random() / (CGFloat) RAND_MAX))) // this is INCLUSIVE; a and b will be part of the results
#define OBJECT_OR3(x,y,z) OBJECT_OR((x),OBJECT_OR((y),(z)))
#define STRING_OR3(x,y,z) STRING_OR((x),STRING_OR((y),(z)))
#define MAX3(x,y,z) (MAX(MAX((x),(y)),(z)))
#define MIN3(x,y,z) (MIN(MIN((x),(y)),(z)))
#define BYTES_TO_KB(x) ((double)(x) / (1024.0))
#define BYTES_TO_MB(x) ((double)(x) / (1024.0 * 1024.0))
#define BYTES_TO_GB(x) ((double)(x) / (1024.0 * 1024.0 * 1024.0))
#define KB_TO_BYTES(x) ((x) * (1024))
#define MB_TO_BYTES(x) ((x) * (1024 * 1024))
#define GB_TO_BYTES(x) ((x) * (1024 * 1024 * 1024))
#define BYTES_TO_KB_NEW(x) ((double)(x) / (1000.0))
#define BYTES_TO_MB_NEW(x) ((double)(x) / (1000.0 * 1000.0))
#define BYTES_TO_GB_NEW(x) ((double)(x) / (1000.0 * 1000.0 * 1000.0))
#define KB_TO_BYTES_NEW(x) ((x) * (1000))
#define MB_TO_BYTES_NEW(x) ((x) * (1000 * 1000))
#define GB_TO_BYTES_NEW(x) ((x) * (1000 * 1000 * 1000))
#define SECONDS_PER_MINUTES(x) ((x) * 60)
#define SECONDS_PER_HOURS(x) (SECONDS_PER_MINUTES((x)) * 60)
#define SECONDS_PER_DAYS(x) (SECONDS_PER_HOURS((x)) * 24)
#define SECONDS_PER_WEEKS(x) (SECONDS_PER_DAYS((x)) * 7)
// !!!: SWIFTY VAR & LET SUPPORT
#define var __auto_type
#define let const __auto_type
// !!!: CONFIGURATION
#ifdef VENDOR_HOMEPAGE
#define kVendorHomepage VENDOR_HOMEPAGE
#else
#define kVendorHomepage @"https://www.corecode.io/"
#endif
#ifdef FEEDBACK_EMAIL
#define kFeedbackEmail FEEDBACK_EMAIL
#else
#define kFeedbackEmail @"feedback@corecode.io"
#endif
#define kExceptionInformationKey @"corelib_exception_info"
// !!!: UNDEFS
// this makes sure youo not compare the return values of our alert*() functions against old values and use NSLog when you should use ASL. remove as appropriate
#ifndef IMADESURENOTTOCOMPAREALERTRETURNVALUESAGAINSTOLDRETURNVALUES
// alert() and related corelib functions previously returned old deprecated return values from NSRunAlertPanel() and friends. now they return new NSAlertFirst/..ButtonReturn values. we undefine the old return values to make sure you don't use them. if you use NSRunAlertPanel() and friends directly in your code you can set the define to prevent the errors after making sure to update return value checks of alert*()
#define NSAlertDefaultReturn
#define NSAlertAlternateReturn
#define NSAlertOtherReturn
#define NSAlertErrorReturn
#define NSOKButton
#define NSCancelButton
#endif
#ifndef DISABLELOGGINGIMPLEMENTATION
#define asl_log
#define asl_NSLog_debug
#define NSLog
#define os_log
#define os_log_info
#define os_log_debug
#define os_log_error
#define os_log_fault
#endif
#ifdef __cplusplus
}
#endif
#endif
#endif
// !!!: INCLUDES
#import "AppKit+CoreCode.h"
#import "Foundation+CoreCode.h"
//
// CoreLib.m
// CoreLib
//
// Created by CoreCode on 17.12.12.
/* Copyright © 2020 CoreCode Limited
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitationthe rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "CoreLib.h"
#ifndef CORELIB
#error you need to include CoreLib.h in your PCH file
#endif
#ifdef USE_SECURITY
#if __has_feature(modules)
@import CommonCrypto.CommonDigest;
#else
#include <CommonCrypto/CommonDigest.h>
#endif
#endif
#if __has_feature(modules)
@import Darwin.POSIX.unistd;
@import Darwin.POSIX.sys.types;
@import Darwin.POSIX.pwd;
#include <assert.h>
#else
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <assert.h>
#endif
CoreLib *cc;
NSUserDefaults *userDefaults;
NSFileManager *fileManager;
NSNotificationCenter *notificationCenter;
NSBundle *bundle;
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
NSFontManager *fontManager;
NSDistributedNotificationCenter *distributedNotificationCenter;
NSApplication *application;
NSWorkspace *workspace;
NSProcessInfo *processInfo;
#endif
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
NSString *_machineType(void);
BOOL _isUserAdmin(void);
__attribute__((noreturn)) void exceptionHandler(NSException *exception)
{
NSString *exceptionDetails = makeString(@" %@ %@ %@ %@", exception.description, exception.reason, exception.userInfo.description, exception.callStackSymbols);
NSString *exceptionInfoToStore = [NSString stringWithFormat:@"Date: %@ Exception:%@", NSDate.date.shortDateAndTimeString, exceptionDetails];
cc_defaults_addtoarray(kExceptionInformationKey, exceptionInfoToStore, 10);
alert_feedback_fatal(exception.name, exceptionDetails);
}
#endif
@implementation CoreLib
@dynamic appCrashLogs, appCrashLogFilenames, appBundleIdentifier, appBuildNumber, appVersionString, appName, resDir, docDir, suppDir, resURL, docURL, suppURL, deskDir, deskURL, prefsPath, prefsURL, homeURLInsideSandbox, homeURLOutsideSandbox
#ifdef USE_SECURITY
, appChecksumSHA, appChecksumIncludingFrameworksSHA;
#else
;
#endif
+ (void)initialize
{
}
- (instancetype)init
{
assert(!cc);
if ((self = [super init]))
{
cc = self;
userDefaults = NSUserDefaults.standardUserDefaults;
fileManager = NSFileManager.defaultManager;
notificationCenter = NSNotificationCenter.defaultCenter;
bundle = NSBundle.mainBundle;
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
fontManager = NSFontManager.sharedFontManager;
distributedNotificationCenter = NSDistributedNotificationCenter.defaultCenter;
workspace = NSWorkspace.sharedWorkspace;
application = NSApplication.sharedApplication;
processInfo = NSProcessInfo.processInfo;
#endif
#ifndef SKIP_CREATE_APPSUPPORT_DIRECTORY
if (self.appName)
{
BOOL exists = self.suppURL.fileExists;
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
if ((!exists && self.suppURL.fileIsSymlink) || // broken symlink
(exists && !self.suppURL.fileIsDirectory)) // not a folder
{
alert_apptitled(makeString(@"This application can not be launched because its 'Application Support' folder is not a folder but a file. Please remove the file '%@' and re-launch this app.", self.suppURL.path), @"OK", nil, nil);
}
else
#endif
if (!exists) // non-existant
{
NSError *error;
BOOL succ = [fileManager createDirectoryAtURL:self.suppURL withIntermediateDirectories:YES attributes:nil error:&error];
if (!succ)
{
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
alert_apptitled(makeString(@"This application can not be launched because the 'Application Support' can not be created at the path '%@'.\nError: %@", self.suppURL.path, error.localizedDescription), @"OK", nil, nil);
#else
cc_log(@"This application can not be launched because the 'Application Support' can not be created at the path '%@'.\nError: %@", self.suppURL.path, error.localizedDescription);
#endif
exit(1);
}
}
}
#endif
#ifdef DEBUG
#ifndef XCTEST
BOOL isSandbox = [@"~/Library/".expanded contains:@"/Library/Containers/"];
#ifdef SANDBOX
assert(isSandbox);
#else
assert(!isSandbox);
#endif
#endif
#ifdef NDEBUG
LOG(@"Warning: you are running in DEBUG mode but have disabled assertions (NDEBUG)");
#endif
#if !defined(XCTEST) || !XCTEST
NSString *bundleID = [bundle objectForInfoDictionaryKey:@"CFBundleIdentifier"];
if (![[self appBundleIdentifier] isEqualToString:bundleID] && self.appName)
{
cc_log_error(@"Error: bundle identifier doesn't match");
exit(666);
}
#endif
if ([(NSNumber *)[bundle objectForInfoDictionaryKey:@"LSUIElement"] boolValue] &&
![(NSString *)[bundle objectForInfoDictionaryKey:@"NSPrincipalClass"] isEqualToString:@"JMDocklessApplication"])
cc_log_debug(@"Warning: app can hide dock symbol but has no fixed principal class");
#ifndef CLI
if (![[(NSString *)[bundle objectForInfoDictionaryKey:@"MacupdateProductPage"] lowercaseString] contains:self.appName.lowercaseString])
cc_log_debug(@"Warning: info.plist key MacupdateProductPage not properly set");
if ([[(NSString *)[bundle objectForInfoDictionaryKey:@"MacupdateProductPage"] lowercaseString] contains:@"/find/"])
cc_log_debug(@"Warning: info.plist key MacupdateProductPage should be updated to proper product page");
if (![[(NSString *)[bundle objectForInfoDictionaryKey:@"StoreProductPage"] lowercaseString] contains:self.appName.lowercaseString])
cc_log_debug(@"Warning: info.plist key StoreProductPage not properly set (%@ NOT CONTAINS %@", [(NSString *)[bundle objectForInfoDictionaryKey:@"StoreProductPage"] lowercaseString], self.appName.lowercaseString);
if (!((NSString *)[bundle objectForInfoDictionaryKey:@"LSApplicationCategoryType"]).length)
LOG(@"Warning: LSApplicationCategoryType not properly set")
#endif
if (NSClassFromString(@"JMRatingWindowController") &&
NSProcessInfo.processInfo.environment[@"XCInjectBundleInto"] != nil)
{
assert(@"icon-appstore".namedImage);
assert(@"icon-macupdate".namedImage);
assert(@"JMRatingWindow.nib".resourceURL);
}
#ifdef USE_SPARKLE
assert(@"dsa_pub.pem".resourceURL);
#endif
#else
#if !defined(NDEBUG) && !defined(CLI)
cc_log_error(@"Warning: you are not running in DEBUG mode but have not disabled assertions (NDEBUG)");
#endif
#endif
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
#ifndef DONT_CRASH_ON_EXCEPTIONS
NSSetUncaughtExceptionHandler(&exceptionHandler);
#endif
#if !defined(XCTEST) || !XCTEST
NSString *frameworkPath = bundle.privateFrameworksPath;
for (NSString *framework in frameworkPath.directoryContents)
{
NSString *smylinkToBinaryPath = makeString(@"%@/%@/%@", frameworkPath, framework, framework.stringByDeletingPathExtension);
if (!smylinkToBinaryPath.fileIsAlias)
{
#ifdef DEBUG
if ([framework hasPrefix:@"libclang"]) continue;
#endif
alert_apptitled(makeString(@"This application is damaged. Either your download was damaged or you used a faulty program to extract the ZIP archive. Please re-download and make sure to use the ZIP decompression built into Mac OS X.\n\nOffending Path: %@", smylinkToBinaryPath), @"OK", nil, nil);
exit(1);
}
#ifdef DEBUG
NSString *versionsPath = makeString(@"%@/%@/Versions", frameworkPath, framework);
for (NSString *versionsEntry in versionsPath.directoryContents)
{
if ((![versionsEntry isEqualToString:@"A"]) && (![versionsEntry isEqualToString:@"Current"]))
{
cc_log_error(@"The frameworks are damaged probably by lowercasing. Either your download was damaged or you used a faulty program to extract the ZIP archive. Please re-download and make sure to use the ZIP decompression built into Mac OS X.");
exit(1);
}
}
NSString *versionAPath = makeString(@"%@/%@/Versions/A", frameworkPath, framework);
for (NSString *entry in versionAPath.directoryContents)
{
if (([entry isEqualToString:@"headers"]) && (![entry isEqualToString:@"resources"]))
{
cc_log_error(@"The frameworks are damaged probably by lowercasing. Either your download was damaged or you used a faulty program to extract the ZIP archive. Please re-download and make sure to use the ZIP decompression built into Mac OS X.");
exit(1);
}
}
#endif
}
#endif
#endif
RANDOM_INIT
}
assert(cc);
return self;
}
- (NSString *)prefsPath
{
return makeString(@"~/Library/Preferences/%@.plist", self.appBundleIdentifier).expanded;
}
- (NSURL *)prefsURL
{
return self.prefsPath.fileURL;
}
- (NSArray *)appCrashLogFilenames // doesn't do anything in sandbox!
{
NSArray <NSString *> *logs1 = @"~/Library/Logs/DiagnosticReports/".expanded.directoryContents;
logs1 = [logs1 filteredUsingPredicateString:@"self BEGINSWITH[cd] %@ AND self ENDSWITH '.crash'", self.appName]; // there is also .spin and .diag but we aren't interested in them ATM
logs1 = [logs1 mapped:^id(NSString *input) { return [@"~/Library/Logs/DiagnosticReports/".stringByExpandingTildeInPath stringByAppendingPathComponent:input]; }];
NSArray <NSString *> *logs2 = @"/Library/Logs/DiagnosticReports/".expanded.directoryContents;
logs2 = [logs2 filteredUsingPredicateString:@"self BEGINSWITH[cd] %@ AND self ENDSWITH '.crash'", self.appName];
logs2 = [logs2 mapped:^id(NSString *input) { return [@"/Library/Logs/DiagnosticReports/" stringByAppendingPathComponent:input]; }];
NSArray <NSString *> *logs = [logs1 arrayByAddingObjectsFromArray:logs2];
return logs;
}
- (NSArray *)appCrashLogs // doesn't do anything in sandbox!
{
NSArray <NSString *> *logFilenames = self.appCrashLogFilenames;
NSArray <NSString *> *logs = [logFilenames mapped:^id(NSString *input) { return [input.contents.string split:@"/System/Library/"][0]; }];
return logs;
}
- (NSString *)appBundleIdentifier
{
return NSBundle.mainBundle.bundleIdentifier;
}
- (NSString *)appVersionString
{
return [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
}
- (NSString *)appName
{
#if defined(XCTEST) && XCTEST
return @"TEST";
#else
return [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleName"];
#endif
}
- (int)appBuildNumber
{
#ifdef CLI
#ifndef CLI_BUNDLEVERSION
#define CLI_BUNDLEVERSION 1
#endif
return @(CLI_BUNDLEVERSION).intValue;
#else
NSString *bundleVersion = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"];
return bundleVersion.intValue;
#endif
}
- (NSString *)resDir
{
return NSBundle.mainBundle.resourcePath;
}
- (NSURL *)resURL
{
return NSBundle.mainBundle.resourceURL;
}
- (NSString *)docDir
{
return NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
}
- (NSString *)deskDir
{
return NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES)[0];
}
- (NSURL *)homeURLInsideSandbox
{
return NSHomeDirectory().fileURL;
}
- (NSURL *)homeURLOutsideSandbox
{
struct passwd *pw = getpwuid(getuid());
assert(pw);
NSString *realHomePath = @(pw->pw_dir);
NSURL *realHomeURL = [NSURL fileURLWithPath:realHomePath];
return realHomeURL;
}
- (NSURL *)docURL
{
return [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
}
- (NSURL *)deskURL
{
return [NSFileManager.defaultManager URLsForDirectory:NSDesktopDirectory inDomains:NSUserDomainMask][0];
}
- (NSString *)suppDir
{
return [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:self.appName];
}
- (NSURL * __nonnull)suppURL
{
NSURL *dir = [NSFileManager.defaultManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask][0];
NSString *appName = self.appName;
if (!appName)
appName = [NSProcessInfo.processInfo.arguments.firstObject split:@"/"].lastObject;
return [dir add:appName];
}
- (NSString *)appChecksumSHA
{
NSURL *u = NSBundle.mainBundle.executableURL;
return u.fileChecksumSHA;
}
- (NSString *)appChecksumIncludingFrameworksSHA
{
NSURL *u = NSBundle.mainBundle.executableURL;
NSString *checksum = [u.fileChecksumSHA clamp:10];
for (NSURL *framework in NSBundle.mainBundle.privateFrameworksURL.directoryContents)
{
NSURL *exe = [NSBundle bundleWithURL:framework].executableURL;
NSString *frameworkChecksum = exe.fileChecksumSHA;
checksum = makeString(@"%@ %@", checksum, [frameworkChecksum clamp:10]);
}
return checksum;
}
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
- (void)sendSupportRequestMail:(NSString *)text
{
NSString *urlString = @"";
NSString *encodedPrefs = @"";
NSString *crashReports = @"";
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
BOOL optionDown = ([NSEvent modifierFlags] & NSEventModifierFlagOption) != 0;
if (optionDown)
{
encodedPrefs = makeString(@"Preferences (BASE64): %@", [self.prefsURL.contents base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]);
}
#ifndef SANDBOX
if ((cc.appCrashLogFilenames).count)
{
NSArray <NSString *> *logFilenames = cc.appCrashLogFilenames;
NSString *crashes = @"";
for (NSString *path in logFilenames)
{
if (![path hasSuffix:@"crash"]) continue;
NSString *token = makeString(@"DSC_%@", path);
if (!token.defaultInt)
{
NSString *additionalCrash = [path.contents.string split:@"/System/Library/"][0];
if (crashes.length + additionalCrash.length < 100000)
{
crashes = [crashes stringByAppendingString:additionalCrash];
token.defaultInt = 1; // we don't wanna send crashes twice, but erasing them is probably not OK
}
}
}
crashReports = makeString(@"Crash Reports: \n\n%@", crashes);
}
#endif
#endif
NSString *appName = cc.appName;
NSString *licenseCode = cc.appChecksumIncludingFrameworksSHA;
NSString *recipient = OBJECT_OR([bundle objectForInfoDictionaryKey:@"FeedbackEmail"], kFeedbackEmail);
NSString *udid = @"N/A";
#if defined(USE_SECURITY) && defined(USE_IOKIT)
Class hostInfoClass = NSClassFromString(@"JMHostInformation");
if (hostInfoClass)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
NSString *macAddress = [hostInfoClass performSelector:@selector(macAddress)];
#pragma clang diagnostic pop
udid = macAddress.SHA1;
}
#endif
if ([NSApp.delegate respondsToSelector:@selector(customSupportRequestAppName)])
appName = [NSApp.delegate performSelector:@selector(customSupportRequestAppName)];
if ([NSApp.delegate respondsToSelector:@selector(customSupportRequestLicense)])
licenseCode = [NSApp.delegate performSelector:@selector(customSupportRequestLicense)];
NSString *subject = makeString(@"%@ v%@ (%i) Support Request (License code: %@)",
appName,
cc.appVersionString,
cc.appBuildNumber,
licenseCode);
NSString *content = makeString(@"%@\n\n\n\nP.S: Hardware: %@ Software: %@ UDID: %@\n%@\n%@",
text,
_machineType(),
NSProcessInfo.processInfo.operatingSystemVersionString,
udid,
encodedPrefs,
crashReports);
urlString = makeString(@"mailto:%@?subject=%@&body=%@", recipient, subject, content);
[urlString.escaped.URL open];
}
#endif
- (void)openURL:(openChoice)choice
{
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
if (choice == openSupportRequestMail)
{
[self sendSupportRequestMail:@"<Insert Support Request Here>"];
return;
}
#endif
NSString *urlString = @"";
if (choice == openBetaSignupMail)
urlString = makeString(@"s%@?subject=%@ Beta Versions&body=Hello\nI would like to test upcoming beta versions of %@.\nBye\n",
[bundle objectForInfoDictionaryKey:@"FeedbackEmail"], cc.appName, cc.appName);
else if (choice == openHomepageWebsite)
urlString = OBJECT_OR([bundle objectForInfoDictionaryKey:@"VendorProductPage"],
makeString(@"%@%@/", kVendorHomepage, [cc.appName.lowercaseString.words[0] split:@"-"][0]));
else if (choice == openAppStoreWebsite)
urlString = [bundle objectForInfoDictionaryKey:@"StoreProductPage"];
else if (choice == openAppStoreApp)
{
NSString *spp = [bundle objectForInfoDictionaryKey:@"StoreProductPage"];
urlString = [spp replaced:@"https" with:@"macappstore"];
urlString = [urlString stringByAppendingString:@"&at=1000lwks"];
if (!urlString)
urlString = [bundle objectForInfoDictionaryKey:@"AlternativetoProductPage"];
}
else if (choice == openMacupdateWebsite)
{
urlString = [bundle objectForInfoDictionaryKey:@"MacupdateProductPage"];
if (!urlString)
urlString = [bundle objectForInfoDictionaryKey:@"FilehorseProductPage"];
}
[urlString.escaped.URL open];
}
@end
@implementation FakeAlertWindow
@end
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-nonliteral"
// obj creation convenience
NSPredicate *makePredicate(NSString *format, ...)
{
assert([format rangeOfString:@"'%@'"].location == NSNotFound);
va_list args;
va_start(args, format);
NSPredicate *pred = [NSPredicate predicateWithFormat:format arguments:args];
va_end(args);
return pred;
}
NSDictionary<NSString *, id> * _makeDictionaryOfVariables(NSString *commaSeparatedKeysString, id firstValue, ...)
{
NSUInteger i = 0;
NSArray <NSString *> *argumentNames = [commaSeparatedKeysString split:@","];
if (!argumentNames.count) return nil;
NSMutableDictionary *dict = makeMutableDictionary();
va_list args;
va_start(args, firstValue);
NSString *firstArgumentName = argumentNames.firstObject.trimmedOfWhitespaceAndNewlines;
dict[firstArgumentName] = OBJECT_OR(firstValue, @"(null)");
for (NSString *name in argumentNames)
{
if (i!=0)
{
id arg = va_arg(args, id);
dict[name.trimmedOfWhitespaceAndNewlines] = OBJECT_OR(arg, @"(null)");
}
i++;
}
va_end(args);
return dict;
}
NSString *makeDescription(NSObject *sender, NSArray *args)
{
NSMutableString *tmp = [NSMutableString new];
for (NSString *arg in args)
{
NSObject *value = [sender valueForKey:arg];
NSString *d = [value description];
[tmp appendFormat:@"\n%@: %@", arg, d];
}
return tmp.immutableObject;
}
NSString *makeLocalizedString(NSString *format, ...)
{
va_list args;
va_start(args, format);
NSString *str = [[NSString alloc] initWithFormat:format.localized arguments:args];
va_end(args);
return str;
}
NSString *makeString(NSString *format, ...)
{
va_list args;
va_start(args, format);
NSString *str = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
return str;
}
NSString *makeTempDirectory()
{
NSError *error = nil;
NSURL *temporaryDirectoryURL =
[fileManager URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:NSHomeDirectory().fileURL
create:YES
error:&error];
assert(temporaryDirectoryURL && !error);
return temporaryDirectoryURL.path;
// this should return a new folder inside the 'TemporaryItems' subfolder of the tmp folder which is cleared on reboot.
// sample path on 11.0 /var/folders/9c/bdxcbnjd29d1ql3h9zfsflp80000gn/T/TemporaryItems/NSIRD_#{appname}_89KPkg/
// sample path on 11.0 /var/folders/9c/bdxcbnjd29d1ql3h9zfsflp80000gn/T/TemporaryItems/(A Document Being Saved By #{appname})
}
NSString *makeTempFilepath(NSString *extension)
{
NSString *tempDir = makeTempDirectory();
if (!tempDir)
return nil;
NSString *fileName = [@"1." stringByAppendingString:extension];
NSString *filePath = @[tempDir, fileName].path;
NSString *finalPath = filePath.uniqueFile;
return finalPath;
}
NSValue *makeRectValue(CGFloat x, CGFloat y, CGFloat width, CGFloat height)
{
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
return [NSValue valueWithRect:CGRectMake(x, y, width, height)];
#else
return [NSValue valueWithCGRect:CGRectMake(x, y, width, height)];
#endif
}
#ifdef USE_LAM
#import "NSData+LAMCompression.h"
#endif
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
void alert_feedback(NSString *usermsg, NSString *details, BOOL fatal)
{
cc_log_error(@"alert_feedback %@ %@", usermsg, details);
dispatch_block_t block = ^
{
static const int maxLen = 400;
NSString *encodedPrefs = @"";
[NSUserDefaults.standardUserDefaults synchronize];
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC && !TARGET_OS_IPHONE
NSData *prefsData = cc.prefsURL.contents;
#ifdef USE_LAM
prefsData = [prefsData lam_compressedDataUsingCompression:LAMCompressionZLIB];
#endif
encodedPrefs = [prefsData base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0];
#ifndef SANDBOX
if ((cc.appCrashLogFilenames).count)
{
NSString *crashes = [cc.appCrashLogs joined:@"\n"];
encodedPrefs = [encodedPrefs stringByAppendingString:@"\n\n"];
encodedPrefs = [encodedPrefs stringByAppendingString:crashes];
}
#endif
#endif
NSString *visibleDetails = details;
if (visibleDetails.length > maxLen)
visibleDetails = makeString(@"%@ …\n(Remaining message omitted)", [visibleDetails clamp:maxLen]);
NSString *message = makeString(@"%@\n\n You can contact our support with detailed information so that we can fix this problem.\n\nInformation: %@", usermsg, visibleDetails);
NSString *mailtoLink = @"";
@try
{
NSString *appName = cc.appName;
NSString *licenseCode = cc.appChecksumIncludingFrameworksSHA;
if ([NSApp.delegate respondsToSelector:@selector(customSupportRequestAppName)])
appName = [NSApp.delegate performSelector:@selector(customSupportRequestAppName)];
if ([NSApp.delegate respondsToSelector:@selector(customSupportRequestLicense)])
licenseCode = [NSApp.delegate performSelector:@selector(customSupportRequestLicense)];
mailtoLink = makeString(@"mailto:%@?subject=%@ v%@ (%i) Problem Report (License code: %@)&body=Hello\nA %@ error in %@ occurred (%@).\n\nBye\n\nP.S. Details: %@\n\n\nP.P.S: Hardware: %@ Software: %@ Admin: %i\n\nPreferences: %@\n",
kFeedbackEmail,
appName,
cc.appVersionString,
cc.appBuildNumber,
licenseCode,
fatal ? @"fatal" : @"",
cc.appName,
usermsg,
details,
_machineType(),
NSProcessInfo.processInfo.operatingSystemVersionString,
_isUserAdmin(),
encodedPrefs);
}
@catch (NSException *)
{
}
#if defined(USE_CRASHHELPER) && USE_CRASHHELPER
if (fatal)
{
NSString *title = makeString(@"%@ Fatal Error", cc.appName);
mailtoLink = [mailtoLink clamp:100000]; // will expand to twice the size and kern.argmax: 262144 causes NSTask with too long arguments to 'silently' fail with a posix spawn error 7
NSDictionary *dict = @{@"title" : title, @"message" : message, @"mailto" : mailtoLink};
NSData *dictjsondata = dict.JSONData;
NSString *dictjsondatahexstring = dictjsondata.hexString;
NSString *crashhelperpath = @[cc.resDir, @"CrashHelper.app/Contents/MacOS/CrashHelper"].path;
NSTask *taskApp = [[NSTask alloc] init];
@try
{
taskApp.launchPath = crashhelperpath;
taskApp.arguments = @[dictjsondatahexstring];
[taskApp launch];
while (taskApp.isRunning)
{
[NSThread sleepForTimeInterval:0.05];
}
}
@catch (NSException *exception)
{
cc_log_error(@"could not spawn crash helper %@", exception.userInfo);
if (alert(fatal ? @"Fatal Error".localized : @"Error".localized,
message,
@"Send to support".localized, fatal ? @"Quit".localized : @"Continue".localized, nil) == NSAlertFirstButtonReturn)
{
[mailtoLink.escaped.URL open];
}
}
}
else
#endif
{
if (alert(fatal ? @"Fatal Error".localized : @"Error".localized,
message,
@"Send to support".localized, fatal ? @"Quit".localized : @"Continue".localized, nil) == NSAlertFirstButtonReturn)
{
[mailtoLink.escaped.URL open];
}
}
if (fatal)
exit(1);
};
dispatch_sync_main(block);
}
void alert_feedback_fatal(NSString *usermsg, NSString *details)
{
alert_feedback(usermsg, details, YES);
exit(1);
}
void alert_feedback_nonfatal(NSString *usermsg, NSString *details)
{
alert_feedback(usermsg, details, NO);
}
NSInteger _alert_input(NSString *prompt, NSArray *buttons, NSString **result, BOOL useSecurePrompt)
{
assert(buttons);
assert(result);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = prompt;
cc_log_error(@"Alert Input: %@@", prompt.strippedOfNewlines);
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
NSTextField *input;
if (useSecurePrompt)
input = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 310, 24)];
else
input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 310, 24)];
alert.accessoryView = input;
alert.window.initialFirstResponder = alert.accessoryView;
NSInteger selectedButton = [alert runModal];
cc_log_error(@"Alert Input: finished %li", (long)selectedButton);
[input validateEditing];
*result = input.stringValue;
return selectedButton;
}
NSInteger alert_checkbox(NSString *title, NSString *prompt, NSArray <NSString *>*buttons, NSString *checkboxTitle, NSUInteger *checkboxStatus)
{
assert(buttons);
assert(checkboxStatus);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = title;
alert.informativeText = prompt;
cc_log_error(@"Alert Checkbox: %@ - %@", title.strippedOfNewlines, prompt.strippedOfNewlines);
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
NSButton *input = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 310, 24)];
[input setButtonType:NSButtonTypeSwitch];
input.state = (NSInteger )*checkboxStatus;
input.title = checkboxTitle;
alert.accessoryView = input;
NSInteger selectedButton = [alert runModal];
*checkboxStatus = (NSUInteger)input.state;
cc_log_error(@"Alert Checkbox: finished %li %lu", (long)selectedButton, (unsigned long)*checkboxStatus);
return selectedButton;
}
NSInteger alert_colorwell(NSString *prompt, NSArray <NSString *>*buttons, NSColor **selectedColor)
{
assert(buttons);
assert(selectedColor);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = prompt;
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
NSColorWell *input = [[NSColorWell alloc] initWithFrame:NSMakeRect(0, 0, 310, 24)];
input.color = *selectedColor;
cc_log_error(@"Alert Colorwell: %@", prompt.strippedOfNewlines);
alert.accessoryView = input;
NSInteger selectedButton = [alert runModal];
cc_log_error(@"Alert Colorwell: finished %li", selectedButton);
*selectedColor = input.color;
return selectedButton;
}
NSInteger alert_inputtext(NSString *prompt, NSArray *buttons, NSString **result)
{
assert(buttons);
assert(result);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = prompt;
cc_log_error(@"Alert Inputtext: %@", prompt.strippedOfNewlines);
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
NSScrollView *scrollview = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 310, 200)];
NSSize contentSize = [scrollview contentSize];
[scrollview setBorderType:NSNoBorder];
[scrollview setHasVerticalScroller:YES];
[scrollview setHasHorizontalScroller:NO];
[scrollview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
NSTextView *theTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, contentSize.width, contentSize.height)];
[theTextView setMinSize:NSMakeSize(0.0, contentSize.height)];
[theTextView setMaxSize:NSMakeSize((CGFloat)FLT_MAX, (CGFloat)FLT_MAX)];
[theTextView setVerticallyResizable:YES];
[theTextView setHorizontallyResizable:NO];
[theTextView setAutoresizingMask:NSViewWidthSizable];
[[theTextView textContainer] setContainerSize:NSMakeSize(contentSize.width, (CGFloat)FLT_MAX)];
[[theTextView textContainer] setWidthTracksTextView:YES];
[scrollview setDocumentView:theTextView];
NSString *inputString = *result;
if (inputString.length)
theTextView.string = inputString;
alert.accessoryView = scrollview;
NSInteger selectedButton = [alert runModal];
*result = theTextView.string;
cc_log_error(@"Alert Inputtext: finished %li %@", (long)selectedButton, *result);
return selectedButton;
}
NSInteger alert_outputtext(NSString *message, NSArray *buttons, NSString *text)
{
assert(buttons);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = message;
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
cc_log_error(@"Alert Outputtext: %@", message.strippedOfNewlines);
NSScrollView *scrollview = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 310, 200)];
NSSize contentSize = [scrollview contentSize];
[scrollview setBorderType:NSNoBorder];
[scrollview setHasVerticalScroller:YES];
[scrollview setHasHorizontalScroller:NO];
[scrollview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
NSTextView *theTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, contentSize.width, contentSize.height)];
[theTextView setMinSize:NSMakeSize(0.0, contentSize.height)];
[theTextView setMaxSize:NSMakeSize((CGFloat)FLT_MAX, (CGFloat)FLT_MAX)];
[theTextView setVerticallyResizable:YES];
[theTextView setHorizontallyResizable:NO];
[theTextView setAutoresizingMask:NSViewWidthSizable];
[[theTextView textContainer] setContainerSize:NSMakeSize(contentSize.width, (CGFloat)FLT_MAX)];
[[theTextView textContainer] setWidthTracksTextView:YES];
[scrollview setDocumentView:theTextView];
theTextView.editable = NO;
theTextView.string = text;
alert.accessoryView = scrollview;
NSInteger selectedButton = [alert runModal];
cc_log_error(@"Alert Outputtext: finished %li", (long)selectedButton);
return selectedButton;
}
NSInteger alert_selection_popup(NSString *prompt, NSArray<NSString *> *choices, NSArray<NSString *> *buttons, NSUInteger *result)
{
assert(buttons);
assert(choices);
assert(result);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = prompt;
cc_log_error(@"Alert Selectionpopup: %@", prompt.strippedOfNewlines);
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
NSPopUpButton *input = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 310, 24)];
for (NSString *str in choices)
[input addItemWithTitle:str];
alert.accessoryView = input;
NSInteger selectedButton = [alert runModal];
[input validateEditing];
*result = (NSUInteger)input.indexOfSelectedItem;
cc_log_error(@"Alert Selectionpopup: finished %li %lu", (long)selectedButton, (unsigned long)*result);
return selectedButton;
}
NSInteger alert_selection_matrix(NSString *prompt, NSArray<NSString *> *choices, NSArray<NSString *> *buttons, NSUInteger *result)
{
assert(buttons);
assert(result);
ASSERT_MAINTHREAD;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = prompt;
cc_log_error(@"Alert Selectionmatrix: %@", prompt.strippedOfNewlines);
if (buttons.count > 0)
[alert addButtonWithTitle:buttons[0]];
if (buttons.count > 1)
[alert addButtonWithTitle:buttons[1]];
if (buttons.count > 2)
[alert addButtonWithTitle:buttons[2]];
NSButtonCell *thepushbutton = [[NSButtonCell alloc] init];
[thepushbutton setButtonType:NSButtonTypeRadio];
NSMatrix *thepushbuttons = [[NSMatrix alloc] initWithFrame:NSMakeRect(0,0,269,17 * choices.count)
mode:NSRadioModeMatrix
prototype:thepushbutton
numberOfRows:(int)choices.count
numberOfColumns:1];
for (NSUInteger i = 0; i < choices.count; i++)
{
[thepushbuttons selectCellAtRow:(int)i column:0];
NSString *title = choices[i];
if (title.length > 150)
title = makeString(@"%@ […] %@", [title substringToIndex:70], [title substringFromIndex:title.length-70]);
[thepushbuttons.selectedCell setTitle:title];
}
[thepushbuttons selectCellAtRow:0 column:0];
[thepushbuttons sizeToFit];
alert.accessoryView = thepushbuttons;
//[[alert window] makeFirstResponder:thepushbuttons];
NSInteger selectedButton = [alert runModal];
//U [[alert window] setInitialFirstResponder: thepushbuttons];
*result = (NSUInteger)thepushbuttons.selectedRow;
cc_log_error(@"Alert Selectionmatrix: finished %li %lu", (long)selectedButton, (unsigned long)*result);
return selectedButton;
}
NSInteger alert_input(NSString *prompt, NSArray *buttons, NSString **result)
{
return _alert_input(prompt, buttons, result, NO);
}
NSInteger alert_inputsecure(NSString *prompt, NSArray *buttons, NSString **result)
{
return _alert_input(prompt, buttons, result, YES);
}
__attribute__((annotate("returns_localized_nsstring"))) static inline NSString *LocalizationNotNeeded(NSString *s) { return s; }
NSInteger alert(NSString *title, NSString *message, NSString *defaultButton, NSString *alternateButton, NSString *otherButton)
{
return alert_customicon(title, message, defaultButton, alternateButton, otherButton, nil);
}
NSInteger alert_customicon(NSString *title, NSString *message, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, NSImage *customIcon)
{
ASSERT_MAINTHREAD;
[NSApp activateIgnoringOtherApps:YES];
cc_log_error(@"Alert: %@ - %@", title.strippedOfNewlines, message.strippedOfNewlines);
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = title;
alert.informativeText = LocalizationNotNeeded(message);
alert.icon = customIcon;
alert.window.level = NSScreenSaverWindowLevel;
if (defaultButton)
{
NSButton *b = [alert addButtonWithTitle:LocalizationNotNeeded(defaultButton)];
if (defaultButton.associatedValue)
[b setKeyEquivalent:defaultButton.associatedValue];
}
if (alternateButton)
{
NSButton *b = [alert addButtonWithTitle:alternateButton];
if (alternateButton.associatedValue)
[b setKeyEquivalent:alternateButton.associatedValue];
}
if (otherButton)
[alert addButtonWithTitle:otherButton];
NSInteger result = [alert runModal];
cc_log_error(@"Alert: finished %li", (long)result);
return result;
}
NSInteger alert_apptitled(NSString *message, NSString *defaultButton, NSString *alternateButton, NSString *otherButton)
{
return alert(cc.appName, message, defaultButton, alternateButton, otherButton);
}
void alert_dontwarnagain_version(NSString *identifier, NSString *title, NSString *message, NSString *defaultButton, NSString *dontwarnButton)
{
assert(defaultButton && dontwarnButton);
dispatch_block_t block = ^
{
NSString *defaultKey = makeString(@"_%@_%@_asked", identifier, cc.appVersionString);
if (!defaultKey.defaultInt)
{
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = title;
alert.informativeText = message;
[alert addButtonWithTitle:defaultButton];
alert.showsSuppressionButton = YES;
alert.suppressionButton.title = dontwarnButton;
cc_log_error(@"Alert Dontwarnagain: %@ - %@", title.strippedOfNewlines, message.strippedOfNewlines);
[NSApp activateIgnoringOtherApps:YES];
[alert runModal];
cc_log_error(@"Alert Dontwarnagain: finished");
defaultKey.defaultInt = alert.suppressionButton.state;
}
};
if ([NSThread currentThread] == [NSThread mainThread])
block();
else
dispatch_async_main(block);
}
void alert_dontwarnagain_ever(NSString *identifier, NSString *title, NSString *message, NSString *defaultButton, NSString *dontwarnButton)
{
dispatch_block_t block = ^
{
NSString *defaultKey = makeString(@"_%@_asked", identifier);
if (!defaultKey.defaultInt)
{
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = title;
alert.informativeText = message;
[alert addButtonWithTitle:defaultButton];
alert.showsSuppressionButton = YES;
alert.suppressionButton.title = dontwarnButton;
cc_log_error(@"Alert Dontwarnagainever: %@ - %@", title.strippedOfNewlines, message.strippedOfNewlines);
[NSApp activateIgnoringOtherApps:YES];
[alert runModal];
cc_log_error(@"Alert Dontwarnagainever: finished");
defaultKey.defaultInt = alert.suppressionButton.state;
}
};
if ([NSThread currentThread] == [NSThread mainThread])
block();
else
dispatch_async_main(block);
}
#pragma clang diagnostic pop
CGFloat _attributedStringHeightForWidth(NSAttributedString *string, CGFloat width)
{
NSSize answer = NSZeroSize;
if ([string length] > 0)
{ // CREDITS: https://stackoverflow.com/questions/8945040/measure-string-height-in-cocoa
// Checking for empty string is necessary since Layout Manager will give the nominal
// height of one line if length is 0. Our API specifies 0.0 for an empty string.
NSSize size = NSMakeSize(width, (CGFloat)FLT_MAX);
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:size];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:string];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[layoutManager setHyphenationFactor:0.0];
// NSLayoutManager is lazy, so we need the following kludge to force layout:
[layoutManager glyphRangeForTextContainer:textContainer];
answer = [layoutManager usedRectForTextContainer:textContainer].size;
// Adjust if there is extra height for the cursor
NSSize extraLineSize = [layoutManager extraLineFragmentRect].size;
if (extraLineSize.height > 0)
{
answer.height -= extraLineSize.height;
}
}
return answer.height;
}
void alert_nonmodal(NSString *title, NSString *message, NSString *button)
{
alert_nonmodal_customicon(title, message, button, nil);
}
void alert_nonmodal_customicon_block(NSString *title, NSString *message, NSString *button, NSImage *customIcon, BasicBlock block)
{
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:message attributes:@{NSFontAttributeName : [NSFont systemFontOfSize:11]}];
CGFloat messageHeight = (CGFloat)MAX(30.0, _attributedStringHeightForWidth(attributedString, 300));
CGFloat height = 100 + messageHeight;
FakeAlertWindow *fakeAlertWindow = [[FakeAlertWindow alloc] initWithContentRect:NSMakeRect(0.0, 0.0, 420.0, height)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
NSTextField *alertTitle = [[NSTextField alloc] initWithFrame:NSMakeRect(100.0, height-30, 300.0, 17.0)];
NSTextView *alertMessage = [[NSTextView alloc] initWithFrame:NSMakeRect(100.0-3, 50, 300.0, height-50-40)];
NSImageView *alertImage = [[NSImageView alloc] initWithFrame:NSMakeRect(20.0, height-80, 64, 64)];
NSButton *firstButton = [[NSButton alloc] initWithFrame:NSMakeRect(315.0, 12, 90, 30)];
alertTitle.stringValue = title;
alertMessage.string = message;
alertTitle.font = [NSFont boldSystemFontOfSize:14];
alertTitle.alignment = NSTextAlignmentLeft;
alertTitle.bezeled = NO;
[alertTitle setDrawsBackground:NO];
[alertTitle setLineBreakMode:NSLineBreakByWordWrapping];
[alertTitle setEditable:NO];
[alertTitle setSelectable:NO];
[fakeAlertWindow.contentView addSubview:alertTitle];
alertMessage.font = [NSFont systemFontOfSize:11];
alertMessage.alignment = NSTextAlignmentLeft;
[alertMessage setDrawsBackground:NO];
[alertMessage setEditable:NO];
[alertMessage setSelectable:NO];
[fakeAlertWindow.contentView addSubview:alertMessage];
alertImage.image = OBJECT_OR(customIcon, @"AppIcon".namedImage);
[fakeAlertWindow.contentView addSubview:alertImage];
firstButton.bezelStyle = NSBezelStyleRounded;
firstButton.title = button;
firstButton.keyEquivalent = @"\r";
[fakeAlertWindow.contentView addSubview:firstButton];
__weak FakeAlertWindow *weakWindow = fakeAlertWindow;
firstButton.actionBlock = ^(id sender)
{
[weakWindow close];
if (block)
block();
};
fakeAlertWindow.releasedWhenClosed = NO;
[fakeAlertWindow center];
[NSApp activateIgnoringOtherApps:YES];
[fakeAlertWindow makeKeyAndOrderFront:@""];
}
void alert_nonmodal_customicon(NSString *title, NSString *message, NSString *button, NSImage *customIcon)
{
alert_nonmodal_customicon_block(title, message, button, customIcon, nil);
}
void alert_nonmodal_checkbox(NSString *title, NSString *message, NSString *button, NSString *checkboxTitle, NSInteger checkboxStatusIn, IntInBlock resultBlock)
{
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:message attributes:@{NSFontAttributeName : [NSFont systemFontOfSize:11]}];
CGFloat messageHeight = (CGFloat)MAX(50.0, _attributedStringHeightForWidth(attributedString, 300));
CGFloat height = 120 + messageHeight;
FakeAlertWindow *fakeAlertWindow = [[FakeAlertWindow alloc] initWithContentRect:NSMakeRect(0.0, 0.0, 420.0, height)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
NSTextField *alertTitle = [[NSTextField alloc] initWithFrame:NSMakeRect(100.0, height-30, 300.0, 17.0)];
NSTextView *alertMessage = [[NSTextView alloc] initWithFrame:NSMakeRect(100.0-3, 70, 300.0, height-70-40)];
NSImageView *alertImage = [[NSImageView alloc] initWithFrame:NSMakeRect(20.0, height-80, 64, 64)];
NSButton *firstButton = [[NSButton alloc] initWithFrame:NSMakeRect(315.0, 12, 90, 30)];
alertTitle.stringValue = title;
alertMessage.string = message;
alertTitle.font = [NSFont boldSystemFontOfSize:14];
alertTitle.alignment = NSTextAlignmentLeft;
alertTitle.bezeled = NO;
[alertTitle setDrawsBackground:NO];
[alertTitle setLineBreakMode:NSLineBreakByWordWrapping];
[alertTitle setEditable:NO];
[alertTitle setSelectable:NO];
[fakeAlertWindow.contentView addSubview:alertTitle];
alertMessage.font = [NSFont systemFontOfSize:11];
alertMessage.alignment = NSTextAlignmentLeft;
[alertMessage setDrawsBackground:NO];
[alertMessage setEditable:NO];
[alertMessage setSelectable:NO];
[fakeAlertWindow.contentView addSubview:alertMessage];
alertImage.image = @"AppIcon".namedImage;
[fakeAlertWindow.contentView addSubview:alertImage];
firstButton.bezelStyle = NSBezelStyleRounded;
firstButton.title = button;
firstButton.keyEquivalent = @"\r";
[fakeAlertWindow.contentView addSubview:firstButton];
NSButton *input = [[NSButton alloc] initWithFrame:NSMakeRect(105, 55, 310, 24)];
[input setButtonType:NSButtonTypeSwitch];
input.state = checkboxStatusIn;
input.title = checkboxTitle;
[fakeAlertWindow.contentView addSubview:input];
__weak FakeAlertWindow *weakWindow = fakeAlertWindow;
__weak NSButton *weakCheckbox = input;
firstButton.actionBlock = ^(id sender)
{
resultBlock((int)weakCheckbox.state);
[weakWindow close];
};
fakeAlertWindow.releasedWhenClosed = NO;
[fakeAlertWindow center];
[NSApp activateIgnoringOtherApps:YES];
[fakeAlertWindow makeKeyAndOrderFront:@""];
}
// colors
NSColor *makeColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a)
{
return [NSColor colorWithCalibratedRed:(r) green:(g) blue:(b) alpha:(a)];
}
NSColor *makeColor255(CGFloat r, CGFloat g, CGFloat b, CGFloat a)
{
return [NSColor colorWithCalibratedRed:(r) / 255.0 green:(g) / 255.0 blue:(b) / 255.0 alpha:(a) / 255.0];
}
#else
UIColor *makeColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a)
{
return [UIColor colorWithRed:(r) green:(g) blue:(b) alpha:(a)];
}
UIColor *makeColor255(CGFloat r, CGFloat g, CGFloat b, CGFloat a)
{
return [UIColor colorWithRed:(r) / (CGFloat)255.0 green:(g) / (CGFloat)255.0 blue:(b) / (CGFloat)255.0 alpha:(a) / (CGFloat)255.0];
}
#endif
// randoms
__inline__ CGFloat generateRandomFloatBetween(CGFloat a, CGFloat b)
{
return a + (b - a) * (random() / (CGFloat) RAND_MAX);
}
__inline__ int generateRandomIntBetween(int a, int b)
{
int range = b - a < 0 ? b - a - 1 : b - a + 1;
long rand = random();
int value = (int)(range * ((CGFloat)rand / (CGFloat) RAND_MAX));
return value == range ? a : a + value;
}
// logging support
#undef asl_log
#undef os_log
#if __has_feature(modules) && ((defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101200) || (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 100000)))
@import asl;
@import os.log;
#else
#include <asl.h>
#include <os/log.h>
#endif
static NSFileHandle *logfileHandle;
static cc_log_type minimumLogType;
void cc_log_enablecapturetofile(NSURL *fileURL, unsigned long long filesizeLimit, cc_log_type _minimumLogType) // ASL broken on 10.12+ and especially logging to file not working anymore
{
assert(!logfileHandle);
if (!fileURL.fileExists)
[NSData.data writeToURL:fileURL atomically:YES]; // create file with weird API
else if (filesizeLimit) // truncate first
{
NSString *path = fileURL.path;
NSDictionary *attr = [fileManager attributesOfItemAtPath:path error:NULL];
NSNumber *fs = attr[NSFileSize];
unsigned long long filesize = fs.unsignedLongLongValue;
if (filesize > filesizeLimit)
{
NSFileHandle *fh = [NSFileHandle fileHandleForUpdatingURL:fileURL error:nil];
@try
{
[fh seekToFileOffset:(filesize - filesizeLimit)];
NSData *data = [fh readDataToEndOfFile];
[fh seekToFileOffset:0];
[fh writeData:data];
[fh truncateFileAtOffset:filesizeLimit];
[fh synchronizeFile];
}
@catch (id)
{
cc_log_emerg(@"Fatal: your disk is full - please never let that happen again");
}
@finally
{
[fh closeFile];
}
}
}
// now open for appending
logfileHandle = [NSFileHandle fileHandleForUpdatingURL:fileURL error:nil];
if (!logfileHandle)
{
cc_log_error(@"could not open file %@ for log file usage", fileURL.path);
}
minimumLogType = _minimumLogType;
}
void _cc_log_tologfile(int level, NSString *string)
{
if (logfileHandle && (level <= minimumLogType))
{
static const char* levelNames[8] = {ASL_STRING_EMERG, ASL_STRING_ALERT, ASL_STRING_CRIT, ASL_STRING_ERR, ASL_STRING_WARNING, ASL_STRING_NOTICE, ASL_STRING_INFO, ASL_STRING_DEBUG};
assert(level < 8);
NSString *levelStr = @(levelNames[level]);
NSString *dayString = [NSDate.date stringUsingFormat:@"MMM dd"];
NSString *timeString = [NSDate.date stringUsingDateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterShortStyle];
NSString *finalString = makeString(@"%@ %@ %@[%i] <%@>: %@\n",
dayString,
timeString,
cc.appName,
NSProcessInfo.processInfo.processIdentifier,
levelStr,
string);
NSData *data = [finalString dataUsingEncoding:NSUTF8StringEncoding];
if (data)
{
@try
{
[logfileHandle seekToEndOfFile];
[logfileHandle writeData:data];
}
@catch (id)
{
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_FAULT, "%{public}s", "Fatal: your disk is full - please never let that happen again");
}
}
else
{
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_FAULT, "%{public}s", makeString(@"could not open create data from string '%@' for log", finalString).UTF8String);
}
}
}
static NSString *_ccLogToPrefsLock = @"_cc_log_toprefs_LOCK";
void _cc_log_toprefs(int level, NSString *string)
{
#ifndef CLI
#ifndef DONTLOGTOUSERDEFAULTS
@synchronized (_ccLogToPrefsLock)
{
static int lastPosition[8] = {0,0,0,0,0,0,0,0};
assert(level < 8);
NSString *key = makeString(@"corelib_asl_lev%i_pos%i", level, lastPosition[level]);
key.defaultString = makeString(@"date: %@ message: %@", NSDate.date.description, string);
lastPosition[level]++;
if (lastPosition[level] > 9)
lastPosition[level] = 0;
}
#endif
#endif
}
void cc_log_level(cc_log_type level, NSString *format, ...)
{
va_list args;
va_start(args, format);
NSString *str = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
_cc_log_tologfile(level, str);
_cc_log_toprefs(level, str);
#ifdef CLI
if (level <= CC_LOG_LEVEL_ERROR)
fprintf(stderr, "\033[91m%s\033[0m\n", str.UTF8String);
else if ([str.lowercaseString hasPrefix:@"warning"])
fprintf(stderr, "\033[93m%s\033[0m\n", str.UTF8String);
else
fprintf(stdout, "%s\n", str.UTF8String);
#else
const char *utf = str.UTF8String;
if (level == ASL_LEVEL_DEBUG || level == ASL_LEVEL_INFO)
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEBUG, "%{public}s", utf);
else if (level == ASL_LEVEL_NOTICE || level == ASL_LEVEL_WARNING)
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEFAULT, "%{public}s", utf);
else if (level == ASL_LEVEL_ERR)
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_ERROR, "%{public}s", utf);
else if (level == ASL_LEVEL_CRIT || level == ASL_LEVEL_ALERT || level == ASL_LEVEL_EMERG)
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_FAULT, "%{public}s", utf);
#endif
#ifdef DEBUG
if (level <= CC_LOG_LEVEL_ERROR && ![format contains:@" launching "] && ![format contains:@"Notification: "] && ![format contains:@"OpenMainWindow"] )
{
// just for breakpoints
}
#endif
}
void log_to_prefs(NSString *str)
{
static int lastPosition = 0;
NSString *key = makeString(@"corelib_logtoprefs_pos%i", lastPosition);
key.defaultString = makeString(@"date: %@ message: %@", NSDate.date.description, str);
lastPosition++;
if (lastPosition > 42)
lastPosition = 0;
}
void cc_defaults_addtoarray(NSString *key, NSObject *entry, NSUInteger maximumEntries)
{
NSArray *currentArray = [NSUserDefaults.standardUserDefaults objectForKey:key];
if (!currentArray || ![currentArray isKindOfClass:NSArray.class])
currentArray = @[];
currentArray = [currentArray arrayByAddingObject:entry];
while (currentArray.count > maximumEntries)
currentArray = [currentArray arrayByRemovingObjectAtIndex:0];
[NSUserDefaults.standardUserDefaults setObject:currentArray forKey:key];
}
// gcd convenience
void dispatch_after_main(float seconds, dispatch_block_t block)
{
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), block);
}
void dispatch_after_back(float seconds, dispatch_block_t block)
{
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_global_queue(0, 0), block);
}
void dispatch_async_main(dispatch_block_t block)
{
dispatch_async(dispatch_get_main_queue(), block);
}
void dispatch_async_back(dispatch_block_t block)
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, block);
}
void dispatch_sync_main(dispatch_block_t block)
{
if ([NSThread currentThread] == [NSThread mainThread])
block(); // using with dispatch_sync would deadlock when on the main thread
else
dispatch_sync(dispatch_get_main_queue(), block);
}
void dispatch_sync_back(dispatch_block_t block)
{
dispatch_sync(dispatch_get_global_queue(0, 0), block);
}
BOOL dispatch_sync_back_timeout(dispatch_block_t block, float timeoutSeconds) // returns 0 on succ
{
dispatch_block_t newblock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, block);
dispatch_async(dispatch_get_global_queue(0, 0), newblock);
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutSeconds * NSEC_PER_SEC));
return dispatch_block_wait(newblock, popTime) != 0;
}
static dispatch_semaphore_t ccAsyncToSyncSema;
static id ccAsyncToSyncResult;
void dispatch_async_to_sync_resulthandler(id res)
{
assert(ccAsyncToSyncSema);
assert(!ccAsyncToSyncResult);
ccAsyncToSyncResult = res;
dispatch_semaphore_signal(ccAsyncToSyncSema);
}
id dispatch_async_to_sync(BasicBlock block)
{
assert(!ccAsyncToSyncResult);
assert(!ccAsyncToSyncSema);
ccAsyncToSyncSema = dispatch_semaphore_create(0);
block();
dispatch_semaphore_wait(ccAsyncToSyncSema, DISPATCH_TIME_FOREVER);
assert(ccAsyncToSyncResult);
ccAsyncToSyncSema = NULL;
id copy = ccAsyncToSyncResult;
ccAsyncToSyncResult = nil;
return copy;
}
// private
NSString *_machineType()
{
Class hostInfoClass = NSClassFromString(@"JMHostInformation");
if (hostInfoClass)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
NSString *machineType = [hostInfoClass performSelector:@selector(machineType)];
#pragma clang diagnostic pop
return machineType;
}
return @"";
}
BOOL _isUserAdmin()
{
Class hostInfoClass = NSClassFromString(@"JMHostInformation");
if (hostInfoClass)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
NSMethodSignature *sig = [hostInfoClass methodSignatureForSelector:@selector(isUserAdmin)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:@selector(isUserAdmin)];
#pragma clang diagnostic pop
[invocation setTarget:hostInfoClass];
[invocation invoke];
BOOL returnValue;
[invocation getReturnValue:&returnValue];
return returnValue;
}
return YES;
}
//
// JMVisibilityManager.h
// CoreLib
//
// Created by CoreCode on So Jan 20 2013.
/* Copyright © 2020 CoreCode Limited
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitationthe rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "CoreLib.h"
typedef NS_ENUM(uint8_t, visibilitySettingEnum)
{
kVisibleNowhere = 0,
kVisibleDock,
kVisibleMenubar,
kVisibleDockAndMenubar
};
typedef NS_ENUM(uint8_t, visibilityOptionEnum)
{
kStaticVisibility = 0,
kDynamicVisibilityAddDockIconWhenWindowOpen,
};
typedef NS_ENUM(uint8_t, templateSettingEnum)
{
kTemplateNever = 0,
kTemplateWhenDarkMenubar,
kTemplateAlways
};
CONST_KEY_DECLARATION(VisibilitySettingDidChangeNotification)
CONST_KEY_DECLARATION(VisibilityAlertWindowDidResignNotification)
@interface VisibilityManager : NSObject
@property (assign, nonatomic) templateSettingEnum templateSetting;
@property (assign, nonatomic) visibilitySettingEnum visibilitySetting;
@property (assign, nonatomic) visibilityOptionEnum visibilityOption;
@property (strong, nonatomic) NSImage *dockIcon;
@property (strong, nonatomic) NSImage *menubarIcon;
@property (strong, nonatomic) NSMenu *statusItemMenu;
@property (strong, nonatomic) NSPopover *statusItemPopover;
@property (strong, nonatomic) NSString *menuTooltip;
@property (readonly, nonatomic) BOOL permanentlyVisibleInDock; // true if displaying in dock or displaying in dock and menubar
@property (readonly, nonatomic) BOOL currentlyVisibleInDock; // true if permanetly visible in dock or currently visible because the window is open
@property (readonly, nonatomic) BOOL visibleInMenubar;
- (void)handleAppReopen;
- (void)handleWindowOpened;
- (void)handleWindowClosed;
- (void)hidePopover;
- (void)showPopoverWithAnimation:(BOOL)shouldAnimate;
@end
//
// JMVisibilityManager.m
// CoreLib
//
// Created by CoreCode on So Jan 20 2013.
/* Copyright © 2020 CoreCode Limited
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitationthe rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "JMVisibilityManager.h"
#if __has_feature(modules)
@import Carbon;
#else
#import <Carbon/Carbon.h>
#endif
CONST_KEY(JMVisibilityManagerValue)
CONST_KEY(JMVisibilityManagerOptionValue)
CONST_KEY_IMPLEMENTATION(VisibilitySettingDidChangeNotification)
CONST_KEY_IMPLEMENTATION(VisibilityAlertWindowDidResignNotification)
@interface VisibilityManager ()
{
visibilitySettingEnum _visibilitySetting;
visibilityOptionEnum _visibilityOption;
NSImage *_dockIcon;
NSImage *_menubarIcon;
BOOL _windowIsOpen;
BOOL _dockIconIsCurrentlyVisible;
}
@property (strong, nonatomic) NSStatusItem *statusItem;
@property (strong, nonatomic) id globalEventMonitor;
@property (strong, nonatomic) id localEventMonitor;
@property (strong, nonatomic) id activeSpaceChangeObserver;
@end
@implementation VisibilityManager
@dynamic visibilitySetting, visibilityOption, dockIcon, menubarIcon, menuTooltip, currentlyVisibleInDock, permanentlyVisibleInDock, visibleInMenubar;
+ (void)initialize
{
NSMutableDictionary *defaultValues = [NSMutableDictionary dictionary];
defaultValues[kJMVisibilityManagerValueKey] = @(kVisibleDock);
[NSUserDefaults.standardUserDefaults registerDefaults:defaultValues];
}
- (instancetype)init
{
if ((self = [super init]))
{
#ifdef DEBUG
assert([(NSString *)[NSBundle.mainBundle objectForInfoDictionaryKey:@"LSUIElement"] boolValue]);
#endif
_visibilitySetting = kVisibleNowhere;
_templateSetting = kTemplateNever;
_visibilityOption = (visibilityOptionEnum) kJMVisibilityManagerOptionValueKey.defaultInt;
BOOL optionDown = ([NSEvent modifierFlags] & NSEventModifierFlagOption) != 0;
visibilitySettingEnum storedSetting = (visibilitySettingEnum) kJMVisibilityManagerValueKey.defaultInt;
if (storedSetting == kVisibleNowhere && optionDown)
self.visibilitySetting = kVisibleDock;
else
self.visibilitySetting = storedSetting;
}
return self;
}
- (void)dealloc
{
if (self.localEventMonitor)
{
[NSEvent removeMonitor:self.localEventMonitor];
}
if (self.globalEventMonitor)
{
[NSEvent removeMonitor:self.globalEventMonitor];
}
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdirect-ivar-access"
- (void)_redisplay
{
[self _showOrHideDockIcon];
self.menubarIcon = _menubarIcon;
self.dockIcon = _dockIcon;
}
- (void)_showOrHideDockIcon
{
if (_dockIconIsCurrentlyVisible && ![self currentlyVisibleInDock])
[self _transform:NO];
else if (!_dockIconIsCurrentlyVisible && [self currentlyVisibleInDock])
[self _transform:YES];
}
- (void)setVisibilityOption:(visibilityOptionEnum)newOption
{
_visibilityOption = newOption;
[self _redisplay];
kJMVisibilityManagerOptionValueKey.defaultInt = newOption;
[NSUserDefaults.standardUserDefaults synchronize];
[notificationCenter postNotificationName:kVisibilitySettingDidChangeNotificationKey object:self];
}
- (visibilityOptionEnum)visibilityOption
{
return _visibilityOption;
}
- (void)setVisibilitySetting:(visibilitySettingEnum)newSetting
{
//[self willChangeValueForKey:@"visibilitySetting"];
_visibilitySetting = newSetting;
//[self didChangeValueForKey:@"visibilitySetting"];
[self _redisplay];
kJMVisibilityManagerValueKey.defaultInt = newSetting;
[NSUserDefaults.standardUserDefaults synchronize];
[notificationCenter postNotificationName:kVisibilitySettingDidChangeNotificationKey object:self];
}
- (visibilitySettingEnum)visibilitySetting
{
return _visibilitySetting;
}
- (BOOL)permanentlyVisibleInDock
{
return ((_visibilitySetting == kVisibleDock) || (_visibilitySetting == kVisibleDockAndMenubar));
}
- (BOOL)currentlyVisibleInDock
{
return ((_visibilitySetting == kVisibleDock) ||
(_visibilitySetting == kVisibleDockAndMenubar) ||
((_visibilitySetting == kVisibleMenubar) && (_visibilityOption == kDynamicVisibilityAddDockIconWhenWindowOpen) && _windowIsOpen));
}
- (BOOL)visibleInMenubar
{
BOOL visible = ((_visibilitySetting == kVisibleMenubar) || (_visibilitySetting == kVisibleDockAndMenubar));
return visible;
}
- (void)setDockIcon:(NSImage *)newDockIcon
{
_dockIcon = newDockIcon;
if ([self currentlyVisibleInDock] && _dockIcon)
NSApp.applicationIconImage = _dockIcon;
}
- (NSImage *)dockIcon
{
return _dockIcon;
}
- (void)setMenubarIcon:(NSImage *)newMenubarIcon
{
if ((self.templateSetting == kTemplateAlways) ||
((self.templateSetting == kTemplateWhenDarkMenubar) && [[NSAppearance currentAppearance].name contains:@"NSAppearanceNameVibrantDark"]))
newMenubarIcon.template = YES;
else
newMenubarIcon.template = NO;
_menubarIcon = newMenubarIcon;
if ([self visibleInMenubar])
{
if (self.statusItem == nil)
{
self.statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
self.statusItem.button.enabled = YES;
}
if (self.statusItemPopover)
{
if (self.activeSpaceChangeObserver)
{
[NSNotificationCenter.defaultCenter removeObserver:self.activeSpaceChangeObserver];
}
[NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note)
{
[self hidePopover];
}];
self.statusItem.menu = nil;
// If statusItem.button is clicked, show/hide popup.
// If some other part of the app is clicked, hide the popup.
if (!self.localEventMonitor)
{
enum NSEventMask monitorMask = NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskKeyDown;
self.localEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:monitorMask handler:^NSEvent *(NSEvent *event)
{
// We want to show statusMenu if the user option clicks
BOOL gotOptionKey = (event.modifierFlags & NSEventModifierFlagOption) == NSEventModifierFlagOption;
BOOL gotRightClick = event.type == NSEventTypeRightMouseDown;
if (event.window == self.statusItem.button.window && (gotOptionKey || gotRightClick))
{
[self.statusItem.button highlight:YES];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self.statusItem popUpStatusItemMenu:self.statusItemMenu];
#pragma clang diagnostic pop
[self.statusItem.button highlight:NO];
return nil;
}
// If the popover is currently shown, then let the popover handle all the clicks (pass on the event as is)
if ((event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown) && self.statusItemPopover)
{
if (self.statusItemPopover.isShown && event.window == self.statusItemPopover.contentViewController.view.window)
{
return event;
}
}
// If the button got a click and the popover is hidden, show it
if (event.window == self.statusItem.button.window && !self.statusItemPopover.isShown)
{
[self showPopoverWithAnimation:NO];
dispatch_after_main(0.75, ^
{
self.statusItemPopover.contentViewController.view.window.collectionBehavior = NSWindowCollectionBehaviorParticipatesInCycle;
});
// Returning a nil signifies that we've consumed this event.
// This has the side effect of avoiding the obnoxious NSBeep sound.
return nil;
}
// If the popover is currently hidden and the click was in some other window in this app itself,
// pass it on as it is so that the particular window / UI element can process it.
if ((event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown) && self.statusItemPopover)
{
if (!self.statusItemPopover.isShown && event.window != self.statusItemPopover.contentViewController.view.window)
return event;
}
// If we got a keystroke other than ESC, pass it on as it is, so that other UI elements can process it.
if (event.type == NSEventTypeKeyDown && event.keyCode != 53) {
return event;
}
if ([event.window class] == [NSPanel class] || [event.window class] == [FakeAlertWindow class])
{
NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:NSWindowDidResignKeyNotification
object:event.window
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note)
{
[NSNotificationCenter.defaultCenter postNotificationName:kVisibilityAlertWindowDidResignNotificationKey object:nil userInfo:nil];
[center removeObserver:token];
}];
return event;
}
// We're about to close the popover because all any circumstances under which we'd
// keep the popup open were unfulfilled. Tear down the global event monitor that we'd
// setup to detect clicks on external apps.
if (self.globalEventMonitor)
{
[NSEvent removeMonitor:self.globalEventMonitor];
self.globalEventMonitor = nil;
}
// Close popup and remove highlight
[self.statusItemPopover close];
[self.statusItem.button highlight:NO];
// Returning a nil signifies that we've consumed this event.
// This has the side effect of avoiding the obnoxious NSBeep sound.
return nil;
}];
}
}
else
{
self.statusItem.menu = self.statusItemMenu;
}
self.statusItem.button.image = _menubarIcon;
}
else
{
if (self.statusItem)
{
[NSStatusBar.systemStatusBar removeStatusItem:_statusItem];
_statusItem = nil;
}
}
}
- (void)hidePopover
{
if (self.statusItemPopover && self.self.statusItemPopover.isShown)
{
[self.statusItemPopover close];
[self.statusItem.button highlight:NO];
}
}
- (void)showPopoverWithAnimation:(BOOL)shouldAnimate
{
if (self.statusItemPopover && !self.statusItemPopover.isShown)
{
[self.statusItem.button highlight:YES];
NSView *buttonView = (NSView *)self.statusItem.button;
self.statusItemPopover.animates = shouldAnimate;
[self.statusItemPopover showRelativeToRect:NSZeroRect ofView:buttonView preferredEdge:NSRectEdgeMaxY];
// Setup a global event monitor to detect outside clicks so we can dismiss this popup
if (self.globalEventMonitor)
{
[NSEvent removeMonitor:self.globalEventMonitor];
self.globalEventMonitor = nil;
}
enum NSEventMask globalMonitorMask = NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDown;
self.globalEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:globalMonitorMask handler:^(NSEvent *_event)
{
[self.statusItemPopover close];
[self.statusItem.button highlight:NO];
// We want to tear down the global monitor right after the first
// outside click is detected and the popover is hidden.
if (self.globalEventMonitor)
{
[NSEvent removeMonitor:self.globalEventMonitor];
self.globalEventMonitor = nil;
}
}];
}
}
- (NSImage *)menubarIcon
{
return _menubarIcon;
}
- (void)setMenuTooltip:(NSString *)menuTooltip
{
_statusItem.button.toolTip = menuTooltip;
}
- (NSString *)menuTooltip
{
return _statusItem.button.toolTip;
}
- (void)handleAppReopen
{
BOOL optionDown = ([NSEvent modifierFlags] & NSEventModifierFlagOption) != 0;
if (self.visibilitySetting == kVisibleNowhere && optionDown)
self.visibilitySetting = kVisibleDock;
}
- (void)handleWindowOpened
{
_windowIsOpen = YES;
[self _redisplay];
}
- (void)handleWindowClosed
{
_windowIsOpen = NO;
[self _redisplay];
}
- (void)_transform:(BOOL)foreground
{
ProcessSerialNumber psn = {0, kCurrentProcess};
TransformProcessType(&psn, foreground ? kProcessTransformToForegroundApplication : kProcessTransformToUIElementApplication);
[NSApplication.sharedApplication activateIgnoringOtherApps:YES];
_dockIconIsCurrentlyVisible = foreground;
}
#pragma GCC diagnostic pop
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment