Browse Source
* Show Avalonia context menu and toolip... In native text box in integration test app. Only implemented for win32 right now. This is to test the two popup behaviors required for native controls: - The context menu needs focus to be transferred to Avalonia - The ToolTip must not transfer focus to Avalonia * Added Popup.TakesFocusFromNativeControl. By default, if a popup is shown when a native control is focused, focus is transferred back to Avalonia in order for the popup to receive input. If this property is set to false, then the shown popup will not receive input until it receives an interaction which explicitly focuses the popup, such as a mouse click. The effect of this property can be seen in the Embedding tag of the IntegrationTestApp: hovering over the native text box shows an Avalonia `ToolTip` which does not steal focus from the native text box. Right-clicking to open an Avalonia `ContextMenu` does steal focus so the menu items can be selected using the arrow keys. Currently only implemented on a win32. * Show tooltip and context menu on macOS. * Implement TakeFocus on macOS. * Add integration tests. Only tested on win32 so far. * Integration tests won't work on macOS. As can be expected at this point, really. * Update API diff.pull/17171/head
committed by
GitHub
23 changed files with 458 additions and 32 deletions
@ -1,9 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace IntegrationTestApp.Embedding; |
|
||||
|
|
||||
internal interface INativeControlFactory |
|
||||
{ |
|
||||
IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault); |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
@ -1,20 +1,77 @@ |
|||||
using System; |
using System; |
||||
using System.Text; |
|
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
|
using Avalonia.Threading; |
||||
using MonoMac.AppKit; |
using MonoMac.AppKit; |
||||
using MonoMac.WebKit; |
using MonoMac.Foundation; |
||||
|
|
||||
namespace IntegrationTestApp.Embedding; |
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(); |
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(); |
public override void RightMouseDown(NSEvent theEvent) |
||||
textView.TextStorage.Append(new("Native text box")); |
{ |
||||
|
ContextMenuRequested?.Invoke(this, EventArgs.Empty); |
||||
|
} |
||||
|
|
||||
return new MacOSViewHandle(textView); |
public override void RightMouseUp(NSEvent theEvent) |
||||
|
{ |
||||
|
// Don't call base to prevent default action.
|
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,20 +1,86 @@ |
|||||
using Avalonia.Controls; |
using System; |
||||
|
using Avalonia.Controls; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
|
|
||||
namespace IntegrationTestApp.Embedding; |
namespace IntegrationTestApp.Embedding; |
||||
|
|
||||
internal class NativeTextBox : NativeControlHost |
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) |
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) |
||||
{ |
{ |
||||
return Factory?.CreateControl(parent, () => base.CreateNativeControlCore(parent)) |
if (Factory is null) |
||||
?? base.CreateNativeControlCore(parent); |
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) |
protected override void DestroyNativeControlCore(IPlatformHandle control) |
||||
{ |
{ |
||||
base.DestroyNativeControlCore(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); |
||||
|
} |
||||
} |
} |
||||
|
|||||
@ -1,21 +1,89 @@ |
|||||
using System; |
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.InteropServices; |
||||
using System.Text; |
using System.Text; |
||||
|
using Avalonia.Controls; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
|
using static IntegrationTestApp.Embedding.WinApi; |
||||
|
|
||||
namespace IntegrationTestApp.Embedding; |
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", |
return new Win32TextBox(parent); |
||||
@"Native text box", |
} |
||||
(uint)(WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_BORDER), |
|
||||
0, 0, 1, 1, |
private class Win32TextBox : INativeTextBoxImpl |
||||
parent.Handle, |
{ |
||||
IntPtr.Zero, |
private readonly IntPtr _oldWndProc; |
||||
WinApi.GetModuleHandle(null), |
private readonly WndProcDelegate _wndProc; |
||||
IntPtr.Zero); |
private TRACKMOUSEEVENT _trackMouseEvent; |
||||
return new Win32WindowControlHandle(handle, "HWND"); |
|
||||
|
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); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue