Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the focus proxy window in X11 #12751

Merged
merged 12 commits into from
Sep 4, 2023
90 changes: 90 additions & 0 deletions src/Avalonia.X11/X11FocusProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using static Avalonia.X11.XLib;

namespace Avalonia.X11
{
/// <summary>
/// An invisible X window that owns the input focus and forwards events to the owner window.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
///
/// <see href="https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html" />
/// <see href="https://gitlab.gnome.org/GNOME/gtk/-/blob/3.22.30/gdk/x11/gdkwindow-x11.c#L823" />
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;

/// <summary>
/// Initializes instance and creates the underlying X window.
/// </summary>
///
/// <param name="platform">The X11 platform.</param>
/// <param name="parent">The parent window to proxy the focus for.</param>
/// <param name="eventHandler">An event handler that will handle X events that come to the proxy.</param>
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);
vlad-lubenskyi marked this conversation as resolved.
Show resolved Hide resolved
_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;
}
}
}
20 changes: 15 additions & 5 deletions src/Avalonia.X11/X11Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand All @@ -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<object>
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -922,6 +930,8 @@ private void Cleanup(bool fromDestroyNotification)
{
_renderHandle = IntPtr.Zero;
}

_focusProxy.Cleanup();
}

private bool ActivateTransientChildIfNeeded()
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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]
Expand All @@ -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);
Expand Down