From 213c17b7b779c18ba8f4b7aaaa4a02ed80e16753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Korczy=C5=84ski?= Date: Sat, 20 Apr 2024 02:07:04 +0100 Subject: [PATCH] feat: Add API for fetching window Z-order (#14909) * 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 691541adf65ebc80ae023121d47382ded3cc4a4c. * 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 --- native/Avalonia.Native/src/OSX/WindowImpl.h | 2 + native/Avalonia.Native/src/OSX/WindowImpl.mm | 14 ++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 11 ++- src/Avalonia.Controls/Window.cs | 23 ++++++ .../Remote/PreviewerWindowImpl.cs | 2 + src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 + src/Avalonia.Native/WindowImpl.cs | 10 +++ src/Avalonia.Native/avn.idl | 2 + src/Avalonia.X11/X11Window.cs | 74 +++++++++++++++++++ .../Avalonia.Headless/HeadlessWindowImpl.cs | 16 ++++ .../Interop/UnmanagedMethods.cs | 5 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 34 +++++++++ 12 files changed, 194 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 049ef755ff..047a0d2c84 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -82,6 +82,8 @@ BEGIN_INTERFACE_MAP() virtual HRESULT GetExtendTitleBarHeight (double*ret) override; virtual HRESULT SetExtendTitleBarHeight (double value) override; + + virtual HRESULT GetWindowZOrder (long* zOrder) override; void EnterFullScreenMode (); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index be761e0af7..1cdf81e2fb 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -366,6 +366,20 @@ HRESULT WindowImpl::GetWindowState(AvnWindowState *ret) { } } +HRESULT WindowImpl::GetWindowZOrder(long* zOrder) { + START_COM_CALL; + @autoreleasepool { + if (zOrder == nullptr) { + return E_POINTER; + } + + // negate the value to match expected z-order in Avalonia + // (top-most window should have the highest z-order value) + *zOrder = -[Window orderedIndex]; + return S_OK; + } +} + HRESULT WindowImpl::TakeFocusFromChildren() { START_COM_CALL; diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index a66e5e138f..383515b9f9 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -143,6 +143,15 @@ namespace Avalonia.Platform /// Sets how big the non-client titlebar area should be. /// /// -1 for platform default, otherwise the height in DIPs. - void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight); + void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight); + + /// + /// Fills zOrder with numbers that represent the relative order of the windows in the z-order. + /// The topmost window should have the highest number. + /// Both the windows and zOrder lists are expected to be the same length. + /// + /// A span of windows to get their z-order + /// Span to be filled with associated window z-order + internal void GetWindowsZOrder(Span windows, Span zOrder); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index b2c9ebd986..a3c70d5b82 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -842,6 +842,29 @@ namespace Avalonia.Controls } } + /// + /// Sorts the windows ascending by their Z order - the topmost window will be the last in the list. + /// + /// + public static void SortWindowsByZOrder(Window[] windows) + { + if (windows.Length == 0) + return; + + if (windows[0].PlatformImpl is not { } platformImpl) + throw new InvalidOperationException("Window.PlatformImpl is null"); + +#if NET5_0_OR_GREATER + Span zOrder = stackalloc long[windows.Length]; + platformImpl.GetWindowsZOrder(windows, zOrder); + zOrder.Sort(windows.AsSpan()); +#else + long[] zOrder = new long[windows.Length]; + platformImpl.GetWindowsZOrder(windows, zOrder); + Array.Sort(zOrder, windows); +#endif + } + private void UpdateEnabled() { bool isEnabled = true; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 9463224b99..f6598d0928 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -153,5 +153,7 @@ namespace Avalonia.DesignerSupport.Remote public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { } + + public void GetWindowsZOrder(Span windows, Span zOrder) => throw new NotSupportedException(); } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index cc183135ba..568e56a130 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -178,6 +178,8 @@ namespace Avalonia.DesignerSupport.Remote { } + public void GetWindowsZOrder(Span windows, Span zOrder) => throw new NotSupportedException(); + public IPopupPositioner PopupPositioner { get; } public Action GotInputWhenDisabled { get; set; } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 11b264bfdd..10a0afc6da 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -103,6 +103,8 @@ namespace Avalonia.Native public Thickness OffScreenMargin { get; } = new Thickness(); + public IntPtr? ZOrder => _native.WindowZOrder; + private bool _isExtended; public bool IsClientAreaExtendedToDecorations => _isExtended; @@ -237,5 +239,13 @@ namespace Avalonia.Native return base.TryGetFeature(featureType); } + + public void GetWindowsZOrder(Span windows, Span zOrder) + { + for (int i = 0; i < windows.Length; i++) + { + zOrder[i] = (windows[i].PlatformImpl as WindowImpl)?.ZOrder?.ToInt64() ?? 0; + } + } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index e3ffe11034..5ea53d22d1 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -2,6 +2,7 @@ @clr-access internal @clr-map bool int @clr-map u_int64_t ulong +@clr-map long IntPtr @cpp-preamble @@ #pragma once #include "com.h" @@ -750,6 +751,7 @@ interface IAvnWindow : IAvnWindowBase HRESULT SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints); HRESULT GetExtendTitleBarHeight(double*ret); HRESULT SetExtendTitleBarHeight(double value); + HRESULT GetWindowZOrder(long*ret); } [uuid(939b6599-40a8-4710-a4c8-5d72d8f174fb)] diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 55387057e3..85eda99418 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1460,5 +1460,79 @@ namespace Avalonia.X11 32, PropertyMode.Replace, new[] { atom }, 1); } + + /// + public void GetWindowsZOrder(Span windows, Span outputZOrder) + { + // a mapping of parent windows to their children, sorted by z-order (bottom to top) + var windowsChildren = new Dictionary>(); + + var indexInWindowsSpan = new Dictionary(); + for (var i = 0; i < windows.Length; i++) + if (windows[i].PlatformImpl is { } platformImpl) + indexInWindowsSpan[platformImpl.Handle.Handle] = i; + + foreach (var window in windows) + { + if (window.PlatformImpl is not X11Window x11Window) + continue; + + var node = x11Window.Handle.Handle; + while (node != IntPtr.Zero) + { + if (windowsChildren.ContainsKey(node)) + { + break; + } + + if (XQueryTree(_x11.Display, node, out _, out var parent, + out var childrenPtr, out var childrenCount) == 0) + { + break; + } + + if (childrenPtr != IntPtr.Zero) + { + var children = (IntPtr*)childrenPtr; + windowsChildren[node] = new List(childrenCount); + for (var i = 0; i < childrenCount; i++) + { + windowsChildren[node].Add(children[i]); + } + XFree(childrenPtr); + } + + node = parent; + } + } + + var stack = new Stack(); + var zOrder = 0; + stack.Push(_x11.RootWindow); + + while (stack.Count > 0) + { + var currentWindow = stack.Pop(); + + if (!windowsChildren.TryGetValue(currentWindow, out var children)) + { + continue; + } + + if (indexInWindowsSpan.TryGetValue(currentWindow, out var index)) + { + outputZOrder[index] = zOrder; + } + + zOrder++; + + // Children are returned bottom to top, so we need to push them in reverse order + // In order to traverse bottom children first + for (int i = children.Count - 1; i >= 0; i--) + { + stack.Push(children[i]); + } + } + } } } diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 815452bf30..e0d6df5c53 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -16,12 +16,15 @@ namespace Avalonia.Headless { internal class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow { + private static int _nextGlobalZOrder = 1; + private readonly IKeyboardDevice _keyboard; private readonly Stopwatch _st = Stopwatch.StartNew(); private readonly Pointer _mousePointer; private WriteableBitmap? _lastRenderedFrame; private readonly object _sync = new object(); private readonly PixelFormat _frameBufferFormat; + private int _zOrder; public bool IsPopup { get; } public HeadlessWindowImpl(bool isPopup, PixelFormat frameBufferFormat) @@ -80,7 +83,10 @@ namespace Avalonia.Headless public void Show(bool activate, bool isDialog) { if (activate) + { + _zOrder = _nextGlobalZOrder++; Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); + } } public void Hide() @@ -102,6 +108,7 @@ namespace Avalonia.Headless public Action? PositionChanged { get; set; } public void Activate() { + _zOrder = _nextGlobalZOrder++; Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); } @@ -412,5 +419,14 @@ namespace Avalonia.Headless { } + + public void GetWindowsZOrder(Span windows, Span zOrder) + { + for (int i = 0; i < windows.Length; ++i) + { + if (windows[i].PlatformImpl is HeadlessWindowImpl headlessWindowImpl) + zOrder[i] = headlessWindowImpl._zOrder; + } + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 89ae3c5db7..8ed687a330 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1274,6 +1274,11 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern int GetSystemMetrics(SystemMetric smIndex); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool EnumChildWindows(IntPtr parentHwnd, EnumWindowsProc enumFunc, IntPtr lParam); + + public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + [DllImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLongPtrW", ExactSpelling = true)] public static extern uint GetWindowLongPtr(IntPtr hWnd, int nIndex); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e34a64a267..fa2223a466 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -184,6 +184,8 @@ namespace Avalonia.Win32 internal IInputRoot Owner => _owner ?? throw new InvalidOperationException($"{nameof(SetInputRoot)} must have been called"); + internal WindowImpl? ParentImpl => _parent; + public Action? Activated { get; set; } public Func? Closing { get; set; } @@ -1573,6 +1575,38 @@ namespace Avalonia.Win32 ExtendClientArea(); } + /// + public void GetWindowsZOrder(Span windows, Span zOrder) + { + var handlesToIndex = new Dictionary(windows.Length); + var outputArray = new long[windows.Length]; + + for (int i = 0; i < windows.Length; i++) + { + if (windows[i].PlatformImpl is WindowImpl platformImpl) + handlesToIndex.Add(platformImpl.Handle.Handle, i); + } + + long nextZOrder = 0; + bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam) + { + if (handlesToIndex.TryGetValue(hWnd, out var index)) + { + // We negate the z-order so that the topmost window has the highest number. + outputArray[index] = -nextZOrder; + nextZOrder++; + } + return nextZOrder < outputArray.Length; + } + + EnumChildWindows(IntPtr.Zero, EnumWindowsProc, IntPtr.Zero); + + for (int i = 0; i < windows.Length; i++) + { + zOrder[i] = outputArray[i]; + } + } + /// public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended;