mirror of
https://github.com/WerWolv/ImHex.git
synced 2026-03-28 07:47:03 -05:00
This PR drops the use of brew for dependency management in favor of macports so we can support lower macOS versions instead of just the lowest one supported by Apple Closes #2586
480 lines
21 KiB
Objective-C
480 lines
21 KiB
Objective-C
#if defined(OS_MACOS)
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
#include <CoreFoundation/CFBundle.h>
|
|
#include <ApplicationServices/ApplicationServices.h>
|
|
#include <Foundation/NSUserDefaults.h>
|
|
#include <AppKit/NSScreen.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <CoreText/CoreText.h>
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
|
|
#define GLFW_EXPOSE_NATIVE_COCOA
|
|
#include <GLFW/glfw3.h>
|
|
#include <GLFW/glfw3native.h>
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import <AppleScriptObjC/AppleScriptObjC.h>
|
|
#import <UserNotifications/UserNotifications.h>
|
|
|
|
#include <hex/helpers/keys.hpp>
|
|
|
|
void errorMessageMacos(const char *cMessage) {
|
|
CFStringRef strMessage = CFStringCreateWithCString(NULL, cMessage, kCFStringEncodingUTF8);
|
|
CFUserNotificationDisplayAlert(0, kCFUserNotificationStopAlertLevel, NULL, NULL, NULL, strMessage, NULL, NULL, NULL, NULL, NULL);
|
|
}
|
|
|
|
void openFile(const char *path);
|
|
void registerFont(const char *fontName, const char *fontPath);
|
|
|
|
void openWebpageMacos(const char *url) {
|
|
CFURLRef urlRef = CFURLCreateWithBytes(NULL, (uint8_t*)(url), strlen(url), kCFStringEncodingASCII, NULL);
|
|
LSOpenCFURLRef(urlRef, NULL);
|
|
CFRelease(urlRef);
|
|
}
|
|
|
|
bool isMacosSystemDarkModeEnabled(void) {
|
|
NSString * appleInterfaceStyle = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"];
|
|
|
|
if (appleInterfaceStyle && [appleInterfaceStyle length] > 0) {
|
|
return [[appleInterfaceStyle lowercaseString] containsString:@"dark"];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
float getBackingScaleFactor(void) {
|
|
return [[NSScreen mainScreen] backingScaleFactor];
|
|
}
|
|
|
|
void macOSCloseButtonPressed(void);
|
|
|
|
@interface CloseButtonHandler : NSObject
|
|
@end
|
|
|
|
@implementation CloseButtonHandler
|
|
- (void)pressed:(id)sender {
|
|
macOSCloseButtonPressed();
|
|
}
|
|
@end
|
|
|
|
void setupMacosWindowStyle(GLFWwindow *window, bool borderlessWindowMode) {
|
|
NSWindow* cocoaWindow = glfwGetCocoaWindow(window);
|
|
|
|
cocoaWindow.titleVisibility = NSWindowTitleHidden;
|
|
|
|
if (borderlessWindowMode) {
|
|
cocoaWindow.titlebarAppearsTransparent = YES;
|
|
cocoaWindow.styleMask |= NSWindowStyleMaskFullSizeContentView;
|
|
|
|
// Setup liquid glass background effect
|
|
{
|
|
NSView* glfwContentView = [cocoaWindow contentView];
|
|
|
|
NSOpenGLContext* context = [NSOpenGLContext currentContext];
|
|
if (!context) {
|
|
glfwMakeContextCurrent(window);
|
|
context = [NSOpenGLContext currentContext];
|
|
}
|
|
|
|
GLint opaque = 0;
|
|
[context setValues:&opaque forParameter:NSOpenGLCPSurfaceOpacity];
|
|
[context update];
|
|
|
|
NSView* containerView = [[NSView alloc] initWithFrame:[glfwContentView frame]];
|
|
containerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
[containerView setWantsLayer:YES];
|
|
|
|
Class glassEffectClass = NSClassFromString(@"NSGlassEffectView");
|
|
NSView* effectView = nil;
|
|
if (glassEffectClass) {
|
|
// Use the new liquid glass effect
|
|
effectView = [[glassEffectClass alloc] initWithFrame:[containerView bounds]];
|
|
} else {
|
|
// Fall back to NSVisualEffectView for older systems
|
|
NSVisualEffectView* visualEffectView = [[NSVisualEffectView alloc] initWithFrame:[containerView bounds]];
|
|
visualEffectView.material = NSVisualEffectMaterialHUDWindow;
|
|
visualEffectView.blendingMode = NSVisualEffectBlendingModeBehindWindow;
|
|
visualEffectView.state = NSVisualEffectStateActive;
|
|
effectView = visualEffectView;
|
|
}
|
|
|
|
effectView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
|
|
[containerView addSubview:effectView];
|
|
|
|
[glfwContentView removeFromSuperview];
|
|
glfwContentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
[containerView addSubview:glfwContentView];
|
|
|
|
[cocoaWindow setContentView:containerView];
|
|
}
|
|
|
|
[cocoaWindow setOpaque:NO];
|
|
[cocoaWindow setHasShadow:YES];
|
|
[cocoaWindow setBackgroundColor:[NSColor colorWithWhite: 0 alpha: 0.001f]];
|
|
}
|
|
|
|
NSButton *closeButton = [cocoaWindow standardWindowButton:NSWindowCloseButton];
|
|
[closeButton setAction:@selector(pressed:)];
|
|
[closeButton setTarget:[CloseButtonHandler alloc]];
|
|
}
|
|
|
|
bool isMacosFullScreenModeEnabled(GLFWwindow *window) {
|
|
NSWindow* cocoaWindow = glfwGetCocoaWindow(window);
|
|
return (cocoaWindow.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen;
|
|
}
|
|
|
|
void enumerateFontsMacos(void) {
|
|
CFArrayRef fontDescriptors = CTFontManagerCopyAvailableFontFamilyNames();
|
|
CFIndex count = CFArrayGetCount(fontDescriptors);
|
|
|
|
for (CFIndex i = 0; i < count; i++) {
|
|
CFStringRef fontName = (CFStringRef)CFArrayGetValueAtIndex(fontDescriptors, i);
|
|
|
|
// Get font path - skip fonts without valid URLs
|
|
CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{ (__bridge NSString *)kCTFontFamilyNameAttribute : (__bridge NSString *)fontName };
|
|
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(attributes);
|
|
CFURLRef fontURL = CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);
|
|
|
|
if (fontURL != NULL) {
|
|
CFStringRef fontPath = CFURLCopyFileSystemPath(fontURL, kCFURLPOSIXPathStyle);
|
|
|
|
if (fontPath != NULL) {
|
|
registerFont([(__bridge NSString *)fontName UTF8String], [(__bridge NSString *)fontPath UTF8String]);
|
|
CFRelease(fontPath);
|
|
}
|
|
|
|
CFRelease(fontURL);
|
|
}
|
|
|
|
CFRelease(descriptor);
|
|
}
|
|
|
|
CFRelease(fontDescriptors);
|
|
}
|
|
|
|
void macosHandleTitlebarDoubleClickGesture(GLFWwindow *window) {
|
|
NSWindow* cocoaWindow = glfwGetCocoaWindow(window);
|
|
|
|
// Consult user preferences: "System Settings -> Desktop & Dock -> Double-click a window's title bar to"
|
|
NSString* action = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleActionOnDoubleClick"];
|
|
|
|
if (action == nil || [action isEqualToString:@"None"]) {
|
|
// Nothing to do
|
|
} else if ([action isEqualToString:@"Minimize"]) {
|
|
if ([cocoaWindow isMiniaturizable]) {
|
|
[cocoaWindow miniaturize:nil];
|
|
}
|
|
} else if ([action isEqualToString:@"Maximize"]) {
|
|
// `[NSWindow zoom:_ sender]` takes over pumping the main runloop for the duration of the resize,
|
|
// and would interfere with our renderer's frame logic. Schedule it for the next frame
|
|
|
|
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
|
|
if ([cocoaWindow isZoomable]) {
|
|
[cocoaWindow zoom:nil];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void macosSetWindowMovable(GLFWwindow *window, bool movable) {
|
|
NSWindow* cocoaWindow = glfwGetCocoaWindow(window);
|
|
|
|
[cocoaWindow setMovable:movable];
|
|
}
|
|
|
|
bool macosIsWindowBeingResizedByUser(GLFWwindow *window) {
|
|
NSWindow* cocoaWindow = glfwGetCocoaWindow(window);
|
|
|
|
return cocoaWindow.inLiveResize;
|
|
}
|
|
|
|
void macosMarkContentEdited(GLFWwindow *window, bool edited) {
|
|
NSWindow* cocoaWindow = glfwGetCocoaWindow(window);
|
|
|
|
[cocoaWindow setDocumentEdited:edited];
|
|
}
|
|
|
|
static NSArray* getRunningInstances(NSString *bundleIdentifier) {
|
|
return [NSRunningApplication runningApplicationsWithBundleIdentifier: bundleIdentifier];
|
|
}
|
|
|
|
bool macosIsMainInstance(void) {
|
|
NSArray *applications = getRunningInstances(@"net.WerWolv.ImHex");
|
|
return applications.count == 0;
|
|
}
|
|
|
|
extern void macosEventDataReceived(const unsigned char *data, size_t length);
|
|
static OSErr handleAppleEvent(const AppleEvent *event, AppleEvent *reply, void *refcon) {
|
|
(void)reply;
|
|
(void)refcon;
|
|
|
|
// Extract the raw binary data from the event's parameter
|
|
AEDesc paramDesc;
|
|
OSErr err = AEGetParamDesc(event, keyDirectObject, typeWildCard, ¶mDesc);
|
|
if (err != noErr) {
|
|
NSLog(@"Failed to get parameter: %d", err);
|
|
return err;
|
|
}
|
|
|
|
// Convert the AEDesc to NSData
|
|
NSAppleEventDescriptor *descriptor = [[NSAppleEventDescriptor alloc] initWithAEDescNoCopy:¶mDesc];
|
|
NSData *binaryData = descriptor.data;
|
|
|
|
// Process the binary data
|
|
if (binaryData) {
|
|
macosEventDataReceived(binaryData.bytes, binaryData.length);
|
|
}
|
|
|
|
return noErr;
|
|
}
|
|
|
|
void macosInstallEventListener(void) {
|
|
AEInstallEventHandler('misc', 'imhx', NewAEEventHandlerUPP(handleAppleEvent), 0, false);
|
|
}
|
|
|
|
void macosSendMessageToMainInstance(const unsigned char *data, size_t size) {
|
|
NSString *bundleIdentifier = @"net.WerWolv.ImHex";
|
|
|
|
NSData *binaryData = [NSData dataWithBytes:data length:size];
|
|
// Find the target application by its bundle identifier
|
|
NSAppleEventDescriptor *targetApp = [NSAppleEventDescriptor descriptorWithBundleIdentifier:bundleIdentifier];
|
|
if (!targetApp) {
|
|
NSLog(@"Application with bundle identifier %@ not found.", bundleIdentifier);
|
|
return;
|
|
}
|
|
|
|
// Create the Apple event
|
|
NSAppleEventDescriptor *event = [[NSAppleEventDescriptor alloc] initWithEventClass:'misc'
|
|
eventID:'imhx'
|
|
targetDescriptor:targetApp
|
|
returnID:kAutoGenerateReturnID
|
|
transactionID:kAnyTransactionID];
|
|
|
|
// Add a parameter with raw binary data
|
|
NSAppleEventDescriptor *binaryDescriptor = [NSAppleEventDescriptor descriptorWithDescriptorType:typeData
|
|
data:binaryData];
|
|
[event setParamDescriptor:binaryDescriptor forKeyword:keyDirectObject];
|
|
|
|
// Send the event
|
|
OSStatus status = AESendMessage([event aeDesc], NULL, kAENoReply, kAEDefaultTimeout);
|
|
if (status != noErr) {
|
|
NSLog(@"Failed to send Apple event: %d", status);
|
|
}
|
|
}
|
|
|
|
@interface HexDocument : NSDocument
|
|
|
|
@end
|
|
|
|
@implementation HexDocument
|
|
|
|
- (BOOL) readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError {
|
|
NSString* urlString = [url absoluteString];
|
|
const char* utf8String = [urlString UTF8String];
|
|
|
|
const char *prefix = "file://";
|
|
if (strncmp(utf8String, prefix, strlen(prefix)) == 0)
|
|
utf8String += strlen(prefix);
|
|
|
|
openFile(utf8String);
|
|
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
void macosGetKey(enum Keys key, int *output) {
|
|
*output = 0x00;
|
|
switch (key) {
|
|
case Space: *output = ' '; break;
|
|
case Apostrophe: *output = '\''; break;
|
|
case Comma: *output = ','; break;
|
|
case Minus: *output = '-'; break;
|
|
case Period: *output = '.'; break;
|
|
case Slash: *output = '/'; break;
|
|
case Num0: *output = '0'; break;
|
|
case Num1: *output = '1'; break;
|
|
case Num2: *output = '2'; break;
|
|
case Num3: *output = '3'; break;
|
|
case Num4: *output = '4'; break;
|
|
case Num5: *output = '5'; break;
|
|
case Num6: *output = '6'; break;
|
|
case Num7: *output = '7'; break;
|
|
case Num8: *output = '8'; break;
|
|
case Num9: *output = '9'; break;
|
|
case Semicolon: *output = ';'; break;
|
|
case Equals: *output = '='; break;
|
|
case A: *output = 'a'; break;
|
|
case B: *output = 'b'; break;
|
|
case C: *output = 'c'; break;
|
|
case D: *output = 'd'; break;
|
|
case E: *output = 'e'; break;
|
|
case F: *output = 'f'; break;
|
|
case G: *output = 'g'; break;
|
|
case H: *output = 'h'; break;
|
|
case I: *output = 'i'; break;
|
|
case J: *output = 'j'; break;
|
|
case K: *output = 'k'; break;
|
|
case L: *output = 'l'; break;
|
|
case M: *output = 'm'; break;
|
|
case N: *output = 'n'; break;
|
|
case O: *output = 'o'; break;
|
|
case P: *output = 'p'; break;
|
|
case Q: *output = 'q'; break;
|
|
case R: *output = 'r'; break;
|
|
case S: *output = 's'; break;
|
|
case T: *output = 't'; break;
|
|
case U: *output = 'u'; break;
|
|
case V: *output = 'v'; break;
|
|
case W: *output = 'w'; break;
|
|
case X: *output = 'x'; break;
|
|
case Y: *output = 'y'; break;
|
|
case Z: *output = 'z'; break;
|
|
case LeftBracket: *output = '/'; break;
|
|
case Backslash: *output = '\\'; break;
|
|
case RightBracket: *output = ']'; break;
|
|
case GraveAccent: *output = '`'; break;
|
|
case World1: break;
|
|
case World2: break;
|
|
case Escape: break;
|
|
case Enter: *output = NSEnterCharacter; break;
|
|
case Tab: *output = NSTabCharacter; break;
|
|
case Backspace: *output = NSBackspaceCharacter; break;
|
|
case Insert: *output = NSInsertFunctionKey; break;
|
|
case Delete: *output = NSDeleteCharacter; break;
|
|
case Right: *output = NSRightArrowFunctionKey; break;
|
|
case Left: *output = NSLeftArrowFunctionKey; break;
|
|
case Down: *output = NSDownArrowFunctionKey; break;
|
|
case Up: *output = NSUpArrowFunctionKey; break;
|
|
case PageUp: *output = NSPageUpFunctionKey; break;
|
|
case PageDown: *output = NSPageDownFunctionKey; break;
|
|
case Home: *output = NSHomeFunctionKey; break;
|
|
case End: *output = NSEndFunctionKey; break;
|
|
case CapsLock: break;
|
|
case ScrollLock: *output = NSScrollLockFunctionKey; break;
|
|
case NumLock: break;
|
|
case PrintScreen: *output = NSPrintScreenFunctionKey; break;
|
|
case Pause: *output = NSPauseFunctionKey; break;
|
|
case F1: *output = NSF1FunctionKey; break;
|
|
case F2: *output = NSF2FunctionKey; break;
|
|
case F3: *output = NSF3FunctionKey; break;
|
|
case F4: *output = NSF4FunctionKey; break;
|
|
case F5: *output = NSF5FunctionKey; break;
|
|
case F6: *output = NSF6FunctionKey; break;
|
|
case F7: *output = NSF7FunctionKey; break;
|
|
case F8: *output = NSF8FunctionKey; break;
|
|
case F9: *output = NSF9FunctionKey; break;
|
|
case F10: *output = NSF10FunctionKey; break;
|
|
case F11: *output = NSF11FunctionKey; break;
|
|
case F12: *output = NSF12FunctionKey; break;
|
|
case F13: *output = NSF13FunctionKey; break;
|
|
case F14: *output = NSF14FunctionKey; break;
|
|
case F15: *output = NSF15FunctionKey; break;
|
|
case F16: *output = NSF16FunctionKey; break;
|
|
case F17: *output = NSF17FunctionKey; break;
|
|
case F18: *output = NSF18FunctionKey; break;
|
|
case F19: *output = NSF19FunctionKey; break;
|
|
case F20: *output = NSF20FunctionKey; break;
|
|
case F21: *output = NSF21FunctionKey; break;
|
|
case F22: *output = NSF22FunctionKey; break;
|
|
case F23: *output = NSF23FunctionKey; break;
|
|
case F24: *output = NSF24FunctionKey; break;
|
|
case F25: *output = NSF25FunctionKey; break;
|
|
case KeyPad0: *output = '0'; break;
|
|
case KeyPad1: *output = '1'; break;
|
|
case KeyPad2: *output = '2'; break;
|
|
case KeyPad3: *output = '3'; break;
|
|
case KeyPad4: *output = '4'; break;
|
|
case KeyPad5: *output = '5'; break;
|
|
case KeyPad6: *output = '6'; break;
|
|
case KeyPad7: *output = '7'; break;
|
|
case KeyPad8: *output = '8'; break;
|
|
case KeyPad9: *output = '9'; break;
|
|
case KeyPadDecimal: *output = '.'; break;
|
|
case KeyPadDivide: *output = '/'; break;
|
|
case KeyPadMultiply: *output = '*'; break;
|
|
case KeyPadSubtract: *output = '-'; break;
|
|
case KeyPadAdd: *output = '+'; break;
|
|
case KeyPadEnter: *output = NSEnterCharacter; break;
|
|
case KeyPadEqual: *output = '='; break;
|
|
case Menu: *output = NSMenuFunctionKey; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
|
|
static bool isRunningInAppBundle(void) {
|
|
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
|
return [bundlePath.pathExtension.lowercaseString isEqualToString:@"app"];
|
|
}
|
|
|
|
@interface NotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
|
|
@end
|
|
|
|
@implementation NotificationDelegate
|
|
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
willPresentNotification:(UNNotification *)notification
|
|
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
|
|
if (@available(macOS 11.0, *)) {
|
|
completionHandler(UNNotificationPresentationOptionBanner |
|
|
UNNotificationPresentationOptionSound |
|
|
UNNotificationPresentationOptionList);
|
|
} else {
|
|
// For macOS 10.15 and earlier
|
|
completionHandler(UNNotificationPresentationOptionAlert |
|
|
UNNotificationPresentationOptionSound);
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
void toastMessageMacos(const char *title, const char *message) {
|
|
@autoreleasepool {
|
|
// Only show notification if we're inside a bundle
|
|
if (!isRunningInAppBundle()) {
|
|
return;
|
|
}
|
|
|
|
NSString *nsTitle = [NSString stringWithUTF8String:title];
|
|
NSString *nsMessage = [NSString stringWithUTF8String:message];
|
|
|
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
|
|
|
// Request permission if needed
|
|
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound)
|
|
completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
|
|
|
(void)error;
|
|
if (!granted) return;
|
|
|
|
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
|
content.title = nsTitle;
|
|
content.body = nsMessage;
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
|
|
// Show notification immediately
|
|
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO];
|
|
|
|
NSString *identifier = [[NSUUID UUID] UUIDString];
|
|
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
|
|
content:content
|
|
trigger:trigger];
|
|
|
|
[center addNotificationRequest:request withCompletionHandler:nil];
|
|
}];
|
|
}
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
#endif
|