Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macOS: Fix interaction between ExtendClientAreaToDecorationsHint and SystemDecorations #10677

Merged
merged 12 commits into from
Mar 16, 2023
10 changes: 7 additions & 3 deletions native/Avalonia.Native/src/OSX/WindowImpl.mm
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,13 @@

case SystemDecorationsFull:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];

if (!_isClientAreaExtended) {
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
}

if (currentWindowState == Maximized) {
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;

Expand Down Expand Up @@ -611,7 +614,8 @@
}

bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
bool hasTrafficLights = (_decorations == SystemDecorationsFull) &&
(_isClientAreaExtended ? wantsChrome : true);

NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];
Expand Down
6 changes: 6 additions & 0 deletions samples/IntegrationTestApp/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowSystemDecorations" SelectedIndex="2">
<ComboBoxItem Name="ShowWindowSystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
Expand Down
4 changes: 4 additions & 0 deletions samples/IntegrationTestApp/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ private void ShowWindow()
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 systemDecorations = this.GetControl<ComboBox>("ShowWindowSystemDecorations");
var extendClientArea = this.GetControl<CheckBox>("ShowWindowExtendClientAreaToDecorationsHint");
var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
var owner = (Window)this.GetVisualRoot()!;

Expand Down Expand Up @@ -95,6 +97,8 @@ private void ShowWindow()
}

sizeTextBox.Text = string.Empty;
window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false;
window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex;
window.WindowState = (WindowState)stateComboBox.SelectedIndex;

switch (modeComboBox.SelectedIndex)
Expand Down
24 changes: 18 additions & 6 deletions samples/IntegrationTestApp/ShowWindowTest.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
x:DataType="Window"
Title="Show Window Test">
<integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
Expand Down Expand Up @@ -35,13 +35,25 @@
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>

<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="8">SystemDecorations</Label>
<ComboBox Name="CurrentSystemDecorations" Grid.Column="1" Grid.Row="8" SelectedIndex="{Binding SystemDecorations}">
<ComboBoxItem Name="SystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>

<CheckBox Name="CurrentExtendClientAreaToDecorationsHint" Grid.ColumnSpan="2" Grid.Row="9"
IsChecked="{Binding ExtendClientAreaToDecorationsHint}">
ExtendClientAreaToDecorationsHint
</CheckBox>

<Label Grid.Column="0" Grid.Row="10">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="10" IsReadOnly="True" />

<Label Grid.Row="9" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />

<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
<Button Name="HideButton" Grid.Row="12" Command="{Binding $parent[Window].Hide}">Hide</Button>

</Grid>
</integrationTestApp:MeasureBorder>
Expand Down
19 changes: 13 additions & 6 deletions tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@

namespace Avalonia.IntegrationTests.Appium
{
public record class WindowChrome(
AppiumWebElement? Close,
AppiumWebElement? Minimize,
AppiumWebElement? Maximize,
AppiumWebElement? FullScreen);

internal static class ElementExtensions
{
public static IReadOnlyList<AppiumWebElement> GetChildren(this AppiumWebElement element) =>
element.FindElementsByXPath("*/*");

public static (AppiumWebElement close, AppiumWebElement minimize, AppiumWebElement maximize) GetChromeButtons(this AppiumWebElement window)
public static WindowChrome GetChromeButtons(this AppiumWebElement window)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var closeButton = window.FindElementByXPath("//XCUIElementTypeButton[1]");
var fullscreenButton = window.FindElementByXPath("//XCUIElementTypeButton[2]");
var minimizeButton = window.FindElementByXPath("//XCUIElementTypeButton[3]");
return (closeButton, minimizeButton, fullscreenButton);
var closeButton = window.FindElementsByAccessibilityId("_XCUI:CloseWindow").FirstOrDefault();
var fullscreenButton = window.FindElementsByAccessibilityId("_XCUI:FullScreenWindow").FirstOrDefault();
var minimizeButton = window.FindElementsByAccessibilityId("_XCUI:MinimizeWindow").FirstOrDefault();
var zoomButton = window.FindElementsByAccessibilityId("_XCUI:ZoomWindow").FirstOrDefault();
return new(closeButton, minimizeButton, zoomButton, fullscreenButton);
}

throw new NotSupportedException("GetChromeButtons not supported on this platform.");
Expand Down Expand Up @@ -138,7 +145,7 @@ public static IDisposable OpenWindowWithClick(this AppiumWebElement element)
var text = windows.Select(x => x.Text).ToList();
var newWindow = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"))
.First(x => x.Text == newWindowTitle);
var (close, _, _) = ((AppiumWebElement)newWindow).GetChromeButtons();
var close = ((AppiumWebElement)newWindow).FindElementByAccessibilityId("_XCUI:CloseWindow");
close!.Click();
Thread.Sleep(1000);
});
Expand Down
13 changes: 11 additions & 2 deletions tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal enum TestPlatforms
internal class PlatformFactAttribute : FactAttribute
{
private readonly string? _reason;
private string? _skip;

public PlatformFactAttribute(TestPlatforms platforms, string? reason = null)
{
Expand All @@ -28,8 +29,16 @@ public PlatformFactAttribute(TestPlatforms platforms, string? reason = null)

public override string? Skip
{
get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}" + (_reason is not null ? $" reason: \"{_reason}\"" : "");
set => throw new NotSupportedException();
get
{
if (_skip is not null)
return _skip;
if (!IsSupported())
return $"Ignored on {RuntimeInformation.OSDescription}" +
(_reason is not null ? $" reason: '{_reason}'" : "");
return null;
}
set => _skip = value;
}

