Browse Source

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 <[email protected]>
Bartosz Korczyński 1 year ago
parent
commit
7a7ab8e5f8

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

+ 14 - 0
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;
 

+ 10 - 1
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -143,6 +143,15 @@ namespace Avalonia.Platform
         /// Sets how big the non-client titlebar area should be.
         /// </summary>
         /// <param name="titleBarHeight">-1 for platform default, otherwise the height in DIPs.</param>
-        void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight);       
+        void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight);
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="windows">A span of windows to get their z-order</param>
+        /// <param name="zOrder">Span to be filled with associated window z-order</param>
+        internal void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder);
     }
 }

+ 23 - 0
src/Avalonia.Controls/Window.cs

@@ -842,6 +842,29 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Sorts the windows ascending by their Z order - the topmost window will be the last in the list.
+        /// </summary>
+        /// <param name="windows"></param>
+        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<long> 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;

+ 2 - 0
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -153,5 +153,7 @@ namespace Avalonia.DesignerSupport.Remote
         public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
         {            
         }
+
+        public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder) => throw new NotSupportedException();
     }
 }

+ 2 - 0
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -178,6 +178,8 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
+        public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder) => throw new NotSupportedException();
+
         public IPopupPositioner PopupPositioner { get; }
 
         public Action GotInputWhenDisabled { get; set; }

+ 10 - 0
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<Window> windows, Span<long> zOrder)
+        {
+            for (int i = 0; i < windows.Length; i++)
+            {
+                zOrder[i] = (windows[i].PlatformImpl as WindowImpl)?.ZOrder?.ToInt64() ?? 0;
+            }
+        }
     }
 }

+ 2 - 0
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)]

+ 74 - 0
src/Avalonia.X11/X11Window.cs

@@ -1460,5 +1460,79 @@ namespace Avalonia.X11
                 32, PropertyMode.Replace, new[] { atom }, 1);
 
         }
+
+        /// <inheritdoc/>
+        public void GetWindowsZOrder(Span<Window> windows, Span<long> outputZOrder)
+        {
+            // a mapping of parent windows to their children, sorted by z-order (bottom to top)
+            var windowsChildren = new Dictionary<IntPtr, List<IntPtr>>();
+
+            var indexInWindowsSpan = new Dictionary<IntPtr, int>();
+            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<IntPtr>(childrenCount);
+                        for (var i = 0; i < childrenCount; i++)
+                        {
+                            windowsChildren[node].Add(children[i]);
+                        }
+                        XFree(childrenPtr);
+                    }
+
+                    node = parent;
+                }
+            }
+
+            var stack = new Stack<IntPtr>();
+            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]);
+                }
+            }
+        }
     }
 }

+ 16 - 0
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<PixelPoint>? 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<Window> windows, Span<long> zOrder)
+        {
+            for (int i = 0; i < windows.Length; ++i)
+            {
+                if (windows[i].PlatformImpl is HeadlessWindowImpl headlessWindowImpl)
+                    zOrder[i] = headlessWindowImpl._zOrder;
+            }
+        }
     }
 }

+ 5 - 0
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);
 

+ 34 - 0
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<WindowCloseReason, bool>? Closing { get; set; }
@@ -1573,6 +1575,38 @@ namespace Avalonia.Win32
             ExtendClientArea();
         }
 
+        /// <inheritdoc/>
+        public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder)
+        {
+            var handlesToIndex = new Dictionary<IntPtr, int>(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];
+            }
+        }
+
         /// <inheritdoc/>
         public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended;