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 child window order with multiple child windows #8880

Merged
merged 5 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 0 additions & 2 deletions native/Avalonia.Native/src/OSX/WindowImpl.mm
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@
if(_parent != nullptr)
{
_parent->_children.remove(this);

_parent->BringToFront();
}

auto cparent = dynamic_cast<WindowImpl *>(parent);
Expand Down
27 changes: 27 additions & 0 deletions samples/IntegrationTestApp/MacOSIntegration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;

namespace IntegrationTestApp
{
public static class MacOSIntegration
{
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")]
private static extern IntPtr GetHandle(string name);

[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern long Int64_objc_msgSend(IntPtr receiver, IntPtr selector);

private static readonly IntPtr s_orderedIndexSelector;

static MacOSIntegration()
{
s_orderedIndexSelector = GetHandle("orderedIndex");;
}

public static long GetOrderedIndex(Window window)
{
return Int64_objc_msgSend(window.PlatformImpl!.Handle.Handle, s_orderedIndexSelector);
}
}
}
13 changes: 13 additions & 0 deletions samples/IntegrationTestApp/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using Microsoft.CodeAnalysis;

namespace IntegrationTestApp
{
Expand Down Expand Up @@ -63,6 +65,17 @@ private void ShowWindow()
WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
};

if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
// Make sure the windows have unique names and AutomationIds.
var existing = lifetime.Windows.OfType<ShowWindowTest>().Count();
if (existing > 0)
{
AutomationProperties.SetAutomationId(window, window.Name + (existing + 1));
window.Title += $" {existing + 1}";
}
}

if (size.HasValue)
{
window.Width = size.Value.Width;
Expand Down
8 changes: 6 additions & 2 deletions samples/IntegrationTestApp/ShowWindowTest.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
Expand Down Expand Up @@ -31,6 +31,10 @@
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>Fullscreen</ComboBoxItem>
</ComboBox>
<Button Name="HideButton" Grid.Row="8" Command="{Binding $parent[Window].Hide}">Hide</Button>

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

<Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</Window>
28 changes: 25 additions & 3 deletions samples/IntegrationTestApp/ShowWindowTest.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
using System;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using Avalonia.Threading;

namespace IntegrationTestApp
{
public class ShowWindowTest : Window
{
private readonly DispatcherTimer? _timer;
private readonly TextBox? _orderTextBox;

public ShowWindowTest()
{
InitializeComponent();
DataContext = this;
PositionChanged += (s, e) => this.GetControl<TextBox>("Position").Text = $"{Position}";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_orderTextBox = this.GetControl<TextBox>("Order");
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
_timer.Tick += TimerOnTick;
_timer.Start();
}
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Expand All @@ -36,5 +47,16 @@ protected override void OnOpened(EventArgs e)
ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}
}

protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_timer?.Stop();
}

private void TimerOnTick(object? sender, EventArgs e)
{
_orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString();
}
}
}
89 changes: 43 additions & 46 deletions tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public WindowTests_MacOS(TestAppFixture fixture)
tab.Click();
return;
}
catch (WebDriverException e) when (retry++ < 3)
catch (WebDriverException) when (retry++ < 3)
{
// MacOS sometimes seems to need a bit of time to get itself back in order after switching out
// of fullscreen.
Expand All @@ -49,19 +49,16 @@ public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent()
{
mainWindow.Click();

var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");

Assert.Equal(0, secondaryWindowIndex);
Assert.Equal(1, mainWindowIndex);
Assert.Equal(1, secondaryWindowIndex);
}
}

