diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSApplicationViewExtension.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSApplicationViewExtension.cs index c00e606f5ff5..7ad1db5401db 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacOSApplicationViewExtension.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSApplicationViewExtension.cs @@ -15,10 +15,6 @@ private MacOSApplicationViewExtension() public static void Register() => ApiExtensibility.Register(typeof(IApplicationViewExtension), _ => _instance); - public void ExitFullScreenMode() => NativeUno.uno_application_exit_full_screen(); - - public bool TryEnterFullScreenMode() => NativeUno.uno_application_enter_full_screen(); - public bool TryResizeView(Size size) { var main = NativeUno.uno_app_get_main_window(); diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOverlappedPresenter.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOverlappedPresenter.cs new file mode 100644 index 000000000000..7bcdaf27e79d --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOverlappedPresenter.cs @@ -0,0 +1,47 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Windowing.Native; + +using Uno.Foundation.Logging; + +namespace Uno.UI.Runtime.Skia.MacOS; + +internal class MacOSNativeOverlappedPresenter : INativeOverlappedPresenter +{ + private nint _handle; + + public MacOSNativeOverlappedPresenter(MacOSWindowNative window) + { + _handle = window.Handle; + } + + public OverlappedPresenterState State => (OverlappedPresenterState)NativeUno.uno_window_get_overlapped_presenter_state(_handle); + + public void Maximize() => NativeUno.uno_window_maximize(_handle); + + public void Minimize(bool activateWindow) => NativeUno.uno_window_minimize(_handle, activateWindow); + + public void Restore(bool activateWindow) => NativeUno.uno_window_restore(_handle, activateWindow); + + public void SetBorderAndTitleBar(bool hasBorder, bool hasTitleBar) => NativeUno.uno_window_set_border_and_title_bar(_handle, hasBorder, hasTitleBar); + + public void SetIsAlwaysOnTop(bool isAlwaysOnTop) => NativeUno.uno_window_set_always_on_top(_handle, isAlwaysOnTop); + + public void SetIsMaximizable(bool isMaximizable) => NativeUno.uno_window_set_maximizable(_handle, isMaximizable); + + public void SetIsMinimizable(bool isMinimizable) => NativeUno.uno_window_set_minimizable(_handle, isMinimizable); + + public void SetIsModal(bool isModal) + { + // we cannot set `modalPanel`, it's a readonly property, subclasses of NSPanel are modal, NSWindow are not + // so we warn if we try to set a value that does not match the native window + if (!NativeUno.uno_window_set_modal(_handle, isModal)) + { + if (this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().Warn($"Cannot change NSWindow {_handle} `modalPanel` after creation (readonly)."); + } + } + } + + public void SetIsResizable(bool isResizable) => NativeUno.uno_window_set_resizable(_handle, isResizable); +} diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs index 77e547645bd5..9ae553008e9b 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs @@ -84,7 +84,7 @@ private void MetalDraw(double nativeWidth, double nativeHeight, nint texture) { if (this.Log().IsEnabled(LogLevel.Trace)) { - this.Log().Trace($"Window {_nativeWindow.Handle} drawing {nativeWidth}x{nativeHeight} texture: {texture} FullScreen: {NativeUno.uno_application_is_full_screen()}"); + this.Log().Trace($"Window {_nativeWindow.Handle} drawing {nativeWidth}x{nativeHeight} texture: {texture}"); } var scale = (float)_displayInformation.RawPixelsPerViewPixel; @@ -116,7 +116,7 @@ private unsafe void SoftDraw(double nativeWidth, double nativeHeight, nint* data { if (this.Log().IsEnabled(LogLevel.Trace)) { - this.Log().Trace($"Window {_nativeWindow.Handle} drawing {nativeWidth}x{nativeHeight} FullScreen: {NativeUno.uno_application_is_full_screen()}"); + this.Log().Trace($"Window {_nativeWindow.Handle} drawing {nativeWidth}x{nativeHeight}"); } var scale = (float)_displayInformation.RawPixelsPerViewPixel; diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowWrapper.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowWrapper.cs index fe41d5ddd511..4d51aa4c23ce 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowWrapper.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowWrapper.cs @@ -1,8 +1,10 @@ using System.ComponentModel; +using Microsoft.UI.Windowing; using Windows.Foundation; using Windows.UI.Core.Preview; +using Uno.Disposables; using Uno.UI.Xaml.Controls; using WinUIApplication = Microsoft.UI.Xaml.Application; @@ -24,6 +26,11 @@ public MacOSWindowWrapper(MacOSWindowNative window) public override object NativeWindow => _window; + public override string Title { + get => NativeUno.uno_window_get_title(_window.Handle); + set => NativeUno.uno_window_set_title(_window.Handle, value); + } + private void OnHostSizeChanged(object? sender, Size size) { Bounds = new Rect(default, size); @@ -53,4 +60,16 @@ private void OnWindowClosing(object? sender, CancelEventArgs e) } private void OnWindowClosed(object? sender, EventArgs e) => RaiseClosed(); + + protected override IDisposable ApplyFullScreenPresenter() + { + NativeUno.uno_window_enter_full_screen(_window.Handle); + return Disposable.Create(() => NativeUno.uno_window_exit_full_screen(_window.Handle)); + } + + protected override IDisposable ApplyOverlappedPresenter(OverlappedPresenter presenter) + { + presenter.SetNative(new MacOSNativeOverlappedPresenter(_window)); + return Disposable.Create(() => presenter.SetNative(null)); + } } diff --git a/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs b/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs index 985c3f429eb4..2e10339029a5 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs @@ -204,6 +204,9 @@ internal static unsafe partial void uno_set_window_events_callbacks( [LibraryImport("libUnoNativeMac.dylib")] internal static partial void uno_window_invalidate(nint window); + [LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)] + internal static partial string uno_window_get_title(nint window); + [LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)] internal static partial nint uno_window_set_title(nint window, string title); @@ -214,14 +217,41 @@ internal static unsafe partial void uno_set_window_close_callbacks( [LibraryImport("libUnoNativeMac.dylib")] [return: MarshalAs(UnmanagedType.I1)] - internal static partial bool uno_application_is_full_screen(); + internal static partial bool uno_window_enter_full_screen(nint window); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_exit_full_screen(nint window); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_maximize(nint window); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_minimize(nint window, [MarshalAs(UnmanagedType.I1)] bool activateWindow); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_restore(nint window, [MarshalAs(UnmanagedType.I1)] bool activateWindow); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial int uno_window_get_overlapped_presenter_state(nint window); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_set_always_on_top(nint window, [MarshalAs(UnmanagedType.I1)] bool isAlwaysOnTop); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_set_border_and_title_bar(nint window, [MarshalAs(UnmanagedType.I1)] bool hasBorder, [MarshalAs(UnmanagedType.I1)] bool hasTitleBar); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_set_maximizable(nint window, [MarshalAs(UnmanagedType.I1)] bool isMaximizable); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_window_set_minimizable(nint window, [MarshalAs(UnmanagedType.I1)] bool isMinimizable); [LibraryImport("libUnoNativeMac.dylib")] [return: MarshalAs(UnmanagedType.I1)] - internal static partial bool uno_application_enter_full_screen(); + internal static partial bool uno_window_set_modal(nint window, [MarshalAs(UnmanagedType.I1)] bool isModal); [LibraryImport("libUnoNativeMac.dylib")] - internal static partial void uno_application_exit_full_screen(); + internal static partial void uno_window_set_resizable(nint window, [MarshalAs(UnmanagedType.I1)] bool isResizable); [LibraryImport("libUnoNativeMac.dylib")] internal static partial nint uno_window_get_metal_context(nint window); diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.h b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.h index 2f5b72f1c4c6..46f8832eb003 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.h +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.h @@ -23,11 +23,6 @@ void uno_application_set_icon(const char *path); bool uno_application_open_url(const char *url); bool uno_application_query_url_support(const char *url); -bool uno_application_enter_full_screen(void); -void uno_application_exit_full_screen(void); - -bool uno_application_is_full_screen(void); - typedef bool (*application_can_exit_fn_ptr)(void); application_can_exit_fn_ptr uno_get_application_can_exit_callback(void); void uno_set_application_can_exit_callback(application_can_exit_fn_ptr p); diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.m b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.m index 1542af9e5103..3a1adbc2eedc 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.m +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOApplication.m @@ -90,45 +90,6 @@ bool uno_application_query_url_support(const char *url) return [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:u] != nil; } -bool uno_application_is_full_screen(void) -{ - NSWindow *win = [[NSApplication sharedApplication] keyWindow]; - // keyWindow might not be set, yet - so we return false - bool result = win; - if (result) { - result = (win.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen; - } -#if DEBUG - NSLog(@"uno_application_is_fullscreen %@ %s", win, result ? "true" : "false"); -#endif - return result; -} - -bool uno_application_enter_full_screen(void) -{ - NSWindow *win = [[NSApplication sharedApplication] keyWindow]; - bool result = win; - if (result && (win.styleMask & NSWindowStyleMaskFullScreen) != NSWindowStyleMaskFullScreen) { - [win toggleFullScreen:nil]; - result = true; - } -#if DEBUG - NSLog(@"uno_application_enter_fullscreen %@ %s", win, result ? "true" : "false"); -#endif - return result; -} - -void uno_application_exit_full_screen(void) -{ - NSWindow *win = [[NSApplication sharedApplication] keyWindow]; - if (win && (win.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { - [win toggleFullScreen:nil]; - } -#if DEBUG - NSLog(@"uno_application_exit_fullscreen %@", win); -#endif -} - static application_can_exit_fn_ptr application_can_exit; inline application_can_exit_fn_ptr uno_get_application_can_exit_callback(void) diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h index 202ca1611949..057a94cfbb2b 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h @@ -31,6 +31,7 @@ void uno_set_resize_callback(resize_fn_ptr p); - (void)sendEvent:(NSEvent *)event; +- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame; - (bool)windowShouldClose:(NSWindow *)sender; - (void)windowWillClose:(NSNotification *)notification; @@ -41,8 +42,32 @@ NSWindow* uno_app_get_main_window(void); NSWindow* uno_window_create(double width, double height); void uno_window_invalidate(NSWindow *window); bool uno_window_resize(NSWindow *window, double width, double height); + +char* uno_window_get_title(NSWindow *window); void uno_window_set_title(NSWindow *window, const char* title); +bool uno_window_is_full_screen(NSWindow *window); +bool uno_window_enter_full_screen(NSWindow *window); +void uno_window_exit_full_screen(NSWindow *window); + +void uno_window_minimize(NSWindow *window, bool activateWindow); +void uno_window_restore(NSWindow *window, bool activateWindow); + + +typedef NS_ENUM(sint32, OverlappedPresenterState) { + OverlappedPresenterStateMaximized, + OverlappedPresenterStateMinimized, + OverlappedPresenterStateRestored, +}; +OverlappedPresenterState uno_window_get_overlapped_presenter_state(NSWindow *window); + +void uno_window_set_always_on_top(NSWindow* window, bool isAlwaysOnTop); +void uno_window_set_border_and_title_bar(NSWindow *window, bool hasBorder, bool hasTitleBar); +void uno_window_set_maximizable(NSWindow* window, bool isMaximizable); +void uno_window_set_minimizable(NSWindow* window, bool isMinimizable); +bool uno_window_set_modal(NSWindow *window, bool isModal); +void uno_window_set_resizable(NSWindow *window, bool isResizable); + // https://learn.microsoft.com/en-us/uwp/api/windows.system.virtualkey?view=winrt-22621 typedef NS_ENUM(sint32, VirtualKey) { VirtualKeyNone = 0, diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m index cab690caac5b..7cf7c23f14c6 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m @@ -165,11 +165,185 @@ void uno_window_set_min_size(NSWindow *window, double width, double height) window.minSize = CGSizeMake(width, height); } +char* uno_window_get_title(NSWindow *window) +{ + return strdup(window.title.UTF8String); +} + void uno_window_set_title(NSWindow *window, const char* title) { window.title = [NSString stringWithUTF8String:title]; } +bool uno_window_is_full_screen(NSWindow *window) +{ + bool result = window != nil; + if (result) { + result = (window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen; + } +#if DEBUG + NSLog(@"uno_window_is_full_screen %@ %s", window, result ? "true" : "false"); +#endif + return result; +} + +bool uno_window_enter_full_screen(NSWindow *window) +{ + bool result = window != nil; + if (result && (window.styleMask & NSWindowStyleMaskFullScreen) != NSWindowStyleMaskFullScreen) { + [window toggleFullScreen:nil]; + result = true; + } +#if DEBUG + NSLog(@"uno_window_enter_full_screen %@ %s", window, result ? "true" : "false"); +#endif + return result; +} + +void uno_window_exit_full_screen(NSWindow *window) +{ + if (window && (window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { + [window toggleFullScreen:nil]; + } +#if DEBUG + NSLog(@"uno_window_exit_full_screen %@", window); +#endif +} + +// on macOS double-clicking on the titlebar maximize the window (not the green icon) +void uno_window_maximize(NSWindow *window) +{ +#if DEBUG + NSLog(@"uno_window_maximize %@", window); +#endif + [window performZoom:nil]; +} + +void uno_window_minimize(NSWindow *window, bool activateWindow) +{ +#if DEBUG + NSLog(@"uno_window_minimize %@ %s", window, activateWindow ? "true" : "false"); +#endif + [window miniaturize:nil]; + if (activateWindow) { + [window makeMainWindow]; + } +} + +void uno_window_restore(NSWindow *window, bool activateWindow) +{ +#if DEBUG + NSLog(@"uno_window_restore %@ %s", window, activateWindow ? "true" : "false"); +#endif + switch(uno_window_get_overlapped_presenter_state(window)) { + case OverlappedPresenterStateMaximized: + [window zoom:nil]; + break; + case OverlappedPresenterStateMinimized: + [window deminiaturize:nil]; + break; + default: + break; + } + if (activateWindow) { + [window makeMainWindow]; + } +} + +OverlappedPresenterState uno_window_get_overlapped_presenter_state(NSWindow *window) +{ + if (window.isZoomed) { + return OverlappedPresenterStateMaximized; + } else if (window.isMiniaturized) { + return OverlappedPresenterStateMinimized; + } else { + return OverlappedPresenterStateRestored; + } +} + +void uno_window_set_always_on_top(NSWindow* window, bool isAlwaysOnTop) +{ + NSWindowLevel level = window.level; + if (isAlwaysOnTop) { + level = NSStatusWindowLevel; + } else { + level = NSNormalWindowLevel; + } +#if DEBUG + NSLog(@"uno_window_set_always_on_top %@ 0x%x %s 0x%x", window, (uint)level, isAlwaysOnTop ? "true" : "false", (uint)level); +#endif + window.level = level; +} + +void uno_window_set_border_and_title_bar(NSWindow *window, bool hasBorder, bool hasTitleBar) +{ + NSWindowStyleMask style = window.styleMask; + if (!hasBorder) + style |= NSWindowStyleMaskBorderless; + else + style ^= NSWindowStyleMaskBorderless; + if (hasTitleBar) + style |= NSWindowStyleMaskTitled; + else + style ^= NSWindowStyleMaskTitled; +#if DEBUG + NSLog(@"uno_window_set_border_and_title_bar %@ 0x%x hasBorder %s hasTitleBar %s 0x%x", window, (uint)window.styleMask, + hasBorder ? "true" : "false", hasTitleBar ? "true" : "false", (uint)style); +#endif + window.styleMask = style; +} + +void uno_window_set_maximizable(NSWindow* window, bool isMaximizable) +{ +#if DEBUG + NSWindowCollectionBehavior cb = window.collectionBehavior; +#endif + // unlike Windows on macOS the (green) maximizable button is for full screen, not zoomed + // `windowShouldZoom:toFrame:` will check the collectionBehavior to [dis]allow zooming + if (isMaximizable) { + [window setCollectionBehavior:NSWindowCollectionBehaviorDefault]; + [[window standardWindowButton:NSWindowZoomButton] setEnabled:YES]; + } else { + [window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorFullScreenDisallowsTiling]; + [[window standardWindowButton:NSWindowZoomButton] setEnabled:NO]; + } +#if DEBUG + NSLog(@"uno_window_set_maximizable %@ 0x%x %s 0x%x", window, (uint)cb, isMaximizable ? "true" : "false", (uint)window.collectionBehavior); +#endif +} + +void uno_window_set_minimizable(NSWindow* window, bool isMinimizable) +{ + NSWindowStyleMask style = window.styleMask; + if (isMinimizable) + style |= NSWindowStyleMaskMiniaturizable; + else + style ^= NSWindowStyleMaskMiniaturizable; +#if DEBUG + NSLog(@"uno_window_set_minimizable %@ 0x%x %s 0x%x", window, (uint)window.styleMask, isMinimizable ? "true" : "false", (uint)style); +#endif + window.styleMask = style; +} + +bool uno_window_set_modal(NSWindow *window, bool isModal) +{ + // this is a read-only property so we simply log if we can't change it o the requested value + return isModal == window.isModalPanel; +} + +void uno_window_set_resizable(NSWindow *window, bool isResizable) +{ + NSWindowStyleMask style = window.styleMask; + if (isResizable) + style |= NSWindowStyleMaskResizable; + else + style ^= NSWindowStyleMaskResizable; +#if DEBUG + NSLog(@"uno_window_set_resizable %@ 0x%x %s 0x%x", window, (uint)window.styleMask, isResizable ? "true" : "false", (uint)style); +#endif + window.styleMask = style; +} + inline window_did_change_screen_fn_ptr uno_get_window_did_change_screen_callback(void) { return window_did_change_screen; @@ -571,6 +745,11 @@ - (void)sendEvent:(NSEvent *)event { } } +- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame { + // if we disable the (green) maximize button then we don't allow zooming + return window.collectionBehavior != (NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorFullScreenDisallowsTiling); +} + - (bool)windowShouldClose:(NSWindow *)sender { // see `ISystemNavigationManagerPreviewExtension`