Browse Source

Merge pull request #10265 from AvaloniaUI/fixes/macos-noresize-fullscreen

Various macOS and win32 fixes
Steven Kirk 2 years ago
parent
commit
01e3062dc0

+ 13 - 4
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

+ 1 - 1
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;
     }

+ 2 - 3
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);

+ 6 - 23
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];
     }
 }
 

+ 3 - 3
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;
 };

+ 54 - 47
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];
+}

+ 1 - 0
samples/IntegrationTestApp/MainWindow.axaml

@@ -140,6 +140,7 @@
               <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
               <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
             </ComboBox>
+            <CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
             <Button Name="ShowWindow">Show Window</Button>
             <Button Name="SendToBack">Send to Back</Button>
             <Button Name="EnterFullscreen">Enter Fullscreen</Button>

+ 2 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -66,11 +66,13 @@ namespace IntegrationTestApp
             var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
             var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
             var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
+            var canResizeCheckBox = this.GetControl<CheckBox>("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)

+ 14 - 1
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);
                 }
             }
         }

+ 34 - 18
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<Size?, ShowWindowMode, WindowStartupLocation> StartupLocationData()
+        public static TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool> StartupLocationData()
         {
             var sizes = new Size?[] { null, new Size(400, 300) };
-            var data = new TheoryData<Size?, ShowWindowMode, WindowStartupLocation>();
+            var data = new TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool>();
 
             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<Size?, ShowWindowMode, WindowState> WindowStateData()
+        public static TheoryData<Size?, ShowWindowMode, WindowState, bool> WindowStateData()
         {
             var sizes = new Size?[] { null, new Size(400, 300) };
-            var data = new TheoryData<Size?, ShowWindowMode, WindowState>();
+            var data = new TheoryData<Size?, ShowWindowMode, WindowState, bool>();
 
             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();
         }

+ 33 - 5
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();
         }