Browse Source

[X11] attempt to use _NET_WM_STATE_FOCUSED or _NET_ACTIVE_WINDOW for tracking window activation, if available (#18464)

Nikita Tsukanov 8 months ago
parent
commit
410b46edfc

+ 91 - 0
src/Avalonia.X11/ActivityTrackingHelper.cs

@@ -0,0 +1,91 @@
+using System;
+using System.Linq;
+using Avalonia.Threading;
+
+namespace Avalonia.X11;
+
+internal class WindowActivationTrackingHelper : IDisposable
+{
+    private readonly AvaloniaX11Platform _platform;
+    private readonly X11Window _window;
+    private bool _active;
+
+    public event Action<bool>? ActivationChanged;
+    
+    public WindowActivationTrackingHelper(AvaloniaX11Platform platform, X11Window window)
+    {
+        _platform = platform;
+        _window = window;
+        _platform.Globals.NetActiveWindowPropertyChanged += OnNetActiveWindowChanged;
+        _platform.Globals.WindowActivationTrackingModeChanged += OnWindowActivationTrackingModeChanged;
+    }
+
+    void SetActive(bool active)
+    {
+        if (active != _active)
+        {
+            _active = active;
+            ActivationChanged?.Invoke(active);
+        }
+    }
+
+    void RequeryActivation()
+    {
+        // Update the active state from WM-set properties
+        
+        if (Mode == X11Globals.WindowActivationTrackingMode._NET_ACTIVE_WINDOW) 
+            OnNetActiveWindowChanged();
+        
+        if (Mode == X11Globals.WindowActivationTrackingMode._NET_WM_STATE_FOCUSED)
+            OnNetWmStateChanged(XLib.XGetWindowPropertyAsIntPtrArray(_platform.Display, _window.Handle.Handle,
+                _platform.Info.Atoms._NET_WM_STATE, _platform.Info.Atoms.XA_ATOM) ?? []);
+    }
+
+    private void OnWindowActivationTrackingModeChanged() =>
+        DispatcherTimer.RunOnce(RequeryActivation, TimeSpan.FromSeconds(1), DispatcherPriority.Input);
+
+    private X11Globals.WindowActivationTrackingMode Mode => _platform.Globals.ActivationTrackingMode;
+    
+    public void OnEvent(ref XEvent ev)
+    {
+        if (ev.type is not XEventName.FocusIn and not XEventName.FocusOut)
+            return;
+        
+        // Always attempt to activate transient children on focus events
+        if (ev.type == XEventName.FocusIn && _window.ActivateTransientChildIfNeeded()) return;
+
+        if (Mode != X11Globals.WindowActivationTrackingMode.FocusEvents)
+            return;
+        
+        // See: https://github.com/fltk/fltk/issues/295
+        if ((NotifyMode)ev.FocusChangeEvent.mode is not NotifyMode.NotifyNormal)
+            return;
+        
+        SetActive(ev.type == XEventName.FocusIn);
+    }
+    
+    private void OnNetActiveWindowChanged()
+    {
+        if (Mode == X11Globals.WindowActivationTrackingMode._NET_ACTIVE_WINDOW)
+        {
+            var value = XLib.XGetWindowPropertyAsIntPtrArray(_platform.Display, _platform.Info.RootWindow,
+                _platform.Info.Atoms._NET_ACTIVE_WINDOW,
+                (IntPtr)_platform.Info.Atoms.XA_WINDOW);
+            if (value == null || value.Length == 0)
+                SetActive(false);
+            else
+                SetActive(value[0] == _window.Handle.Handle);
+        }
+    }
+
+    public void Dispose()
+    {
+        _platform.Globals.NetActiveWindowPropertyChanged -= OnNetActiveWindowChanged;
+    }
+
+    public void OnNetWmStateChanged(IntPtr[] atoms)
+    {
+        if (Mode == X11Globals.WindowActivationTrackingMode._NET_WM_STATE_FOCUSED)
+            SetActive(atoms.Contains(_platform.Info.Atoms._NET_WM_STATE_FOCUSED));
+    }
+}

+ 1 - 0
src/Avalonia.X11/X11Atoms.cs

@@ -193,6 +193,7 @@ namespace Avalonia.X11
         public IntPtr MANAGER;
         public IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION;
         public IntPtr INCR;
+        public IntPtr _NET_WM_STATE_FOCUSED;
 
         private readonly Dictionary<string, IntPtr> _namesToAtoms  = new Dictionary<string, IntPtr>();
         private readonly Dictionary<IntPtr, string> _atomsToNames = new Dictionary<IntPtr, string>();

+ 69 - 8
src/Avalonia.X11/X11Globals.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using System.Runtime.InteropServices;
 using static Avalonia.X11.XLib;
 
@@ -15,11 +16,21 @@ namespace Avalonia.X11
         private string? _wmName;
         private IntPtr _compositionAtomOwner;
         private bool _isCompositionEnabled;
+        private WindowActivationTrackingMode _activationTrackingMode;
 
         public event Action? WindowManagerChanged;
         public event Action? CompositionChanged;
         public event Action<IntPtr>? RootPropertyChanged;
+        public event Action? NetActiveWindowPropertyChanged;
         public event Action? RootGeometryChangedChanged;
+        public event Action? WindowActivationTrackingModeChanged;
+        
+        public enum WindowActivationTrackingMode
+        {
+            FocusEvents,
+            _NET_ACTIVE_WINDOW,
+            _NET_WM_STATE_FOCUSED
+        }
 
         public X11Globals(AvaloniaX11Platform plat)
         {
@@ -31,7 +42,7 @@ namespace Avalonia.X11
             XSelectInput(_x11.Display, _rootWindow,
                 new IntPtr((int)(EventMask.StructureNotifyMask | EventMask.PropertyChangeMask)));
             _compositingAtom = XInternAtom(_x11.Display, "_NET_WM_CM_S" + _screenNumber, false);
-            UpdateWmName();
+            OnNewWindowManager();
             UpdateCompositingAtomOwner();
         }
         
@@ -73,6 +84,19 @@ namespace Avalonia.X11
                 }
             }
         }
