Skip to content

Commit 93df05e

Browse files
authored
Merge pull request #12751 from vlad-lubenskyi/x11-focus-proxy
Use the focus proxy window in X11
2 parents 52a3bee + 5a92167 commit 93df05e

File tree

2 files changed

+105
-5
lines changed

2 files changed

+105
-5
lines changed

src/Avalonia.X11/X11FocusProxy.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using static Avalonia.X11.XLib;
3+
4+
namespace Avalonia.X11
5+
{
6+
/// <summary>
7+
/// An invisible X window that owns the input focus and forwards events to the owner window.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// This is a known Linux technique for an auxiliary invisible window to hold the input focus
12+
/// for the main window. It is required by XEmbed protocol, but it also works for regular cases
13+
/// that don't imply embedded windows.
14+
/// </para>
15+
/// </remarks>
16+
///
17+
/// <see href="https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html" />
18+
/// <see href="https://gitlab.gnome.org/GNOME/gtk/-/blob/3.22.30/gdk/x11/gdkwindow-x11.c#L823" />
19+
internal class X11FocusProxy
20+
{
21+
private const int InvisibleBorder = 0;
22+
private const int DepthCopyFromParent = 0;
23+
private readonly IntPtr _visualCopyFromParent = IntPtr.Zero;
24+
private readonly (int X, int Y) _outOfScreen = (-1, -1);
25+
private readonly (int Width, int Height) _smallest = (1, 1);
26+
27+
internal IntPtr _handle;
28+
private readonly AvaloniaX11Platform _platform;
29+
private readonly X11PlatformThreading.EventHandler _ownerEventHandler;
30+
31+
/// <summary>
32+
/// Initializes instance and creates the underlying X window.
33+
/// </summary>
34+
///
35+
/// <param name="platform">The X11 platform.</param>
36+
/// <param name="parent">The parent window to proxy the focus for.</param>
37+
/// <param name="eventHandler">An event handler that will handle X events that come to the proxy.</param>
38+
internal X11FocusProxy(AvaloniaX11Platform platform, IntPtr parent,
39+
X11PlatformThreading.EventHandler eventHandler)
40+
{
41+
_handle = PrepareXWindow(platform.Info.Display, parent);
42+
_platform = platform;
43+
_ownerEventHandler = eventHandler;
44+
_platform.Windows[_handle] = OnEvent;
45+
}
46+
47+
internal void Cleanup()
48+
{
49+
if (_handle != IntPtr.Zero)
50+
{
51+
_platform.Windows.Remove(_handle);
52+
_handle = IntPtr.Zero;
53+
}
54+
}
55+
56+
private void OnEvent(ref XEvent ev)
57+
{
58+
if (ev.type is XEventName.FocusIn or XEventName.FocusOut)
59+
{
60+
this._ownerEventHandler(ref ev);
61+
}
62+
63+
if (ev.type is XEventName.KeyPress or XEventName.KeyRelease)
64+
{
65+
this._ownerEventHandler(ref ev);
66+
}
67+
}
68+
69+
private IntPtr PrepareXWindow(IntPtr display, IntPtr parent)
70+
{
71+
var valueMask = default(EventMask)
72+
| EventMask.FocusChangeMask
73+
| EventMask.KeyPressMask
74+
| EventMask.KeyReleaseMask;
75+
var attrs = new XSetWindowAttributes();
76+
var handle = XCreateWindow(display, parent,
77+
_outOfScreen.X, _outOfScreen.Y,
78+
_smallest.Width, _smallest.Height,
79+
InvisibleBorder,
80+
DepthCopyFromParent,
81+
(int)CreateWindowArgs.InputOutput,
82+
_visualCopyFromParent,
83+
new UIntPtr((uint)valueMask),
84+
ref attrs);
85+
XMapWindow(display, handle);
86+
XSelectInput(display, handle, new IntPtr((uint)valueMask));
87+
return handle;
88+
}
89+
}
90+
}

