From 1ea054ad65e8427f890fbedb48d255beb728d1e6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 4 Sep 2023 08:16:32 +0200 Subject: [PATCH] Merge pull request #12751 from vlad-lubenskyi/x11-focus-proxy Use the focus proxy window in X11 --- src/Avalonia.X11/X11FocusProxy.cs | 90 +++++++++++++++++++++++++++++++ src/Avalonia.X11/X11Window.cs | 20 +++++-- 2 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.X11/X11FocusProxy.cs diff --git a/src/Avalonia.X11/X11FocusProxy.cs b/src/Avalonia.X11/X11FocusProxy.cs new file mode 100644 index 0000000000..4fdaf643ab --- /dev/null +++ b/src/Avalonia.X11/X11FocusProxy.cs @@ -0,0 +1,90 @@ +using System; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11 +{ + /// + /// An invisible X window that owns the input focus and forwards events to the owner window. + /// + /// + /// + /// This is a known Linux technique for an auxiliary invisible window to hold the input focus + /// for the main window. It is required by XEmbed protocol, but it also works for regular cases + /// that don't imply embedded windows. + /// + /// + /// + /// + /// + internal class X11FocusProxy + { + private const int InvisibleBorder = 0; + private const int DepthCopyFromParent = 0; + private readonly IntPtr _visualCopyFromParent = IntPtr.Zero; + private readonly (int X, int Y) _outOfScreen = (-1, -1); + private readonly (int Width, int Height) _smallest = (1, 1); + + internal IntPtr _handle; + private readonly AvaloniaX11Platform _platform; + private readonly X11PlatformThreading.EventHandler _ownerEventHandler; + + /// + /// Initializes instance and creates the underlying X window. + /// + /// + /// The X11 platform. + /// The parent window to proxy the focus for. + /// An event handler that will handle X events that come to the proxy. + internal X11FocusProxy(AvaloniaX11Platform platform, IntPtr parent, + X11PlatformThreading.EventHandler eventHandler) + { + _handle = PrepareXWindow(platform.Info.Display, parent); + _platform = platform; + _ownerEventHandler = eventHandler; + _platform.Windows[_handle] = OnEvent; + } + + internal void Cleanup() + { + if (_handle != IntPtr.Zero) + { + _platform.Windows.Remove(_handle); + _handle = IntPtr.Zero; + } + } + + private void OnEvent(ref XEvent ev) + { + if (ev.type is XEventName.FocusIn or XEventName.FocusOut) + { + this._ownerEventHandler(ref ev); + } + + if (ev.type is XEventName.KeyPress or XEventName.KeyRelease) + { + this._ownerEventHandler(ref ev); + } + } + + private IntPtr PrepareXWindow(IntPtr display, IntPtr parent) + { + var valueMask = default(EventMask) + | EventMask.FocusChangeMask + | EventMask.KeyPressMask + | EventMask.KeyReleaseMask; + var attrs = new XSetWindowAttributes(); + var handle = XCreateWindow(display, parent, + _outOfScreen.X, _outOfScreen.Y, + _smallest.Width, _smallest.Height, + InvisibleBorder, + DepthCopyFromParent, + (int)CreateWindowArgs.InputOutput, + _visualCopyFromParent, + new UIntPtr((uint)valueMask), + ref attrs); + XMapWindow(display, handle); + XSelectInput(display, handle, new IntPtr((uint)valueMask)); + return handle; + } + } +} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index aa74081b74..1bdeb91f0c 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -64,6 +64,7 @@ namespace Avalonia.X11 private RawEventGrouper? _rawEventGrouper; private bool _useRenderWindow = false; private bool _usePositioningFlags = false; + private X11FocusProxy _focusProxy; private enum XSyncState { @@ -157,6 +158,8 @@ namespace Avalonia.X11 _renderHandle = _handle; Handle = new PlatformHandle(_handle, "XID"); + _focusProxy = new X11FocusProxy(platform, _handle, OnEvent); + SetWmClass(_focusProxy._handle, "FocusProxy"); _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask @@ -176,7 +179,7 @@ namespace Avalonia.X11 XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, new[] { _x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL }, 1); - SetWmClass(_platform.Options.WmClass); + SetWmClass(_handle, _platform.Options.WmClass); } var surfaces = new List @@ -213,7 +216,7 @@ namespace Avalonia.X11 InitializeIme(); XChangeProperty(_x11.Display, _handle, _x11.Atoms.WM_PROTOCOLS, _x11.Atoms.XA_ATOM, 32, - PropertyMode.Replace, new[] { _x11.Atoms.WM_DELETE_WINDOW, _x11.Atoms._NET_WM_SYNC_REQUEST }, 2); + PropertyMode.Replace, new[] { _x11.Atoms.WM_DELETE_WINDOW, _x11.Atoms.WM_TAKE_FOCUS, _x11.Atoms._NET_WM_SYNC_REQUEST }, 3); if (_x11.HasXSync) { @@ -548,6 +551,11 @@ namespace Avalonia.X11 _xSyncValue.Hi = ev.ClientMessageEvent.ptr4.ToInt32(); _xSyncState = XSyncState.WaitConfigure; } + else if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_TAKE_FOCUS) + { + IntPtr time = ev.ClientMessageEvent.ptr2; + XSetInputFocus(_x11.Display, _focusProxy._handle, RevertTo.Parent, time); + } } } else if (ev.type == XEventName.KeyPress || ev.type == XEventName.KeyRelease) @@ -923,6 +931,8 @@ namespace Avalonia.X11 { _renderHandle = IntPtr.Zero; } + + _focusProxy.Cleanup(); } private bool ActivateTransientChildIfNeeded() @@ -1078,7 +1088,7 @@ namespace Avalonia.X11 else { XRaiseWindow(_x11.Display, _handle); - XSetInputFocus(_x11.Display, _handle, 0, IntPtr.Zero); + XSetInputFocus(_x11.Display, _focusProxy._handle, 0, IntPtr.Zero); } } @@ -1170,7 +1180,7 @@ namespace Avalonia.X11 } } - public void SetWmClass(string wmClass) + public void SetWmClass(IntPtr handle, string wmClass) { // See https://tronche.com/gui/x/icccm/sec-4.html#WM_CLASS // We don't actually parse the application's command line, so we only use RESOURCE_NAME and argv[0] @@ -1186,7 +1196,7 @@ namespace Avalonia.X11 { hint->res_name = pAppId; hint->res_class = pWmClass; - XSetClassHint(_x11.Display, _handle, hint); + XSetClassHint(_x11.Display, handle, hint); } XFree(hint);