+        
+        public WindowActivationTrackingMode ActivationTrackingMode
+        {
+            get => _activationTrackingMode;
+            set
+            {
+                if (_activationTrackingMode != value)
+                {
+                    _activationTrackingMode = value;
+                    WindowActivationTrackingModeChanged?.Invoke();
+                }
+            }
+        }
 
         private IntPtr GetSupportingWmCheck(IntPtr window)
         {
@@ -128,12 +152,15 @@ namespace Avalonia.X11
                 UpdateCompositingAtomOwner();
         }
 
-        private void UpdateWmName() => WmName = GetWmName();
-
-        private string? GetWmName()
+        IntPtr GetActiveWm() => GetSupportingWmCheck(_rootWindow) is { } wmWindow
+                                && wmWindow != IntPtr.Zero
+                                && wmWindow == GetSupportingWmCheck(wmWindow)
+            ? wmWindow
+            : IntPtr.Zero;
+        
+        private string? GetWmName(IntPtr wm)
         {
-            var wm = GetSupportingWmCheck(_rootWindow);
-            if (wm == IntPtr.Zero || wm != GetSupportingWmCheck(wm))
+            if (wm == IntPtr.Zero)
                 return null;
             XGetWindowProperty(_x11.Display, wm, _x11.Atoms._NET_WM_NAME,
                 IntPtr.Zero, new IntPtr(0x7fffffff),
@@ -152,13 +179,47 @@ namespace Avalonia.X11
                 XFree(prop);
             }
         }