src/Avalonia.X11/X11Window.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client
6464
private RawEventGrouper? _rawEventGrouper;
6565
private bool _useRenderWindow = false;
6666
private bool _usePositioningFlags = false;
67+
private X11FocusProxy _focusProxy;
6768

6869
private enum XSyncState
6970
{
@@ -157,6 +158,8 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov
157158
_renderHandle = _handle;
158159

159160
Handle = new PlatformHandle(_handle, "XID");
161+
_focusProxy = new X11FocusProxy(platform, _handle, OnEvent);
162+
SetWmClass(_focusProxy._handle, "FocusProxy");
160163
_realSize = new PixelSize(defaultWidth, defaultHeight);
161164
platform.Windows[_handle] = OnEvent;
162165
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask
@@ -176,7 +179,7 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov
176179
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM,
177180
32, PropertyMode.Replace, new[] { _x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL }, 1);
178181

179-
SetWmClass(_platform.Options.WmClass);
182+
SetWmClass(_handle, _platform.Options.WmClass);
180183
}
181184

182185
var surfaces = new List<object>
@@ -213,7 +216,7 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov
213216
InitializeIme();
214217

215218
XChangeProperty(_x11.Display, _handle, _x11.Atoms.WM_PROTOCOLS, _x11.Atoms.XA_ATOM, 32,
216-
PropertyMode.Replace, new[] { _x11.Atoms.WM_DELETE_WINDOW, _x11.Atoms._NET_WM_SYNC_REQUEST }, 2);
219+
PropertyMode.Replace, new[] { _x11.Atoms.WM_DELETE_WINDOW, _x11.Atoms.WM_TAKE_FOCUS, _x11.Atoms._NET_WM_SYNC_REQUEST }, 3);
217220

218221
if (_x11.HasXSync)
219222
{
@@ -548,6 +551,11 @@ private void OnEvent(ref XEvent ev)
548551
_xSyncValue.Hi = ev.ClientMessageEvent.ptr4.ToInt32();
549552
_xSyncState = XSyncState.WaitConfigure;
550553
}
554+
else if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_TAKE_FOCUS)
555+
{
556+
IntPtr time = ev.ClientMessageEvent.ptr2;
557+
XSetInputFocus(_x11.Display, _focusProxy._handle, RevertTo.Parent, time);
558+
}
551559
}
552560
}
553561
else if (ev.type == XEventName.KeyPress || ev.type == XEventName.KeyRelease)
@@ -922,6 +930,8 @@ private void Cleanup(bool fromDestroyNotification)
922930
{
923931
_renderHandle = IntPtr.Zero;
924932
}
933+
934+
_focusProxy.Cleanup();
925935
}
926936

927937
private bool ActivateTransientChildIfNeeded()
@@ -1077,7 +1087,7 @@ public void Activate()
10771087
else
10781088
{
10791089
XRaiseWindow(_x11.Display, _handle);
1080-
XSetInputFocus(_x11.Display, _handle, 0, IntPtr.Zero);
1090+
XSetInputFocus(_x11.Display, _focusProxy._handle, 0, IntPtr.Zero);
10811091
}
10821092
}
10831093

@@ -1169,7 +1179,7 @@ public void SetTitle(string? title)
11691179
}
11701180
}
11711181

1172-
public void SetWmClass(string wmClass)
1182+
public void SetWmClass(IntPtr handle, string wmClass)
11731183
{
11741184
// See https://tronche.com/gui/x/icccm/sec-4.html#WM_CLASS
11751185
// 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)
11851195
{
11861196
hint->res_name = pAppId;
11871197
hint->res_class = pWmClass;
1188-
XSetClassHint(_x11.Display, _handle, hint);
1198+
XSetClassHint(_x11.Display, handle, hint);
11891199
}
11901200

11911201
XFree(hint);

0 commit comments

Comments
 (0)