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

feat: Add API for fetching window Z-order #14909

Merged
merged 12 commits into from
Apr 20, 2024

Conversation

BAndysc
Copy link
Contributor

@BAndysc BAndysc commented Mar 11, 2024

What does the pull request do?

Adds new API to Window which returns window z-order. The top most window has the highest value.

✅ I have tested Windows, macOS and Linux (Ubuntu both on XWayland and XOrg)! ✅

What is the current behavior?

n/a

What is the updated/expected behavior with this PR?

n/a

How was the solution implemented (if it's not obvious)?

As described in #6309 (comment), it uses GetWindow on Windows, NSWindow.orderedIndex on macOS and XQueryTree on X. Note: on Windows and X by default the topmost window has the highest zorder value, on macOS it is reversed, thus on macOS the value has to be negated to mimic the same behaviour.

My questions:

  • macOS Api returns NSInteger = Int, which is 4 bytes on 32 bits systems and 8 bytes on 64 bit systems. Therefore I've decided to go with returning IntPtr on c# side. Let me know if instead I should just use long.

Checklist

Breaking changes

n/a

Obsoletions / Deprecations

n/a

Fixed issues

Fixed #6309

(side note: this is required for proper Dock support wieslawsoltes/Dock#329)

@BAndysc
Copy link
Contributor Author

BAndysc commented Mar 11, 2024

Edit: Tested Windows (11), macOS 14.3.1, Linux (Ubuntu with XWayland and X) and everything seems to work.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0046038-beta. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Comment on lines 110 to 111
// macOS returns z-order in reverse order - the topmost window has the lowest z-order
return new IntPtr(-_native.WindowZOrder.ToInt64());
Copy link
Member

Choose a reason for hiding this comment

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

nit: this negation should happen on native side, since technically Avalonia.Native project isn't supposed to be macOS specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, changed

return E_POINTER;
}

*zOrder = [Window orderedIndex];
Copy link
Member

Choose a reason for hiding this comment

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

This new prop can be used in

_orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed the GetOrderedIndex to the new WindowZOrder, I guess those tests could be expanded to all desktop platforms now (though rather than checking the absolute z-order value, it should check its relation to the parent window)

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0046040-beta. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@@ -1133,6 +1133,46 @@ public PixelPoint Position
}
}

public IntPtr? ZOrder
Copy link
Member

Choose a reason for hiding this comment

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

Enumerating all windows in the the entire desktop doesn't seem like a good idea.

Copy link
Member

Choose a reason for hiding this comment

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

Especially since that code goes deep into the window tree, so stuff like GTK buttons gets enumerated too.

Also, are you sure that stacking order even matches one from XQueryTree?

Copy link
Member

Choose a reason for hiding this comment

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

image

Doesn't seem to be the case, windows are stacked as Konsole>Nemo>GEdit, while XQueryTree returns Nemo-GEdit-Konsole

Copy link
Contributor Author

@BAndysc BAndysc Mar 13, 2024

Choose a reason for hiding this comment

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

https://tronche.com/gui/x/xlib/window-information/XQueryTree.html

That's what the specification says:

The children are listed in current stacking order, from bottommost (first) to topmost (last).

And that seem to be the case:

Screen.Recording.2024-03-13.at.13.29.19.mov

Those nemo, gedit and Konsole on your screeenshot are something different than the windows themselves. Take a look that under Qt Selection Owner for konsole the order is correct and the windows have the correct title indicating those represent the actual windows. Maybe this is QT thing to comply with the specs?


But I do agree maybe traversing the from root is not perfect. In that case, I guess in order to get correct z-order within the app, I have to traverse up the tree from each window, find a common parent, then find the index within the parent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kekekeks Updated the implementation to traverse only those windows which are required to compute the relative (within the app) z-order.

Copy link
Member

Choose a reason for hiding this comment

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

And that seem to be the case

Might be WM-specific behavior. I'm running KWin 5.20.5 with XOrg 1.20.11, no wayland.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0046157-beta. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@maxkatz6 maxkatz6 added the backport-candidate-11.1.x Consider this PR for backporting to 11.1 branch label Mar 15, 2024
@MrJul
Copy link
Member