+
+        private WindowActivationTrackingMode GetWindowActivityTrackingMode(IntPtr wm)
+        {
+            if (Environment.GetEnvironmentVariable("AVALONIA_DEBUG_FORCE_X11_ACTIVATION_TRACKING_MODE") is
+                    { } forcedModeString
+                && Enum.TryParse<WindowActivationTrackingMode>(forcedModeString, true, out var forcedMode))
+                return forcedMode;
+            
+            if (wm == IntPtr.Zero)
+                return WindowActivationTrackingMode.FocusEvents;
+            var supportedFeatures = XGetWindowPropertyAsIntPtrArray(_x11.Display, _x11.RootWindow,
+                _x11.Atoms._NET_SUPPORTED, _x11.Atoms.XA_ATOM) ?? [];
+
+            if (supportedFeatures.Contains(_x11.Atoms._NET_WM_STATE_FOCUSED))
+                return WindowActivationTrackingMode._NET_WM_STATE_FOCUSED;
+            
+            if (supportedFeatures.Contains(_x11.Atoms._NET_ACTIVE_WINDOW))
+                return WindowActivationTrackingMode._NET_ACTIVE_WINDOW;
+            
+            return WindowActivationTrackingMode.FocusEvents;
+        }
+
+        private void OnNewWindowManager()
+        {
+            var wm = GetActiveWm();
+            WmName = GetWmName(wm);
+            ActivationTrackingMode = GetWindowActivityTrackingMode(wm);
+        }
         
         private void OnRootWindowEvent(ref XEvent ev)
         {
             if (ev.type == XEventName.PropertyNotify)
             {
-                if(ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK)
-                    UpdateWmName();
+                if (ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK)
+                {
+                    OnNewWindowManager();
+                }
+
+                if (ev.PropertyEvent.atom == _x11.Atoms._NET_ACTIVE_WINDOW)
+                    NetActiveWindowPropertyChanged?.Invoke();
+
                 RootPropertyChanged?.Invoke(ev.PropertyEvent.atom);
             }
 

+ 60 - 56
src/Avalonia.X11/X11Window.cs

@@ -63,6 +63,7 @@ namespace Avalonia.X11
         private double? _scalingOverride;
         private bool _disabled;
         private TransparencyHelper? _transparencyHelper;
+        private WindowActivationTrackingHelper? _activationTracker;
         private RawEventGrouper? _rawEventGrouper;
         private bool _useRenderWindow = false;
         private bool _useCompositorDrivenRenderWindowResize = false;
@@ -231,6 +232,9 @@ namespace Avalonia.X11
             _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
             _transparencyHelper.SetTransparencyRequest(Array.Empty<WindowTransparencyLevel>());
 
+            _activationTracker = new(_platform, this);
+            _activationTracker.ActivationChanged += HandleActivation;
+
             CreateIC();
 
             XFlush(_x11.Display);
@@ -510,6 +514,8 @@ namespace Avalonia.X11
             if(_mode.OnEvent(ref ev))
                 return;
 
+            _activationTracker?.OnEvent(ref ev);
+
             if (ev.type == XEventName.MapNotify)
             {
                 _mapped = true;
@@ -524,24 +530,6 @@ namespace Avalonia.X11
             {
                 EnqueuePaint();
             }
-            else if (ev.type == XEventName.FocusIn)
-            {
-                if (ActivateTransientChildIfNeeded())
-                    return;
-                // See: https://github.com/fltk/fltk/issues/295
-                if ((NotifyMode)ev.FocusChangeEvent.mode is not NotifyMode.NotifyNormal)
-                    return;
-                Activated?.Invoke();
-                _imeControl?.SetWindowActive(true);
-            }
-            else if (ev.type == XEventName.FocusOut)
-            {
-                // See: https://github.com/fltk/fltk/issues/295
-                if ((NotifyMode)ev.FocusChangeEvent.mode is not NotifyMode.NotifyNormal)
-                    return;
-                _imeControl?.SetWindowActive(false);
-                Deactivated?.Invoke();
-            }
             else if (ev.type == XEventName.MotionNotify)
                 MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state);
             else if (ev.type == XEventName.LeaveNotify)
@@ -679,6 +667,22 @@ namespace Avalonia.X11
             }
         }
 
+        private void HandleActivation(bool active)
+        {
+            if (active)
+            {
+                if (ActivateTransientChildIfNeeded())
+                    return;
+                Activated?.Invoke();
+                _imeControl?.SetWindowActive(true);
+            }
+            else
+            {
+                _imeControl?.SetWindowActive(false);
+                Deactivated?.Invoke();
+            }
+        }
+
         private Thickness? GetFrameExtents()
         {
             if (_systemDecorations != SystemDecorations.Full)
@@ -773,58 +777,56 @@ namespace Avalonia.X11
             }
         }
 
-        private void OnPropertyChange(IntPtr atom, bool hasValue)
+        private void OnPropertyChange(IntPtr property, bool hasValue)
         {
-            if (atom == _x11.Atoms._NET_FRAME_EXTENTS)
+            if (property == _x11.Atoms._NET_FRAME_EXTENTS)
             {
                 // Occurs once the window has been mapped, which is the earliest the extents
                 // can be retrieved, so invoke event to force update of TopLevel.FrameSize.
                 Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified);
             }
 
