Skip to content

Make popup focus stealing configurable. #16642

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

Merged
merged 8 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/Avalonia.nupkg.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Controls.Primitives.IPopupHost.TakeFocus</Target>
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0009</DiagnosticId>
<Target>T:Avalonia.Diagnostics.StyleDiagnostics</Target>
Expand Down
9 changes: 0 additions & 9 deletions samples/IntegrationTestApp/Embedding/INativeControlFactory.cs

This file was deleted.

18 changes: 18 additions & 0 deletions samples/IntegrationTestApp/Embedding/INativeTextBoxFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using Avalonia.Platform;

namespace IntegrationTestApp.Embedding;

internal interface INativeTextBoxImpl
{
IPlatformHandle Handle { get; }
string Text { get; set; }
event EventHandler? ContextMenuRequested;
event EventHandler? Hovered;
event EventHandler? PointerExited;
}

internal interface INativeTextBoxFactory
{
INativeTextBoxImpl CreateControl(IPlatformHandle parent);
}
71 changes: 64 additions & 7 deletions samples/IntegrationTestApp/Embedding/MacOSTextBoxFactory.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,77 @@
using System;
using System.Text;
using Avalonia.Platform;
using Avalonia.Threading;
using MonoMac.AppKit;
using MonoMac.WebKit;
using MonoMac.Foundation;

namespace IntegrationTestApp.Embedding;

internal class MacOSTextBoxFactory : INativeControlFactory
internal class MacOSTextBoxFactory : INativeTextBoxFactory
{
public IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault)
public INativeTextBoxImpl CreateControl(IPlatformHandle parent)
{
MacHelper.EnsureInitialized();
return new MacOSTextBox();
}

private class MacOSTextBox : NSTextView, INativeTextBoxImpl
{
private DispatcherTimer _timer;

public MacOSTextBox()
{
TextStorage.Append(new("Native text box"));
Handle = new MacOSViewHandle(this);
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromMilliseconds(400);
_timer.Tick += (_, _) =>
{
Hovered?.Invoke(this, EventArgs.Empty);
_timer.Stop();
};
}

public new IPlatformHandle Handle { get; }

public string Text
{
get => TextStorage.Value;
set => TextStorage.Replace(new NSRange(0, TextStorage.Length), value);
}

public event EventHandler? ContextMenuRequested;
public event EventHandler? Hovered;
public event EventHandler? PointerExited;

public override void MouseEntered(NSEvent theEvent)
{
_timer.Stop();
_timer.Start();
base.MouseEntered(theEvent);
}

public override void MouseExited(NSEvent theEvent)
{
_timer.Stop();
PointerExited?.Invoke(this, EventArgs.Empty);
base.MouseExited(theEvent);
}

public override void MouseMoved(NSEvent theEvent)
{
_timer.Stop();
_timer.Start();
base.MouseMoved(theEvent);
}

var textView = new NSTextView();
textView.TextStorage.Append(new("Native text box"));
public override void RightMouseDown(NSEvent theEvent)
{
ContextMenuRequested?.Invoke(this, EventArgs.Empty);
}

return new MacOSViewHandle(textView);
public override void RightMouseUp(NSEvent theEvent)
{
// Don't call base to prevent default action.
}
}
}
74 changes: 70 additions & 4 deletions samples/IntegrationTestApp/Embedding/NativeTextBox.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,86 @@
using Avalonia.Controls;
using System;
using Avalonia.Controls;
using Avalonia.Platform;

namespace IntegrationTestApp.Embedding;

internal class NativeTextBox : NativeControlHost
{
public static INativeControlFactory? Factory { get; set; }
private ContextMenu? _contextMenu;
private INativeTextBoxImpl? _impl;
private TextBlock _tipTextBlock;
private string _initialText = string.Empty;

public NativeTextBox()
{
_tipTextBlock = new TextBlock
{
Text = "Avalonia ToolTip",
Name = "NativeTextBoxToolTip",
};

ToolTip.SetTip(this, _tipTextBlock);
ToolTip.SetShowDelay(this, 1000);
ToolTip.SetServiceEnabled(this, false);
}

public string Text
{
get => _impl?.Text ?? _initialText;
set
{
if (_impl is not null)
_impl.Text = value;
else
_initialText = value;
}
}

public static INativeTextBoxFactory? Factory { get; set; }

protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
return Factory?.CreateControl(parent, () => base.CreateNativeControlCore(parent))
?? base.CreateNativeControlCore(parent);
if (Factory is null)
return base.CreateNativeControlCore(parent);

_impl = Factory.CreateControl(parent);
_impl.Text = _initialText;
_impl.ContextMenuRequested += OnContextMenuRequested;
_impl.Hovered += OnHovered;
_impl.PointerExited += OnPointerExited;
return _impl.Handle;
}

protected override void DestroyNativeControlCore(IPlatformHandle control)
{
base.DestroyNativeControlCore(control);
}