[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip()
{
var mainWindow = FindWindow(_session, "MainWindow");
var mainWindow = GetWindow("MainWindow");

using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
{
Expand All @@ -70,24 +67,21 @@ public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resiz
.ClickAndHold()
.Perform();

var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");

new Actions(_session)
.MoveToElement(mainWindow, 100, 1)
.Release()
.Perform();

Assert.Equal(0, secondaryWindowIndex);
Assert.Equal(1, mainWindowIndex);
Assert.Equal(1, secondaryWindowIndex);
}
}

[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen()
{
var mainWindow = FindWindow(_session, "MainWindow");
var mainWindow = GetWindow("MainWindow");
var buttons = mainWindow.GetChromeButtons();

buttons.maximize.Click();
Expand All @@ -98,14 +92,8 @@ public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen(
{
using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
{
var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");

Assert.Equal(0, secondaryWindowIndex);
Assert.Equal(1, mainWindowIndex);

Thread.Sleep(5000);
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
Assert.Equal(1, secondaryWindowIndex);
}
}
finally
Expand All @@ -122,13 +110,8 @@ public void WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent()
using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual))
{
mainWindow.Click();

var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");

Assert.Equal(0, secondaryWindowIndex);
Assert.Equal(1, mainWindowIndex);
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
Assert.Equal(1, secondaryWindowIndex);
}
}

Expand All @@ -141,22 +124,35 @@ public void WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent()
{
mainWindow.Click();

var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");

Assert.Equal(1, secondaryWindowIndex);
Assert.Equal(0, mainWindowIndex);
Assert.Equal(2, secondaryWindowIndex);

var sendToBack = _session.FindElementByAccessibilityId("SendToBack");
sendToBack.Click();
}
}

[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Owned_Is_Correct_After_Closing_Window()
{
using (OpenWindow(new PixelSize(300, 500), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner))
{
// Open a second child window, and close it.
using (OpenWindow(new PixelSize(200, 200), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner))
{
}

var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");

Assert.Equal(1, secondaryWindowIndex);
}
}

[PlatformFact(TestPlatforms.MacOS)]
public void Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown()
{
var window = FindWindow(_session, "MainWindow");
var window = GetWindow("MainWindow");
var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons();

Assert.True(closeButton.Enabled);
Expand All @@ -176,7 +172,7 @@ public void Minimize_Button_Is_Disabled_On_Modal_Dialog()
{
using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
{
var secondaryWindow = FindWindow(_session, "SecondaryWindow");
var secondaryWindow = GetWindow("SecondaryWindow");
var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons();

Assert.True(closeButton.Enabled);
Expand All @@ -192,7 +188,7 @@ public void Minimize_Button_Minimizes_Window(ShowWindowMode mode)
{
using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual))
{
var secondaryWindow = FindWindow(_session, "SecondaryWindow");
var secondaryWindow = GetWindow("SecondaryWindow");
var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons();

miniaturizeButton.Click();
Expand Down Expand Up @@ -220,7 +216,7 @@ public void Hidden_Child_Window_Is_Not_Reshown_When_Parent_Clicked()
// causes Appium to think it's a different window.
OpenWindow(null, ShowWindowMode.Owned, WindowStartupLocation.Manual);

var secondaryWindow = FindWindow(_session, "SecondaryWindow");
var secondaryWindow = GetWindow("SecondaryWindow");
var hideButton = secondaryWindow.FindElementByAccessibilityId("HideButton");

hideButton.Click();
Expand All @@ -236,7 +232,7 @@ public void Hidden_Child_Window_Is_Not_Reshown_When_Parent_Clicked()
_session.FindElementByAccessibilityId("RestoreAll").Click();

// Close the window manually.
secondaryWindow = FindWindow(_session, "SecondaryWindow");
secondaryWindow = GetWindow("SecondaryWindow");
secondaryWindow.GetChromeButtons().close.Click();
}

Expand All @@ -259,18 +255,19 @@ private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStart
return showButton.OpenWindowWithClick();
}

private static int GetWindowOrder(IReadOnlyCollection<AppiumWebElement> elements, string identifier)
private AppiumWebElement GetWindow(string identifier)
{
return elements.TakeWhile(x =>
x.FindElementByXPath("XCUIElementTypeWindow")?.GetAttribute("identifier") != identifier).Count();
// The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed
// but in the meantime use the `parent::' selector to return the parent "real" window.
return _session.FindElementByXPath(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also changed this to send an XPath query rather than doing multiple roundtrips to appium to find the window.

$"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow");
}

private static AppiumWebElement FindWindow(AppiumDriver<AppiumWebElement> session, string identifier)
private int GetWindowOrder(string identifier)
{
var windows = session.FindElementsByXPath("XCUIElementTypeWindow");
return windows.First(x =>
x.FindElementsByXPath("XCUIElementTypeWindow")
.Any(y => y.GetAttribute("identifier") == identifier));
var window = GetWindow(identifier);
var order = window.FindElementByXPath("//*[@identifier='Order']");
return int.Parse(order.Text);
}

public enum ShowWindowMode
Expand Down