diff --git a/src/Avalonia.X11/X11FocusProxy.cs b/src/Avalonia.X11/X11FocusProxy.cs new file mode 100644 index 00000000000..4fdaf643abe --- /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 a2019c276b8..77db7301c9a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -64,6 +64,7 @@ internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client private RawEventGrouper? _rawEventGrouper; private bool _useRenderWindow = false; private bool _usePositioningFlags = false; + private X11FocusProxy _focusProxy; private enum XSyncState { @@ -157,6 +158,8 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov _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 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov 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 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov 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 @@ private void OnEvent(ref XEvent ev) _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) @@ -922,6 +930,8 @@ private void Cleanup(bool fromDestroyNotification) { _renderHandle = IntPtr.Zero; } + + _focusProxy.Cleanup(); } private bool ActivateTransientChildIfNeeded() @@ -1077,7 +1087,7 @@ public void Activate() else { XRaiseWindow(_x11.Display, _handle); - XSetInputFocus(_x11.Display, _handle, 0, IntPtr.Zero); + XSetInputFocus(_x11.Display, _focusProxy._handle, 0, IntPtr.Zero); } } @@ -1169,7 +1179,7 @@ public void SetTitle(string? title) } } - 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] @@ -1185,7 +1195,7 @@ public void SetWmClass(string wmClass) { hint->res_name = pAppId; hint->res_class = pWmClass; - XSetClassHint(_x11.Display, _handle, hint); + XSetClassHint(_x11.Display, handle, hint); } XFree(hint);