MrJul commented Mar 15, 2024

System vs app windows

On the public API side, I'd definitely use long instead of IntPtr here.
IntPtr leaks too much about the implementations and isn't a type normally used in such a high level API.

More generally, I'm a bit concerned about the API usage here.
The current implementation has an "almost opaque" ZOrder, in the sense that it depends on what the system returns.

Imagine an user having a two windows app and wondering why the first window has ZOrder=2 and the second one ZOrder=6, simply because there are 3 other unrelated apps in between on the desktop.
Is it a useful piece of information? It could be, but there's no way to know what windows 3 to 5 are, so ultimately the user can't do anything with it.
Shouldn't that be hidden away, with ZOrder only taking the current application's windows into account?

Performance

I'm also a bit concerned about potential performance problems. The property iterates through most windows on Win32 and X11: that seems a bit heavy weight, shouldn't that be a method instead to indicate that accessing the z-order isn't "free"?

A common usage of this new property is probably lifetime.Windows.OrderBy(w => w.ZOrder). That would iterate through most windows on the desktop N times (with N being the number of windows in the app), and that's only because OrderBy fetches the key only once. Use a custom comparer, and that iteration count can now explode depending on the sort algorithm.

Stability

Continuing with ZOrder being a property and sorting, is ZOrder guaranteed to stable during such a sort?
Quoting the remarks for Win32 GetWindow():

The EnumChildWindows function is more reliable than calling GetWindow in a loop. An application that calls GetWindow to perform this task risks being caught in an infinite loop or referencing a handle to a window that has been destroyed.

Makes me think it isn't. It doesn't seem stable even for a single loop.

Shouldn't we provide something like GetOrderedWindows() instead and use the appropriate underlying API?

@maxkatz6
Copy link
Member

See #6479 as well, another issue that can be closed after this PR.
It has proposal API of:

Window GetTopWindow(Window w1, Window w2) // likely still would involve enumeration of all windows on each call
void SetTopWindow(Window w) // no activation, which might not be possible on all platforms

@kekekeks
Copy link
Member

A common usage of this new property is probably lifetime.Windows.OrderBy(w => w.ZOrder)

The API should probably accept multiple windows at once and maybe just sort those over ZOrder, since what platform backends are returning isn't stable and not really useful as a raw value.

@BAndysc
Copy link
Contributor Author

BAndysc commented Mar 28, 2024

@MrJul @kekekeks @maxkatz6 I have redesigned the PR to expose a single method:

IClassicDesktopStyleApplicationLifetime::SortWindowsByZOrder(Window[] windows)

which sorts the array by windows z order, bottom to top.

I have also changed Windows API to use EnumChildWindows, which unfortunately still need to traverse through all windows, but at least this is a single API call and also it will no longer traverse once it finds all windows (in my tests it does reduce the number of callback calls a lot - apparently there are a lot of hidden windows in Windows).

Also Linux X11 version now only calls XQueryTree n + | path to root | times.


Unfortunately Windows and Linux need to allocate some additional memory, I don't have an idea how to reduce the allocations.

@kekekeks
Copy link
Member

Note that lifetime is an entirely optional concept and no non-lifetime related APIs should be put there

@BAndysc
Copy link
Contributor Author

BAndysc commented Mar 28, 2024

Note that lifetime is an entirely optional concept and no non-lifetime related APIs should be put there

Where could I put it then to access os specific things? I could just put it in Window class, but it might be a bit weird that Window class has a method that in fact has nothing to do with the instance you call it on. But maybe it is a minor inconvenience?

@kekekeks
Copy link
Member

A static method on the Window would be fine, I think.

A few other points:

  1. we probably want to have that API for WindowBase, so it would support popups as well
  2. the platform-specific implementation should be queried from the first window from the list, rather than through the locator

@BAndysc
Copy link
Contributor Author

BAndysc commented Mar 28, 2024

