69 changed files with 1629 additions and 1003 deletions
@ -0,0 +1,46 @@ |
|||||
|
//
|
||||
|
// AvnTextInputMethod.h
|
||||
|
// Avalonia.Native.OSX
|
||||
|
//
|
||||
|
// Created by Benedikt Stebner on 22.11.22.
|
||||
|
// Copyright © 2022 Avalonia. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#ifndef AvnTextInputMethod_h |
||||
|
#define AvnTextInputMethod_h |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
#include "com.h" |
||||
|
#include "comimpl.h" |
||||
|
#include "avalonia-native.h" |
||||
|
#import "AvnTextInputMethodDelegate.h" |
||||
|
|
||||
|
class AvnTextInputMethod: public virtual ComObject, public virtual IAvnTextInputMethod{ |
||||
|
private: |
||||
|
id<AvnTextInputMethodDelegate> _inputMethodDelegate; |
||||
|
public: |
||||
|
FORWARD_IUNKNOWN() |
||||
|
|
||||
|
BEGIN_INTERFACE_MAP() |
||||
|
INTERFACE_MAP_ENTRY(IAvnTextInputMethod, IID_IAvnTextInputMethod) |
||||
|
END_INTERFACE_MAP() |
||||
|
|
||||
|
virtual ~AvnTextInputMethod(); |
||||
|
|
||||
|
AvnTextInputMethod(id<AvnTextInputMethodDelegate> inputMethodDelegate); |
||||
|
|
||||
|
bool IsActive (); |
||||
|
|
||||
|
HRESULT SetClient (IAvnTextInputMethodClient* client) override; |
||||
|
|
||||
|
virtual void Reset () override; |
||||
|
|
||||
|
virtual void SetCursorRect (AvnRect rect) override; |
||||
|
|
||||
|
virtual void SetSurroundingText (char* text, int anchorOffset, int cursorOffset) override; |
||||
|
|
||||
|
public: |
||||
|
ComPtr<IAvnTextInputMethodClient> Client; |
||||
|
}; |
||||
|
#endif /* AvnTextInputMethod_h */ |
||||
@ -0,0 +1,41 @@ |
|||||
|
// |
||||
|
// AvnTextInputMethod.mm |
||||
|
// Avalonia.Native.OSX |
||||
|
// |
||||
|
// Created by Benedikt Stebner on 23.11.22. |
||||
|
// Copyright © 2022 Avalonia. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#include "AvnTextInputMethod.h" |
||||
|
|
||||
|
AvnTextInputMethod::~AvnTextInputMethod() { |
||||
|
Client = nullptr; |
||||
|
} |
||||
|
|
||||
|
AvnTextInputMethod::AvnTextInputMethod(id<AvnTextInputMethodDelegate> inputMethodDelegate) { |
||||
|
_inputMethodDelegate = inputMethodDelegate; |
||||
|
} |
||||
|
|
||||
|
bool AvnTextInputMethod::IsActive() { |
||||
|
return Client != nullptr; |
||||
|
} |
||||
|
|
||||
|
HRESULT AvnTextInputMethod::SetClient(IAvnTextInputMethodClient *client) { |
||||
|
START_COM_CALL; |
||||
|
|
||||
|
Client = client; |
||||
|
|
||||
|
return S_OK; |
||||
|
} |
||||
|
|
||||
|
void AvnTextInputMethod::Reset() { |
||||
|
} |
||||
|
|
||||
|
void AvnTextInputMethod::SetSurroundingText(char* text, int anchorOffset, int cursorOffset) { |
||||
|
[_inputMethodDelegate setText:[NSString stringWithUTF8String:text]]; |
||||
|
[_inputMethodDelegate setSelection: anchorOffset : cursorOffset]; |
||||
|
} |
||||
|
|
||||
|
void AvnTextInputMethod::SetCursorRect(AvnRect rect) { |
||||
|
[_inputMethodDelegate setCursorRect: rect]; |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
//
|
||||
|
// AvnTextInputMethodHost.h
|
||||
|
// Avalonia.Native.OSX
|
||||
|
//
|
||||
|
// Created by Benedikt Stebner on 24.11.22.
|
||||
|
// Copyright © 2022 Avalonia. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#ifndef AvnTextInputMethodHost_h |
||||
|
#define AvnTextInputMethodHost_h |
||||
|
|
||||
|
@protocol AvnTextInputMethodDelegate |
||||
|
@required |
||||
|
-(void) setText:(NSString* _Nonnull) text; |
||||
|
-(void) setCursorRect:(AvnRect) cursorRect; |
||||
|
-(void) setSelection: (int) start : (int) end; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
#endif /* AvnTextInputMethodHost_h */ |
||||
@ -1,77 +0,0 @@ |
|||||
namespace Avalonia.Controls |
|
||||
{ |
|
||||
public enum FlyoutPlacementMode |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Preferred location is above the target element
|
|
||||
/// </summary>
|
|
||||
Top = 0, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is below the target element
|
|
||||
/// </summary>
|
|
||||
Bottom = 1, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is to the left of the target element
|
|
||||
/// </summary>
|
|
||||
Left = 2, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is to the right of the target element
|
|
||||
/// </summary>
|
|
||||
Right = 3, |
|
||||
|
|
||||
//TODO
|
|
||||
// <summary>
|
|
||||
// Preferred location is centered on the screen
|
|
||||
// </summary>
|
|
||||
//Full = 4,
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is above the target element, with the left edge of the flyout
|
|
||||
/// aligned with the left edge of the target element
|
|
||||
/// </summary>
|
|
||||
TopEdgeAlignedLeft = 5, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
|
|
||||
/// </summary>
|
|
||||
TopEdgeAlignedRight = 6, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
|
|
||||
/// </summary>
|
|
||||
BottomEdgeAlignedLeft = 7, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
|
|
||||
/// </summary>
|
|
||||
BottomEdgeAlignedRight = 8, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
|
|
||||
/// </summary>
|
|
||||
LeftEdgeAlignedTop = 9, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
|
|
||||
/// </summary>
|
|
||||
LeftEdgeAlignedBottom = 10, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
|
|
||||
/// </summary>
|
|
||||
RightEdgeAlignedTop = 11, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
|
|
||||
/// </summary>
|
|
||||
RightEdgeAlignedBottom = 12, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Preferred location is determined automatically.
|
|
||||
/// </summary>
|
|
||||
Auto = 13 |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,512 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Controls.Diagnostics; |
||||
|
using Avalonia.Controls.Primitives.PopupPositioning; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Platform; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Logging; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
namespace Avalonia.Controls.Primitives |
||||
|
{ |
||||
|
public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider |
||||
|
{ |
||||
|
/// <inheritdoc cref="Popup.PlacementModeProperty"/>
|
||||
|
public static readonly StyledProperty<PlacementMode> PlacementProperty = |
||||
|
Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>(); |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
|
||||
|
public static readonly StyledProperty<double> HorizontalOffsetProperty = |
||||
|
Popup.HorizontalOffsetProperty.AddOwner<PopupFlyoutBase>(); |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.VerticalOffsetProperty"/>
|
||||
|
public static readonly StyledProperty<double> VerticalOffsetProperty = |
||||
|
Popup.VerticalOffsetProperty.AddOwner<PopupFlyoutBase>(); |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
|
||||
|
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty = |
||||
|
Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>(); |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
|
||||
|
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty = |
||||
|
Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ShowMode"/> property
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty = |
||||
|
AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty = |
||||
|
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>(); |
||||
|
|
||||
|
private readonly Lazy<Popup> _popupLazy; |
||||
|
private Rect? _enlargedPopupRect; |
||||
|
private PixelRect? _enlargePopupRectScreenPixelRect; |
||||
|
private IDisposable? _transientDisposable; |
||||
|
private Action<IPopupHost?>? _popupHostChangedHandler; |
||||
|
|
||||
|
static PopupFlyoutBase() |
||||
|
{ |
||||
|
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); |
||||
|
} |
||||
|
|
||||
|
public PopupFlyoutBase() |
||||
|
{ |
||||
|
_popupLazy = new Lazy<Popup>(() => CreatePopup()); |
||||
|
} |
||||
|
|
||||
|
protected Popup Popup => _popupLazy.Value; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the desired placement.
|
||||
|
/// </summary>
|
||||
|
public PlacementMode Placement |
||||
|
{ |
||||
|
get => GetValue(PlacementProperty); |
||||
|
set => SetValue(PlacementProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.PlacementGravity"/>
|
||||
|
public PopupGravity PlacementGravity |
||||
|
{ |
||||
|
get => GetValue(PlacementGravityProperty); |
||||
|
set => SetValue(PlacementGravityProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.PlacementAnchor"/>
|
||||
|
public PopupAnchor PlacementAnchor |
||||
|
{ |
||||
|
get => GetValue(PlacementAnchorProperty); |
||||
|
set => SetValue(PlacementAnchorProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.HorizontalOffset"/>
|
||||
|
public double HorizontalOffset |
||||
|
{ |
||||
|
get => GetValue(HorizontalOffsetProperty); |
||||
|
set => SetValue(HorizontalOffsetProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Popup.VerticalOffset"/>
|
||||
|
public double VerticalOffset |
||||
|
{ |
||||
|
get => GetValue(VerticalOffsetProperty); |
||||
|
set => SetValue(VerticalOffsetProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the desired ShowMode
|
||||
|
/// </summary>
|
||||
|
public FlyoutShowMode ShowMode |
||||
|
{ |
||||
|
get => GetValue(ShowModeProperty); |
||||
|
set => SetValue(ShowModeProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets an element that should receive pointer input events even when underneath
|
||||
|
/// the flyout's overlay.
|
||||
|
/// </summary>
|
||||
|
public IInputElement? OverlayInputPassThroughElement |
||||
|
{ |
||||
|
get => GetValue(OverlayInputPassThroughElementProperty); |
||||
|
set => SetValue(OverlayInputPassThroughElementProperty, value); |
||||
|
} |
||||
|
|
||||
|
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; |
||||
|
|
||||
|
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged |
||||
|
{ |
||||
|
add => _popupHostChangedHandler += value; |
||||
|
remove => _popupHostChangedHandler -= value; |
||||
|
} |
||||
|
|
||||
|
public event EventHandler<CancelEventArgs>? Closing; |
||||
|
public event EventHandler? Opening; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Shows the Flyout at the given Control
|
||||
|
/// </summary>
|
||||
|
/// <param name="placementTarget">The control to show the Flyout at</param>
|
||||
|
public sealed override void ShowAt(Control placementTarget) |
||||
|
{ |
||||
|
ShowAtCore(placementTarget); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
|
||||
|
/// </summary>
|
||||
|
/// <param name="placementTarget">The target control</param>
|
||||
|
/// <param name="showAtPointer">True to show at pointer</param>
|
||||
|
public void ShowAt(Control placementTarget, bool showAtPointer) |
||||
|
{ |
||||
|
ShowAtCore(placementTarget, showAtPointer); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Hides the Flyout
|
||||
|
/// </summary>
|
||||
|
public sealed override void Hide() |
||||
|
{ |
||||
|
HideCore(); |
||||
|
} |
||||
|
|
||||
|
/// <returns>True, if action was handled</returns>
|
||||
|
protected virtual bool HideCore(bool canCancel = true) |
||||
|
{ |
||||
|
if (!IsOpen) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (canCancel) |
||||
|
{ |
||||
|
if (CancelClosing()) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
IsOpen = false; |
||||
|
Popup.IsOpen = false; |
||||
|
|
||||
|
((ISetLogicalParent)Popup).SetParent(null); |
||||
|
|
||||
|
// Ensure this isn't active
|
||||
|
_transientDisposable?.Dispose(); |
||||
|
_transientDisposable = null; |
||||
|
_enlargedPopupRect = null; |
||||
|
_enlargePopupRectScreenPixelRect = null; |
||||
|
|
||||
|
if (Target != null) |
||||
|
{ |
||||
|
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree; |
||||
|
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp; |
||||
|
} |
||||
|
|
||||
|
OnClosed(); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <returns>True, if action was handled</returns>
|
||||
|
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false) |
||||
|
{ |
||||
|
if (placementTarget == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(placementTarget)); |
||||
|
} |
||||
|
|
||||
|
if (IsOpen) |
||||
|
{ |
||||
|
if (placementTarget == Target) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
else // Close before opening a new one
|
||||
|
{ |
||||
|
_ = HideCore(false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Popup.Parent != null && Popup.Parent != placementTarget) |
||||
|
{ |
||||
|
((ISetLogicalParent)Popup).SetParent(null); |
||||
|
} |
||||
|
|
||||
|
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget) |
||||
|
{ |
||||
|
Popup.PlacementTarget = Target = placementTarget; |
||||
|
((ISetLogicalParent)Popup).SetParent(placementTarget); |
||||
|
Popup.TemplatedParent = placementTarget.TemplatedParent; |
||||
|
} |
||||
|
|
||||
|
if (Popup.Child == null) |
||||
|
{ |
||||
|
Popup.Child = CreatePresenter(); |
||||
|
} |
||||
|
|
||||
|
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; |
||||
|
|
||||
|
if (CancelOpening()) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
PositionPopup(showAtPointer); |
||||
|
IsOpen = Popup.IsOpen = true; |
||||
|
OnOpened(); |
||||
|
|
||||
|
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree; |
||||
|
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp; |
||||
|
|
||||
|
if (ShowMode == FlyoutShowMode.Standard) |
||||
|
{ |
||||
|
// Try and focus content inside Flyout
|
||||
|
if (Popup.Child.Focusable) |
||||
|
{ |
||||
|
FocusManager.Instance?.Focus(Popup.Child); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next); |
||||
|
if (nextFocus != null) |
||||
|
{ |
||||
|
FocusManager.Instance?.Focus(nextFocus); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) |
||||
|
{ |
||||
|
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) |
||||
|
{ |
||||
|
_ = HideCore(false); |
||||
|
} |
||||
|
|
||||
|
private void HandleTransientDismiss(RawInputEventArgs args) |
||||
|
{ |
||||
|
if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move) |
||||
|
{ |
||||
|
// In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
|
||||
|
// shown as long as the pointer is within a certain px distance from the
|
||||
|
// flyout itself. I'm not sure what WinUI uses, but I'm defaulting to
|
||||
|
// 100px, which seems about right
|
||||
|
// enlargedPopupRect is the Flyout bounds enlarged 100px
|
||||
|
// For windowed popups, enlargedPopupRect is in screen coordinates,
|
||||
|
// for overlay popups, its in OverlayLayer coordinates
|
||||
|
|
||||
|
if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null) |
||||
|
{ |
||||
|
// Only do this once when the Flyout opens & cache the result
|
||||
|
if (Popup?.Host is PopupRoot root) |
||||
|
{ |
||||
|
// Get the popup root bounds and convert to screen coordinates
|
||||
|
|
||||
|
var tmp = root.Bounds.Inflate(100); |
||||
|
_enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight)); |
||||
|
} |
||||
|
else if (Popup?.Host is OverlayPopupHost host) |
||||
|
{ |
||||
|
// Overlay popups are in OverlayLayer coordinates, just use that
|
||||
|
_enlargedPopupRect = host.Bounds.Inflate(100); |
||||
|
} |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot) |
||||
|
{ |
||||
|
// As long as the pointer stays within the enlargedPopupRect
|
||||
|
// the flyout stays open. If it leaves, close it
|
||||
|
// Despite working in screen coordinates, leaving the TopLevel
|
||||
|
// window will not close this (as pointer events stop), which
|
||||
|
// does match UWP
|
||||
|
var pt = eventRoot.PointToScreen(pArgs.Position); |
||||
|
if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false) |
||||
|
{ |
||||
|
HideCore(false); |
||||
|
} |
||||
|
} |
||||
|
else if (Popup?.Host is OverlayPopupHost) |
||||
|
{ |
||||
|
// Same as above here, but just different coordinate space
|
||||
|
// so we don't need to translate
|
||||
|
if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false) |
||||
|
{ |
||||
|
HideCore(false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void OnOpening(CancelEventArgs args) |
||||
|
{ |
||||
|
Opening?.Invoke(this, args); |
||||
|
} |
||||
|
|
||||
|
protected virtual void OnClosing(CancelEventArgs args) |
||||
|
{ |
||||
|
Closing?.Invoke(this, args); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Used to create the content the Flyout displays
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
protected abstract Control CreatePresenter(); |
||||
|
|
||||
|
private Popup CreatePopup() |
||||
|
{ |
||||
|
var popup = new Popup |
||||
|
{ |
||||
|
WindowManagerAddShadowHint = false, |
||||
|
IsLightDismissEnabled = true, |
||||
|
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
|
||||
|
OverlayDismissEventPassThrough = false |
||||
|
}; |
||||
|
|
||||
|
popup.Opened += OnPopupOpened; |
||||
|
popup.Closed += OnPopupClosed; |
||||
|
popup.Closing += OnPopupClosing; |
||||
|
popup.KeyUp += OnPlacementTargetOrPopupKeyUp; |
||||
|
return popup; |
||||
|
} |
||||
|
|
||||
|
private void OnPopupOpened(object? sender, EventArgs e) |
||||
|
{ |
||||
|
IsOpen = true; |
||||
|
|
||||
|
_popupHostChangedHandler?.Invoke(Popup.Host); |
||||
|
} |
||||
|
|
||||
|
private void OnPopupClosing(object? sender, CancelEventArgs e) |
||||
|
{ |
||||
|
if (IsOpen) |
||||
|
{ |
||||
|
e.Cancel = CancelClosing(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnPopupClosed(object? sender, EventArgs e) |
||||
|
{ |
||||
|
HideCore(false); |
||||
|
|
||||
|
_popupHostChangedHandler?.Invoke(null); |
||||
|
} |
||||
|
|
||||
|
// This method is handling both popup logical tree and target logical tree.
|
||||
|
private void OnPlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e) |
||||
|
{ |
||||
|
if (!e.Handled |
||||
|
&& IsOpen |
||||
|
&& Target?.ContextFlyout == this) |
||||
|
{ |
||||
|
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); |
||||
|
|
||||
|
if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true) |
||||
|
{ |
||||
|
e.Handled = HideCore(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void PositionPopup(bool showAtPointer) |
||||
|
{ |
||||
|
Size sz; |
||||
|
// Popup.Child can't be null here, it was set in ShowAtCore.
|
||||
|
if (Popup.Child!.DesiredSize.IsDefault) |
||||
|
{ |
||||
|
// Popup may not have been shown yet. Measure content
|
||||
|
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness()); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
sz = Popup.Child.DesiredSize; |
||||
|
} |
||||
|
|
||||
|
Popup.VerticalOffset = VerticalOffset; |
||||
|
Popup.HorizontalOffset = HorizontalOffset; |
||||
|
Popup.PlacementAnchor = PlacementAnchor; |
||||
|
Popup.PlacementGravity = PlacementGravity; |
||||
|
if (showAtPointer) |
||||
|
{ |
||||
|
Popup.PlacementMode = PlacementMode.Pointer; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Popup.PlacementMode = Placement; |
||||
|
Popup.PlacementConstraintAdjustment = |
||||
|
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX | |
||||
|
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args) |
||||
|
{ |
||||
|
if (args.Sender is Control c) |
||||
|
{ |
||||
|
if (args.OldValue is FlyoutBase) |
||||
|
{ |
||||
|
c.ContextRequested -= OnControlContextRequested; |
||||
|
} |
||||
|
if (args.NewValue is FlyoutBase) |
||||
|
{ |
||||
|
c.ContextRequested += OnControlContextRequested; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e) |
||||
|
{ |
||||
|
if (!e.Handled |
||||
|
&& sender is Control control |
||||
|
&& control.ContextFlyout is { } flyout) |
||||
|
{ |
||||
|
if (control.ContextMenu != null) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (flyout is PopupFlyoutBase popupFlyout) |
||||
|
{ |
||||
|
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
|
||||
|
var triggeredByPointerInput = e.TryGetPosition(null, out _); |
||||
|
e.Handled = popupFlyout.ShowAtCore(control, triggeredByPointerInput); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
flyout.ShowAt(control); |
||||
|
e.Handled = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool CancelClosing() |
||||
|
{ |
||||
|
var eventArgs = new CancelEventArgs(); |
||||
|
OnClosing(eventArgs); |
||||
|
return eventArgs.Cancel; |
||||
|
} |
||||
|
|
||||
|
private bool CancelOpening() |
||||
|
{ |
||||
|
var eventArgs = new CancelEventArgs(); |
||||
|
OnOpening(eventArgs); |
||||
|
return eventArgs.Cancel; |
||||
|
} |
||||
|
|
||||
|
internal static void SetPresenterClasses(Control? presenter, Classes classes) |
||||
|
{ |
||||
|
if(presenter is null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
//Remove any classes no longer in use, ignoring pseudo classes
|
||||
|
for (int i = presenter.Classes.Count - 1; i >= 0; i--) |
||||
|
{ |
||||
|
if (!classes.Contains(presenter.Classes[i]) && |
||||
|
!presenter.Classes[i].Contains(':')) |
||||
|
{ |
||||
|
presenter.Classes.RemoveAt(i); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Add new classes
|
||||
|
presenter.Classes.AddRange(classes); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Native.Interop; |
||||
|
|
||||
|
namespace Avalonia.Native |
||||
|
{ |
||||
|
internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable |
||||
|
{ |
||||
|
private ITextInputMethodClient _client; |
||||
|
private IAvnTextInputMethodClient _nativeClient; |
||||
|
private readonly IAvnTextInputMethod _inputMethod; |
||||
|
|
||||
|
public AvaloniaNativeTextInputMethod(IAvnWindowBase nativeWindow) |
||||
|
{ |
||||
|
_inputMethod = nativeWindow.InputMethod; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_inputMethod.Dispose(); |
||||
|
_nativeClient?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
_inputMethod.Reset(); |
||||
|
} |
||||
|
|
||||
|
public void SetClient(ITextInputMethodClient client) |
||||
|
{ |
||||
|
if (_client is { SupportsSurroundingText: true }) |
||||
|
{ |
||||
|
_client.SurroundingTextChanged -= OnSurroundingTextChanged; |
||||
|
_client.CursorRectangleChanged -= OnCursorRectangleChanged; |
||||
|
|
||||
|
_nativeClient?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
_nativeClient = null; |
||||
|
_client = client; |
||||
|
|
||||
|
if (client != null) |
||||
|
{ |
||||
|
_nativeClient = new AvnTextInputMethodClient(client); |
||||
|
|
||||
|
OnSurroundingTextChanged(this, EventArgs.Empty); |
||||
|
OnCursorRectangleChanged(this, EventArgs.Empty); |
||||
|
|
||||
|
_client.SurroundingTextChanged += OnSurroundingTextChanged; |
||||
|
_client.CursorRectangleChanged += OnCursorRectangleChanged; |
||||
|
} |
||||
|
|
||||
|
_inputMethod.SetClient(_nativeClient); |
||||
|
} |
||||
|
|
||||
|
private void OnCursorRectangleChanged(object sender, EventArgs e) |
||||
|
{ |
||||
|
if (_client == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_inputMethod.SetCursorRect(_client.CursorRectangle.ToAvnRect()); |
||||
|
} |
||||
|
|
||||
|
private void OnSurroundingTextChanged(object sender, EventArgs e) |
||||
|
{ |
||||
|
if (_client == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var surroundingText = _client.SurroundingText; |
||||
|
|
||||
|
_inputMethod.SetSurroundingText( |
||||
|
surroundingText.Text, |
||||
|
surroundingText.AnchorOffset, |
||||
|
surroundingText.CursorOffset |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public void SetCursorRect(Rect rect) |
||||
|
{ |
||||
|
_inputMethod.SetCursorRect(rect.ToAvnRect()); |
||||
|
} |
||||
|
|
||||
|
public void SetOptions(TextInputOptions options) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient |
||||
|
{ |
||||
|
private readonly ITextInputMethodClient _client; |
||||
|
|
||||
|
public AvnTextInputMethodClient(ITextInputMethodClient client) |
||||
|
{ |
||||
|
_client = client; |
||||
|
} |
||||
|
|
||||
|
public void SetPreeditText(string preeditText) |
||||
|
{ |
||||
|
if (_client.SupportsPreedit) |
||||
|
{ |
||||
|
_client.SetPreeditText(preeditText); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SelectInSurroundingText(int start, int end) |
||||
|
{ |
||||
|
if (_client.SupportsSurroundingText) |
||||
|
{ |
||||
|
_client.SelectInSurroundingText(start, end); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"profiles": { |
||||
|
"Avalonia.Benchmarks": { |
||||
|
"commandName": "Project" |
||||
|
}, |
||||
|
"Avalonia.Benchmarks (in-process)": { |
||||
|
"commandName": "Project", |
||||
|
"commandLineArgs": "--inprocess" |
||||
|
}, |
||||
|
"Avalonia.Benchmarks (debug)": { |
||||
|
"commandName": "Project", |
||||
|
"commandLineArgs": "--debug" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue