diff --git a/src/Avalonia.X11/ActivityTrackingHelper.cs b/src/Avalonia.X11/ActivityTrackingHelper.cs new file mode 100644 index 0000000000..a3bfca5e90 --- /dev/null +++ b/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? 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)); + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index ad4b841e1e..b851974bad 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/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 _namesToAtoms = new Dictionary(); private readonly Dictionary _atomsToNames = new Dictionary(); diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs index c2593bf185..a81f69f8f3 100644 --- a/src/Avalonia.X11/X11Globals.cs +++ b/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? 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(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); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 7fbeaa35cd..7cf4d3e2bb 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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()); + _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(); - for (var c = 0; c < nitems.ToInt64(); c++) - newAtoms.Add(*(ptr+c)); - XFree(prop); + var newAtoms = new HashSet(XGetWindowPropertyAsIntPtrArray(_x11.Display, _handle, + _x11.Atoms._NET_WM_STATE, + (IntPtr)Atom.XA_ATOM) ?? []); + foreach(var atom in atoms) if (enable) newAtoms.Add(atom); diff --git a/src/Avalonia.X11/XLib.Helpers.cs b/src/Avalonia.X11/XLib.Helpers.cs new file mode 100644 index 0000000000..b32d5e3b26 --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 3daf9fff1e..cfd3a03c8f 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/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";