-            if (atom == _x11.Atoms._NET_WM_STATE)
+            if (property == _x11.Atoms._NET_WM_STATE)
             {
                 WindowState state = WindowState.Normal;
-                if(hasValue)
+                var atoms = hasValue
+                    ? XGetWindowPropertyAsIntPtrArray(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE,
+                          (IntPtr)Atom.XA_ATOM)
+                      ?? []
+                    : [];
+                int maximized = 0;
+                foreach (var atom in atoms)
                 {
+                    if (atom == _x11.Atoms._NET_WM_STATE_HIDDEN)
+                    {
+                        state = WindowState.Minimized;
+                        break;
+                    }
 
-                    XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE, IntPtr.Zero, new IntPtr(256),
-                        false, (IntPtr)Atom.XA_ATOM, out _, out _, out var nitems, out _,
-                        out var prop);
-                    int maximized = 0;
-                    var pitems = (IntPtr*)prop.ToPointer();
-                    for (var c = 0; c < nitems.ToInt32(); c++)
+                    if(atom == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
                     {
-                        if (pitems[c] == _x11.Atoms._NET_WM_STATE_HIDDEN)
-                        {
-                            state = WindowState.Minimized;
-                            break;
-                        }
+                        state = WindowState.FullScreen;
+                        break;
+                    }
 
-                        if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
+                    if (atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
+                        atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
+                    {
+                        maximized++;
+                        if (maximized == 2)
                         {
-                            state = WindowState.FullScreen;
+                            state = WindowState.Maximized;
                             break;
                         }
-
-                        if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
-                            pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
-                        {
-                            maximized++;
-                            if (maximized == 2)
-                            {
-                                state = WindowState.Maximized;
-                                break;
-                            }
-                        }
                     }
-                    XFree(prop);
                 }
                 if (_lastWindowState != state)
                 {
                     _lastWindowState = state;
                     WindowStateChanged?.Invoke(state);
                 }
+
+                _activationTracker?.OnNetWmStateChanged(atoms);
             }
 
         }
@@ -1030,6 +1032,12 @@ namespace Avalonia.X11
                 _rawEventGrouper = null;
             }
             
+            if (_activationTracker != null)
+            {
+                _activationTracker.Dispose();
+                _activationTracker = null;
+            }
+            
             if (_transparencyHelper != null)
             {
                 _transparencyHelper.Dispose();
@@ -1075,7 +1083,7 @@ namespace Avalonia.X11
             }
         }
 
-        private bool ActivateTransientChildIfNeeded()
+        internal bool ActivateTransientChildIfNeeded()
         {
             if (_disabled)
             {
@@ -1448,14 +1456,10 @@ namespace Avalonia.X11
 
             if (!_mapped)
             {
-                XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE, IntPtr.Zero, new IntPtr(256),
-                    false, (IntPtr)Atom.XA_ATOM, out _, out _, out var nitems, out _,
-                    out var prop);
-                var ptr = (IntPtr*)prop.ToPointer();
-                var newAtoms = new HashSet<IntPtr>();
-                for (var c = 0; c < nitems.ToInt64(); c++) 
-                    newAtoms.Add(*(ptr+c));
-                XFree(prop);
+                var newAtoms = new HashSet<IntPtr>(XGetWindowPropertyAsIntPtrArray(_x11.Display, _handle,
+                    _x11.Atoms._NET_WM_STATE,
+                    (IntPtr)Atom.XA_ATOM) ?? []);
+                
                 foreach(var atom in atoms)
                     if (enable)
                         newAtoms.Add(atom);

+ 29 - 0
src/Avalonia.X11/XLib.Helpers.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.X11;
+
+internal static partial class XLib
+{
+    public static IntPtr[]? XGetWindowPropertyAsIntPtrArray(IntPtr display, IntPtr window, IntPtr atom, IntPtr reqType)
+    {
+        if (XGetWindowProperty(display, window, atom, IntPtr.Zero, new IntPtr(0x7fffffff),
+                false, reqType, out var actualType, out var actualFormat, out var nitems, out _,
+                out var prop) != 0)
+            return null;
+
+        try
+        {
+            if (actualType != reqType || actualFormat != 32 || nitems == IntPtr.Zero)
+                return null;
+
+            var buffer = new IntPtr[nitems.ToInt32()];
+            Marshal.Copy(prop, buffer, 0, buffer.Length);
+            return buffer;
+        }
+        finally
+        {
+            XFree(prop);
+        }
+    }
+}

+ 1 - 1
src/Avalonia.X11/XLib.cs

@@ -14,7 +14,7 @@ using Avalonia.Platform.Interop;
 
 namespace Avalonia.X11
 {
-    internal unsafe static class XLib
+    internal unsafe static partial class XLib
     {
         private const string libX11 = "libX11.so.6";
         private const string libX11Randr = "libXrandr.so.2";