diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index d3b7b4ede6..23abf1d53f 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -223,6 +223,19 @@ } } +// From chromium: +// +// > The delegate or the window class should implement this method so that +// > -[NSWindow isZoomed] can be then determined by whether or not the current +// > window frame is equal to the zoomed frame. +// +// If we don't implement this, then isZoomed always returns true for a non- +// resizable window ¯\_(ツ)_/¯ +- (NSRect)windowWillUseStandardFrame:(NSWindow*)window + defaultFrame:(NSRect)newFrame { + return newFrame; +} + -(BOOL)canBecomeKeyWindow { if(_canBecomeKeyWindow) @@ -261,10 +274,6 @@ -(void) setEnabled:(bool)enable { _isEnabled = enable; - - [[self standardWindowButton:NSWindowCloseButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowZoomButton] setEnabled:enable]; } -(void)becomeKeyWindow diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index 9820a9f052..972d03d08c 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -29,7 +29,7 @@ private: [Window setLevel:NSPopUpMenuWindowLevel]; } protected: - virtual NSWindowStyleMask GetStyle() override + virtual NSWindowStyleMask CalculateStyleMask() override { return NSWindowStyleMaskBorderless; } diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..93decef136 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -105,9 +105,8 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); protected: - virtual NSWindowStyleMask GetStyle(); - - void UpdateStyle(); + virtual NSWindowStyleMask CalculateStyleMask() = 0; + virtual void UpdateStyle(); private: void CreateNSWindow (bool isDialog); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..59102e15a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, lastSize = NSSize { 100, 100 }; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - lastMenu = nullptr; CreateNSWindow(usePanel); [Window setContentView:StandardContainer]; - [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - [Window setContentMinSize:lastMinSize]; [Window setContentMaxSize:lastMaxSize]; - [Window setOpaque:false]; } @@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() { return false; } -NSWindowStyleMask WindowBaseImpl::GetStyle() { - return NSWindowStyleMaskBorderless; -} - void WindowBaseImpl::UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask:CalculateStyleMask()]; } void WindowBaseImpl::CleanNSWindow() { @@ -580,21 +572,12 @@ void WindowBaseImpl::CleanNSWindow() { } } -void WindowBaseImpl::CreateNSWindow(bool isDialog) { - if (isDialog) { - if (![Window isKindOfClass:[AvnPanel class]]) { - CleanNSWindow(); - - Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - - [Window setHidesOnDeactivate:false]; - } +void WindowBaseImpl::CreateNSWindow(bool usePanel) { + if (usePanel) { + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + [Window setHidesOnDeactivate:false]; } else { - if (![Window isKindOfClass:[AvnWindow class]]) { - CleanNSWindow(); - - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - } + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; } } diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 3861aaf170..29bb659039 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP() WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); - void HideOrShowTrafficLights (); - virtual HRESULT Show (bool activate, bool isDialog) override; virtual HRESULT SetEnabled (bool enable) override; @@ -100,9 +98,11 @@ BEGIN_INTERFACE_MAP() bool CanBecomeKeyWindow (); protected: - virtual NSWindowStyleMask GetStyle() override; + virtual NSWindowStyleMask CalculateStyleMask() override; + void UpdateStyle () override; private: + void ZOrderChildWindows(); void OnInitialiseNSWindow(); NSString *_lastTitle; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ce82f7d83f..cf1ee6943d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase OnInitialiseNSWindow(); } -void WindowImpl::HideOrShowTrafficLights() { - if (Window == nil) { - return; - } - - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; -} - void WindowImpl::OnInitialiseNSWindow(){ [GetWindowProtocol() setCanBecomeKeyWindow:true]; @@ -67,8 +54,6 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); - HideOrShowTrafficLights(); - return SetWindowState(_lastWindowState); } } @@ -134,14 +119,19 @@ void WindowImpl::BringToFront() } [Window invalidateShadow]; + ZOrderChildWindows(); + } +} + +void WindowImpl::ZOrderChildWindows() +{ + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + auto window = (*iterator)->Window; - for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) - { - auto window = (*iterator)->Window; - - // #9565: Only bring window to front if it's on the currently active space - if ([window isOnActiveSpace]) - (*iterator)->BringToFront(); + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) { + (*iterator)->BringToFront(); } } } @@ -161,13 +151,15 @@ bool WindowImpl::CanBecomeKeyWindow() void WindowImpl::StartStateTransition() { _transitioningWindowState = true; + UpdateStyle(); } void WindowImpl::EndStateTransition() { _transitioningWindowState = false; - + UpdateStyle(); + // Ensure correct order of child windows after fullscreen transition. - BringToFront(); + ZOrderChildWindows(); } SystemDecorations WindowImpl::Decorations() { @@ -225,16 +217,12 @@ bool WindowImpl::IsZoomed() { } void WindowImpl::DoZoom() { - switch (_decorations) { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; + if (_decorations == SystemDecorationsNone || + _decorations == SystemDecorationsBorderOnly || + _canResize == false) { + [Window setFrame:[Window screen].visibleFrame display:true]; + } else { + [Window performZoom:Window]; } } @@ -261,8 +249,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateStyle(); - HideOrShowTrafficLights(); - switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -419,9 +405,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } [GetWindowProtocol() setIsExtended:enable]; - - HideOrShowTrafficLights(); - UpdateStyle(); } @@ -577,14 +560,16 @@ bool WindowImpl::IsOwned() { return _parent != nullptr; } -NSWindowStyleMask WindowImpl::GetStyle() { - unsigned long s = NSWindowStyleMaskBorderless; +NSWindowStyleMask WindowImpl::CalculateStyleMask() { + // Use the current style mask and only clear the flags we're going to be modifying. + unsigned long s = [Window styleMask] & + ~(NSWindowStyleMaskFullSizeContentView | + NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskResizable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskTexturedBackground); - if(_actualWindowState == FullScreen) - { - s |= NSWindowStyleMaskFullScreen; - } - switch (_decorations) { case SystemDecorationsNone: s = s | NSWindowStyleMaskFullSizeContentView; @@ -597,7 +582,7 @@ NSWindowStyleMask WindowImpl::GetStyle() { case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; - if (_canResize && _isEnabled) { + if ((_canResize && _isEnabled) || _transitioningWindowState) { s = s | NSWindowStyleMaskResizable; } break; @@ -612,3 +597,25 @@ NSWindowStyleMask WindowImpl::GetStyle() { } return s; } + +void WindowImpl::UpdateStyle() { + WindowBaseImpl::UpdateStyle(); + + if (Window == nil) { + return; + } + + bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; + + NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; + NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; + NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton]; + + [closeButton setHidden:!hasTrafficLights]; + [closeButton setEnabled:_isEnabled]; + [miniaturizeButton setHidden:!hasTrafficLights]; + [miniaturizeButton setEnabled:_isEnabled]; + [zoomButton setHidden:!hasTrafficLights]; + [zoomButton setEnabled:_isEnabled && _canResize]; +} diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 4e5a8463df..a6f463fdcb 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -140,6 +140,7 @@ Maximized FullScreen + Can Resize diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 3cd5350cce..087f25666b 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -66,11 +66,13 @@ namespace IntegrationTestApp var locationComboBox = this.GetControl("ShowWindowLocation"); var stateComboBox = this.GetControl("ShowWindowState"); var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; + var canResizeCheckBox = this.GetControl("ShowWindowCanResize"); var owner = (Window)this.GetVisualRoot()!; var window = new ShowWindowTest { WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, + CanResize = canResizeCheckBox.IsChecked.Value, }; if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 14344f34cf..39593331c1 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1182,8 +1182,21 @@ namespace Avalonia.Win32 var y = monitorInfo.rcWork.top; var cx = Math.Abs(monitorInfo.rcWork.right - x); var cy = Math.Abs(monitorInfo.rcWork.bottom - y); + var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); - SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); + if (!style.HasFlag(WindowStyles.WS_SIZEFRAME)) + { + // When calling SetWindowPos on a maximized window it automatically adjusts + // for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS + // NO BORDERS, meaning that the window is placed wrong when we have CanResize + // == false. Account for this here. + var borderThickness = BorderThickness; + x -= (int)borderThickness.Left; + cx += (int)borderThickness.Left + (int)borderThickness.Right; + cy += (int)borderThickness.Bottom; + } + + SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED); } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 7bb991aae6..ec24caa18c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -30,9 +30,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(StartupLocationData))] - public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location) + public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location, bool canResize) { - using var window = OpenWindow(size, mode, location); + using var window = OpenWindow(size, mode, location, canResize: canResize); var info = GetWindowInfo(); if (size.HasValue) @@ -63,9 +63,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(WindowStateData))] - public void WindowState(Size? size, ShowWindowMode mode, WindowState state) + public void WindowState(Size? size, ShowWindowMode mode, WindowState state, bool canResize) { - using var window = OpenWindow(size, mode, state: state); + using var window = OpenWindow(size, mode, state: state, canResize: canResize); try { @@ -230,10 +230,10 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(new Rgba32(255, 0, 0), centerColor); } - public static TheoryData StartupLocationData() + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -243,7 +243,8 @@ namespace Avalonia.IntegrationTests.Appium { if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned)) { - data.Add(size, mode, location); + data.Add(size, mode, location, true); + data.Add(size, mode, location, false); } } } @@ -252,10 +253,10 @@ namespace Avalonia.IntegrationTests.Appium return data; } - public static TheoryData WindowStateData() + public static TheoryData WindowStateData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -273,7 +274,8 @@ namespace Avalonia.IntegrationTests.Appium mode != ShowWindowMode.NonOwned) continue; - data.Add(size, mode, state); + data.Add(size, mode, state, true); + data.Add(size, mode, state, false); } } } @@ -311,25 +313,39 @@ namespace Avalonia.IntegrationTests.Appium Size? size, ShowWindowMode mode, WindowStartupLocation location = WindowStartupLocation.Manual, - WindowState state = Controls.WindowState.Normal) + WindowState state = Controls.WindowState.Normal, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); - + if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } + + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (stateComboBox.GetComboBoxValue() != state.ToString()) + { + stateComboBox.Click(); + _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + } - stateComboBox.Click(); - _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); return showButton.OpenWindowWithClick(); } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index d9817ecdd1..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -323,21 +323,49 @@ namespace Avalonia.IntegrationTests.Appium secondaryWindow.GetChromeButtons().close.Click(); } - private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + [PlatformTheory(TestPlatforms.MacOS)] + [InlineData(ShowWindowMode.NonOwned)] + [InlineData(ShowWindowMode.Owned)] + [InlineData(ShowWindowMode.Modal)] + public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMode mode) + { + using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false)) + { + var secondaryWindow = GetWindow("SecondaryWindow"); + var (_, _, zoomButton) = secondaryWindow.GetChromeButtons(); + Assert.False(zoomButton.Enabled); + } + } + + private IDisposable OpenWindow( + PixelSize? size, + ShowWindowMode mode, + WindowStartupLocation location, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } + + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); return showButton.OpenWindowWithClick(); }