From 1a5c8fcf3f9f92067f07385da71f2df742276fde Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 21 May 2020 01:12:19 +0300 Subject: [PATCH] X11 transparency reporting and blur support --- src/Avalonia.X11/TransparencyHelper.cs | 83 +++++++++++ src/Avalonia.X11/X11Atoms.cs | 2 + src/Avalonia.X11/X11Globals.cs | 183 +++++++++++++++++++++++++ src/Avalonia.X11/X11Platform.cs | 2 + src/Avalonia.X11/X11Window.cs | 15 +- 5 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.X11/TransparencyHelper.cs create mode 100644 src/Avalonia.X11/X11Globals.cs diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs new file mode 100644 index 0000000000..6d62d21ec6 --- /dev/null +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -0,0 +1,83 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.X11 +{ + class TransparencyHelper : IDisposable, X11Globals.IGlobalsSubscriber + { + private readonly X11Info _x11; + private readonly IntPtr _window; + private readonly X11Globals _globals; + private WindowTransparencyLevel _currentLevel; + private WindowTransparencyLevel _requestedLevel; + private bool _isCompositing; + private bool _blurAtomsAreSet; + + public Action TransparencyLevelChanged { get; set; } + public WindowTransparencyLevel CurrentLevel => _currentLevel; + + public TransparencyHelper(X11Info x11, IntPtr window, X11Globals globals) + { + _x11 = x11; + _window = window; + _globals = globals; + _globals.AddSubscriber(this); + } + + public void SetTransparencyRequest(WindowTransparencyLevel level) + { + _requestedLevel = level; + UpdateTransparency(); + } + + private void UpdateTransparency() + { + var newLevel = UpdateAtomsAndGetTransparency(); + if (newLevel != _currentLevel) + { + _currentLevel = newLevel; + TransparencyLevelChanged?.Invoke(newLevel); + } + } + + private WindowTransparencyLevel UpdateAtomsAndGetTransparency() + { + if (_requestedLevel >= WindowTransparencyLevel.Blur) + { + if (!_blurAtomsAreSet) + { + IntPtr value = IntPtr.Zero; + XLib.XChangeProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION, + _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref value, 1); + _blurAtomsAreSet = true; + } + } + else + { + if (_blurAtomsAreSet) + { + XLib.XDeleteProperty(_x11.Display, _window, _x11.Atoms._KDE_NET_WM_BLUR_BEHIND_REGION); + _blurAtomsAreSet = false; + } + } + + if (!_globals.IsCompositionEnabled) + return WindowTransparencyLevel.None; + if (_requestedLevel >= WindowTransparencyLevel.Blur && CanBlur) + return WindowTransparencyLevel.Blur; + return WindowTransparencyLevel.Transparent; + } + + + private bool CanBlur => _globals.WmName == "KWin" && _globals.IsCompositionEnabled; + + public void Dispose() + { + _globals.RemoveSubscriber(this); + } + + void X11Globals.IGlobalsSubscriber.WmChanged(string wmName) => UpdateTransparency(); + + void X11Globals.IGlobalsSubscriber.CompositionChanged(bool compositing) => UpdateTransparency(); + } +} diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index 523b65c115..dd16fae386 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -185,6 +185,8 @@ namespace Avalonia.X11 public readonly IntPtr UTF8_STRING; public readonly IntPtr UTF16_STRING; public readonly IntPtr ATOM_PAIR; + public readonly IntPtr MANAGER; + public readonly IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION; public X11Atoms(IntPtr display) diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs new file mode 100644 index 0000000000..54032831f6 --- /dev/null +++ b/src/Avalonia.X11/X11Globals.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + unsafe class X11Globals + { + private readonly AvaloniaX11Platform _plat; + private readonly int _screenNumber; + private readonly X11Info _x11; + private readonly IntPtr _rootWindow; + private readonly IntPtr _compositingAtom; + private readonly List _subscribers = new List(); + + private string _wmName; + private IntPtr _compositionAtomOwner; + private bool _isCompositionEnabled; + + public X11Globals(AvaloniaX11Platform plat) + { + _plat = plat; + _x11 = plat.Info; + _screenNumber = XDefaultScreen(_x11.Display); + _rootWindow = XRootWindow(_x11.Display, _screenNumber); + plat.Windows[_rootWindow] = OnRootWindowEvent; + XSelectInput(_x11.Display, _rootWindow, + new IntPtr((int)(EventMask.StructureNotifyMask | EventMask.PropertyChangeMask))); + _compositingAtom = XInternAtom(_x11.Display, "_NET_WM_CM_S" + _screenNumber, false); + UpdateWmName(); + UpdateCompositingAtomOwner(); + } + + public string WmName + { + get => _wmName; + private set + { + if (_wmName != value) + { + _wmName = value; + // The collection might change during enumeration + foreach (var s in _subscribers.ToList()) + s.WmChanged(value); + } + } + } + + private IntPtr CompositionAtomOwner + { + get => _compositionAtomOwner; + set + { + if (_compositionAtomOwner != value) + { + _compositionAtomOwner = value; + IsCompositionEnabled = _compositionAtomOwner != IntPtr.Zero; + } + } + } + + public bool IsCompositionEnabled + { + get => _isCompositionEnabled; + set + { + if (_isCompositionEnabled != value) + { + _isCompositionEnabled = value; + // The collection might change during enumeration + foreach (var s in _subscribers.ToList()) + s.CompositionChanged(value); + } + } + } + + + IntPtr GetSupportingWmCheck(IntPtr window) + { + XGetWindowProperty(_x11.Display, _rootWindow, _x11.Atoms._NET_SUPPORTING_WM_CHECK, + IntPtr.Zero, new IntPtr(IntPtr.Size), false, + _x11.Atoms.XA_WINDOW, out IntPtr actualType, out int actualFormat, out IntPtr nitems, + out IntPtr bytesAfter, out IntPtr prop); + if (nitems.ToInt32() != 1) + return IntPtr.Zero; + try + { + if (actualType != _x11.Atoms.XA_WINDOW) + return IntPtr.Zero; + return *(IntPtr*)prop.ToPointer(); + } + finally + { + XFree(prop); + } + } + + void UpdateCompositingAtomOwner() + { + // This procedure is described in https://tronche.com/gui/x/icccm/sec-2.html#s-2.8 + + // Check the server-side selection owner + var newOwner = XGetSelectionOwner(_x11.Display, _compositingAtom); + while (CompositionAtomOwner != newOwner) + { + // We have a new owner, unsubscribe from the previous one first + if (CompositionAtomOwner != IntPtr.Zero) + { + _plat.Windows.Remove(CompositionAtomOwner); + XSelectInput(_x11.Display, CompositionAtomOwner, IntPtr.Zero); + } + + // Set it as the current owner and select input + CompositionAtomOwner = newOwner; + if (CompositionAtomOwner != IntPtr.Zero) + { + _plat.Windows[newOwner] = HandleCompositionAtomOwnerEvents; + XSelectInput(_x11.Display, CompositionAtomOwner, new IntPtr((int)(EventMask.StructureNotifyMask))); + } + + // Check for the new owner again and repeat the procedure if it was changed between XGetSelectionOwner and XSelectInput call + newOwner = XGetSelectionOwner(_x11.Display, _compositingAtom); + } + } + + private void HandleCompositionAtomOwnerEvents(XEvent ev) + { + if(ev.type == XEventName.DestroyNotify) + UpdateCompositingAtomOwner(); + } + + void UpdateWmName() => WmName = GetWmName(); + + string GetWmName() + { + var wm = GetSupportingWmCheck(_rootWindow); + if (wm == IntPtr.Zero || wm != GetSupportingWmCheck(wm)) + return null; + XGetWindowProperty(_x11.Display, wm, _x11.Atoms._NET_WM_NAME, + IntPtr.Zero, new IntPtr(0x7fffffff), + false, _x11.Atoms.UTF8_STRING, out var actualType, out var actualFormat, + out var nitems, out _, out var prop); + if (nitems == IntPtr.Zero) + return null; + try + { + if (actualFormat != 8) + return null; + return Marshal.PtrToStringAnsi(prop, nitems.ToInt32()); + } + finally + { + XFree(prop); + } + } + + private void OnRootWindowEvent(XEvent ev) + { + if (ev.type == XEventName.PropertyNotify) + { + if(ev.PropertyEvent.atom == _x11.Atoms._NET_SUPPORTING_WM_CHECK) + UpdateWmName(); + } + + if (ev.type == XEventName.ClientMessage) + { + if(ev.ClientMessageEvent.message_type == _x11.Atoms.MANAGER + && ev.ClientMessageEvent.ptr2 == _compositingAtom) + UpdateCompositingAtomOwner(); + } + } + + public interface IGlobalsSubscriber + { + void WmChanged(string wmName); + void CompositionChanged(bool compositing); + } + + public void AddSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Add(subscriber); + public void RemoveSubscriber(IGlobalsSubscriber subscriber) => _subscribers.Remove(subscriber); + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a1bfa682f5..c2b3926ffd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -26,6 +26,7 @@ namespace Avalonia.X11 public IX11Screens X11Screens { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } + public X11Globals Globals { get; private set; } public void Initialize(X11PlatformOptions options) { Options = options; @@ -36,6 +37,7 @@ namespace Avalonia.X11 throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay); + Globals = new X11Globals(this); //TODO: log if (options.UseDBusMenu) DBusHelper.TryInitialize(); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index bce82b2954..4899f0efc0 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -43,6 +43,7 @@ namespace Avalonia.X11 private bool _wasMappedAtLeastOnce = false; private double? _scalingOverride; private bool _disabled; + private TransparencyHelper _transparencyHelper; public object SyncRoot { get; } = new object(); @@ -176,6 +177,9 @@ namespace Avalonia.X11 UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); + _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); + _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); + XFlush(_x11.Display); if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); @@ -301,7 +305,11 @@ namespace Avalonia.X11 public Func Closing { get; set; } public Action WindowStateChanged { get; set; } - public Action TransparencyLevelChanged { get; set; } + public Action TransparencyLevelChanged + { + get => _transparencyHelper.TransparencyLevelChanged; + set => _transparencyHelper.TransparencyLevelChanged = value; + } public Action Closed { get; set; } public Action PositionChanged { get; set; } @@ -1087,8 +1095,9 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => + _transparencyHelper.SetTransparencyRequest(transparencyLevel); - public WindowTransparencyLevel TransparencyLevel { get; private set; } + public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel; } }