From 32077c102243a89ebadfe2afbac7e151e215c391 Mon Sep 17 00:00:00 2001 From: Christopher-Marcel Esser Date: Thu, 17 Nov 2022 15:37:31 +0100 Subject: [PATCH 1/4] Use .NET for macOS instead of direct native calls. --- .../SpiderEye.Playground.Mac.csproj | 2 +- README.md | 4 +- Source/SpiderEye.Mac/CocoaApplication.cs | 65 +-- Source/SpiderEye.Mac/CocoaStatusIcon.cs | 74 ++-- .../CocoaSynchronizationContext.cs | 75 ---- Source/SpiderEye.Mac/CocoaWebview.cs | 378 ++++++------------ Source/SpiderEye.Mac/CocoaWindow.cs | 201 ++++------ Source/SpiderEye.Mac/Dialogs/CocoaDialog.cs | 51 --- .../SpiderEye.Mac/Dialogs/CocoaFileDialog.cs | 61 --- .../Dialogs/CocoaFolderSelectDialog.cs | 54 ++- .../SpiderEye.Mac/Dialogs/CocoaMessageBox.cs | 60 ++- .../Dialogs/CocoaOpenFileDialog.cs | 81 ++-- .../Dialogs/CocoaSaveFileDialog.cs | 68 +++- Source/SpiderEye.Mac/Interop/CGPoint.cs | 17 - Source/SpiderEye.Mac/Interop/CGRect.cs | 19 - Source/SpiderEye.Mac/Interop/CGSize.cs | 17 - Source/SpiderEye.Mac/Interop/Delegates.cs | 18 - Source/SpiderEye.Mac/Interop/KeyMapper.cs | 106 ----- Source/SpiderEye.Mac/Interop/NSAlertStyle.cs | 9 - Source/SpiderEye.Mac/Interop/NSBlock.cs | 73 ---- Source/SpiderEye.Mac/Interop/NSColor.cs | 16 - Source/SpiderEye.Mac/Interop/NSDialog.cs | 61 --- .../Interop/NSEventModifierFlags.cs | 15 - .../SpiderEye.Mac/Interop/NSImageScaling.cs | 30 -- Source/SpiderEye.Mac/Interop/NSKey.cs | 78 ---- Source/SpiderEye.Mac/Interop/NSRunLoop.cs | 53 --- Source/SpiderEye.Mac/Interop/NSString.cs | 46 --- .../Interop/NSWindowStyleMask.cs | 17 - .../Interop/NativeClassDefinition.cs | 94 ----- .../Interop/NativeClassInstance.cs | 34 -- Source/SpiderEye.Mac/Interop/URL.cs | 22 - Source/SpiderEye.Mac/MacApplication.cs | 19 +- .../SpiderEye.Mac/Menu/CocoaLabelMenuItem.cs | 174 +++++--- Source/SpiderEye.Mac/Menu/CocoaMenu.cs | 37 +- Source/SpiderEye.Mac/Menu/CocoaMenuItem.cs | 84 +--- .../Menu/CocoaSeparatorMenuItem.cs | 11 +- Source/SpiderEye.Mac/Menu/CocoaSubMenu.cs | 70 ++++ .../SpiderEye.Mac/Menu/MenuExtensions.App.cs | 4 +- Source/SpiderEye.Mac/Native/AppKit.cs | 31 -- Source/SpiderEye.Mac/Native/Dispatch.cs | 42 -- Source/SpiderEye.Mac/Native/Foundation.cs | 46 --- Source/SpiderEye.Mac/Native/ObjC.cs | 136 ------- Source/SpiderEye.Mac/Native/Webkit.cs | 21 - Source/SpiderEye.Mac/SpiderEye.Mac.csproj | 4 +- 44 files changed, 641 insertions(+), 1937 deletions(-) delete mode 100644 Source/SpiderEye.Mac/CocoaSynchronizationContext.cs delete mode 100644 Source/SpiderEye.Mac/Dialogs/CocoaDialog.cs delete mode 100644 Source/SpiderEye.Mac/Dialogs/CocoaFileDialog.cs delete mode 100644 Source/SpiderEye.Mac/Interop/CGPoint.cs delete mode 100644 Source/SpiderEye.Mac/Interop/CGRect.cs delete mode 100644 Source/SpiderEye.Mac/Interop/CGSize.cs delete mode 100644 Source/SpiderEye.Mac/Interop/Delegates.cs delete mode 100644 Source/SpiderEye.Mac/Interop/KeyMapper.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSAlertStyle.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSBlock.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSColor.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSDialog.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSEventModifierFlags.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSImageScaling.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSKey.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSRunLoop.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSString.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NSWindowStyleMask.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NativeClassDefinition.cs delete mode 100644 Source/SpiderEye.Mac/Interop/NativeClassInstance.cs delete mode 100644 Source/SpiderEye.Mac/Interop/URL.cs create mode 100644 Source/SpiderEye.Mac/Menu/CocoaSubMenu.cs delete mode 100644 Source/SpiderEye.Mac/Native/AppKit.cs delete mode 100644 Source/SpiderEye.Mac/Native/Dispatch.cs delete mode 100644 Source/SpiderEye.Mac/Native/Foundation.cs delete mode 100644 Source/SpiderEye.Mac/Native/ObjC.cs delete mode 100644 Source/SpiderEye.Mac/Native/Webkit.cs diff --git a/Playground/SpiderEye.Playground.Mac/SpiderEye.Playground.Mac.csproj b/Playground/SpiderEye.Playground.Mac/SpiderEye.Playground.Mac.csproj index e90c0549..12832575 100644 --- a/Playground/SpiderEye.Playground.Mac/SpiderEye.Playground.Mac.csproj +++ b/Playground/SpiderEye.Playground.Mac/SpiderEye.Playground.Mac.csproj @@ -2,7 +2,7 @@ - net6.0 + net6.0-macos $(DefineConstants);MAC diff --git a/README.md b/README.md index 623559b0..9318db63 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ What's the name supposed to mean? Simple: what kind of view does a spiders eye h | Windows | 7, 8.x, 10, 11 | .NET 6.0 | WinForms WebBrowser control | IE 9-11 (depending on OS and installed version) | | Windows | 7, 8.1, 10, 11 | .NET 6.0 | WebView2 | Edge Chromium | | Linux | any 64bit distro where .NET 6.0 runs | .NET 6.0 | WebKit2GTK | WebKit | -| macOS | x64 10.13 or newer | .NET 6.0 | WKWebView | WebKit | +| macOS | x64 10.14 or newer | .NET 6.0 | WKWebView | WebKit | | Linux Dependencies | Used for | Optional | | ----- | ----- | ----- | @@ -170,7 +170,7 @@ First you need an `Info.plist` file like you'd have for any other macOS app. Her CFBundleShortVersionString 1.0.0 LSMinimumSystemVersion - 10.13 + 10.14 CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType diff --git a/Source/SpiderEye.Mac/CocoaApplication.cs b/Source/SpiderEye.Mac/CocoaApplication.cs index 369f5930..8c9693e7 100644 --- a/Source/SpiderEye.Mac/CocoaApplication.cs +++ b/Source/SpiderEye.Mac/CocoaApplication.cs @@ -1,7 +1,7 @@ using System; using System.Threading; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; +using AppKit; +using Foundation; namespace SpiderEye.Mac { @@ -11,65 +11,38 @@ internal class CocoaApplication : IApplication public SynchronizationContext SynchronizationContext { get; } - public IntPtr Handle { get; } - - private static readonly NativeClassDefinition AppDelegateDefinition; - private readonly NativeClassInstance appDelegate; - - static CocoaApplication() - { - AppDelegateDefinition = CreateAppDelegate(); - } - public CocoaApplication() { - Factory = new CocoaUiFactory(); - SynchronizationContext = new CocoaSynchronizationContext(); + NSApplication.Init(); - Handle = AppKit.Call("NSApplication", "sharedApplication"); - appDelegate = AppDelegateDefinition.CreateInstance(this); + Factory = new CocoaUiFactory(); + SynchronizationContext = SynchronizationContext.Current!; - ObjC.Call(Handle, "setActivationPolicy:", IntPtr.Zero); - ObjC.Call(Handle, "setDelegate:", appDelegate.Handle); + NSApplication.SharedApplication.Delegate = new CocoaAppDelegate(); + NSApplication.SharedApplication.ActivationPolicy = NSApplicationActivationPolicy.Regular; } public void Run() { - ObjC.Call(Handle, "run"); + NSApplication.SharedApplication.Run(); } public void Exit() { - ObjC.Call(Handle, "terminate:", Handle); - appDelegate.Dispose(); + NSApplication.SharedApplication.Terminate(NSApplication.SharedApplication); } - private static NativeClassDefinition CreateAppDelegate() + private class CocoaAppDelegate : NSApplicationDelegate { - // note: NSApplicationDelegate is not available at runtime and returns null, it's kept for completeness - var definition = NativeClassDefinition.FromClass( - "SpiderEyeAppDelegate", - AppKit.GetClass("NSResponder"), - AppKit.GetProtocol("NSApplicationDelegate"), - AppKit.GetProtocol("NSTouchBarProvider")); - - definition.AddMethod( - "applicationShouldTerminateAfterLastWindowClosed:", - "c@:@", - (self, op, notification) => (byte)(Application.ExitWithLastWindow ? 1 : 0)); - - definition.AddMethod( - "applicationDidFinishLaunching:", - "v@:@", - (self, op, notification) => - { - var instance = definition.GetParent(self); - ObjC.Call(instance.Handle, "activateIgnoringOtherApps:", true); - }); - - definition.FinishDeclaration(); - - return definition; + public override void DidFinishLaunching(NSNotification notification) + { + NSApplication.SharedApplication.ActivateIgnoringOtherApps(true); + } + + public override bool ApplicationShouldTerminateAfterLastWindowClosed(NSApplication sender) + { + return Application.ExitWithLastWindow; + } } } } diff --git a/Source/SpiderEye.Mac/CocoaStatusIcon.cs b/Source/SpiderEye.Mac/CocoaStatusIcon.cs index 623cd1bc..292ef644 100644 --- a/Source/SpiderEye.Mac/CocoaStatusIcon.cs +++ b/Source/SpiderEye.Mac/CocoaStatusIcon.cs @@ -1,7 +1,5 @@ -using System; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; -using SpiderEye.Tools; +using AppKit; +using Foundation; namespace SpiderEye.Mac { @@ -9,8 +7,11 @@ internal class CocoaStatusIcon : IStatusIcon { public string? Title { - get; // TODO: see if setting title is useful on macOS StatusBar - set; + get { return statusItem.Title; } + set + { + statusItem.Title = value ?? string.Empty; + } } public AppIcon? Icon @@ -19,7 +20,16 @@ public AppIcon? Icon set { icon = value; - UpdateIcon(value); + + NSImage? image = null; + if (value != null && value.Icons.Length > 0) + { + byte[] data = value.GetIconData(value.DefaultIcon); + using var nsData = NSData.FromArray(data); + image = new NSImage(nsData); + } + + statusItem.Button.Image = image; } } @@ -29,59 +39,29 @@ public Menu? Menu set { menu = value; - UpdateMenu(value); + statusItem.Menu = (NSMenu?)value?.NativeMenu; } } - private readonly IntPtr statusItem; - private readonly IntPtr statusBarButton; + private readonly NSStatusItem statusItem; private AppIcon? icon; private Menu? menu; public CocoaStatusIcon(string title) { - var statusBar = AppKit.Call("NSStatusBar", "systemStatusBar"); - statusItem = ObjC.Call(statusBar, "statusItemWithLength:", -2.0); // -1 = variable size; -2 = square size - ObjC.Call(statusItem, "setHighlightMode:", true); - statusBarButton = ObjC.Call(statusItem, "button"); - ObjC.Call(statusBarButton, "setImageScaling:", new UIntPtr((uint)NSImageScaling.ProportionallyUpOrDown)); - Title = title; - } - - public void Dispose() - { - // don't think anything needs to be done here - } - - private unsafe void UpdateIcon(AppIcon? icon) - { - var image = IntPtr.Zero; - if (icon != null && icon.Icons.Length > 0) - { - byte[] data = icon.GetIconData(icon.DefaultIcon); - fixed (byte* dataPtr = data) - { - IntPtr nsData = Foundation.Call( - "NSData", - "dataWithBytesNoCopy:length:freeWhenDone:", - (IntPtr)dataPtr, - new IntPtr(data.Length), - IntPtr.Zero); + var statusBar = NSStatusBar.SystemStatusBar; - image = AppKit.Call("NSImage", "alloc"); - ObjC.Call(image, "initWithData:", nsData); - ObjC.Call(statusBarButton, "setImage:", image); - } - } - - ObjC.Call(statusBarButton, "setImage:", image); + statusItem = statusBar.CreateStatusItem(NSStatusItemLength.Square); + statusItem.HighlightMode = true; + statusItem.Title = title; + statusItem.Button.ImageScaling = NSImageScale.ProportionallyUpOrDown; } - private void UpdateMenu(Menu? menu) + public void Dispose() { - var nativeMenu = NativeCast.To(menu?.NativeMenu); - ObjC.Call(statusItem, "setMenu:", nativeMenu?.Handle ?? IntPtr.Zero); + statusItem.StatusBar.RemoveStatusItem(statusItem); + statusItem.Dispose(); } } } diff --git a/Source/SpiderEye.Mac/CocoaSynchronizationContext.cs b/Source/SpiderEye.Mac/CocoaSynchronizationContext.cs deleted file mode 100644 index 5ea9d4e3..00000000 --- a/Source/SpiderEye.Mac/CocoaSynchronizationContext.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac -{ - internal sealed class CocoaSynchronizationContext : SynchronizationContext - { - public bool IsMainThread - { - get { return Environment.CurrentManagedThreadId == mainThreadId; } - } - - private readonly int mainThreadId; - - public CocoaSynchronizationContext() - : this(Environment.CurrentManagedThreadId) - { - } - - private CocoaSynchronizationContext(int mainThreadId) - { - this.mainThreadId = mainThreadId; - } - - public override SynchronizationContext CreateCopy() - { - return new CocoaSynchronizationContext(mainThreadId); - } - - public override void Post(SendOrPostCallback d, object? state) - { - if (d == null) { throw new ArgumentNullException(nameof(d)); } - - var data = new InvokeState(d, state); - var handle = GCHandle.Alloc(data, GCHandleType.Normal); - Dispatch.AsyncFunction(Dispatch.MainQueue, GCHandle.ToIntPtr(handle), InvokeCallback); - } - - public override void Send(SendOrPostCallback d, object? state) - { - if (d == null) { throw new ArgumentNullException(nameof(d)); } - - if (IsMainThread) { d(state); } - else - { - var data = new InvokeState(d, state); - var handle = GCHandle.Alloc(data, GCHandleType.Normal); - Dispatch.SyncFunction(Dispatch.MainQueue, GCHandle.ToIntPtr(handle), InvokeCallback); - } - } - - private static void InvokeCallback(IntPtr data) - { - var handle = GCHandle.FromIntPtr(data); - var state = (InvokeState)handle.Target!; - - try { state.Callback(state.State); } - finally { handle.Free(); } - } - - private sealed class InvokeState - { - public readonly SendOrPostCallback Callback; - public readonly object? State; - - public InvokeState(SendOrPostCallback callback, object? state) - { - Callback = callback; - State = state; - } - } - } -} diff --git a/Source/SpiderEye.Mac/CocoaWebview.cs b/Source/SpiderEye.Mac/CocoaWebview.cs index 39f1ddc8..67306eae 100644 --- a/Source/SpiderEye.Mac/CocoaWebview.cs +++ b/Source/SpiderEye.Mac/CocoaWebview.cs @@ -1,15 +1,14 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; +using System; using System.Threading.Tasks; +using CoreGraphics; +using Foundation; using SpiderEye.Bridge; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; using SpiderEye.Tools; +using WebKit; namespace SpiderEye.Mac { - internal class CocoaWebview : IWebview + internal class CocoaWebview : WKWebView, IWebview { public event NavigatingEventHandler? Navigating; public event PageLoadEventHandler? PageLoaded; @@ -20,78 +19,59 @@ internal class CocoaWebview : IWebview public bool UseBrowserTitle { get; set; } public bool EnableDevTools { - get { return enableDevToolsField; } + get + { + using var key = new NSString("developerExtrasEnabled"); + using var value = (NSNumber)preferences.ValueForKey(key); + return value.BoolValue; + } set { - enableDevToolsField = value; - IntPtr boolValue = Foundation.Call("NSNumber", "numberWithBool:", value); - ObjC.Call(preferences, "setValue:forKey:", boolValue, NSString.Create("developerExtrasEnabled")); + using var boolValue = new NSNumber(value); + using var key = new NSString("developerExtrasEnabled"); + preferences.SetValueForKey(boolValue, key); } } private Uri Uri { - get { return URL.GetAsUri(ObjC.Call(Handle, "URL"))!; } + get { return this.Url; } } - public readonly IntPtr Handle; - - private static readonly NativeClassDefinition CallbackClassDefinition; - private static readonly NativeClassDefinition SchemeHandlerDefinition; - - private readonly NativeClassInstance callbackClass; - private readonly NativeClassInstance schemeHandler; - + private const string SCHEME = "spidereye"; private readonly WebviewBridge bridge; private readonly Uri customHost; - private readonly IntPtr preferences; - - private bool enableDevToolsField; - - static CocoaWebview() - { - CallbackClassDefinition = CreateCallbackClass(); - SchemeHandlerDefinition = CreateSchemeHandler(); - } + private readonly WKPreferences preferences; + private readonly IDisposable titleObserver; public CocoaWebview(WebviewBridge bridge) + : base(CGRect.Empty, CreateConfiguration()) { this.bridge = bridge ?? throw new ArgumentNullException(nameof(bridge)); - IntPtr configuration = WebKit.Call("WKWebViewConfiguration", "new"); - IntPtr manager = ObjC.Call(configuration, "userContentController"); - - callbackClass = CallbackClassDefinition.CreateInstance(this); - schemeHandler = SchemeHandlerDefinition.CreateInstance(this); + customHost = new Uri(UriTools.GetRandomResourceUrl(SCHEME)); - const string scheme = "spidereye"; - customHost = new Uri(UriTools.GetRandomResourceUrl(scheme)); - ObjC.Call(configuration, "setURLSchemeHandler:forURLScheme:", schemeHandler.Handle, NSString.Create(scheme)); + WKUserContentController manager = Configuration.UserContentController; + manager.AddScriptMessageHandler(new CocoaScriptMessageHandler(this), "external"); + using var initScriptSource = new NSString(Resources.GetInitScript("Mac")); + var script = new WKUserScript(initScriptSource, WKUserScriptInjectionTime.AtDocumentStart, false); + manager.AddUserScript(script); - ObjC.Call(manager, "addScriptMessageHandler:name:", callbackClass.Handle, NSString.Create("external")); - IntPtr script = WebKit.Call("WKUserScript", "alloc"); - ObjC.Call( - script, - "initWithSource:injectionTime:forMainFrameOnly:", - NSString.Create(Resources.GetInitScript("Mac")), - IntPtr.Zero, - IntPtr.Zero); - ObjC.Call(manager, "addUserScript:", script); + NavigationDelegate = new CocoaNavigationDelegate(); - Handle = WebKit.Call("WKWebView", "alloc"); - ObjC.Call(Handle, "initWithFrame:configuration:", CGRect.Zero, configuration); - ObjC.Call(Handle, "setNavigationDelegate:", callbackClass.Handle); + using var boolValue = NSNumber.FromBoolean(false); + using var key = new NSString("drawsBackground"); + SetValueForKey(boolValue, key); - IntPtr boolValue = Foundation.Call("NSNumber", "numberWithBool:", false); - ObjC.Call(Handle, "setValue:forKey:", boolValue, NSString.Create("drawsBackground")); - ObjC.Call(Handle, "addObserver:forKeyPath:options:context:", callbackClass.Handle, NSString.Create("title"), IntPtr.Zero, IntPtr.Zero); + titleObserver = AddObserver("title", NSKeyValueObservingOptions.New, observedChange => + { + if (UseBrowserTitle) + { + TitleChanged?.Invoke(this, (NSString?)observedChange.NewValue ?? string.Empty); + } + }); - preferences = ObjC.Call(configuration, "preferences"); - } - - public void UpdateBackgroundColor(IntPtr color) - { - ObjC.Call(Handle, "setBackgroundColor:", color); + preferences = Configuration.Preferences; } public void LoadUri(Uri uri) @@ -100,256 +80,126 @@ public void LoadUri(Uri uri) if (!uri.IsAbsoluteUri) { uri = new Uri(customHost, uri); } - IntPtr nsUrl = Foundation.Call("NSURL", "URLWithString:", NSString.Create(uri.ToString())); - IntPtr request = Foundation.Call("NSURLRequest", "requestWithURL:", nsUrl); - ObjC.Call(Handle, "loadRequest:", request); + using var request = NSUrlRequest.FromUrl(uri); + LoadRequest(request); } public Task ExecuteScriptAsync(string script) { var taskResult = new TaskCompletionSource(); - NSBlock? block = null; - - ScriptEvalCallbackDelegate callback = (IntPtr self, IntPtr result, IntPtr error) => + EvaluateJavaScript(script, (result, error) => { - try + if (error != null) { - if (error != IntPtr.Zero) - { - string? message = NSString.GetString(ObjC.Call(error, "localizedDescription")); - taskResult.TrySetException(new ScriptException($"Script execution failed with: \"{message}\"")); - } - else - { - string? content = NSString.GetString(result); - taskResult.TrySetResult(content); - } + taskResult.SetException(new ScriptException("Script execution failed.", new NSErrorException(error))); } - catch (Exception ex) { taskResult.TrySetException(ex); } - finally { block!.Dispose(); } - }; - - block = new NSBlock(callback); - ObjC.Call( - Handle, - "evaluateJavaScript:completionHandler:", - NSString.Create(script), - block.Handle); - + else + { + taskResult.SetResult((NSString)result); + } + }); return taskResult.Task; } - public void Dispose() + protected override void Dispose(bool disposing) { - // webview will be released automatically - callbackClass.Dispose(); - schemeHandler.Dispose(); + if (disposing) + { + titleObserver.Dispose(); + } + + base.Dispose(disposing); } - private static NativeClassDefinition CreateCallbackClass() + private static WKWebViewConfiguration CreateConfiguration() { - // note: WKScriptMessageHandler is not available at runtime and returns null, it's kept for completeness - var definition = NativeClassDefinition.FromObject( - "SpiderEyeWebviewCallbacks", - WebKit.GetProtocol("WKNavigationDelegate"), - WebKit.GetProtocol("WKScriptMessageHandler")); - - definition.AddMethod( - "webView:decidePolicyForNavigationAction:decisionHandler:", - "v@:@@@", - (self, op, view, navigationAction, decisionHandler) => - { - var instance = definition.GetParent(self); - var args = new NavigatingEventArgs(instance.Uri); - instance.Navigating?.Invoke(instance, args); - - var block = Marshal.PtrToStructure(decisionHandler); - var callback = Marshal.GetDelegateForFunctionPointer(block.Invoke); - callback(decisionHandler, args.Cancel ? IntPtr.Zero : new IntPtr(1)); - }); - - definition.AddMethod( - "webView:didFinishNavigation:", - "v@:@@", - (self, op, view, navigation) => - { - var instance = definition.GetParent(self); - instance.PageLoaded?.Invoke(instance, new PageLoadEventArgs(instance.Uri, true)); - }); - - definition.AddMethod( - "webView:didFailNavigation:withError:", - "v@:@@@", - (self, op, view, navigation, error) => - { - var instance = definition.GetParent(self); - instance.PageLoaded?.Invoke(instance, new PageLoadEventArgs(instance.Uri, false)); - }); - - definition.AddMethod( - "observeValueForKeyPath:ofObject:change:context:", - "v@:@@@@", - (self, op, keyPath, obj, change, context) => - { - var instance = definition.GetParent(self); - ObservedValueChanged(instance, keyPath); - }); - - definition.AddMethod( - "userContentController:didReceiveScriptMessage:", - "v@:@@", - (self, op, notification, message) => - { - var instance = definition.GetParent(self); - ScriptCallback(instance, message); - }); - - definition.FinishDeclaration(); - - return definition; + var configuration = new WKWebViewConfiguration(); + configuration.SetUrlSchemeHandler(new CocoaUrlSchemeHandler(), SCHEME); + return configuration; } - private static NativeClassDefinition CreateSchemeHandler() + private class CocoaNavigationDelegate : WKNavigationDelegate { - // note: WKURLSchemeHandler is not available at runtime and returns null, it's kept for completeness - var definition = NativeClassDefinition.FromObject( - "SpiderEyeSchemeHandler", - WebKit.GetProtocol("WKURLSchemeHandler")); - - definition.AddMethod( - "webView:startURLSchemeTask:", - "v@:@@", - (self, op, view, schemeTask) => - { - var instance = definition.GetParent(self); - UriSchemeStartCallback(instance, schemeTask); - }); - - definition.AddMethod( - "webView:stopURLSchemeTask:", - "v@:@@", - (self, op, view, schemeTask) => { /* don't think anything needs to be done here */ }); + public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler) + { + var cocoaWebView = (CocoaWebview)webView; + var args = new NavigatingEventArgs(cocoaWebView.Uri); + cocoaWebView.Navigating?.Invoke(cocoaWebView, args); + decisionHandler.Invoke(args.Cancel ? WKNavigationActionPolicy.Cancel : WKNavigationActionPolicy.Allow); + } - definition.FinishDeclaration(); + public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) + { + var cocoaWebView = (CocoaWebview)webView; + cocoaWebView.PageLoaded?.Invoke(cocoaWebView, new PageLoadEventArgs(cocoaWebView.Uri, true)); + } - return definition; + public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) + { + var cocoaWebView = (CocoaWebview)webView; + cocoaWebView.PageLoaded?.Invoke(cocoaWebView, new PageLoadEventArgs(cocoaWebView.Uri, false)); + } } - private static void ObservedValueChanged(CocoaWebview instance, IntPtr keyPath) + private class CocoaScriptMessageHandler : WKScriptMessageHandler { - string? key = NSString.GetString(keyPath); - if (key == "title" && instance.UseBrowserTitle) + private readonly CocoaWebview cocoaWebView; + + public CocoaScriptMessageHandler(CocoaWebview cocoaWebView) { - string? title = NSString.GetString(ObjC.Call(instance.Handle, "title")); - instance.TitleChanged?.Invoke(instance, title ?? string.Empty); + this.cocoaWebView = cocoaWebView; } - } - private static async void ScriptCallback(CocoaWebview instance, IntPtr message) - { - if (instance.EnableScriptInterface) + public override async void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message) { - IntPtr body = ObjC.Call(message, "body"); - IntPtr isString = ObjC.Call(body, "isKindOfClass:", Foundation.GetClass("NSString")); - if (isString != IntPtr.Zero) + if (cocoaWebView.EnableScriptInterface && message.Body is NSString body) { - string data = NSString.GetString(body)!; - await instance.bridge.HandleScriptCall(data); + await cocoaWebView.bridge.HandleScriptCall(body); } } } - private static void UriSchemeStartCallback(CocoaWebview instance, IntPtr schemeTask) + private class CocoaUrlSchemeHandler : NSObject, IWKUrlSchemeHandler { - try + public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask) { - IntPtr request = ObjC.Call(schemeTask, "request"); - IntPtr url = ObjC.Call(request, "URL"); - - var uri = URL.GetAsUri(url)!; - var host = new Uri(uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)); - if (host == instance.customHost) + try { - using var contentStream = Application.ContentProvider.GetStreamAsync(uri).GetAwaiter().GetResult(); - if (contentStream != null) + var cocoaWebView = (CocoaWebview)webView; + var request = urlSchemeTask.Request; + Uri uri = request.Url; + + var host = new Uri(uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)); + if (host == cocoaWebView.customHost) { - if (contentStream is UnmanagedMemoryStream unmanagedMemoryStream) + using var contentStream = Application.ContentProvider.GetStreamAsync(uri).GetAwaiter().GetResult(); + if (contentStream != null) { - unsafe - { - long length = unmanagedMemoryStream.Length - unmanagedMemoryStream.Position; - var data = (IntPtr)unmanagedMemoryStream.PositionPointer; - FinishUriSchemeCallback(url, schemeTask, data, length, uri); - return; - } - } - else - { - byte[] data; - long length; - if (contentStream is MemoryStream memoryStream) - { - data = memoryStream.GetBuffer(); - length = memoryStream.Length; - } - else - { - using var copyStream = new MemoryStream(); - contentStream.CopyTo(copyStream); - data = copyStream.GetBuffer(); - length = copyStream.Length; - } - - unsafe - { - fixed (byte* dataPtr = data) - { - FinishUriSchemeCallback(url, schemeTask, (IntPtr)dataPtr, length, uri); - return; - } - } + using var data = NSData.FromStream(contentStream)!; + using var response = new NSUrlResponse(uri, MimeTypes.FindForUri(uri), (nint)contentStream.Length, null); + urlSchemeTask.DidReceiveResponse(response); + urlSchemeTask.DidReceiveData(data); + urlSchemeTask.DidFinish(); + return; } } - } - FinishUriSchemeCallbackWithError(schemeTask, 404); + FinishUriSchemeCallbackWithError(urlSchemeTask, 404); + } + catch { FinishUriSchemeCallbackWithError(urlSchemeTask, 500); } } - catch { FinishUriSchemeCallbackWithError(schemeTask, 500); } - } - - private static void FinishUriSchemeCallback(IntPtr url, IntPtr schemeTask, IntPtr data, long contentLength, Uri uri) - { - IntPtr response = Foundation.Call("NSURLResponse", "alloc"); - ObjC.Call( - response, - "initWithURL:MIMEType:expectedContentLength:textEncodingName:", - url, - NSString.Create(MimeTypes.FindForUri(uri)), - new IntPtr(contentLength), - IntPtr.Zero); - - ObjC.Call(schemeTask, "didReceiveResponse:", response); - - IntPtr nsData = Foundation.Call( - "NSData", - "dataWithBytesNoCopy:length:freeWhenDone:", - data, - new IntPtr(contentLength), - IntPtr.Zero); - ObjC.Call(schemeTask, "didReceiveData:", nsData); - ObjC.Call(schemeTask, "didFinish"); - } + public void StopUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask) + { + // don't think anything needs to be done here + } - private static void FinishUriSchemeCallbackWithError(IntPtr schemeTask, int errorCode) - { - var error = Foundation.Call( - "NSError", - "errorWithDomain:code:userInfo:", - NSString.Create("com.bildstein.spidereye"), - new IntPtr(errorCode), - IntPtr.Zero); - ObjC.Call(schemeTask, "didFailWithError:", error); + private static void FinishUriSchemeCallbackWithError(IWKUrlSchemeTask urlSchemeTask, int errorCode) + { + using var domain = new NSString("com.bildstein.spidereye"); + using var error = new NSError(domain, errorCode); + urlSchemeTask.DidFailWithError(error); + } } } } diff --git a/Source/SpiderEye.Mac/CocoaWindow.cs b/Source/SpiderEye.Mac/CocoaWindow.cs index a11b348e..ed382c41 100644 --- a/Source/SpiderEye.Mac/CocoaWindow.cs +++ b/Source/SpiderEye.Mac/CocoaWindow.cs @@ -1,61 +1,61 @@ -using System; -using System.Runtime.InteropServices; +using System; +using AppKit; +using CoreGraphics; +using Foundation; using SpiderEye.Bridge; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; +using SpiderEye.Tools; namespace SpiderEye.Mac { - internal class CocoaWindow : IWindow + internal class CocoaWindow : NSWindow, IWindow { public event CancelableEventHandler? Closing; public event EventHandler? Closed; public event EventHandler? Shown; - public string? Title + string? IWindow.Title { - get { return NSString.GetString(ObjC.Call(Handle, "title")); } - set { ObjC.Call(Handle, "setTitle:", NSString.Create(value ?? string.Empty)); } + get { return this.Title; } + set { this.Title = value ?? string.Empty; } } public Size Size { get { - var frame = Marshal.PtrToStructure(ObjC.Call(Handle, "frame")); + var frame = this.Frame; return new Size((int)frame.Size.Width, (int)frame.Size.Height); } set { - ObjC.Call(Handle, "setContentSize:", new CGSize(value.Width, value.Height)); + this.SetContentSize(new CGSize(value.Width, value.Height)); } } - public Size MinSize + Size IWindow.MinSize { get { - var size = Marshal.PtrToStructure(ObjC.Call(Handle, "contentMinSize")); + var size = this.ContentMinSize; return new Size(size.Width, size.Height); } set { - ObjC.Call(Handle, "setContentMinSize:", new CGSize(value.Width, value.Height)); + this.ContentMinSize = new CGSize(value.Width, value.Height); } } - public Size MaxSize + Size IWindow.MaxSize { get { - var size = Marshal.PtrToStructure(ObjC.Call(Handle, "contentMaxSize")); + var size = this.ContentMaxSize; return new Size(size.Width, size.Height); } set { if (value == Size.Zero) { value = new Size(float.MaxValue, float.MaxValue); } - - ObjC.Call(Handle, "setContentMaxSize:", new CGSize(value.Width, value.Height)); + this.ContentMaxSize = new CGSize(value.Width, value.Height); } } @@ -65,7 +65,7 @@ public bool CanResize set { canResizeField = value; - StyleMask = GetWantedStyleMask(); + StyleMask = GetWantedStyleMask(StyleMask, borderStyleField, value); } } @@ -79,20 +79,24 @@ public WindowBorderStyle BorderStyle // the title gets reset when setting it to borderless // so we just store the title, set the border and set the title back again string? title = Title; - StyleMask = GetWantedStyleMask(); + StyleMask = GetWantedStyleMask(StyleMask, value, canResizeField); Title = title; } } - public string? BackgroundColor + string? IWindow.BackgroundColor { get { return backgroundColorField; } set { backgroundColorField = value; - IntPtr bgColor = NSColor.FromHex(value); - ObjC.Call(Handle, "setBackgroundColor:", bgColor); - webview.UpdateBackgroundColor(bgColor); + + ColorTools.ParseHex(value, out byte r, out byte g, out byte b); + using var color = NSColor.FromRgba(r, g, b, (byte)255); + BackgroundColor = color; + + using var key = new NSString("backgroundColor"); + webview.SetValueForKey(color, key); } } @@ -122,157 +126,110 @@ public IWebview Webview get { return webview; } } - private NSWindowStyleMask StyleMask - { - get { return (NSWindowStyleMask)ObjC.Call(Handle, "styleMask"); } - set { ObjC.Call(Handle, "setStyleMask:", new IntPtr((int)value)); } - } - - public readonly IntPtr Handle; - - private static readonly NativeClassDefinition WindowDelegateDefinition; - - private readonly NativeClassInstance windowDelegate; private readonly CocoaWebview webview; private bool canResizeField; private WindowBorderStyle borderStyleField; private string? backgroundColorField; - static CocoaWindow() - { - WindowDelegateDefinition = CreateWindowDelegate(); - } - public CocoaWindow(WindowConfiguration config, WebviewBridge bridge) + : base(new CGRect(0, 0, config.Size.Width, config.Size.Height), GetWantedStyleMask(0ul, WindowBorderStyle.Default, config.CanResize), NSBackingStore.Buffered, false) { if (config == null) { throw new ArgumentNullException(nameof(config)); } if (bridge == null) { throw new ArgumentNullException(nameof(bridge)); } - Handle = AppKit.Call("NSWindow", "alloc"); - canResizeField = config.CanResize; - var style = GetWantedStyleMask(); - ObjC.SendMessage( - Handle, - ObjC.RegisterName("initWithContentRect:styleMask:backing:defer:"), - new CGRect(0, 0, config.Size.Width, config.Size.Height), - new UIntPtr((uint)style), - new UIntPtr(2), - false); + StyleMask = GetWantedStyleMask(StyleMask, borderStyleField, canResizeField); webview = new CocoaWebview(bridge); - ObjC.Call(Handle, "setContentView:", webview.Handle); + ContentView = webview; webview.TitleChanged += Webview_TitleChanged; - windowDelegate = WindowDelegateDefinition.CreateInstance(this); - ObjC.Call(Handle, "setDelegate:", windowDelegate.Handle); + Delegate = new CocoaWindowDelegate(this); } public void Show() { - ObjC.Call(Handle, "center"); - ObjC.Call(Handle, "makeKeyAndOrderFront:", IntPtr.Zero); + Center(); + MakeKeyAndOrderFront(null); MacApplication.SynchronizationContext.Post(s => Shown?.Invoke(this, EventArgs.Empty), null); } - public void Close() - { - ObjC.Call(Handle, "close", IntPtr.Zero); - } - public void EnterFullscreen() { - if (!StyleMask.HasFlag(NSWindowStyleMask.FullScreen)) + if (!StyleMask.HasFlag(NSWindowStyle.FullScreenWindow)) { - ObjC.Call(Handle, "toggleFullScreen:", Handle); + ToggleFullScreen(this); } } public void ExitFullscreen() { - if (StyleMask.HasFlag(NSWindowStyleMask.FullScreen)) + if (StyleMask.HasFlag(NSWindowStyle.FullScreenWindow)) { - ObjC.Call(Handle, "toggleFullScreen:", Handle); + ToggleFullScreen(this); } } public void Maximize() { - if (ObjC.Call(Handle, "isZoomed") == IntPtr.Zero) { ObjC.Call(Handle, "zoom:", Handle); } + if (!IsZoomed) + { + Zoom(this); + } } public void Unmaximize() { - if (ObjC.Call(Handle, "isZoomed") != IntPtr.Zero) { ObjC.Call(Handle, "zoom:", Handle); } + if (IsZoomed) + { + Zoom(this); + } } public void Minimize() { - ObjC.Call(Handle, "miniaturize:", IntPtr.Zero); + Miniaturize(null); } public void Unminimize() { - ObjC.Call(Handle, "deminiaturize:", IntPtr.Zero); + Deminiaturize(null); } - public void Dispose() + protected override void Dispose(bool disposing) { - // window will be released automatically - webview.Dispose(); - windowDelegate.Dispose(); - } + if (disposing) + { + webview.Dispose(); + } - private static NativeClassDefinition CreateWindowDelegate() - { - var definition = NativeClassDefinition.FromObject( - "SpiderEyeWindowDelegate", - AppKit.GetProtocol("NSWindowDelegate")); - - definition.AddMethod( - "windowShouldClose:", - "c@:@", - (self, op, window) => - { - var instance = definition.GetParent(self); - var args = new CancelableEventArgs(); - instance.Closing?.Invoke(instance, args); - - return args.Cancel ? (byte)0 : (byte)1; - }); - - definition.AddMethod( - "windowWillClose:", - "v@:@", - (self, op, notification) => - { - var instance = definition.GetParent(self); - instance.webview.TitleChanged -= instance.Webview_TitleChanged; - instance.Closed?.Invoke(instance, EventArgs.Empty); - }); - - definition.FinishDeclaration(); - - return definition; + base.Dispose(disposing); } - private NSWindowStyleMask GetWantedStyleMask() + private class CocoaWindowDelegate : NSWindowDelegate { - bool isFullscreen = StyleMask.HasFlag(NSWindowStyleMask.FullScreen); - NSWindowStyleMask style = NSWindowStyleMask.Closable | NSWindowStyleMask.Miniaturizable; - style |= borderStyleField switch + private readonly CocoaWindow cocoaWindow; + + public CocoaWindowDelegate(CocoaWindow cocoaWindow) { - WindowBorderStyle.Default => NSWindowStyleMask.Titled, - WindowBorderStyle.None => NSWindowStyleMask.Borderless, - _ => throw new ArgumentException($"Invalid border style value of {borderStyleField}", nameof(BorderStyle)), - }; - if (canResizeField) { style |= NSWindowStyleMask.Resizable; } - if (isFullscreen) { style |= NSWindowStyleMask.FullScreen; } + this.cocoaWindow = cocoaWindow; + } - return style; + public override bool WindowShouldClose(NSObject sender) + { + var args = new CancelableEventArgs(); + cocoaWindow.Closing?.Invoke(cocoaWindow, args); + return !args.Cancel; + } + + public override void WillClose(NSNotification notification) + { + cocoaWindow.webview.TitleChanged -= cocoaWindow.Webview_TitleChanged; + cocoaWindow.Closed?.Invoke(cocoaWindow, EventArgs.Empty); + } } private void Webview_TitleChanged(object? sender, string title) @@ -282,5 +239,21 @@ private void Webview_TitleChanged(object? sender, string title) Application.Invoke(() => Title = title ?? string.Empty); } } + + private static NSWindowStyle GetWantedStyleMask(NSWindowStyle styleMask, WindowBorderStyle borderStyle, bool canResize) + { + bool isFullscreen = styleMask.HasFlag(NSWindowStyle.FullScreenWindow); + NSWindowStyle style = NSWindowStyle.Closable | NSWindowStyle.Miniaturizable; + style |= borderStyle switch + { + WindowBorderStyle.Default => NSWindowStyle.Titled, + WindowBorderStyle.None => NSWindowStyle.Borderless, + _ => throw new ArgumentException($"Invalid border style value of {borderStyle}", nameof(BorderStyle)), + }; + if (canResize) { style |= NSWindowStyle.Resizable; } + if (isFullscreen) { style |= NSWindowStyle.FullScreenWindow; } + + return style; + } } } diff --git a/Source/SpiderEye.Mac/Dialogs/CocoaDialog.cs b/Source/SpiderEye.Mac/Dialogs/CocoaDialog.cs deleted file mode 100644 index 32e2a998..00000000 --- a/Source/SpiderEye.Mac/Dialogs/CocoaDialog.cs +++ /dev/null @@ -1,51 +0,0 @@ -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; -using SpiderEye.Tools; - -namespace SpiderEye.Mac -{ - internal abstract class CocoaDialog : IDialog - { - public string? Title { get; set; } - - public DialogResult Show() - { - return Show(null); - } - - public DialogResult Show(IWindow? parent) - { - var window = NativeCast.To(parent); - var dialog = CreateDialog(); - - ObjC.Call(dialog.Handle, "setTitle:", NSString.Create(Title)); - ObjC.Call(dialog.Handle, "setCanCreateDirectories:", true); - - int result = dialog.Run(window); - var mappedResult = MapResult(result); - BeforeReturn(dialog, mappedResult); - - return mappedResult; - } - - protected abstract NSDialog CreateDialog(); - - protected virtual void BeforeShow(NSDialog dialog) - { - } - - protected virtual void BeforeReturn(NSDialog dialog, DialogResult result) - { - } - - private static DialogResult MapResult(int result) - { - return result switch - { - 1 => DialogResult.Ok, - 0 => DialogResult.Cancel, - _ => DialogResult.None, - }; - } - } -} diff --git a/Source/SpiderEye.Mac/Dialogs/CocoaFileDialog.cs b/Source/SpiderEye.Mac/Dialogs/CocoaFileDialog.cs deleted file mode 100644 index 2bb306e8..00000000 --- a/Source/SpiderEye.Mac/Dialogs/CocoaFileDialog.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac -{ - internal abstract class CocoaFileDialog : CocoaDialog, IFileDialog - { - public string? InitialDirectory { get; set; } - public string? FileName { get; set; } - public ICollection FileFilters { get; } - - protected CocoaFileDialog() - { - FileFilters = new List(); - } - - protected override void BeforeShow(NSDialog dialog) - { - if (!string.IsNullOrWhiteSpace(InitialDirectory)) - { - var url = Foundation.Call("NSURL", "fileURLWithPath:", NSString.Create(InitialDirectory)); - ObjC.Call(dialog.Handle, "setDirectoryURL:", url); - } - - if (!string.IsNullOrWhiteSpace(FileName)) - { - ObjC.Call(dialog.Handle, "setNameFieldStringValue:", NSString.Create(FileName)); - } - - SetFileFilters(dialog.Handle, FileFilters); - } - - protected override void BeforeReturn(NSDialog dialog, DialogResult result) - { - if (result == DialogResult.Ok) - { - var selection = ObjC.Call(dialog.Handle, "URL"); - FileName = NSString.GetString(ObjC.Call(selection, "path")); - } - else { FileName = null; } - } - - private static void SetFileFilters(IntPtr dialog, IEnumerable filters) - { - var fileTypes = filters - .SelectMany(t => t.Filters.Select(u => u.TrimStart('*', '.'))) - .Distinct() - .Select(t => NSString.Create(t)); - - if (fileTypes.Any()) - { - var data = fileTypes.ToArray(); - var array = Foundation.Call("NSArray", "arrayWithObjects:count:", data, new IntPtr(data.Length)); - ObjC.Call(dialog, "setAllowedFileTypes:", array); - } - } - } -} diff --git a/Source/SpiderEye.Mac/Dialogs/CocoaFolderSelectDialog.cs b/Source/SpiderEye.Mac/Dialogs/CocoaFolderSelectDialog.cs index 6e116dfb..66d83dc2 100644 --- a/Source/SpiderEye.Mac/Dialogs/CocoaFolderSelectDialog.cs +++ b/Source/SpiderEye.Mac/Dialogs/CocoaFolderSelectDialog.cs @@ -1,37 +1,57 @@ -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; +using System; +using AppKit; namespace SpiderEye.Mac { - internal class CocoaFolderSelectDialog : CocoaDialog, IFolderSelectDialog + internal class CocoaFolderSelectDialog : IFolderSelectDialog { + public string? Title { get; set; } public string? SelectedPath { get; set; } - protected override NSDialog CreateDialog() + public DialogResult Show() { - var panel = NSDialog.CreateOpenPanel(); + return Show(null); + } - ObjC.Call(panel.Handle, "setCanChooseFiles:", false); - ObjC.Call(panel.Handle, "setCanChooseDirectories:", true); - ObjC.Call(panel.Handle, "setAllowsMultipleSelection:", false); + public DialogResult Show(IWindow? parent) + { + using var panel = NSOpenPanel.OpenPanel; + panel.Title = Title ?? string.Empty; + panel.CanCreateDirectories = true; + panel.CanChooseFiles = false; + panel.CanChooseDirectories = true; + panel.AllowsMultipleSelection = false; if (!string.IsNullOrWhiteSpace(SelectedPath)) { - var url = Foundation.Call("NSURL", "fileURLWithPath:", NSString.Create(SelectedPath)); - ObjC.Call(panel.Handle, "setDirectoryURL:", url); + panel.DirectoryUrl = new Uri(SelectedPath); } - return panel; + nint result; + if (parent == null) + { + result = panel.RunModal(); + } + else + { + panel.BeginSheet((CocoaWindow)parent, result => NSApplication.SharedApplication.StopModalWithCode(result)); + result = NSApplication.SharedApplication.RunModalForWindow(panel); + } + + var mappedResult = MapResult(result); + SelectedPath = mappedResult == DialogResult.Ok ? panel.Url.Path : null; + + return mappedResult; } - protected override void BeforeReturn(NSDialog dialog, DialogResult result) + private static DialogResult MapResult(nint result) { - if (result == DialogResult.Ok) + return result switch { - var selection = ObjC.Call(dialog.Handle, "URL"); - SelectedPath = NSString.GetString(ObjC.Call(selection, "path")); - } - else { SelectedPath = null; } + 1 => DialogResult.Ok, + 0 => DialogResult.Cancel, + _ => DialogResult.None, + }; } } } diff --git a/Source/SpiderEye.Mac/Dialogs/CocoaMessageBox.cs b/Source/SpiderEye.Mac/Dialogs/CocoaMessageBox.cs index f93854b7..88ce07f2 100644 --- a/Source/SpiderEye.Mac/Dialogs/CocoaMessageBox.cs +++ b/Source/SpiderEye.Mac/Dialogs/CocoaMessageBox.cs @@ -1,7 +1,4 @@ -using System; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; -using SpiderEye.Tools; +using AppKit; namespace SpiderEye.Mac { @@ -18,42 +15,37 @@ public DialogResult Show() public DialogResult Show(IWindow? parent) { - var window = NativeCast.To(parent); - using var alert = NSDialog.CreateAlert(); - ObjC.Call(alert.Handle, "setShowsHelp:", IntPtr.Zero); - ObjC.Call(alert.Handle, "setAlertStyle:", new UIntPtr((uint)NSAlertStyle.Informational)); - ObjC.Call(alert.Handle, "setMessageText:", NSString.Create(Title ?? string.Empty)); - ObjC.Call(alert.Handle, "setInformativeText:", NSString.Create(Message ?? string.Empty)); - AddButtons(alert.Handle, Buttons); + using var alert = new NSAlert(); + alert.ShowsHelp = false; + alert.AlertStyle = NSAlertStyle.Informational; + alert.MessageText = Title ?? string.Empty; + alert.InformativeText = Message ?? string.Empty; - return (DialogResult)alert.Run(window); - } - - private static void AddButtons(IntPtr alert, MessageBoxButtons buttons) - { - switch (buttons) + var buttonDetails = Buttons switch { - case MessageBoxButtons.OkCancel: - AddButton(alert, "Ok", DialogResult.Ok); - AddButton(alert, "Cancel", DialogResult.Cancel); - break; + MessageBoxButtons.OkCancel => new[] { ("Ok", DialogResult.Ok), ("Cancel", DialogResult.Cancel) }, + MessageBoxButtons.YesNo => new[] { ("Yes", DialogResult.Yes), ("No", DialogResult.No) }, + _ => new[] { ("Ok", DialogResult.Ok) }, + }; - case MessageBoxButtons.YesNo: - AddButton(alert, "Yes", DialogResult.Yes); - AddButton(alert, "No", DialogResult.No); - break; + foreach (var (title, dialogResult) in buttonDetails) + { + using var button = alert.AddButton(title); + button.Tag = (int)dialogResult; + } - case MessageBoxButtons.Ok: - default: - AddButton(alert, "Ok", DialogResult.Ok); - break; + nint result; + if (parent == null) + { + result = alert.RunModal(); + } + else + { + alert.BeginSheet((CocoaWindow)parent, response => NSApplication.SharedApplication.StopModalWithCode((nint)response)); + result = NSApplication.SharedApplication.RunModalForWindow(alert.Window); } - } - private static void AddButton(IntPtr alert, string title, DialogResult result) - { - IntPtr button = ObjC.Call(alert, "addButtonWithTitle:", NSString.Create(title)); - ObjC.Call(button, "setTag:", new IntPtr((int)result)); + return (DialogResult)result; } } } diff --git a/Source/SpiderEye.Mac/Dialogs/CocoaOpenFileDialog.cs b/Source/SpiderEye.Mac/Dialogs/CocoaOpenFileDialog.cs index f8fc62d4..ea5f7bc2 100644 --- a/Source/SpiderEye.Mac/Dialogs/CocoaOpenFileDialog.cs +++ b/Source/SpiderEye.Mac/Dialogs/CocoaOpenFileDialog.cs @@ -1,44 +1,79 @@ using System; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; +using System.Collections.Generic; +using System.Linq; +using AppKit; namespace SpiderEye.Mac { - internal class CocoaOpenFileDialog : CocoaFileDialog, IOpenFileDialog + internal class CocoaOpenFileDialog : IOpenFileDialog { + public string? Title { get; set; } + public string? InitialDirectory { get; set; } + public string? FileName { get; set; } + public ICollection FileFilters { get; } = new List(); public bool Multiselect { get; set; } + public string[]? SelectedFiles { get; private set; } - public string[]? SelectedFiles + public DialogResult Show() { - get; - private set; + return Show(null); } - protected override NSDialog CreateDialog() + public DialogResult Show(IWindow? parent) { - var panel = NSDialog.CreateOpenPanel(); + using var panel = NSOpenPanel.OpenPanel; + panel.Title = Title ?? string.Empty; + panel.CanCreateDirectories = true; + panel.CanChooseFiles = true; + panel.CanChooseDirectories = false; + panel.AllowsMultipleSelection = Multiselect; - ObjC.Call(panel.Handle, "setCanChooseFiles:", true); - ObjC.Call(panel.Handle, "setCanChooseDirectories:", false); - ObjC.Call(panel.Handle, "setAllowsMultipleSelection:", Multiselect); + if (!string.IsNullOrWhiteSpace(InitialDirectory)) + { + panel.DirectoryUrl = new Uri(InitialDirectory); + } - return panel; - } + if (!string.IsNullOrWhiteSpace(FileName)) + { + panel.NameFieldStringValue = FileName; + } - protected override void BeforeReturn(NSDialog dialog, DialogResult result) - { - base.BeforeReturn(dialog, result); + var fileTypes = FileFilters + .SelectMany(t => t.Filters.Select(u => u.TrimStart('*', '.'))) + .Distinct() + .ToArray(); + + if (fileTypes.Length > 0) + { + panel.AllowedFileTypes = fileTypes; + } - var urls = ObjC.Call(dialog.Handle, "URLs"); - int count = ObjC.Call(urls, "count").ToInt32(); - string[] files = new string[count]; - for (int i = 0; i < count; i++) + nint result; + if (parent == null) + { + result = panel.RunModal(); + } + else { - var url = ObjC.Call(urls, "objectAtIndex:", new IntPtr(i)); - files[i] = NSString.GetString(ObjC.Call(url, "path"))!; + panel.BeginSheet((CocoaWindow)parent, result => NSApplication.SharedApplication.StopModalWithCode(result)); + result = NSApplication.SharedApplication.RunModalForWindow(panel); } - SelectedFiles = files; + var mappedResult = MapResult(result); + FileName = mappedResult == DialogResult.Ok ? panel.Url.Path : null; + SelectedFiles = panel.Urls.Select(u => u.Path).ToArray(); + + return mappedResult; + } + + private static DialogResult MapResult(nint result) + { + return result switch + { + 1 => DialogResult.Ok, + 0 => DialogResult.Cancel, + _ => DialogResult.None, + }; } } } diff --git a/Source/SpiderEye.Mac/Dialogs/CocoaSaveFileDialog.cs b/Source/SpiderEye.Mac/Dialogs/CocoaSaveFileDialog.cs index fd9e3949..04d92aaf 100644 --- a/Source/SpiderEye.Mac/Dialogs/CocoaSaveFileDialog.cs +++ b/Source/SpiderEye.Mac/Dialogs/CocoaSaveFileDialog.cs @@ -1,15 +1,75 @@ -using SpiderEye.Mac.Interop; +using System; +using System.Collections.Generic; +using System.Linq; +using AppKit; namespace SpiderEye.Mac { - internal class CocoaSaveFileDialog : CocoaFileDialog, ISaveFileDialog + internal class CocoaSaveFileDialog : ISaveFileDialog { + public string? Title { get; set; } + public string? InitialDirectory { get; set; } + public string? FileName { get; set; } + public ICollection FileFilters { get; } = new List(); public bool OverwritePrompt { get; set; } - protected override NSDialog CreateDialog() + public DialogResult Show() + { + return Show(null); + } + + public DialogResult Show(IWindow? parent) { // TODO: can't disable overwrite prompt on macOS - return NSDialog.CreateSavePanel(); + using var panel = NSSavePanel.SavePanel; + panel.Title = Title ?? string.Empty; + panel.CanCreateDirectories = true; + + if (!string.IsNullOrWhiteSpace(InitialDirectory)) + { + panel.DirectoryUrl = new Uri(InitialDirectory); + } + + if (!string.IsNullOrWhiteSpace(FileName)) + { + panel.NameFieldStringValue = FileName; + } + + var fileTypes = FileFilters + .SelectMany(t => t.Filters.Select(u => u.TrimStart('*', '.'))) + .Distinct() + .ToArray(); + + if (fileTypes.Length > 0) + { + panel.AllowedFileTypes = fileTypes; + } + + nint result; + if (parent == null) + { + result = panel.RunModal(); + } + else + { + panel.BeginSheet((CocoaWindow)parent, result => NSApplication.SharedApplication.StopModalWithCode(result)); + result = NSApplication.SharedApplication.RunModalForWindow(panel); + } + + var mappedResult = MapResult(result); + FileName = mappedResult == DialogResult.Ok ? panel.Url.Path : null; + + return mappedResult; + } + + private static DialogResult MapResult(nint result) + { + return result switch + { + 1 => DialogResult.Ok, + 0 => DialogResult.Cancel, + _ => DialogResult.None, + }; } } } diff --git a/Source/SpiderEye.Mac/Interop/CGPoint.cs b/Source/SpiderEye.Mac/Interop/CGPoint.cs deleted file mode 100644 index 831ddb20..00000000 --- a/Source/SpiderEye.Mac/Interop/CGPoint.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Interop -{ - [StructLayout(LayoutKind.Sequential)] - internal struct CGPoint - { - public double X; - public double Y; - - public CGPoint(double x, double y) - { - X = x; - Y = y; - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/CGRect.cs b/Source/SpiderEye.Mac/Interop/CGRect.cs deleted file mode 100644 index ebe80549..00000000 --- a/Source/SpiderEye.Mac/Interop/CGRect.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Interop -{ - [StructLayout(LayoutKind.Sequential)] - internal struct CGRect - { - public static readonly CGRect Zero = default; - - public CGPoint Origin; - public CGSize Size; - - public CGRect(double x, double y, double width, double height) - { - Origin = new CGPoint(x, y); - Size = new CGSize(width, height); - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/CGSize.cs b/Source/SpiderEye.Mac/Interop/CGSize.cs deleted file mode 100644 index 308b5c62..00000000 --- a/Source/SpiderEye.Mac/Interop/CGSize.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Interop -{ - [StructLayout(LayoutKind.Sequential)] - internal struct CGSize - { - public double Width; - public double Height; - - public CGSize(double width, double height) - { - Width = width; - Height = height; - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/Delegates.cs b/Source/SpiderEye.Mac/Interop/Delegates.cs deleted file mode 100644 index a061d18f..00000000 --- a/Source/SpiderEye.Mac/Interop/Delegates.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace SpiderEye.Mac.Interop -{ - internal delegate byte ShouldTerminateDelegate(IntPtr self, IntPtr op, IntPtr notification); - internal delegate void ScriptCallbackDelegate(IntPtr self, IntPtr op, IntPtr notification, IntPtr msg); - internal delegate void ScriptEvalCallbackDelegate(IntPtr self, IntPtr result, IntPtr error); - internal delegate void NavigationDecideDelegate(IntPtr self, IntPtr op, IntPtr view, IntPtr navigationAction, IntPtr decisionHandler); - internal delegate void LoadFinishedDelegate(IntPtr self, IntPtr op, IntPtr view, IntPtr navigation); - internal delegate void LoadFailedDelegate(IntPtr self, IntPtr op, IntPtr view, IntPtr navigation, IntPtr error); - internal delegate void SchemeHandlerDelegate(IntPtr self, IntPtr op, IntPtr view, IntPtr schemeTask); - internal delegate void ObserveValueDelegate(IntPtr self, IntPtr op, IntPtr keyPath, IntPtr obj, IntPtr change, IntPtr context); - internal delegate byte WindowShouldCloseDelegate(IntPtr self, IntPtr op, IntPtr window); - internal delegate void NotificationDelegate(IntPtr self, IntPtr op, IntPtr notification); - internal delegate void MenuCallbackDelegate(IntPtr self, IntPtr op, IntPtr menu); - internal delegate void DispatchDelegate(IntPtr context); - internal delegate void NavigationDecisionDelegate(IntPtr block, IntPtr result); -} diff --git a/Source/SpiderEye.Mac/Interop/KeyMapper.cs b/Source/SpiderEye.Mac/Interop/KeyMapper.cs deleted file mode 100644 index a8587eb6..00000000 --- a/Source/SpiderEye.Mac/Interop/KeyMapper.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using SpiderEye.Tools; - -namespace SpiderEye.Mac.Interop -{ - internal static class KeyMapper - { - public static NSEventModifierFlags GetModifier(ModifierKey key) - { - NSEventModifierFlags result = NSEventModifierFlags.None; - - foreach (var flag in EnumTools.GetFlags(key)) - { - switch (flag) - { - case ModifierKey.None: - continue; - - case ModifierKey.Shift: - result |= NSEventModifierFlags.Shift; - break; - - case ModifierKey.Control: - result |= NSEventModifierFlags.Control; - break; - - case ModifierKey.Alt: - result |= NSEventModifierFlags.Option; - break; - - case ModifierKey.Super: - case ModifierKey.Primary: - result |= NSEventModifierFlags.Command; - break; - - default: - throw new NotSupportedException($"Unsupported modifier key: \"{flag}\""); - } - } - - return result; - } - - public static string? GetKey(Key key) - { - if (Keymap.TryGetValue(key, out string? value)) { return value; } - else { throw new NotSupportedException($"Unsupported key: \"{key}\""); } - } - - private static readonly Dictionary Keymap = new() - { - { Key.None, null }, - { Key.F1, NSKey.F1 }, - { Key.F2, NSKey.F2 }, - { Key.F3, NSKey.F3 }, - { Key.F4, NSKey.F4 }, - { Key.F5, NSKey.F5 }, - { Key.F6, NSKey.F6 }, - { Key.F7, NSKey.F7 }, - { Key.F8, NSKey.F8 }, - { Key.F9, NSKey.F9 }, - { Key.F10, NSKey.F10 }, - { Key.F11, NSKey.F11 }, - { Key.F12, NSKey.F12 }, - { Key.Number1, "1" }, - { Key.Number2, "2" }, - { Key.Number3, "3" }, - { Key.Number4, "4" }, - { Key.Number5, "5" }, - { Key.Number6, "6" }, - { Key.Number7, "7" }, - { Key.Number8, "8" }, - { Key.Number9, "9" }, - { Key.Number0, "0" }, - { Key.A, "a" }, - { Key.B, "b" }, - { Key.C, "c" }, - { Key.D, "d" }, - { Key.E, "e" }, - { Key.F, "f" }, - { Key.G, "g" }, - { Key.H, "h" }, - { Key.I, "i" }, - { Key.J, "j" }, - { Key.K, "k" }, - { Key.L, "l" }, - { Key.M, "m" }, - { Key.N, "n" }, - { Key.O, "o" }, - { Key.P, "p" }, - { Key.Q, "q" }, - { Key.R, "r" }, - { Key.S, "s" }, - { Key.T, "t" }, - { Key.U, "u" }, - { Key.V, "v" }, - { Key.W, "w" }, - { Key.X, "x" }, - { Key.Y, "y" }, - { Key.Z, "z" }, - { Key.Insert, NSKey.Insert }, - { Key.Delete, "\0x007f" }, // Note: different than NSKey.Delete - }; - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSAlertStyle.cs b/Source/SpiderEye.Mac/Interop/NSAlertStyle.cs deleted file mode 100644 index abc6389e..00000000 --- a/Source/SpiderEye.Mac/Interop/NSAlertStyle.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SpiderEye.Mac.Interop -{ - internal enum NSAlertStyle - { - Warning = 0, - Informational = 1, - Critical = 2, - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSBlock.cs b/Source/SpiderEye.Mac/Interop/NSBlock.cs deleted file mode 100644 index 5997f252..00000000 --- a/Source/SpiderEye.Mac/Interop/NSBlock.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac.Interop -{ - internal sealed class NSBlock : IDisposable - { - public readonly IntPtr Handle; - private readonly Delegate callback; - - public unsafe NSBlock(Delegate callback) - { - this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); - - var blp = (BlockLiteral*)Marshal.AllocHGlobal(sizeof(BlockLiteral)); - var bdp = (BlockDescriptor*)Marshal.AllocHGlobal(sizeof(BlockDescriptor)); - - blp->Isa = ObjC.GetClass("__NSStackBlock"); - blp->Flags = 0; - blp->Reserved = 0; - blp->Invoke = Marshal.GetFunctionPointerForDelegate(callback); - blp->Descriptor = bdp; - - bdp->Reserved = IntPtr.Zero; - bdp->Size = new IntPtr(sizeof(BlockLiteral)); - bdp->CopyHelper = IntPtr.Zero; - bdp->DisposeHelper = IntPtr.Zero; - - Handle = (IntPtr)blp; - } - - public unsafe void Dispose() - { - var blp = (BlockLiteral*)Handle; - - Marshal.FreeHGlobal((IntPtr)blp->Descriptor); - Marshal.FreeHGlobal(Handle); - } - - [StructLayout(LayoutKind.Sequential)] - public unsafe struct BlockLiteral - { - public IntPtr Isa; - public BlockFlags Flags; - public int Reserved; - public IntPtr Invoke; - public BlockDescriptor* Descriptor; - } - - [StructLayout(LayoutKind.Sequential)] - public struct BlockDescriptor - { - public IntPtr Reserved; - public IntPtr Size; - public IntPtr CopyHelper; - public IntPtr DisposeHelper; - } - - [Flags] - public enum BlockFlags : int - { - RefcountMask = 0xFFFF, - NeedsFree = 1 << 24, - HasCopyDispose = 1 << 25, - HasCxxObj = 1 << 26, - IsGC = 1 << 27, - IsGlobal = 1 << 28, - HasDescriptor = 1 << 29, - HasSignature = 1 << 30, - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSColor.cs b/Source/SpiderEye.Mac/Interop/NSColor.cs deleted file mode 100644 index 9c846852..00000000 --- a/Source/SpiderEye.Mac/Interop/NSColor.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using SpiderEye.Mac.Native; -using SpiderEye.Tools; - -namespace SpiderEye.Mac.Interop -{ - internal static class NSColor - { - public static IntPtr FromHex(string? hex) - { - ColorTools.ParseHex(hex, out byte r, out byte g, out byte b); - - return AppKit.Call("NSColor", "colorWithRed:green:blue:alpha:", r / 255d, g / 255d, b / 255d, 1d); - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSDialog.cs b/Source/SpiderEye.Mac/Interop/NSDialog.cs deleted file mode 100644 index 70e0f687..00000000 --- a/Source/SpiderEye.Mac/Interop/NSDialog.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac.Interop -{ - internal class NSDialog : IDisposable - { - public readonly IntPtr Handle; - private readonly IntPtr window; - private readonly bool release; - - private delegate void Callback(IntPtr self, IntPtr result); - - private NSDialog(IntPtr handle, IntPtr window, bool release) - { - Handle = handle; - this.window = window; - this.release = release; - } - - public static NSDialog CreateAlert() - { - var alert = AppKit.Call("NSAlert", "new"); - return new NSDialog(alert, ObjC.Call(alert, "window"), true); - } - - public static NSDialog CreateSavePanel() - { - var panel = AppKit.Call("NSSavePanel", "savePanel"); - return new NSDialog(panel, panel, false); - } - - public static NSDialog CreateOpenPanel() - { - var panel = AppKit.Call("NSOpenPanel", "openPanel"); - return new NSDialog(panel, panel, false); - } - - public int Run(CocoaWindow? parent) - { - if (Handle == IntPtr.Zero) { throw new InvalidOperationException("Dialog is null"); } - - if (parent == null) { return ObjC.Call(Handle, "runModal").ToInt32(); } - - NSBlock? block = null; - block = new NSBlock((Callback)((s, r) => - { - ObjC.Call(MacApplication.Handle, "stopModalWithCode:", r); - block!.Dispose(); - })); - - ObjC.Call(Handle, "beginSheetModalForWindow:completionHandler:", parent.Handle, block.Handle); - return ObjC.Call(MacApplication.Handle, "runModalForWindow:", window).ToInt32(); - } - - public void Dispose() - { - if (release && Handle != IntPtr.Zero) { ObjC.Call(Handle, "release"); } - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSEventModifierFlags.cs b/Source/SpiderEye.Mac/Interop/NSEventModifierFlags.cs deleted file mode 100644 index eeaf2b17..00000000 --- a/Source/SpiderEye.Mac/Interop/NSEventModifierFlags.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace SpiderEye.Mac.Interop -{ - [Flags] - internal enum NSEventModifierFlags : ulong - { - None = 0, - Shift = 1 << 17, - Control = 1 << 18, - Option = 1 << 19, - Command = 1 << 20, - Function = 1 << 23, - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSImageScaling.cs b/Source/SpiderEye.Mac/Interop/NSImageScaling.cs deleted file mode 100644 index bba86218..00000000 --- a/Source/SpiderEye.Mac/Interop/NSImageScaling.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace SpiderEye.Mac.Interop -{ - /// - /// Image scaling types. - /// - internal enum NSImageScaling - { - /// - /// If it is too large for the destination, scale the image - /// down while preserving the aspect ratio. - /// - ProportionallyDown = 0, - - /// - /// Scale each dimension to exactly fit destination. - /// - AxesIndependently = 1, - - /// - /// Do not scale the image. - /// - None = 2, - - /// - /// Scale the image to its maximum possible dimensions while both - /// staying within the destination area and preserving its aspect ratio. - /// - ProportionallyUpOrDown = 3, - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSKey.cs b/Source/SpiderEye.Mac/Interop/NSKey.cs deleted file mode 100644 index 9deea089..00000000 --- a/Source/SpiderEye.Mac/Interop/NSKey.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace SpiderEye.Mac.Interop -{ - internal static class NSKey - { - public const string UpArrow = "\0xF700"; - public const string DownArrow = "\0xF701"; - public const string LeftArrow = "\0xF702"; - public const string RightArrow = "\0xF703"; - public const string F1 = "\0xF704"; - public const string F2 = "\0xF705"; - public const string F3 = "\0xF706"; - public const string F4 = "\0xF707"; - public const string F5 = "\0xF708"; - public const string F6 = "\0xF709"; - public const string F7 = "\0xF70A"; - public const string F8 = "\0xF70B"; - public const string F9 = "\0xF70C"; - public const string F10 = "\0xF70D"; - public const string F11 = "\0xF70E"; - public const string F12 = "\0xF70F"; - public const string F13 = "\0xF710"; - public const string F14 = "\0xF711"; - public const string F15 = "\0xF712"; - public const string F16 = "\0xF713"; - public const string F17 = "\0xF714"; - public const string F18 = "\0xF715"; - public const string F19 = "\0xF716"; - public const string F20 = "\0xF717"; - public const string F21 = "\0xF718"; - public const string F22 = "\0xF719"; - public const string F23 = "\0xF71A"; - public const string F24 = "\0xF71B"; - public const string F25 = "\0xF71C"; - public const string F26 = "\0xF71D"; - public const string F27 = "\0xF71E"; - public const string F28 = "\0xF71F"; - public const string F29 = "\0xF720"; - public const string F30 = "\0xF721"; - public const string F31 = "\0xF722"; - public const string F32 = "\0xF723"; - public const string F33 = "\0xF724"; - public const string F34 = "\0xF725"; - public const string F35 = "\0xF726"; - public const string Insert = "\0xF727"; - public const string Delete = "\0xF728"; - public const string Home = "\0xF729"; - public const string Begin = "\0xF72A"; - public const string End = "\0xF72B"; - public const string PageUp = "\0xF72C"; - public const string PageDown = "\0xF72D"; - public const string PrintScreen = "\0xF72E"; - public const string ScrollLock = "\0xF72F"; - public const string Pause = "\0xF730"; - public const string SysReq = "\0xF731"; - public const string Break = "\0xF732"; - public const string Reset = "\0xF733"; - public const string Stop = "\0xF734"; - public const string Menu = "\0xF735"; - public const string User = "\0xF736"; - public const string System = "\0xF737"; - public const string Print = "\0xF738"; - public const string ClearLine = "\0xF739"; - public const string ClearDisplay = "\0xF73A"; - public const string InsertLine = "\0xF73B"; - public const string DeleteLine = "\0xF73C"; - public const string InsertChar = "\0xF73D"; - public const string DeleteChar = "\0xF73E"; - public const string Prev = "\0xF73F"; - public const string Next = "\0xF740"; - public const string Select = "\0xF741"; - public const string Execute = "\0xF742"; - public const string Undo = "\0xF743"; - public const string Redo = "\0xF744"; - public const string Find = "\0xF745"; - public const string Help = "\0xF746"; - public const string ModeSwitch = "\0xF747"; - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSRunLoop.cs b/Source/SpiderEye.Mac/Interop/NSRunLoop.cs deleted file mode 100644 index cff84c73..00000000 --- a/Source/SpiderEye.Mac/Interop/NSRunLoop.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac.Interop -{ - internal sealed class NSRunLoop - { - private readonly IntPtr loop; - private readonly IntPtr mode; - private readonly IntPtr date; - private readonly IntPtr method; - - private volatile bool isCompleted; - - public NSRunLoop() - { - loop = Foundation.Call("NSRunLoop", "currentRunLoop"); - mode = ObjC.Call(loop, "currentMode"); - date = Foundation.Call("NSDate", "distantFuture"); - method = ObjC.RegisterName("runMode:beforeDate:"); - } - - public static void WaitForTask(Task task) - { - var loop = new NSRunLoop(); - while (!task.IsCompleted) { loop.Iterate(); } - } - - public static T WaitForTask(Task task) - { - WaitForTask((Task)task); - return task.Result; - } - - public void WaitForCompletion() - { - while (!isCompleted) { Iterate(); } - } - - public void Complete() - { - isCompleted = true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Iterate() - { - ObjC.SendMessage(loop, method, mode, date); - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSString.cs b/Source/SpiderEye.Mac/Interop/NSString.cs deleted file mode 100644 index 09596050..00000000 --- a/Source/SpiderEye.Mac/Interop/NSString.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Text; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac.Interop -{ - internal static class NSString - { - public static unsafe IntPtr Create(string? value) - { - if (value == null) { return IntPtr.Zero; } - - fixed (char* ptr = value) - { - return ObjC.SendMessage( - ObjC.GetClass("NSString"), - ObjC.RegisterName("stringWithCharacters:length:"), - (IntPtr)ptr, - new UIntPtr((uint)value.Length)); - } - } - - public static string? GetString(IntPtr handle) - { - if (handle == IntPtr.Zero) { return null; } - - IntPtr utf8 = ObjC.Call(handle, "UTF8String"); - return Utf8PointerToString(utf8); - } - - public static unsafe string? Utf8PointerToString(IntPtr utf8) - { - if (utf8 == IntPtr.Zero) { return null; } - - int count = 0; - byte* ptr = (byte*)utf8; - while (*ptr != 0) - { - count++; - ptr++; - } - - return Encoding.UTF8.GetString((byte*)utf8, count); - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/NSWindowStyleMask.cs b/Source/SpiderEye.Mac/Interop/NSWindowStyleMask.cs deleted file mode 100644 index d40f4f4e..00000000 --- a/Source/SpiderEye.Mac/Interop/NSWindowStyleMask.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace SpiderEye.Mac.Interop -{ - [Flags] - internal enum NSWindowStyleMask - { - Borderless = 0, - Titled = 1 << 0, - Closable = 1 << 1, - Miniaturizable = 1 << 2, - Resizable = 1 << 3, - UnifiedTitleAndToolbar = 1 << 12, - FullScreen = 1 << 14, - FullSizeContentView = 1 << 15, - } -} diff --git a/Source/SpiderEye.Mac/Interop/NativeClassDefinition.cs b/Source/SpiderEye.Mac/Interop/NativeClassDefinition.cs deleted file mode 100644 index 1aa100d7..00000000 --- a/Source/SpiderEye.Mac/Interop/NativeClassDefinition.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac.Interop -{ - internal class NativeClassDefinition - { - public IntPtr Handle { get; private set; } - - private readonly List callbacks; - private readonly IntPtr[] protocols; - - private bool registered; - private IntPtr ivar; - - private NativeClassDefinition(string name, IntPtr parent, IntPtr[] protocols) - { - if (name == null) { throw new ArgumentNullException(nameof(name)); } - - this.protocols = protocols ?? throw new ArgumentNullException(nameof(protocols)); - callbacks = new List(); - Handle = ObjC.AllocateClassPair(parent, name, IntPtr.Zero); - } - - public static NativeClassDefinition FromObject(string name, params IntPtr[] protocols) - { - return FromClass(name, ObjC.GetClass("NSObject"), protocols); - } - - public static NativeClassDefinition FromClass(string name, IntPtr parent, params IntPtr[] protocols) - { - return new NativeClassDefinition(name, parent, protocols); - } - - public void AddMethod(string name, string signature, T callback) - where T : Delegate - { - if (registered) { throw new InvalidOperationException("Native class is already declared and registered"); } - - // keep reference to callback or it will get garbage collected - callbacks.Add(callback); - - ObjC.AddMethod( - Handle, - ObjC.RegisterName(name), - callback, - signature); - } - - public void FinishDeclaration() - { - if (registered) { throw new InvalidOperationException("Native class is already declared and registered"); } - registered = true; - - // variable to hold reference to .NET object that creates an instance - const string variableName = "_SEInstance"; - ObjC.AddVariable(Handle, variableName, new IntPtr(IntPtr.Size), (byte)Math.Log(IntPtr.Size, 2), "@"); - ivar = ObjC.GetVariable(Handle, variableName); - - foreach (IntPtr protocol in protocols) - { - if (protocol == IntPtr.Zero) - { - // must not add null protocol, can cause runtime exception with conformsToProtocol check - continue; - } - - ObjC.AddProtocol(Handle, protocol); - } - - ObjC.RegisterClassPair(Handle); - } - - public NativeClassInstance CreateInstance(object parent) - { - if (!registered) { throw new InvalidOperationException("Native class is not yet fully declared and registered"); } - - IntPtr instance = ObjC.Call(Handle, "new"); - - var parentHandle = GCHandle.Alloc(parent, GCHandleType.Normal); - ObjC.SetVariableValue(instance, ivar, GCHandle.ToIntPtr(parentHandle)); - - return new NativeClassInstance(instance, parentHandle); - } - - public T GetParent(IntPtr self) - { - IntPtr handle = ObjC.GetVariableValue(self, ivar); - return (T)GCHandle.FromIntPtr(handle).Target!; - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/NativeClassInstance.cs b/Source/SpiderEye.Mac/Interop/NativeClassInstance.cs deleted file mode 100644 index 741dbe34..00000000 --- a/Source/SpiderEye.Mac/Interop/NativeClassInstance.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Interop -{ - internal sealed class NativeClassInstance : IDisposable - { - public IntPtr Handle { get; } - - private readonly GCHandle parentHandle; - - internal NativeClassInstance(IntPtr instance, GCHandle parentHandle) - { - Handle = instance; - this.parentHandle = parentHandle; - } - - ~NativeClassInstance() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - parentHandle.Free(); - } - } -} diff --git a/Source/SpiderEye.Mac/Interop/URL.cs b/Source/SpiderEye.Mac/Interop/URL.cs deleted file mode 100644 index fd0c04fc..00000000 --- a/Source/SpiderEye.Mac/Interop/URL.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using SpiderEye.Mac.Native; - -namespace SpiderEye.Mac.Interop -{ - internal static class URL - { - public static string? GetAsString(IntPtr handle) - { - if (handle == IntPtr.Zero) { return null; } - - return NSString.GetString(ObjC.Call(handle, "absoluteString")); - } - - public static Uri? GetAsUri(IntPtr handle) - { - if (handle == IntPtr.Zero) { return null; } - - return new Uri(GetAsString(handle)!); - } - } -} diff --git a/Source/SpiderEye.Mac/MacApplication.cs b/Source/SpiderEye.Mac/MacApplication.cs index afdfbdf2..59647ec1 100644 --- a/Source/SpiderEye.Mac/MacApplication.cs +++ b/Source/SpiderEye.Mac/MacApplication.cs @@ -1,7 +1,5 @@ -using System; -using System.Threading; -using SpiderEye.Mac.Native; -using SpiderEye.Tools; +using System.Threading; +using AppKit; namespace SpiderEye.Mac { @@ -19,15 +17,10 @@ public static Menu? AppMenu set { appMenu = value; - SetAppMenu(value); + NSApplication.SharedApplication.MainMenu = (CocoaMenu?)value?.NativeMenu!; } } - internal static IntPtr Handle - { - get { return app?.Handle ?? IntPtr.Zero; } - } - internal static SynchronizationContext SynchronizationContext { get { return app!.SynchronizationContext; } @@ -46,12 +39,6 @@ public static void Init() AppMenu = CreateDefaultMenu(); } - private static void SetAppMenu(Menu? menu) - { - var nativeMenu = NativeCast.To(menu?.NativeMenu); - ObjC.Call(Handle, "setMainMenu:", nativeMenu?.Handle ?? IntPtr.Zero); - } - private static Menu CreateDefaultMenu() { var menu = new Menu(); diff --git a/Source/SpiderEye.Mac/Menu/CocoaLabelMenuItem.cs b/Source/SpiderEye.Mac/Menu/CocoaLabelMenuItem.cs index 37075794..0bfd564d 100644 --- a/Source/SpiderEye.Mac/Menu/CocoaLabelMenuItem.cs +++ b/Source/SpiderEye.Mac/Menu/CocoaLabelMenuItem.cs @@ -1,6 +1,8 @@ using System; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; +using System.Collections.Generic; +using AppKit; +using ObjCRuntime; +using SpiderEye.Tools; namespace SpiderEye.Mac { @@ -12,77 +14,58 @@ public string? Label { get { - IntPtr title = ObjC.Call(Handle, "title"); - return NSString.GetString(title); + return Title; } set { - IntPtr title = NSString.Create(value); - ObjC.Call(Handle, "setTitle:", title); + Title = value ?? string.Empty; if (subMenu != null) { subMenu.Title = value; } } } - public bool Enabled - { - get { return ObjC.Call(Handle, "enabled") != IntPtr.Zero; } - set { ObjC.Call(Handle, "setEnabled:", value); } - } - - private static readonly NativeClassDefinition CallbackClassDefinition; - - private readonly NativeClassInstance? callbackClass; private CocoaSubMenu? subMenu; - static CocoaLabelMenuItem() - { - CallbackClassDefinition = CreateCallbackClass(); - } - public CocoaLabelMenuItem(string label) - : this(label, "menuCallback:") + : base() { - callbackClass = CallbackClassDefinition.CreateInstance(this); - SetTarget(callbackClass.Handle); + Title = label; + Activated += (sender, args) => Click?.Invoke(this, EventArgs.Empty); } public CocoaLabelMenuItem(string label, string action, string target) : this(label, action) { - SetTarget(ObjC.RegisterName(target)); + Target = Runtime.GetNSObject(Selector.GetHandle(target)); } public CocoaLabelMenuItem(string label, string action, string? target, long tag) : this(label, action) { - SetTarget(ObjC.RegisterName(target)); - SetTag(tag); + Target = target == null ? null : Runtime.GetNSObject(Selector.GetHandle(target)); + Tag = (nint)tag; } private CocoaLabelMenuItem(string label, string action) - : base(AppKit.Call("NSMenuItem", "alloc")) + : base() { - ObjC.Call( - Handle, - "initWithTitle:action:keyEquivalent:", - NSString.Create(label), - ObjC.RegisterName(action), - NSString.Create(string.Empty)); + Title = label; + Action = new Selector(action); + KeyEquivalent = string.Empty; } public void SetShortcut(ModifierKey modifier, Key key) { - NSEventModifierFlags nsModifier = KeyMapper.GetModifier(modifier); + NSEventModifierMask nsModifier = KeyMapper.GetModifier(modifier); string? mappedKey = KeyMapper.GetKey(key); if (mappedKey == null) { return; } - ObjC.Call(Handle, "setKeyEquivalentModifierMask:", new UIntPtr((ulong)nsModifier)); - ObjC.Call(Handle, "setKeyEquivalent:", NSString.Create(mappedKey)); + KeyEquivalentModifierMask = nsModifier; + KeyEquivalent = mappedKey; } public override IMenu CreateSubMenu() { - return subMenu = new CocoaSubMenu(Handle, Label); + return subMenu = new CocoaSubMenu(this, Label); } public CocoaMenu SetSubMenu(string label) @@ -90,37 +73,108 @@ public CocoaMenu SetSubMenu(string label) if (label == null) { throw new ArgumentNullException(nameof(label)); } if (subMenu != null) { throw new InvalidOperationException("Submenu is already created"); } - subMenu = new CocoaSubMenu(Handle, label, true); - + subMenu = new CocoaSubMenu(this, label, true); return subMenu.NativeMenu!; } - private static NativeClassDefinition CreateCallbackClass() + private static class KeyMapper { - var definition = NativeClassDefinition.FromObject("SpiderEyeMenuCallback"); + public static NSEventModifierMask GetModifier(ModifierKey key) + { + NSEventModifierMask result = 0ul; - definition.AddMethod( - "menuCallback:", - "v@:@", - (self, op, menu) => + foreach (var flag in EnumTools.GetFlags(key)) { - var instance = definition.GetParent(self); - instance.Click?.Invoke(instance, EventArgs.Empty); - }); - - definition.FinishDeclaration(); - - return definition; - } + switch (flag) + { + case ModifierKey.None: + continue; + + case ModifierKey.Shift: + result |= NSEventModifierMask.ShiftKeyMask; + break; + + case ModifierKey.Control: + result |= NSEventModifierMask.ControlKeyMask; + break; + + case ModifierKey.Alt: + result |= NSEventModifierMask.AlternateKeyMask; + break; + + case ModifierKey.Super: + case ModifierKey.Primary: + result |= NSEventModifierMask.CommandKeyMask; + break; + + default: + throw new NotSupportedException($"Unsupported modifier key: \"{flag}\""); + } + } + + return result; + } - private void SetTarget(IntPtr target) - { - ObjC.Call(Handle, "setTarget:", target); - } + public static string? GetKey(Key key) + { + if (Keymap.TryGetValue(key, out string? value)) { return value; } + else { throw new NotSupportedException($"Unsupported key: \"{key}\""); } + } - private void SetTag(long tag) - { - ObjC.Call(Handle, "setTag:", new IntPtr(tag)); + private static readonly Dictionary Keymap = new() + { + { Key.None, null }, + { Key.F1, "\0xF704" }, + { Key.F2, "\0xF705" }, + { Key.F3, "\0xF706" }, + { Key.F4, "\0xF707" }, + { Key.F5, "\0xF708" }, + { Key.F6, "\0xF709" }, + { Key.F7, "\0xF70A" }, + { Key.F8, "\0xF70B" }, + { Key.F9, "\0xF70C" }, + { Key.F10, "\0xF70D" }, + { Key.F11, "\0xF70E" }, + { Key.F12, "\0xF70F" }, + { Key.Number1, "1" }, + { Key.Number2, "2" }, + { Key.Number3, "3" }, + { Key.Number4, "4" }, + { Key.Number5, "5" }, + { Key.Number6, "6" }, + { Key.Number7, "7" }, + { Key.Number8, "8" }, + { Key.Number9, "9" }, + { Key.Number0, "0" }, + { Key.A, "a" }, + { Key.B, "b" }, + { Key.C, "c" }, + { Key.D, "d" }, + { Key.E, "e" }, + { Key.F, "f" }, + { Key.G, "g" }, + { Key.H, "h" }, + { Key.I, "i" }, + { Key.J, "j" }, + { Key.K, "k" }, + { Key.L, "l" }, + { Key.M, "m" }, + { Key.N, "n" }, + { Key.O, "o" }, + { Key.P, "p" }, + { Key.Q, "q" }, + { Key.R, "r" }, + { Key.S, "s" }, + { Key.T, "t" }, + { Key.U, "u" }, + { Key.V, "v" }, + { Key.W, "w" }, + { Key.X, "x" }, + { Key.Y, "y" }, + { Key.Z, "z" }, + { Key.Insert, "\0xF727" }, + { Key.Delete, "\0x007f" }, // Note: different than NSKey.Delete + }; } } } diff --git a/Source/SpiderEye.Mac/Menu/CocoaMenu.cs b/Source/SpiderEye.Mac/Menu/CocoaMenu.cs index 54680bd1..b4337fc1 100644 --- a/Source/SpiderEye.Mac/Menu/CocoaMenu.cs +++ b/Source/SpiderEye.Mac/Menu/CocoaMenu.cs @@ -1,54 +1,25 @@ using System; -using SpiderEye.Mac.Interop; -using SpiderEye.Mac.Native; -using SpiderEye.Tools; +using AppKit; namespace SpiderEye.Mac { - internal class CocoaMenu : IMenu + internal class CocoaMenu : NSMenu, IMenu { - public string? Title - { - get - { - IntPtr title = ObjC.Call(Handle, "title"); - return NSString.GetString(title); - } - set - { - IntPtr title = NSString.Create(value); - ObjC.Call(Handle, "setTitle:", title); - } - } - - public readonly IntPtr Handle; - public CocoaMenu() : this(null) { } public CocoaMenu(string? title) + : base(title ?? string.Empty) { - if (title != null) - { - Handle = AppKit.Call("NSMenu", "alloc"); - ObjC.Call(Handle, "initWithTitle:", NSString.Create(title)); - } - else { Handle = AppKit.Call("NSMenu", "new"); } } public void AddItem(IMenuItem item) { if (item == null) { throw new ArgumentNullException(nameof(item)); } - var nativeItem = NativeCast.To(item); - ObjC.Call(Handle, "addItem:", nativeItem.Handle); - } - - public void Dispose() - { - // don't think anything needs to be done here + AddItem((NSMenuItem)item); } } } diff --git a/Source/SpiderEye.Mac/Menu/CocoaMenuItem.cs b/Source/SpiderEye.Mac/Menu/CocoaMenuItem.cs index 2da85a59..9664cba4 100644 --- a/Source/SpiderEye.Mac/Menu/CocoaMenuItem.cs +++ b/Source/SpiderEye.Mac/Menu/CocoaMenuItem.cs @@ -1,90 +1,12 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using SpiderEye.Mac.Native; +using AppKit; namespace SpiderEye.Mac { - internal abstract class CocoaMenuItem : IMenuItem + internal abstract class CocoaMenuItem : NSMenuItem, IMenuItem { - public readonly IntPtr Handle; - - protected CocoaMenuItem(IntPtr handle) - { - Handle = handle; - } - public virtual IMenu CreateSubMenu() { - return new CocoaSubMenu(Handle); - } - - public void Dispose() - { - // don't think anything needs to be done here - } - - protected sealed class CocoaSubMenu : IMenu - { - public string? Title - { - get - { - if (menu == null) { return title; } - else { return menu.Title; } - } - set - { - if (menu == null) { title = value; } - else { menu.Title = value; } - } - } - - public CocoaMenu? NativeMenu - { - get { return menu; } - } - - private readonly IntPtr menuItem; - private string? title; - private CocoaMenu? menu; - - public CocoaSubMenu(IntPtr menuItem) - : this(menuItem, null) - { - } - - public CocoaSubMenu(IntPtr menuItem, string? title) - : this(menuItem, title, false) - { - } - - public CocoaSubMenu(IntPtr menuItem, string? title, bool createImmediately) - { - this.menuItem = menuItem; - this.title = title; - if (createImmediately) { SetNativeMenu(); } - } - - public void AddItem(IMenuItem item) - { - if (item == null) { throw new ArgumentNullException(nameof(item)); } - - if (menu == null) { SetNativeMenu(); } - - menu.AddItem(item); - } - - public void Dispose() - { - menu?.Dispose(); - } - - [MemberNotNull(nameof(menu))] - private void SetNativeMenu() - { - menu = new CocoaMenu(title); - ObjC.Call(menuItem, "setSubmenu:", menu.Handle); - } + return new CocoaSubMenu(this); } } } diff --git a/Source/SpiderEye.Mac/Menu/CocoaSeparatorMenuItem.cs b/Source/SpiderEye.Mac/Menu/CocoaSeparatorMenuItem.cs index 26653dd2..d914a551 100644 --- a/Source/SpiderEye.Mac/Menu/CocoaSeparatorMenuItem.cs +++ b/Source/SpiderEye.Mac/Menu/CocoaSeparatorMenuItem.cs @@ -1,12 +1,17 @@ -using SpiderEye.Mac.Native; +using AppKit; namespace SpiderEye.Mac { - internal class CocoaSeparatorMenuItem : CocoaMenuItem + internal class CocoaSeparatorMenuItem : NSMenuItem, IMenuItem { public CocoaSeparatorMenuItem() - : base(AppKit.Call("NSMenuItem", "separatorItem")) + : base(NSMenuItem.SeparatorItem.Handle) { } + + public IMenu CreateSubMenu() + { + return new CocoaSubMenu(this); + } } } diff --git a/Source/SpiderEye.Mac/Menu/CocoaSubMenu.cs b/Source/SpiderEye.Mac/Menu/CocoaSubMenu.cs new file mode 100644 index 00000000..6185ccf8 --- /dev/null +++ b/Source/SpiderEye.Mac/Menu/CocoaSubMenu.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using AppKit; + +namespace SpiderEye.Mac +{ + internal sealed class CocoaSubMenu : IMenu + { + public string? Title + { + get + { + if (menu == null) { return title; } + else { return menu.Title; } + } + set + { + if (menu == null) { title = value; } + else { menu.Title = value ?? string.Empty; } + } + } + + public CocoaMenu? NativeMenu + { + get { return menu; } + } + + private readonly NSMenuItem menuItem; + private string? title; + private CocoaMenu? menu; + + public CocoaSubMenu(NSMenuItem menuItem) + : this(menuItem, null) + { + } + + public CocoaSubMenu(NSMenuItem menuItem, string? title) + : this(menuItem, title, false) + { + } + + public CocoaSubMenu(NSMenuItem menuItem, string? title, bool createImmediately) + { + this.menuItem = menuItem; + this.title = title; + if (createImmediately) { SetNativeMenu(); } + } + + public void AddItem(IMenuItem item) + { + if (item == null) { throw new ArgumentNullException(nameof(item)); } + + if (menu == null) { SetNativeMenu(); } + + menu.AddItem(item); + } + + public void Dispose() + { + menu?.Dispose(); + } + + [MemberNotNull(nameof(menu))] + private void SetNativeMenu() + { + menu = new CocoaMenu(title); + menuItem.Submenu = menu; + } + } +} diff --git a/Source/SpiderEye.Mac/Menu/MenuExtensions.App.cs b/Source/SpiderEye.Mac/Menu/MenuExtensions.App.cs index 3f6053d0..eed5dfe4 100644 --- a/Source/SpiderEye.Mac/Menu/MenuExtensions.App.cs +++ b/Source/SpiderEye.Mac/Menu/MenuExtensions.App.cs @@ -1,5 +1,5 @@ using System; -using SpiderEye.Mac.Native; +using AppKit; namespace SpiderEye.Mac { @@ -32,7 +32,7 @@ public static MenuItem AddServicesMenu(this MenuItemCollection menuItems, string var menu = new LabelMenuItem(nativeMenu); menuItems.Add(menu); - ObjC.Call(MacApplication.Handle, "setServicesMenu:", submenu.Handle); + NSApplication.SharedApplication.ServicesMenu = submenu; return menu; } diff --git a/Source/SpiderEye.Mac/Native/AppKit.cs b/Source/SpiderEye.Mac/Native/AppKit.cs deleted file mode 100644 index c1f7537a..00000000 --- a/Source/SpiderEye.Mac/Native/AppKit.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Native -{ - internal static class AppKit - { - private const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit"; - - [DllImport(AppKitFramework, EntryPoint = "objc_getClass", CharSet = CharSet.Ansi)] - public static extern IntPtr GetClass(string name); - - [DllImport(AppKitFramework, EntryPoint = "objc_getProtocol", CharSet = CharSet.Ansi)] - public static extern IntPtr GetProtocol(string name); - - public static IntPtr Call(string id, string sel) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel)); - } - - public static IntPtr Call(string id, string sel, double a, double b, double c, double d) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a, b, c, d); - } - - public static IntPtr Call(string id, string sel, IntPtr a) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a); - } - } -} diff --git a/Source/SpiderEye.Mac/Native/Dispatch.cs b/Source/SpiderEye.Mac/Native/Dispatch.cs deleted file mode 100644 index cf9e18b4..00000000 --- a/Source/SpiderEye.Mac/Native/Dispatch.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using SpiderEye.Mac.Interop; - -namespace SpiderEye.Mac.Native -{ - internal static class Dispatch - { - public static IntPtr MainQueue - { - get { return MainQueueHandle; } - } - - private const string DispatchLib = "/usr/lib/system/libdispatch.dylib"; - private const string SystemLib = "/usr/lib/libSystem.dylib"; - - private static readonly IntPtr MainQueueHandle = IntPtr.Zero; - private static readonly IntPtr DispatchLibHandle = IntPtr.Zero; - - static Dispatch() - { - // dispatch_get_main_queue is not exposed (macro?) so we need to access the main queue field instead - DispatchLibHandle = LoadLibrary(DispatchLib, 0); - MainQueueHandle = GetSymbol(DispatchLibHandle, "_dispatch_main_q"); - } - - [DllImport(DispatchLib, EntryPoint = "dispatch_sync_f")] - public static extern void SyncFunction(IntPtr queue, IntPtr context, DispatchDelegate work); - - [DllImport(DispatchLib, EntryPoint = "dispatch_async_f")] - public static extern void AsyncFunction(IntPtr queue, IntPtr context, DispatchDelegate work); - - [DllImport(SystemLib, EntryPoint = "dlopen", CharSet = CharSet.Ansi)] - private static extern IntPtr LoadLibrary(string path, int mode); - - [DllImport(SystemLib, EntryPoint = "dlclose")] - private static extern int ReleaseLibrary(IntPtr handle); - - [DllImport(SystemLib, EntryPoint = "dlsym", CharSet = CharSet.Ansi)] - private static extern IntPtr GetSymbol(IntPtr handle, string symbol); - } -} diff --git a/Source/SpiderEye.Mac/Native/Foundation.cs b/Source/SpiderEye.Mac/Native/Foundation.cs deleted file mode 100644 index 41e5581f..00000000 --- a/Source/SpiderEye.Mac/Native/Foundation.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Native -{ - internal static class Foundation - { - private const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation"; - - [DllImport(FoundationFramework, EntryPoint = "objc_getClass", CharSet = CharSet.Ansi)] - public static extern IntPtr GetClass(string name); - - [DllImport(FoundationFramework, EntryPoint = "objc_getProtocol", CharSet = CharSet.Ansi)] - public static extern IntPtr GetProtocol(string name); - - public static IntPtr Call(string id, string sel) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel)); - } - - public static IntPtr Call(string id, string sel, IntPtr a) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a); - } - - public static IntPtr Call(string id, string sel, IntPtr a, IntPtr b) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a, b); - } - - public static IntPtr Call(string id, string sel, IntPtr a, IntPtr b, IntPtr c) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a, b, c); - } - - public static IntPtr Call(string id, string sel, IntPtr[] a, IntPtr b) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a, b); - } - - public static IntPtr Call(string id, string sel, bool a) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel), a); - } - } -} diff --git a/Source/SpiderEye.Mac/Native/ObjC.cs b/Source/SpiderEye.Mac/Native/ObjC.cs deleted file mode 100644 index 65a7a9e5..00000000 --- a/Source/SpiderEye.Mac/Native/ObjC.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using SpiderEye.Mac.Interop; - -namespace SpiderEye.Mac.Native -{ - internal static class ObjC - { - private const string ObjCLib = "/usr/lib/libobjc.dylib"; - - [DllImport(ObjCLib, EntryPoint = "objc_getClass", CharSet = CharSet.Ansi)] - public static extern IntPtr GetClass(string name); - - [DllImport(ObjCLib, EntryPoint = "objc_allocateClassPair", CharSet = CharSet.Ansi)] - public static extern IntPtr AllocateClassPair(IntPtr superclass, string name, IntPtr extraBytes); - - [DllImport(ObjCLib, EntryPoint = "objc_registerClassPair")] - public static extern void RegisterClassPair(IntPtr cls); - - [DllImport(ObjCLib, EntryPoint = "class_addProtocol")] - public static extern bool AddProtocol(IntPtr cls, IntPtr protocol); - - [DllImport(ObjCLib, EntryPoint = "objc_getProtocol", CharSet = CharSet.Ansi)] - public static extern IntPtr GetProtocol(string name); - - [DllImport(ObjCLib, EntryPoint = "class_addMethod", CharSet = CharSet.Ansi)] - public static extern bool AddMethod(IntPtr cls, IntPtr name, Delegate imp, string types); - - [DllImport(ObjCLib, EntryPoint = "class_addIvar", CharSet = CharSet.Ansi)] - public static extern bool AddVariable(IntPtr cls, string name, IntPtr size, byte alignment, string types); - - [DllImport(ObjCLib, EntryPoint = "class_getInstanceVariable", CharSet = CharSet.Ansi)] - public static extern IntPtr GetVariable(IntPtr cls, string name); - - [DllImport(ObjCLib, EntryPoint = "object_getIvar")] - public static extern IntPtr GetVariableValue(IntPtr obj, IntPtr ivar); - - [DllImport(ObjCLib, EntryPoint = "object_setIvar")] - public static extern IntPtr SetVariableValue(IntPtr obj, IntPtr ivar, IntPtr value); - - [DllImport(ObjCLib, EntryPoint = "sel_registerName", CharSet = CharSet.Ansi)] - public static extern IntPtr RegisterName(string? name); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, IntPtr a); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, UIntPtr a); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, IntPtr a, IntPtr b); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, IntPtr a, IntPtr b, IntPtr c); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, IntPtr a, IntPtr b, IntPtr c, IntPtr d); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, IntPtr a, UIntPtr b); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, IntPtr[] a, IntPtr b); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, [MarshalAs(UnmanagedType.I1)]bool value); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, double value); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, double a, double b, double c, double d); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, CGRect rect, IntPtr a); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, CGRect rect, UIntPtr a, UIntPtr b, [MarshalAs(UnmanagedType.I1)]bool c); - - [DllImport(ObjCLib, EntryPoint = "objc_msgSend")] - public static extern IntPtr SendMessage(IntPtr self, IntPtr op, CGSize size); - - public static IntPtr Call(IntPtr id, string sel) - { - return SendMessage(id, RegisterName(sel)); - } - - public static IntPtr Call(IntPtr id, string sel, IntPtr a) - { - return SendMessage(id, RegisterName(sel), a); - } - - public static IntPtr Call(IntPtr id, string sel, UIntPtr a) - { - return SendMessage(id, RegisterName(sel), a); - } - - public static IntPtr Call(IntPtr id, string sel, double a) - { - return SendMessage(id, RegisterName(sel), a); - } - - public static IntPtr Call(IntPtr id, string sel, bool a) - { - return SendMessage(id, RegisterName(sel), a); - } - - public static IntPtr Call(IntPtr id, string sel, IntPtr a, IntPtr b) - { - return SendMessage(id, RegisterName(sel), a, b); - } - - public static IntPtr Call(IntPtr id, string sel, IntPtr a, IntPtr b, IntPtr c) - { - return SendMessage(id, RegisterName(sel), a, b, c); - } - - public static IntPtr Call(IntPtr id, string sel, IntPtr a, IntPtr b, IntPtr c, IntPtr d) - { - return SendMessage(id, RegisterName(sel), a, b, c, d); - } - - public static IntPtr Call(IntPtr id, string sel, CGRect rect, IntPtr a) - { - return SendMessage(id, RegisterName(sel), rect, a); - } - - public static IntPtr Call(IntPtr id, string sel, CGSize size) - { - return SendMessage(id, RegisterName(sel), size); - } - } -} diff --git a/Source/SpiderEye.Mac/Native/Webkit.cs b/Source/SpiderEye.Mac/Native/Webkit.cs deleted file mode 100644 index 5b2334de..00000000 --- a/Source/SpiderEye.Mac/Native/Webkit.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SpiderEye.Mac.Native -{ - internal static class WebKit - { - private const string WebKitFramework = "/System/Library/Frameworks/WebKit.framework/WebKit"; - - [DllImport(WebKitFramework, EntryPoint = "objc_getClass", CharSet = CharSet.Ansi)] - public static extern IntPtr GetClass(string name); - - [DllImport(WebKitFramework, EntryPoint = "objc_getProtocol", CharSet = CharSet.Ansi)] - public static extern IntPtr GetProtocol(string name); - - public static IntPtr Call(string id, string sel) - { - return ObjC.SendMessage(GetClass(id), ObjC.RegisterName(sel)); - } - } -} diff --git a/Source/SpiderEye.Mac/SpiderEye.Mac.csproj b/Source/SpiderEye.Mac/SpiderEye.Mac.csproj index a2127dcf..f84ce8b0 100644 --- a/Source/SpiderEye.Mac/SpiderEye.Mac.csproj +++ b/Source/SpiderEye.Mac/SpiderEye.Mac.csproj @@ -1,7 +1,7 @@ - + - net6.0 + net6.0-macos10.14 From d6d9439c7ebeabc86b12e4d83f02813142a36cd3 Mon Sep 17 00:00:00 2001 From: Christopher-Marcel Esser Date: Mon, 9 Jan 2023 10:58:48 +0100 Subject: [PATCH 2/4] Fix redirect from HTTP(S) to SpiderEye scheme on macOS. --- Source/SpiderEye.Mac/CocoaWebview.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Source/SpiderEye.Mac/CocoaWebview.cs b/Source/SpiderEye.Mac/CocoaWebview.cs index 67306eae..528ca1c4 100644 --- a/Source/SpiderEye.Mac/CocoaWebview.cs +++ b/Source/SpiderEye.Mac/CocoaWebview.cs @@ -139,6 +139,16 @@ public override void DidFailNavigation(WKWebView webView, WKNavigation navigatio var cocoaWebView = (CocoaWebview)webView; cocoaWebView.PageLoaded?.Invoke(cocoaWebView, new PageLoadEventArgs(cocoaWebView.Uri, false)); } + + public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error) + { + if (error.UserInfo["NSErrorFailingURLKey"] is NSUrl url + && url.Scheme == SCHEME) + { + var cocoaWebView = (CocoaWebview)webView; + cocoaWebView.LoadUri(url); + } + } } private class CocoaScriptMessageHandler : WKScriptMessageHandler From a575d4eddb44bc436608f588cdcde166b7950409 Mon Sep 17 00:00:00 2001 From: Christopher-Marcel Esser Date: Mon, 9 Jan 2023 12:46:41 +0100 Subject: [PATCH 3/4] Fix warning when opening dev tools on macOS. --- Source/SpiderEye.Mac/CocoaWindow.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/SpiderEye.Mac/CocoaWindow.cs b/Source/SpiderEye.Mac/CocoaWindow.cs index ed382c41..aa292857 100644 --- a/Source/SpiderEye.Mac/CocoaWindow.cs +++ b/Source/SpiderEye.Mac/CocoaWindow.cs @@ -142,7 +142,14 @@ public CocoaWindow(WindowConfiguration config, WebviewBridge bridge) StyleMask = GetWantedStyleMask(StyleMask, borderStyleField, canResizeField); webview = new CocoaWebview(bridge); - ContentView = webview; + + // The dev tools view will be added as a subview of the content view. That leads to a warning at runtime + // when opening the dev tools if we don't set a parent view as the content view. + using var parentView = new NSView() { AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.HeightSizable }; + parentView.AddSubview(webview); + webview.AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.HeightSizable; + ContentView = parentView; + MakeFirstResponder(webview); webview.TitleChanged += Webview_TitleChanged; From 599f16d8721ea5051d063ea8f8568111c8ea54fd Mon Sep 17 00:00:00 2001 From: Christopher-Marcel Esser Date: Tue, 10 Jan 2023 12:50:42 +0100 Subject: [PATCH 4/4] Fix redirect on macOS in remaining scenarios. --- Source/SpiderEye.Mac/CocoaWebview.cs | 30 ++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/Source/SpiderEye.Mac/CocoaWebview.cs b/Source/SpiderEye.Mac/CocoaWebview.cs index 528ca1c4..1fbf6237 100644 --- a/Source/SpiderEye.Mac/CocoaWebview.cs +++ b/Source/SpiderEye.Mac/CocoaWebview.cs @@ -142,11 +142,37 @@ public override void DidFailNavigation(WKWebView webView, WKNavigation navigatio public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error) { - if (error.UserInfo["NSErrorFailingURLKey"] is NSUrl url + // `WKWebView` doesn't allow redirects from HTTP(S) to non-HTTP(S) schemes. That's because it's + // disallowed in the Fetch standard. See https://developer.apple.com/forums/thread/681530 for more + // information. Additionally, WebKit doesn't offer any API to handle the redirect properly ourselves. + // Instead, the page breaks - the document's location points to the redirect URL even though that page + // wasn't loaded. And worst of all, the `WKUrlSchemeHandler` isn't called. See + // https://bugs.webkit.org/show_bug.cgi?id=173730, especially comment 21. + if (error.LocalizedDescription.Equals("Redirection to URL with a scheme that is not HTTP(S)", StringComparison.OrdinalIgnoreCase) + && error.UserInfo["NSErrorFailingURLKey"] is NSUrl url && url.Scheme == SCHEME) { + // Simply loading the URL works - sometimes. Sometimes, the web view updates the document location + // but doesn't load the content, never calling the `WKUrlSchemeHandler`. To work around that issue, + // we first switch to another page (`about:blank`), then back to the URL we wanted to get to. var cocoaWebView = (CocoaWebview)webView; - cocoaWebView.LoadUri(url); + + // Ensure the window title doesn't change to a blank string (for `about:blank`) for a split second, + // then back to the actual page's title. + var usedBrowserTitle = cocoaWebView.UseBrowserTitle; + cocoaWebView.UseBrowserTitle = false; + + webView.EvaluateJavaScript( + "window.location.href=\"about:blank\"", + (_, _) => + // Since loading is async, we need to load the actual URL afterward, which would require + // state management to keep track of when to do that. So instead, we can request the loading + // of that URL after kicking off the other page load, simply via `Task.Run`. + Task.Run(() => + { + cocoaWebView.UseBrowserTitle = usedBrowserTitle; + cocoaWebView.LoadUri(url); + })); } } }