private bool IsSupported()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ namespace Avalonia.IntegrationTests.Appium
{
internal class PlatformTheoryAttribute : TheoryAttribute
{
private string? _skip;

public PlatformTheoryAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms;

public TestPlatforms Platforms { get; }

public override string? Skip
{
get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}";
set => throw new NotSupportedException();
get
{
if (_skip is not null)
return _skip;
return !IsSupported() ? $"Ignored on {RuntimeInformation.OSDescription}" : null;
}
set => _skip = value;
}

private bool IsSupported()
Expand Down
124 changes: 102 additions & 22 deletions tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resiz
public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen()
{
var mainWindow = GetWindow("MainWindow");
var buttons = mainWindow.GetChromeButtons();
var fullScreen = mainWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow");

buttons.maximize.Click();
fullScreen.Click();

Thread.Sleep(500);

Expand Down Expand Up @@ -239,17 +239,18 @@ public void WindowOrder_Owned_Is_Correct_After_Closing_Window()
public void Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown()
{
var window = GetWindow("MainWindow");
var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons();
var windowChrome = window.GetChromeButtons();

Assert.True(closeButton.Enabled);
Assert.True(zoomButton.Enabled);
Assert.True(miniaturizeButton.Enabled);
Assert.True(windowChrome.Close!.Enabled);
Assert.True(windowChrome.FullScreen!.Enabled);
Assert.True(windowChrome.Minimize!.Enabled);
Assert.Null(windowChrome.Maximize);

using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
{
Assert.False(closeButton.Enabled);
Assert.False(zoomButton.Enabled);
Assert.False(miniaturizeButton.Enabled);
Assert.False(windowChrome.Close!.Enabled);
Assert.False(windowChrome.FullScreen!.Enabled);
Assert.False(windowChrome.Minimize!.Enabled);
}
}

Expand All @@ -259,11 +260,11 @@ public void Minimize_Button_Is_Disabled_On_Modal_Dialog()
using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
{
var secondaryWindow = GetWindow("SecondaryWindow");
var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons();
var windowChrome = secondaryWindow.GetChromeButtons();

Assert.True(closeButton.Enabled);
Assert.True(zoomButton.Enabled);
Assert.False(miniaturizeButton.Enabled);
Assert.True(windowChrome.Close!.Enabled);
Assert.True(windowChrome.Maximize!.Enabled);
Assert.False(windowChrome.Minimize!.Enabled);
}
}

Expand All @@ -274,7 +275,7 @@ public void Minimize_Button_Disabled_Owned_Window(ShowWindowMode mode)
using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual))
{
var secondaryWindow = GetWindow("SecondaryWindow");
var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons();
var miniaturizeButton = secondaryWindow.FindElementByAccessibilityId("_XCUI:MinimizeWindow");

Assert.False(miniaturizeButton.Enabled);
}
Expand All @@ -288,7 +289,7 @@ public void Minimize_Button_Minimizes_Window(ShowWindowMode mode)
using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual))
{
var secondaryWindow = GetWindow("SecondaryWindow");
var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons();
var miniaturizeButton = secondaryWindow.FindElementByAccessibilityId("_XCUI:MinimizeWindow");

miniaturizeButton.Click();
Thread.Sleep(1000);
Expand Down Expand Up @@ -332,7 +333,7 @@ public void Hidden_Child_Window_Is_Not_Reshown_When_Parent_Clicked()

// Close the window manually.
secondaryWindow = GetWindow("SecondaryWindow");
secondaryWindow.GetChromeButtons().close.Click();
secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click();
}