A static method on the Window would be fine, I think.

A few other points:

  1. we probably want to have that API for WindowBase, so it would support popups as well
  2. the platform-specific implementation should be queried from the first window from the list, rather than through the locator

IWindowingPlatform is not easily accessible from Window now. To make it accessible, I would have to pass this during creating an instance of window implementation:

public IWindowImpl CreateWindow()
{
return new WindowImpl();

public IWindowImpl CreateWindow()
{
return new WindowImpl(_factory, _options);
}

public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false, _frameBufferFormat);
public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true, _frameBufferFormat);

public IWindowImpl CreateWindow() => new WindowStub();
public IWindowImpl CreateEmbeddableWindow()
{
if (s_lastWindow != null)
{
s_lastWindowTransport.Dispose();
try
{
s_lastWindow.Dispose();
}
catch
{
//Ignore
}
}
s_lastWindow =
new PreviewerWindowImpl(s_lastWindowTransport = new DetachableTransportConnection(s_transport));
foreach (var pf in PreFlightMessages)
s_lastWindowTransport.FireOnMessage(s_lastWindowTransport, pf);
return s_lastWindow;
}

Is that ok? Alternatively, I could move all the logic from IWindowingPlatform to IWindowImpl. Which one do you prefer?

@kekekeks
Copy link
Member

IWindowingPlatform is not easily accessible from Window now. To make it accessible, I would have to pass this during creating an instance of window implementation:

Just move the comparer method to IWindowBaseImpl.

@@ -413,5 +413,7 @@ public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{

}

public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder) => throw new NotSupportedException();
Copy link
Member

Choose a reason for hiding this comment

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

Headless platform should try to mimic real platform as much as possible, and not throw exceptions.
For example, we can keep static counter and increment it in HeadlessWindow.Activate(), saving locally per window. And use incremented index in GetWindowsZOrder. A simple solution, which can be then used in tests by somebody.

@@ -153,5 +153,7 @@ public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
}

public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder) => throw new NotSupportedException();
Copy link
Member

@maxkatz6 maxkatz6 Mar 30, 2024

Choose a reason for hiding this comment

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

I am wondering, if we should use headless windowing subsystem in our previewer code. Unrelated to this PR.

@maxkatz6
Copy link
Member

So, do we keep this API in optional IClassicDesktopStyleApplicationLifetime? If so, API diffs should be updated, run nuke command in the repository root, and commit updated files. It will fix the build error. Since IClassicDesktopStyleApplicationLifetime interface was changed.

@BAndysc
Copy link
Contributor Author

BAndysc commented Mar 30, 2024

So, do we keep this API in optional IClassicDesktopStyleApplicationLifetime? If so, API diffs should be updated, run nuke command in the repository root, and commit updated files. It will fix the build error. Since IClassicDesktopStyleApplicationLifetime interface was changed.

Sorry, I forgot to move it to the Window class as a static method. Updated

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0046837-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@maxkatz6
Copy link
Member

Needs to be double checked by @grokys as well.

@maxkatz6 maxkatz6 merged commit 7a7ab8e into AvaloniaUI:master Apr 20, 2024
2 of 10 checks passed
maxkatz6 added a commit that referenced this pull request Apr 20, 2024
* feat: Add API for fetching window Z-order

* Addressed PR comments

* Improve X11Window::ZOrder implementation to avoid traversing windows that are not required to compute z-order

* Move zOrder API from Window to IWindowingPlatform

* Revert "Addressed PR comments"

This reverts commit 691541a.

* Rename

* Missing methods

* Move GetWindowsZOrder from IWindowingPlatform to IWindowImpl

* Cleanup

* Move SortWindowsByZOrder to Window class as a static method

* Implement zOrder for HeadlessWindowImpl

---------

Co-authored-by: Max Katz <[email protected]>
@maxkatz6 maxkatz6 added backported-11.1.x and removed backport-candidate-11.1.x Consider this PR for backporting to 11.1 branch labels Apr 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A way to query z-order of windows in xplat manner (or one of listed alternatives)
5 participants