private void OnContextMenuRequested(object? sender, EventArgs e)
{
if (_contextMenu is null)
{
var menuItem = new MenuItem { Header = "Custom Menu Item" };
menuItem.Click += (s, e) => _impl!.Text = "Context menu item clicked";

_contextMenu = new ContextMenu
{
Name = "NativeTextBoxContextMenu",
Items = { menuItem }
};
}

ToolTip.SetIsOpen(this, false);
_contextMenu.Open(this);
}

private void OnHovered(object? sender, EventArgs e)
{
ToolTip.SetIsOpen(this, true);
}

private void OnPointerExited(object? sender, EventArgs e)
{
ToolTip.SetIsOpen(this, false);
}
}
90 changes: 79 additions & 11 deletions samples/IntegrationTestApp/Embedding/Win32TextBoxFactory.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,89 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Controls;
using Avalonia.Platform;
using static IntegrationTestApp.Embedding.WinApi;

namespace IntegrationTestApp.Embedding;

internal class Win32TextBoxFactory : INativeControlFactory
internal class Win32TextBoxFactory : INativeTextBoxFactory
{
public IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault)
public INativeTextBoxImpl CreateControl(IPlatformHandle parent)
{
var handle = WinApi.CreateWindowEx(0, "EDIT",
@"Native text box",
(uint)(WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_BORDER),
0, 0, 1, 1,
parent.Handle,
IntPtr.Zero,
WinApi.GetModuleHandle(null),
IntPtr.Zero);
return new Win32WindowControlHandle(handle, "HWND");
return new Win32TextBox(parent);
}

private class Win32TextBox : INativeTextBoxImpl
{
private readonly IntPtr _oldWndProc;
private readonly WndProcDelegate _wndProc;
private TRACKMOUSEEVENT _trackMouseEvent;

public Win32TextBox(IPlatformHandle parent)
{
var handle = CreateWindowEx(0, "EDIT",
string.Empty,
(uint)(WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_BORDER),
0, 0, 1, 1,
parent.Handle,
IntPtr.Zero,
GetModuleHandle(null),
IntPtr.Zero);

_wndProc = new(WndProc);
_oldWndProc = SetWindowLongPtr(handle, WinApi.GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProc));

_trackMouseEvent.cbSize = Marshal.SizeOf<TRACKMOUSEEVENT>();
_trackMouseEvent.dwFlags = TME_HOVER | TME_LEAVE;
_trackMouseEvent.hwndTrack = handle;
_trackMouseEvent.dwHoverTime = 400;

Handle = new Win32WindowControlHandle(handle, "HWND");
}

public IPlatformHandle Handle { get; }

public string Text
{
get
{
var sb = new StringBuilder(256);
GetWindowText(Handle.Handle, sb, sb.Capacity);
return sb.ToString();
}
set => SetWindowText(Handle.Handle, value);
}

public event EventHandler? ContextMenuRequested;
public event EventHandler? Hovered;
public event EventHandler? PointerExited;

private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
switch (msg)
{
case WM_CONTEXTMENU:
if (ContextMenuRequested is not null)
{
ContextMenuRequested?.Invoke(this, EventArgs.Empty);
return IntPtr.Zero;
}
break;
case WM_MOUSELEAVE:
PointerExited?.Invoke(this, EventArgs.Empty);
break;
case WM_MOUSEHOVER:
Hovered?.Invoke(this, EventArgs.Empty);
break;
case WM_MOUSEMOVE:
TrackMouseEvent(ref _trackMouseEvent);
break;

}

return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam);
}
}
}
36 changes: 36 additions & 0 deletions samples/IntegrationTestApp/Embedding/WinApi.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace IntegrationTestApp.Embedding;

internal class WinApi
{
public const int GWL_WNDPROC = -4;
public const uint TME_HOVER = 1;
public const uint TME_LEAVE = 2;
public const uint WM_CONTEXTMENU = 0x007B;
public const uint WM_MOUSELEAVE = 0x02A3;
public const uint WM_MOUSEHOVER = 0x02A1;
public const uint WM_MOUSEMOVE = 0x0200;

public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

[Flags]
public enum WindowStyles : uint
{
Expand Down Expand Up @@ -59,12 +70,19 @@ public enum WindowStyles : uint
WS_EX_NOACTIVATE = 0x08000000
}

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll", SetLastError = true)]
public static extern bool DestroyWindow(IntPtr hwnd);

[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string? lpModuleName);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);


[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr CreateWindowEx(
int dwExStyle,
Expand All @@ -79,4 +97,22 @@ public static extern IntPtr CreateWindowEx(
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool SetWindowText(IntPtr hwnd, String lpString);

[DllImport("user32.dll")]
public static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack);

[StructLayout(LayoutKind.Sequential)]
public struct TRACKMOUSEEVENT
{
public int cbSize;
public uint dwFlags;
public IntPtr hwndTrack;
public uint dwHoverTime;
}
}
1 change: 1 addition & 0 deletions samples/IntegrationTestApp/Pages/EmbeddingPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
<embedding:NativeTextBox Name="NativeTextBoxInPopup" Width="200" Height="23"/>
</Popup>
</StackPanel>
<Button Name="Reset" Click="Reset_Click">Reset</Button>
</StackPanel>
</UserControl>
Loading
Loading