[PlatformTheory(TestPlatforms.MacOS)]
Expand All @@ -344,22 +345,92 @@ public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMo
using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false))
{
var secondaryWindow = GetWindow("SecondaryWindow");
var (_, _, zoomButton) = secondaryWindow.GetChromeButtons();
var zoomButton = mode == ShowWindowMode.NonOwned ?
secondaryWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow") :
secondaryWindow.FindElementByAccessibilityId("_XCUI:ZoomWindow");
Assert.False(zoomButton.Enabled);
}
}


[PlatformFact(TestPlatforms.MacOS)]
public void Toggling_SystemDecorations_Should_Preserve_ExtendClientArea()
{
// #10650
using (OpenWindow(extendClientArea: true))
{
var secondaryWindow = GetWindow("SecondaryWindow");

// The XPath of the title bar text _should_ be "XCUIElementTypeStaticText"
// but Appium seems to put a fake node between the window and the title bar
// https://stackoverflow.com/a/71914227/6448
var titleBar = secondaryWindow.FindElementsByXPath("/*/XCUIElementTypeStaticText").Count;

Assert.Equal(0, titleBar);

secondaryWindow.FindElementByAccessibilityId("CurrentSystemDecorations").Click();
_session.FindElementByAccessibilityId("SystemDecorationsNone").SendClick();
secondaryWindow.FindElementByAccessibilityId("CurrentSystemDecorations").Click();
_session.FindElementByAccessibilityId("SystemDecorationsFull").SendClick();

titleBar = secondaryWindow.FindElementsByXPath("/*/XCUIElementTypeStaticText").Count;
Assert.Equal(0, titleBar);
}
}

[PlatformTheory(TestPlatforms.MacOS)]
[InlineData(SystemDecorations.None)]
[InlineData(SystemDecorations.BorderOnly)]
[InlineData(SystemDecorations.Full)]
public void ExtendClientArea_SystemDecorations_Shows_Correct_Buttons(SystemDecorations decorations)
{
// #10650
using (OpenWindow(extendClientArea: true, systemDecorations: decorations))
{
var secondaryWindow = GetWindow("SecondaryWindow");

try
{
var chrome = secondaryWindow.GetChromeButtons();

if (decorations == SystemDecorations.Full)
{
Assert.NotNull(chrome.Close);
Assert.NotNull(chrome.Minimize);
Assert.NotNull(chrome.FullScreen);
}
else
{
Assert.Null(chrome.Close);
Assert.Null(chrome.Minimize);
Assert.Null(chrome.FullScreen);
}
}
finally
{
if (decorations != SystemDecorations.Full)
{
secondaryWindow.FindElementByAccessibilityId("CurrentSystemDecorations").Click();
_session.FindElementByAccessibilityId("SystemDecorationsFull").SendClick();
}
}
}
}

private IDisposable OpenWindow(
PixelSize? size,
ShowWindowMode mode,
WindowStartupLocation location,
bool canResize = true)
PixelSize? size = null,
ShowWindowMode mode = ShowWindowMode.NonOwned,
WindowStartupLocation location = WindowStartupLocation.Manual,
bool canResize = true,
SystemDecorations systemDecorations = SystemDecorations.Full,
bool extendClientArea = false)
{
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");
var systemDecorationsComboBox = _session.FindElementByAccessibilityId("ShowWindowSystemDecorations");
var extendClientAreaCheckBox = _session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");

if (size.HasValue)
sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
Expand All @@ -379,6 +450,15 @@ public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMo
if (canResizeCheckBox.GetIsChecked() != canResize)
canResizeCheckBox.Click();

if (systemDecorationsComboBox.GetComboBoxValue() != systemDecorations.ToString())
{
systemDecorationsComboBox.Click();
_session.FindElementByName(systemDecorations.ToString()).SendClick();
}

if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
extendClientAreaCheckBox.Click();

return showButton.OpenWindowWithClick();
}

Expand Down