Browse Source

Merge branch 'master' into pr/10572

pull/10572/head
Steven Kirk 3 years ago
parent
commit
47eec7e461
  1. 1
      .gitignore
  2. 1
      azure-pipelines-integrationtests.yml
  3. 12
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  4. 46
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.h
  5. 41
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm
  6. 20
      native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h
  7. 6
      native/Avalonia.Native/src/OSX/AvnView.h
  8. 85
      native/Avalonia.Native/src/OSX/AvnView.mm
  9. 2
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  10. 6
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  11. 10
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  12. 2
      native/Avalonia.Native/src/OSX/WindowImpl.h
  13. 2
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  14. 5
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
  15. 18
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  16. 13
      src/Avalonia.Base/AvaloniaProperty.cs
  17. 2
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  18. 1
      src/Avalonia.Base/Input/MouseDevice.cs
  19. 3
      src/Avalonia.Base/Input/PenDevice.cs
  20. 2
      src/Avalonia.Base/Input/PointerEventArgs.cs
  21. 33
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  22. 1
      src/Avalonia.Base/Input/TouchDevice.cs
  23. 4
      src/Avalonia.Base/Media/DrawingGroup.cs
  24. 2
      src/Avalonia.Base/Media/FontManager.cs
  25. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  26. 1
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  27. 2
      src/Avalonia.Base/Rendering/DisplayDirtyRect.cs
  28. 1
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  29. 2
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  30. 1
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  31. 1
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  32. 1
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  33. 2
      src/Avalonia.Base/Styling/DescendentSelector.cs
  34. 27
      src/Avalonia.Base/Styling/OrSelector.cs
  35. 11
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  36. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  37. 3
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  38. 38
      src/Avalonia.Controls/Flyouts/Flyout.cs
  39. 548
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  40. 77
      src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs
  41. 39
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  42. 512
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  43. 23
      src/Avalonia.Controls/ItemsSourceView.cs
  44. 58
      src/Avalonia.Controls/PlacementMode.cs
  45. 2
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  46. 64
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  47. 48
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  48. 2
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  49. 17
      src/Avalonia.Controls/Selection/SelectedItems.cs
  50. 91
      src/Avalonia.Controls/Selection/SelectionModel.cs
  51. 8
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  52. 311
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  53. 4
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  54. 2
      src/Avalonia.Controls/TextBox.cs
  55. 58
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  56. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  57. 118
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  58. 11
      src/Avalonia.Native/WindowImpl.cs
  59. 17
      src/Avalonia.Native/avn.idl
  60. 2
      src/Avalonia.Themes.Simple/Accents/Base.xaml
  61. 2
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  62. 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  63. 22
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  64. 75
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  65. 1
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  66. 15
      tests/Avalonia.Benchmarks/Properties/launchSettings.json
  67. 48
      tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs
  68. 19
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  69. 11
      tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

1
.gitignore

@ -102,6 +102,7 @@ csx
AppPackages/
# NCrunch
.NCrunch_*/
_NCrunch_*/
*.ncrunchsolution.user
nCrunchTemp_*

1
azure-pipelines-integrationtests.yml

@ -24,6 +24,7 @@ jobs:
fi
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
pkill node
pkill testmanagerd
appium > appium.out &
pkill IntegrationTestApp
./build.sh CompileNative

12
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -44,6 +44,9 @@
5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */; };
8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */; };
8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */; };
8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */; };
AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; };
AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; };
@ -97,6 +100,9 @@
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; sourceTree = "<group>"; };
8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethodDelegate.h; sourceTree = "<group>"; };
8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethod.h; sourceTree = "<group>"; };
8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnTextInputMethod.mm; sourceTree = "<group>"; };
AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
@ -143,6 +149,9 @@
isa = PBXGroup;
children = (
855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */,
8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */,
8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */,
8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */,
BC11A5BC2608D58F0017BAD0 /* automation.h */,
BC11A5BD2608D58F0017BAD0 /* automation.mm */,
1A1852DB23E05814008F0DED /* deadlock.mm */,
@ -213,6 +222,8 @@
1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */,
183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */,
18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */,
8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */,
8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */,
18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */,
18391ED5F611FF62C45F196D /* AvnView.h in Headers */,
18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */,
@ -293,6 +304,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */,
8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,
1AFD334123E03C4F0042899B /* controlhost.mm in Sources */,
1A465D10246AB61600C5858B /* dnd.mm in Sources */,

46
native/Avalonia.Native/src/OSX/AvnTextInputMethod.h

@ -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 */

41
native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm

@ -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];
}

20
native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h

@ -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 */

6
native/Avalonia.Native/src/OSX/AvnView.h

@ -5,8 +5,6 @@
#pragma once
#import <Foundation/Foundation.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include "common.h"
#include "WindowImpl.h"
@ -14,7 +12,7 @@
@class AvnAccessibilityElement;
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination, AvnTextInputMethodDelegate>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt;
@ -24,4 +22,4 @@
-(AvnPlatformResizeReason) getResizeReason;
-(void) setResizeReason:(AvnPlatformResizeReason)reason;
+ (AvnPoint)toAvnPoint:(CGPoint)p;
@end
@end

85
native/Avalonia.Native/src/OSX/AvnView.mm

@ -12,6 +12,7 @@
{
ComPtr<WindowBaseImpl> _parent;
NSTrackingArea* _area;
NSMutableAttributedString* _markedText;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
@ -20,6 +21,9 @@
NSObject<IRenderTarget>* _renderTarget;
AvnPlatformResizeReason _resizeReason;
AvnAccessibilityElement* _accessibilityChild;
NSRect _cursorRect;
NSMutableString* _text;
NSRange _selection;
}
- (void)onClosed
@ -518,7 +522,7 @@
- (void)keyDown:(NSEvent *)event
{
[self keyboardEvent:event withType:KeyDown];
[[self inputContext] handleEvent:event];
_lastKeyHandled = [[self inputContext] handleEvent:event];
[super keyDown:event];
}
@ -557,27 +561,50 @@
- (BOOL)hasMarkedText
{
return _lastKeyHandled;
return [_markedText length] > 0;
}
- (NSRange)markedRange
{
if([_markedText length] > 0)
return NSMakeRange(0, [_markedText length] - 1);
return NSMakeRange(NSNotFound, 0);
}
- (NSRange)selectedRange
{
return NSMakeRange(NSNotFound, 0);
return _selection;
}
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
{
if([string isKindOfClass:[NSAttributedString class]])
{
_markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
}
else
{
_markedText = [[NSMutableAttributedString alloc] initWithString:string];
}
if(!_parent->InputMethod->IsActive()){
return;
}
_parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]);
}
- (void)unmarkText
{
[[_markedText mutableString] setString:@""];
if(!_parent->InputMethod->IsActive()){
return;
}
_parent->InputMethod->Client->SetPreeditText(nullptr);
[[self inputContext] discardMarkedText];
}
- (NSArray<NSString *> *)validAttributesForMarkedText
@ -587,30 +614,38 @@
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
return [NSAttributedString new];
return nullptr;
}
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
if(!_lastKeyHandled)
{
//[_text replaceCharactersInRange:replacementRange withString:string];
[self unmarkText];
//if(!_lastKeyHandled)
//{
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
}
}
//}
[[self inputContext] invalidateCharacterCoordinates];
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
return 0;
return NSNotFound;
}
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
CGRect result = { 0 };
return result;
if(!_parent->InputMethod->IsActive()){
return NSZeroRect;
}
return _cursorRect;
}
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
@ -715,4 +750,28 @@
return [[self accessibilityChild] accessibilityFocusedUIElement];
}
- (void) setText:(NSString *)text{
[_text setString:text];
[[self inputContext] discardMarkedText];
}
- (void) setSelection:(int)start :(int)end{
_selection = NSMakeRange(start, end - start);
[[self inputContext] invalidateCharacterCoordinates];
}
- (void) setCursorRect:(AvnRect)rect{
NSRect cursorRect = ToNSRect(rect);
NSRect windowRectOnScreen = [[self window] convertRectToScreen:self.frame];
windowRectOnScreen.size = cursorRect.size;
windowRectOnScreen.origin = NSMakePoint(windowRectOnScreen.origin.x + cursorRect.origin.x, windowRectOnScreen.origin.y + self.frame.size.height - cursorRect.origin.y - cursorRect.size.height);
_cursorRect = windowRectOnScreen;
[[self inputContext] invalidateCharacterCoordinates];
}
@end

2
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -394,7 +394,7 @@
- (BOOL)windowShouldZoom:(NSWindow *_Nonnull)window toFrame:(NSRect)newFrame
{
return true;
return _parent->CanZoom();
}
-(void)windowDidResignKey:(NSNotification *)notification

6
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -8,6 +8,7 @@
#include "rendertarget.h"
#include "INSWindowHolder.h"
#include "AvnTextInputMethod.h"
@class AutoFitContentView;
@class AvnMenu;
@ -103,7 +104,11 @@ BEGIN_INTERFACE_MAP()
id<AvnWindowProtocol> GetWindowProtocol ();
virtual void BringToFront ();
virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override;
virtual bool CanZoom() { return false; }
protected:
virtual NSWindowStyleMask CalculateStyleMask() = 0;
virtual void UpdateStyle();
@ -130,6 +135,7 @@ public:
NSObject <IRenderTarget> *renderTarget;
NSWindow * Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents;
ComPtr<AvnTextInputMethod> InputMethod;
AvnView *View;
};

10
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -15,6 +15,7 @@
#import "WindowProtocol.h"
#import "WindowInterfaces.h"
#include "WindowBaseImpl.h"
#include "AvnTextInputMethod.h"
WindowBaseImpl::~WindowBaseImpl() {
@ -29,6 +30,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
_glContext = gl;
renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl];
View = [[AvnView alloc] initWithParent:this];
InputMethod = new AvnTextInputMethod(View);
StandardContainer = [[AutoFitContentView new] initWithContent:View];
lastPositionSet = { 0, 0 };
@ -605,6 +607,14 @@ void WindowBaseImpl::BringToFront()
// do nothing.
}
HRESULT WindowBaseImpl::GetInputMethod(IAvnTextInputMethod **retOut) {
START_COM_CALL;
*retOut = InputMethod;
return S_OK;
}
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl)
{
@autoreleasepool

2
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -97,6 +97,8 @@ BEGIN_INTERFACE_MAP()
bool CanBecomeKeyWindow ();
bool CanZoom() override { return _isEnabled && _canResize; }
protected:
virtual NSWindowStyleMask CalculateStyleMask() override;
void UpdateStyle () override;

2
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -622,5 +622,5 @@ void WindowImpl::UpdateStyle() {
[miniaturizeButton setHidden:!hasTrafficLights];
[miniaturizeButton setEnabled:_isEnabled];
[zoomButton setHidden:!hasTrafficLights];
[zoomButton setEnabled:_isEnabled && _canResize];
[zoomButton setEnabled:CanZoom()];
}

5
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@ -36,8 +36,9 @@ namespace ControlCatalog.Pages
customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
var flyout = (Flyout)cancellableContextBorder.ContextFlyout!;
flyout.Closing += ContextFlyoutPage_Closing;
flyout.Opening += ContextFlyoutPage_Opening;
}
private ContextPageViewModel? _model;

18
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@ -16,8 +16,13 @@
<MenuItem Header="Item 3" />
</MenuFlyout>
<Flyout Placement="Bottom" x:Key="BasicFlyout">
<Flyout.FlyoutPresenterTheme>
<ControlTheme TargetType="FlyoutPresenter" BasedOn="{StaticResource {x:Type FlyoutPresenter}}">
<Setter Property="CornerRadius" Value="20" />
</ControlTheme>
</Flyout.FlyoutPresenterTheme>
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
<TextBlock Text="Flyout Content with a custom presenter theme!" TextWrapping="Wrap" />
</Panel>
</Flyout>
</UserControl.Resources>
@ -136,6 +141,15 @@
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=Center">
<Button.Flyout>
<Flyout Placement="Center">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=TopEdgeAlignedLeft">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedLeft">
@ -190,7 +204,7 @@
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=RightEdgeAlignedBottom">
<Button Content="Placement=RightEdgeAlignedTop">
<Button.Flyout>
<Flyout Placement="RightEdgeAlignedTop">
<Panel Width="100" Height="100">

13
src/Avalonia.Base/AvaloniaProperty.cs

@ -262,7 +262,18 @@ namespace Avalonia
return result;
}
/// <inheritdoc cref="Register{TOwner, TValue}" />
/// <summary>
/// Registers an attached <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="TOwner">The type of the class that is registering the property.</typeparam>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <param name="name">The name of the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <param name="enableDataValidation">if is set to true enable data validation.</param>
/// <param name="notifying">
/// A method that gets called before and after the property starts being notified on an
/// object; the bool argument will be true before and false afterwards. This callback is

2
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -364,7 +364,7 @@ namespace Avalonia
/// <param name="property">The property.</param>
/// <remarks>
/// You won't usually want to call this method directly, instead use the
/// <see cref="AvaloniaProperty.Register{TOwner, TValue}(string, TValue, bool, Data.BindingMode, Func{TValue, bool}, Func{AvaloniaObject, TValue, TValue}, Action{AvaloniaObject, bool})"/>
/// <see cref="AvaloniaProperty.Register{TOwner, TValue}(string, TValue, bool, Data.BindingMode, Func{TValue, bool}, Func{AvaloniaObject, TValue, TValue}, bool)"/>
/// method.
/// </remarks>
public void Register(Type type, AvaloniaProperty property)

1
src/Avalonia.Base/Input/MouseDevice.cs

@ -4,6 +4,7 @@ using Avalonia.Reactive;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Utilities;
#pragma warning disable CS0618
namespace Avalonia.Input
{

3
src/Avalonia.Base/Input/PenDevice.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Avalonia.Input.Raw;
using Avalonia.Platform;
#pragma warning disable CS0618
namespace Avalonia.Input
{
@ -129,7 +130,7 @@ namespace Avalonia.Input
var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers,
_lastMouseDownButton);
source?.RaiseEvent(e);
source.RaiseEvent(e);
pointer.Capture(null);
_lastMouseDownButton = default;
return e.Handled;

2
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -42,7 +42,9 @@ namespace Avalonia.Input
PointerPointProperties properties,
KeyModifiers modifiers,
Lazy<IReadOnlyList<RawPointerPoint>?>? previousPoints)
#pragma warning disable CS0618
: this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
#pragma warning restore CS0618
{
_previousPoints = previousPoints;
}

33
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -6,7 +6,8 @@ namespace Avalonia.Input
internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
{
private IPointerDevice? _lastActivePointerDevice;
private (IPointer pointer, PixelPoint position)? _lastPointer;
private (IPointer pointer, PixelPoint position)? _currentPointer;
private PixelPoint? _lastKnownPosition;
private readonly IInputRoot _inputRoot;
@ -15,7 +16,7 @@ namespace Avalonia.Input
_inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
}
public PixelPoint? LastPosition => _lastPointer?.position;
public PixelPoint? LastPosition => _lastKnownPosition;
public void OnCompleted()
{
@ -41,14 +42,14 @@ namespace Avalonia.Input
}
if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
&& _lastPointer is (var lastPointer, var lastPosition))
&& _currentPointer is var (lastPointer, lastPosition))
{
_lastPointer = null;
_currentPointer = null;
ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
args.InputModifiers.ToKeyModifiers());
}
else if (pointerDevice.TryGetPointer(args) is IPointer pointer
else if (pointerDevice.TryGetPointer(args) is { } pointer
&& pointer.Type != PointerType.Touch)
{
var element = pointer.Captured ?? args.InputHitTestResult;
@ -62,7 +63,7 @@ namespace Avalonia.Input
public void SceneInvalidated(Rect dirtyRect)
{
if (_lastPointer is (var pointer, var position))
if (_currentPointer is (var pointer, var position))
{
var clientPoint = PointToClient(_inputRoot, position);
@ -80,12 +81,12 @@ namespace Avalonia.Input
private void ClearPointerOver()
{
if (_lastPointer is (var pointer, var position))
if (_currentPointer is (var pointer, var position))
{
var clientPoint = PointToClient(_inputRoot, position);
ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
_lastPointer = null;
_currentPointer = null;
_lastActivePointerDevice = null;
}
@ -100,9 +101,11 @@ namespace Avalonia.Input
// Do not pass rootVisual, when we have unknown position,
// so GetPosition won't return invalid values.
#pragma warning disable CS0618
var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer,
position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default,
timestamp, properties, inputModifiers);
#pragma warning restore CS0618
if (element is Visual v && !v.IsAttachedToVisualTree)
{
@ -122,18 +125,18 @@ namespace Avalonia.Input
root.PointerOverElement = null;
_lastActivePointerDevice = null;
_lastPointer = null;
_currentPointer = null;
}
private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
{
if (element is Visual v)
{
foreach (IInputElement el in v.VisualChildren)
foreach (var el in v.VisualChildren)
{
if (el.IsPointerOver)
if (el is IInputElement { IsPointerOver: true } child)
{
ClearChildrenPointerOver(e, el, true);
ClearChildrenPointerOver(e, child, true);
break;
}
}
@ -151,6 +154,8 @@ namespace Avalonia.Input
ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
{
var pointerOverElement = root.PointerOverElement;
var screenPosition = ((Visual)root).PointToScreen(position);
_lastKnownPosition = screenPosition;
if (element != pointerOverElement)
{
@ -164,7 +169,7 @@ namespace Avalonia.Input
}
}
_lastPointer = (pointer, ((Visual)root).PointToScreen(position));
_currentPointer = (pointer, screenPosition);
}
private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
@ -186,8 +191,10 @@ namespace Avalonia.Input
el = root.PointerOverElement;
#pragma warning disable CS0618
var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position,
timestamp, properties, inputModifiers);
#pragma warning restore CS0618
if (el is Visual v && branch != null && !v.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e, branch, false);

1
src/Avalonia.Base/Input/TouchDevice.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Avalonia.Input.Raw;
using Avalonia.Platform;
#pragma warning disable CS0618
namespace Avalonia.Input
{

4
src/Avalonia.Base/Media/DrawingGroup.cs

@ -117,10 +117,10 @@ namespace Avalonia.Media
// root DrawingGroup, and be the same value as the root _currentDrawingGroup.
//
// Either way, _rootDrawing always references the root drawing.
protected Drawing? _rootDrawing;
private Drawing? _rootDrawing;
// Current DrawingGroup that new children are added to
protected DrawingGroup? _currentDrawingGroup;
private DrawingGroup? _currentDrawingGroup;
// Previous values of _currentDrawingGroup
private Stack<DrawingGroup?>? _previousDrawingGroupStack;

2
src/Avalonia.Base/Media/FontManager.cs

@ -107,7 +107,7 @@ namespace Avalonia.Media
source = new Uri(key.BaseUri, source);
}
if (!_fontCollections.TryGetValue(source, out var fontCollection))
if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting
/// Performs text wrapping returns a list of text lines.
/// </summary>
/// <param name="textRuns"></param>
/// <param name="canReuseTextRunList">Whether <see cref="textRuns"/> can be reused to store the split runs.</param>
/// <param name="canReuseTextRunList">Whether <see cref="TextRun"/> can be reused to store the split runs.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>

1
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -128,6 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">where to apply the opacity.</param>
void PushOpacity(double opacity, Rect bounds);
/// <summary>

2
src/Avalonia.Base/Rendering/DisplayDirtyRect.cs

@ -3,7 +3,7 @@
namespace Avalonia.Rendering
{
/// <summary>
/// Holds the state for a dirty rect rendered when <see cref="IRenderer.DrawDirtyRects"/> is set.
/// Holds the state for a dirty rect rendered when <see cref="IRenderer.SceneInvalidated"/> is set.
/// </summary>
internal class DisplayDirtyRect
{

1
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public GeometryNode(Matrix transform,
IImmutableBrush? brush,
IPen? pen,

2
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -53,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTest(Point p) => GlyphRun.Item.Bounds.ContainsExclusive(p);
public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
public override void Dispose()
{

1
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="pen">The stroke pen.</param>
/// <param name="p1">The start point of the line.</param>
/// <param name="p2">The end point of the line.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public LineNode(
Matrix transform,
IPen pen,

1
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
/// <param name="mask">The opacity mask to push.</param>
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public OpacityMaskNode(IImmutableBrush mask, Rect bounds)
: base(default, Matrix.Identity, mask)
{

1
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@ -20,7 +20,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="boxShadows">The box shadow parameters</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public RectangleNode(
Matrix transform,
IImmutableBrush? brush,

2
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -13,7 +13,7 @@ namespace Avalonia.Styling
public DescendantSelector(Selector? parent)
{
_parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
_parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceded by a selector.");
}
/// <inheritdoc/>

27
src/Avalonia.Base/Styling/OrSelector.cs

@ -10,7 +10,7 @@ namespace Avalonia.Styling
/// <summary>
/// The OR style selector.
/// </summary>
internal class OrSelector : Selector
internal sealed class OrSelector : Selector
{
private readonly IReadOnlyList<Selector> _selectors;
private string? _selectorString;
@ -42,18 +42,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type? TargetType
{
get
{
if (_targetType == null)
{
_targetType = EvaluateTargetType();
}
return _targetType;
}
}
public override Type? TargetType => _targetType ??= EvaluateTargetType();
/// <inheritdoc/>
public override string ToString(Style? owner)
@ -71,7 +60,9 @@ namespace Avalonia.Styling
var activators = new OrActivatorBuilder();
var neverThisInstance = false;
for (var i = 0; i < _selectors.Count; i++)
var count = _selectors.Count;
for (var i = 0; i < count; i++)
{
var match = _selectors[i].Match(control, parent, subscribe);
@ -108,7 +99,9 @@ namespace Avalonia.Styling
internal override void ValidateNestingSelector(bool inControlTheme)
{
for (var i = 0; i < _selectors.Count; i++)
var count = _selectors.Count;
for (var i = 0; i < count; i++)
{
_selectors[i].ValidateNestingSelector(inControlTheme);
}
@ -118,7 +111,9 @@ namespace Avalonia.Styling
{
Type? result = null;
for (var i = 0; i < _selectors.Count; i++)
var count = _selectors.Count;
for (var i = 0; i < count; i++)
{
var selector = _selectors[i];
if (selector.TargetType == null)

11
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -11,7 +11,7 @@ namespace Avalonia.Styling
/// A selector that matches the common case of a type and/or name followed by a collection of
/// style classes and pseudoclasses.
/// </summary>
internal class TypeNameAndClassSelector : Selector
internal sealed class TypeNameAndClassSelector : Selector
{
private readonly Selector? _previous;
private List<string>? _classes;
@ -52,7 +52,7 @@ namespace Avalonia.Styling
return result;
}
protected TypeNameAndClassSelector(Selector? previous)
TypeNameAndClassSelector(Selector? previous)
{
_previous = previous;
}
@ -85,12 +85,7 @@ namespace Avalonia.Styling
/// <inheritdoc/>
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = BuildSelectorString(owner);
}
return _selectorString;
return _selectorString ??= BuildSelectorString(owner);
}
/// <inheritdoc/>

4
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -516,7 +516,9 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,*,Auto,Auto">
<Grid ColumnDefinitions="Auto,*,Auto"
RowDefinitions="Auto,*,Auto,Auto"
ClipToBounds="True">
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
Theme="{StaticResource DataGridTopLeftColumnHeader}" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"

3
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -319,7 +319,8 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid ColumnDefinitions="Auto,*,Auto"
RowDefinitions="Auto,*,Auto,Auto">
RowDefinitions="Auto,*,Auto,Auto"
ClipToBounds="True">
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
Width="22" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"

38
src/Avalonia.Controls/Flyouts/Flyout.cs

@ -1,9 +1,11 @@
using Avalonia.Controls.Primitives;
using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls
{
public class Flyout : FlyoutBase
public class Flyout : PopupFlyoutBase
{
/// <summary>
/// Defines the <see cref="Content"/> property
@ -18,6 +20,21 @@ namespace Avalonia.Controls
private Classes? _classes;
/// <summary>
/// Defines the <see cref="FlyoutPresenterTheme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
AvaloniaProperty.Register<Flyout, ControlTheme?>(nameof(FlyoutPresenterTheme));
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
/// </summary>
public ControlTheme? FlyoutPresenterTheme
{
get => GetValue(FlyoutPresenterThemeProperty);
set => SetValue(FlyoutPresenterThemeProperty, value);
}
/// <summary>
/// Gets or sets the content to display in this flyout
/// </summary>
@ -36,13 +53,22 @@ namespace Avalonia.Controls
};
}
protected override void OnOpened()
protected override void OnOpening(CancelEventArgs args)
{
if (_classes != null)
if (Popup.Child is { } presenter)
{
SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
if (_classes != null)
{
SetPresenterClasses(presenter, FlyoutPresenterClasses);
}
if (FlyoutPresenterTheme is { } theme)
{
presenter.SetValue(Control.ThemeProperty, theme);
}
}
base.OnOpened();
base.OnOpening(args);
}
}
}

548
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -1,17 +1,8 @@
using System;
using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using System.Linq;
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 FlyoutBase : AvaloniaObject, IPopupHostProvider
public abstract class FlyoutBase : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="IsOpen"/> property
@ -26,75 +17,25 @@ namespace Avalonia.Controls.Primitives
public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
/// <summary>
/// Defines the <see cref="Placement"/> property
/// </summary>
public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>();
/// <summary>
/// Defines the AttachedFlyout property
/// </summary>
public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
private readonly Lazy<Popup> _popupLazy;
private bool _isOpen;
private Control? _target;
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
static FlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
public FlyoutBase()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected Popup Popup => _popupLazy.Value;
public event EventHandler? Opened;
public event EventHandler? Closed;
/// <summary>
/// Gets whether this Flyout is currently Open
/// </summary>
public bool IsOpen
{
get => _isOpen;
private set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
}
/// <summary>
/// Gets or sets the desired placement
/// </summary>
public FlyoutPlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the desired ShowMode
/// </summary>
public FlyoutShowMode ShowMode
{
get => GetValue(ShowModeProperty);
set => SetValue(ShowModeProperty, value);
protected set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
}
/// <summary>
@ -103,32 +44,9 @@ namespace Avalonia.Controls.Primitives
public Control? Target
{
get => _target;
private set => SetAndRaise(TargetProperty, ref _target, value);
protected set => SetAndRaise(TargetProperty, ref _target, 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? Closed;
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opened;
public event EventHandler? Opening;
public static FlyoutBase? GetAttachedFlyout(Control element)
{
return element.GetValue(AttachedFlyoutProperty);
@ -145,462 +63,18 @@ namespace Avalonia.Controls.Primitives
flyout?.ShowAt(flyoutOwner);
}
/// <summary>
/// Shows the Flyout at the given Control
/// </summary>
/// <param name="placementTarget">The control to show the Flyout at</param>
public 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 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);
}
public abstract void ShowAt(Control placementTarget);
public abstract void Hide();
protected virtual void OnOpened()
{
Opened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnClosing(CancelEventArgs args)
{
Closing?.Invoke(this, args);
}
protected virtual void OnClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
/// <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;
}
if (showAtPointer)
{
Popup.PlacementMode = PlacementMode.Pointer;
}
else
{
Popup.PlacementMode = PlacementMode.AnchorAndGravity;
Popup.PlacementConstraintAdjustment =
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
}
var trgtBnds = Target?.Bounds ?? default;
switch (Placement)
{
case FlyoutPlacementMode.Top: //Above & centered
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
break;
case FlyoutPlacementMode.TopEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, 0, 0, 0);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
break;
case FlyoutPlacementMode.TopEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
break;
case FlyoutPlacementMode.RightEdgeAlignedTop:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.Right: //Right & centered
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Right;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.RightEdgeAlignedBottom:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.Bottom: //Below & centered
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.BottomEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.BottomEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.LeftEdgeAlignedTop:
Popup.PlacementRect = new Rect(0, 0, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
break;
case FlyoutPlacementMode.Left: //Left & centered
Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Left;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
break;
case FlyoutPlacementMode.LeftEdgeAlignedBottom:
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft;
break;
//includes Auto (not sure what determines that)...
default:
//This is just FlyoutPlacementMode.Top behavior (above & centered)
Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
break;
}
}
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 FlyoutBase flyout)
{
if (control.ContextMenu != null)
{
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
var triggeredByPointerInput = e.TryGetPosition(null, out _);
e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput);
}
}
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);
}
}
}

77
src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs

@ -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
}
}

39
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -1,4 +1,5 @@
using System.Collections;
using System.ComponentModel;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -7,7 +8,7 @@ using Avalonia.Styling;
namespace Avalonia.Controls
{
public class MenuFlyout : FlyoutBase
public class MenuFlyout : PopupFlyoutBase
{
public MenuFlyout()
{
@ -34,6 +35,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<ControlTheme?> ItemContainerThemeProperty =
ItemsControl.ItemContainerThemeProperty.AddOwner<MenuFlyout>();
/// <summary>
/// Defines the <see cref="FlyoutPresenterTheme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
Flyout.FlyoutPresenterThemeProperty.AddOwner<MenuFlyout>();
public Classes FlyoutPresenterClasses => _classes ??= new Classes();
/// <summary>
@ -60,10 +67,19 @@ namespace Avalonia.Controls
/// </summary>
public ControlTheme? ItemContainerTheme
{
get { return GetValue(ItemContainerThemeProperty); }
set { SetValue(ItemContainerThemeProperty, value); }
get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
/// </summary>
public ControlTheme? FlyoutPresenterTheme
{
get => GetValue(FlyoutPresenterThemeProperty);
set => SetValue(FlyoutPresenterThemeProperty, value);
}
private Classes? _classes;
private IEnumerable? _items;
private IDataTemplate? _itemTemplate;
@ -78,13 +94,22 @@ namespace Avalonia.Controls
};
}
protected override void OnOpened()
protected override void OnOpening(CancelEventArgs args)
{
if (_classes != null)
if (Popup.Child is { } presenter)
{
SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
if (_classes != null)
{
SetPresenterClasses(presenter, FlyoutPresenterClasses);
}
if (FlyoutPresenterTheme is { } theme)
{
presenter.SetValue(Control.ThemeProperty, theme);
}
}
base.OnOpened();
base.OnOpening(args);
}
}
}

512
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@ -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);
}
}
}

23
src/Avalonia.Controls/ItemsSourceView.cs

@ -27,6 +27,7 @@ namespace Avalonia.Controls
private readonly IList _inner;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening;
@ -70,7 +71,7 @@ namespace Avalonia.Controls
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks>
internal bool HasKeyIndexMapping => false;
@ -92,6 +93,25 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PreCollectionChanged
{
add
{
AddListenerIfNecessary();
_preCollectionChanged += value;
}
remove
{
_preCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
@ -229,6 +249,7 @@ namespace Avalonia.Controls
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)

58
src/Avalonia.Controls/PlacementMode.cs

@ -13,28 +13,74 @@ namespace Avalonia.Controls
Pointer,
/// <summary>
/// The popup is placed at the bottom left of its target.
/// Preferred location is below the target element.
/// </summary>
Bottom,
/// <summary>
/// The popup is placed at the top right of its target.
/// Preferred location is to the right of the target element.
/// </summary>
Right,
/// <summary>
/// The popup is placed at the top left of its target.
/// Preferred location is to the left of the target element.
/// </summary>
Left,
/// <summary>
/// The popup is placed at the top left of its target.
/// Preferred location is above the target element.
/// </summary>
Top,
/// <summary>
/// The popup is placed according to anchor and gravity rules
/// Preferred location is centered over the target element.
/// </summary>
AnchorAndGravity
Center,
/// <summary>
/// The popup is placed according to <see cref="Popup.PlacementAnchor"/> and <see cref="Popup.PlacementGravity"/> rules.
/// </summary>
AnchorAndGravity,
/// <summary>
/// Preferred location is above the target element, with the left edge of the popup
/// aligned with the left edge of the target element.
/// </summary>
TopEdgeAlignedLeft,
/// <summary>
/// Preferred location is above the target element, with the right edge of popup aligned with right edge of the target element.
/// </summary>
TopEdgeAlignedRight,
/// <summary>
/// Preferred location is below the target element, with the left edge of popup aligned with left edge of the target element.
/// </summary>
BottomEdgeAlignedLeft,
/// <summary>
/// Preferred location is below the target element, with the right edge of popup aligned with right edge of the target element.
/// </summary>
BottomEdgeAlignedRight,
/// <summary>
/// Preferred location is to the left of the target element, with the top edge of popup aligned with top edge of the target element.
/// </summary>
LeftEdgeAlignedTop,
/// <summary>
/// Preferred location is to the left of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
/// </summary>
LeftEdgeAlignedBottom,
/// <summary>
/// Preferred location is to the right of the target element, with the top edge of popup aligned with top edge of the target element.
/// </summary>
RightEdgeAlignedTop,
/// <summary>
/// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
/// </summary>
RightEdgeAlignedBottom
}
}

2
src/Avalonia.Controls/Platform/IInsetsManager.cs

@ -36,7 +36,7 @@ namespace Avalonia.Controls.Platform
SafeAreaPadding = safeArePadding;
}
/// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
/// <inheritdoc cref="IInsetsManager.SafeAreaPadding"/>
public Thickness SafeAreaPadding { get; }
}

64
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -10,6 +10,7 @@ using Avalonia.Layout;
using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents;
using Avalonia.Input.TextInput;
using Avalonia.Data;
namespace Avalonia.Controls.Presenters
{
@ -52,7 +53,7 @@ namespace Avalonia.Controls.Presenters
AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
(o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
/// <summary>
/// Defines the <see cref="PreeditText"/> property.
@ -107,7 +108,7 @@ namespace Avalonia.Controls.Presenters
private int _selectionStart;
private int _selectionEnd;
private bool _caretBlink;
private string? _text;
internal string? _text;
private TextLayout? _textLayout;
private Size _constraint;
@ -526,23 +527,6 @@ namespace Avalonia.Controls.Presenters
}
}
private string? GetText()
{
if (!string.IsNullOrEmpty(_preeditText))
{
if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length)
{
return _preeditText;
}
var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex);
return text;
}
return _text;
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -551,7 +535,7 @@ namespace Avalonia.Controls.Presenters
{
TextLayout result;
var text = GetText();
var text = _text;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
@ -564,7 +548,7 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground;
if(_compositionRegion != null)
if (_compositionRegion != null)
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
new GenericTextRunProperties(typeface, FontSize,
@ -851,7 +835,7 @@ namespace Avalonia.Controls.Presenters
CaretChanged();
}
private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true)
internal void UpdateCaret(CharacterHit characterHit, bool notify = true)
{
_lastCharacterHit = characterHit;
@ -879,7 +863,7 @@ namespace Avalonia.Controls.Presenters
CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
}
if (updateCaretIndex)
if (notify)
{
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
}
@ -899,35 +883,6 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick -= CaretTimerTick;
}
protected void OnPreeditTextChanged(string? oldValue, string? newValue)
{
InvalidateTextLayout();
if (string.IsNullOrEmpty(newValue))
{
UpdateCaret(_lastCharacterHit);
}
else
{
var textPosition = _caretIndex + newValue?.Length ?? 0;
var characterHit = GetCharacterHitFromTextPosition(textPosition);
UpdateCaret(characterHit, false);
}
}
private CharacterHit GetCharacterHitFromTextPosition(int textPosition)
{
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true);
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1));
return characterHit;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@ -935,11 +890,6 @@ namespace Avalonia.Controls.Presenters
switch (change.Property.Name)
{
case nameof(PreeditText):
{
OnPreeditTextChanged(change.OldValue as string, change.NewValue as string);
break;
}
case nameof(CompositionRegion):
case nameof(Foreground):
case nameof(FontSize):

48
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -478,35 +478,29 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var bounds = new Rect(default, target.Bounds.Size);
var anchorRect = rect ?? bounds;
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
var parameters = placement switch
{
positionerParameters.Anchor = PopupAnchor.TopRight;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
positionerParameters.Anchor = PopupAnchor.BottomLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Left)
{
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{
positionerParameters.Anchor = anchor;
positionerParameters.Gravity = gravity;
}
else
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
PlacementMode.Bottom => (PopupAnchor.Bottom, PopupGravity.Bottom),
PlacementMode.Right => (PopupAnchor.Right, PopupGravity.Right),
PlacementMode.Left => (PopupAnchor.Left, PopupGravity.Left),
PlacementMode.Top => (PopupAnchor.Top, PopupGravity.Top),
PlacementMode.Center => (PopupAnchor.None, PopupGravity.None),
PlacementMode.AnchorAndGravity => (anchor, gravity),
PlacementMode.TopEdgeAlignedRight => (PopupAnchor.TopRight, PopupGravity.TopLeft),
PlacementMode.TopEdgeAlignedLeft => (PopupAnchor.TopLeft, PopupGravity.TopRight),
PlacementMode.BottomEdgeAlignedLeft => (PopupAnchor.BottomLeft, PopupGravity.BottomRight),
PlacementMode.BottomEdgeAlignedRight => (PopupAnchor.BottomRight, PopupGravity.BottomLeft),
PlacementMode.LeftEdgeAlignedTop => (PopupAnchor.TopLeft, PopupGravity.BottomLeft),
PlacementMode.LeftEdgeAlignedBottom => (PopupAnchor.BottomLeft, PopupGravity.TopLeft),
PlacementMode.RightEdgeAlignedTop => (PopupAnchor.TopRight, PopupGravity.BottomRight),
PlacementMode.RightEdgeAlignedBottom => (PopupAnchor.BottomRight, PopupGravity.TopRight),
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement,
"Invalid value for Popup.PlacementMode")
};
positionerParameters.Anchor = parameters.Item1;
positionerParameters.Gravity = parameters.Item2;
}
// Invert coordinate system if FlowDirection is RTL

2
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -203,7 +203,7 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{

17
src/Avalonia.Controls/Selection/SelectedItems.cs

@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Controls.Selection
{
internal class SelectedItems<T> : IReadOnlyList<T>
internal class SelectedItems<T> : IReadOnlyList<T?>
{
private readonly SelectionModel<T>? _owner;
private readonly ItemsSourceView<T>? _items;
@ -19,12 +19,9 @@ namespace Avalonia.Controls.Selection
_items = items;
}
[MaybeNull]
public T this[int index]
public T? this[int index]
{
#pragma warning disable CS8766
get
#pragma warning restore CS8766
{
if (index >= Count)
{
@ -64,15 +61,13 @@ namespace Avalonia.Controls.Selection
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
public IEnumerator<T> GetEnumerator()
public IEnumerator<T?> GetEnumerator()
{
if (_owner?.SingleSelect == true)
{
if (_owner.SelectedIndex >= 0)
{
#pragma warning disable CS8603
yield return _owner.SelectedItem;
#pragma warning restore CS8603
}
}
else
@ -83,9 +78,7 @@ namespace Avalonia.Controls.Selection
{
for (var i = range.Begin; i <= range.End; ++i)
{
#pragma warning disable CS8603
yield return items is object ? items[i] : default;
#pragma warning restore CS8603
}
}
}
@ -102,8 +95,8 @@ namespace Avalonia.Controls.Selection
public class Untyped : IReadOnlyList<object?>
{
private readonly IReadOnlyList<T> _source;
public Untyped(IReadOnlyList<T> source) => _source = source;
private readonly IReadOnlyList<T?> _source;
public Untyped(IReadOnlyList<T?> source) => _source = source;
public object? this[int index] => _source[index];
public int Count => _source.Count;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

91
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection
private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
private IList? _initSelectedItems;
private bool _isSourceCollectionChanging;
public SelectionModel()
{
@ -55,7 +56,7 @@ namespace Avalonia.Controls.Selection
if (RangesEnabled && _selectedIndex >= 0)
{
CommitSelect(new IndexRange(_selectedIndex));
CommitSelect(_selectedIndex, _selectedIndex);
}
RaisePropertyChanged(nameof(SingleSelect));
@ -80,7 +81,7 @@ namespace Avalonia.Controls.Selection
{
get
{
if (ItemsView is object)
if (ItemsView is not null)
{
return GetItemAt(_selectedIndex);
}
@ -93,21 +94,19 @@ namespace Avalonia.Controls.Selection
}
set
{
if (ItemsView is object)
if (ItemsView is not null)
{
SelectedIndex = ItemsView.IndexOf(value!);
}
else
{
Clear();
#pragma warning disable CS8601
SetInitSelectedItems(new T[] { value });
#pragma warning restore CS8601
SetInitSelectedItems(new T[] { value! });
}
}
}
public IReadOnlyList<T> SelectedItems
public IReadOnlyList<T?> SelectedItems
{
get
{
@ -206,7 +205,7 @@ namespace Avalonia.Controls.Selection
{
// If the collection is currently changing, commit the update when the
// collection change finishes.
if (!IsSourceCollectionChanging)
if (!_isSourceCollectionChanging)
{
CommitOperation(_operation);
}
@ -278,7 +277,7 @@ namespace Avalonia.Controls.Selection
{
if (base.Source != value)
{
if (_operation is object)
if (_operation is not null)
{
throw new InvalidOperationException("Cannot change source while update is in progress.");
}
@ -296,7 +295,7 @@ namespace Avalonia.Controls.Selection
{
update.Operation.IsSourceUpdate = true;
if (_initSelectedItems is object && ItemsView is object)
if (_initSelectedItems is object && ItemsView is not null)
{
foreach (T i in _initSelectedItems)
{
@ -315,17 +314,23 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
}
private protected override void OnSourceReset()
protected override void OnSourceCollectionChangeStarted()
{
base.OnSourceCollectionChangeStarted();
_isSourceCollectionChanging = true;
}
protected override void OnSourceReset()
{
_selectedIndex = _anchorIndex = -1;
CommitDeselect(new IndexRange(0, int.MaxValue));
CommitDeselect(0, int.MaxValue);
if (SourceReset is object)
if (SourceReset is not null)
{
SourceReset.Invoke(this, EventArgs.Empty);
}
@ -339,7 +344,7 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
protected override void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{
// Note: We're *not* putting this in a using scope. A collection update is still in progress
// so the operation won't get committed by normal means: we have to commit it manually.
@ -347,7 +352,7 @@ namespace Avalonia.Controls.Selection
update.Operation.DeselectedItems = deselectedItems;
if (_selectedIndex == -1 && LostSelection is object)
if (_selectedIndex == -1 && LostSelection is not null)
{
LostSelection(this, EventArgs.Empty);
}
@ -357,7 +362,7 @@ namespace Avalonia.Controls.Selection
CommitOperation(update.Operation, raisePropertyChanged: false);
}
private protected override CollectionChangeState OnItemsAdded(int index, IList items)
protected override CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = SelectedIndex >= index;
@ -420,7 +425,7 @@ namespace Avalonia.Controls.Selection
};
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_operation?.UpdateCount > 0)
{
@ -451,6 +456,16 @@ namespace Avalonia.Controls.Selection
}
}
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
if (!base.IsValidCollectionChange(e))
@ -474,19 +489,11 @@ namespace Avalonia.Controls.Selection
return true;
}
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
protected override void OnSourceCollectionChangeFinished()
{
if (_operation is object)
_isSourceCollectionChanging = false;
if (_operation is not null)
{
CommitOperation(_operation);
}
@ -575,7 +582,7 @@ namespace Avalonia.Controls.Selection
{
index = Math.Max(index, -1);
if (ItemsView is object && index >= ItemsView.Count)
if (ItemsView is not null && index >= ItemsView.Count)
{
index = -1;
}
@ -585,7 +592,7 @@ namespace Avalonia.Controls.Selection
private IndexRange CoerceRange(int start, int end)
{
var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue;
var max = ItemsView is not null ? ItemsView.Count - 1 : int.MaxValue;
if (start > max || (start < 0 && end < 0))
{
@ -643,7 +650,7 @@ namespace Avalonia.Controls.Selection
var oldSelectedIndex = _selectedIndex;
var indexesChanged = false;
if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection)
if (operation.SelectedIndex == -1 && LostSelection is not null && !operation.SkipLostSelection)
{
operation.UpdateCount++;
LostSelection?.Invoke(this, EventArgs.Empty);
@ -652,17 +659,23 @@ namespace Avalonia.Controls.Selection
_selectedIndex = operation.SelectedIndex;
_anchorIndex = operation.AnchorIndex;
if (operation.SelectedRanges is object)
if (operation.SelectedRanges is not null)
{
indexesChanged |= CommitSelect(operation.SelectedRanges) > 0;
foreach (var range in operation.SelectedRanges)
{
indexesChanged |= CommitSelect(range.Begin, range.End) > 0;
}
}
if (operation.DeselectedRanges is object)
if (operation.DeselectedRanges is not null)
{
indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0;
foreach (var range in operation.DeselectedRanges)
{
indexesChanged |= CommitDeselect(range.Begin, range.End) > 0;
}
}
if (SelectionChanged is object || _untypedSelectionChanged is object)
if (SelectionChanged is not null || _untypedSelectionChanged is not null)
{
IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
@ -690,14 +703,14 @@ namespace Avalonia.Controls.Selection
// CollectionChanged event. LostFocus may have caused another item to have been
// selected, but it can't have caused a deselection (as it was called due to
// selection being lost) so we're ok to discard `deselected` here.
var deselectedItems = operation.DeselectedItems ??
var deselectedItems = (IReadOnlyList<T?>?)operation.DeselectedItems ??
SelectedItems<T>.Create(deselected, deselectedSource);
var e = new SelectionModelSelectionChangedEventArgs<T>(
SelectedIndexes<T>.Create(deselected),
SelectedIndexes<T>.Create(selected),
deselectedItems,
SelectedItems<T>.Create(selected, ItemsView));
SelectedItems<T>.Create(selected, Source is not null ? ItemsView : null));
SelectionChanged?.Invoke(this, e);
_untypedSelectionChanged?.Invoke(this, e);
}

8
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<int>? deselectedIndices = null,
IReadOnlyList<int>? selectedIndices = null,
IReadOnlyList<T>? deselectedItems = null,
IReadOnlyList<T>? selectedItems = null)
IReadOnlyList<T?>? deselectedItems = null,
IReadOnlyList<T?>? selectedItems = null)
{
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
SelectedIndexes = selectedIndices ?? Array.Empty<int>();
@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public new IReadOnlyList<T> DeselectedItems { get; }
public new IReadOnlyList<T?> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public new IReadOnlyList<T> SelectedItems { get; }
public new IReadOnlyList<T?> SelectedItems { get; }
protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
{

311
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -2,37 +2,62 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Selection
{
public abstract class SelectionNodeBase<T> : ICollectionChangedListener
/// <summary>
/// Base class for selection models.
/// </summary>
/// <typeparam name="T">The type of the element being selected.</typeparam>
public abstract class SelectionNodeBase<T>
{
private IEnumerable? _source;
private bool _rangesEnabled;
private List<IndexRange>? _ranges;
private int _collectionChanging;
/// <summary>
/// Gets or sets the source collection.
/// </summary>
protected IEnumerable? Source
{
get => _source;
set
{
void OnPreChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeStarted();
void OnChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChanged(e);
void OnPostChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeFinished();
if (_source != value)
{
if (ItemsView?.Inner is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
if (ItemsView is not null)
{
ItemsView.PreCollectionChanged -= OnPreChanged;
ItemsView.CollectionChanged -= OnChanged;
ItemsView.PostCollectionChanged -= OnPostChanged;
}
_source = value;
ItemsView = value is object ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView?.Inner is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
ItemsView = value is not null ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView is not null)
{
ItemsView.PreCollectionChanged += OnPreChanged;
ItemsView.CollectionChanged += OnChanged;
ItemsView.PostCollectionChanged += OnPostChanged;
}
}
}
}
protected bool IsSourceCollectionChanging => _collectionChanging > 0;
/// <summary>
/// Gets an <see cref="ItemsSourceView{T}"/> of the <see cref="Source"/>.
/// </summary>
protected internal ItemsSourceView<T>? ItemsView { get; set; }
/// <summary>
/// Gets or sets a value indicating whether range selection is currently enabled for
/// the selection node.
/// </summary>
protected bool RangesEnabled
{
get => _rangesEnabled;
@ -50,8 +75,6 @@ namespace Avalonia.Controls.Selection
}
}
internal ItemsSourceView<T>? ItemsView { get; set; }
internal IReadOnlyList<IndexRange> Ranges
{
get
@ -65,81 +88,170 @@ namespace Avalonia.Controls.Selection
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Called when the source collection starts changing.
/// </summary>
protected virtual void OnSourceCollectionChangeStarted()
{
++_collectionChanging;
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
/// <summary>
/// Called when the <see cref="Source"/> collection changes.
/// </summary>
/// <param name="e">The details of the collection change.</param>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> calls
/// <see cref="OnItemsAdded(int, IList)"/> and <see cref="OnItemsRemoved(int, IList)"/>
/// in order to calculate how the collection change affects the currently selected items.
/// It then calls <see cref="OnIndexesChanged(int, int)"/> and
/// <see cref="OnSelectionRemoved(int, int, IReadOnlyList{T})"/> if necessary, according
/// to the <see cref="CollectionChangeState"/> returned by those methods.
///
/// Override this method and <see cref="OnSourceCollectionChangeFinished"/> to provide
/// custom handling of source collection changes.
/// </remarks>
protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
OnSourceCollectionChanged(e);
}
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? removed = null;
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
if (--_collectionChanging == 0)
if (!IsValidCollectionChange(e))
{
OnSourceCollectionChangeFinished();
return;
}
}
protected abstract void OnSourceCollectionChangeFinished();
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
private protected abstract void OnSourceReset();
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems);
private protected int CommitSelect(IndexRange range)
{
if (RangesEnabled)
switch (e.Action)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, range);
case NotifyCollectionChangedAction.Add:
{
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
break;
}
case NotifyCollectionChangedAction.Remove:
{
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
removed = change.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:
OnSourceReset();
break;
}
return 0;
if (shiftDelta != 0)
OnIndexesChanged(shiftIndex, shiftDelta);
if (removed is not null)
OnSelectionRemoved(shiftIndex, -shiftDelta, removed);
}
private protected int CommitSelect(IReadOnlyList<IndexRange> ranges)
/// <summary>
/// Called when the source collection has finished changing, and all CollectionChanged
/// handlers have run.
/// </summary>
/// <remarks>
/// Override this method to respond to the end of a collection change instead of acting at
/// the end of <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// in order to ensure that all UI subscribers to the source collection change event have
/// had chance to run.
/// </remarks>
protected virtual void OnSourceCollectionChangeFinished()
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, ranges);
}
}
return 0;
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// detailing the indexes changed by the collection changing.
/// </summary>
/// <param name="shiftIndex">The first index that was shifted.</param>
/// <param name="shiftDelta">
/// If positive, the number of items inserted, or if negative the number of items removed.
/// </param>
protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
}
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// on collection reset.
/// </summary>
protected abstract void OnSourceReset();
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// detailing the items removed by a collection change.
/// </summary>
protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{
}
private protected int CommitDeselect(IndexRange range)
/// <summary>
/// If <see cref="RangesEnabled"/>, adds the specified range to the selection.
/// </summary>
/// <param name="begin">The inclusive index of the start of the range to select.</param>
/// <param name="end">The inclusive index of the end of the range to select.</param>
/// <returns>The number of items selected.</returns>
protected int CommitSelect(int begin, int end)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, range);
return IndexRange.Add(_ranges, new IndexRange(begin, end));
}
return 0;
}
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges)
/// <summary>
/// If <see cref="RangesEnabled"/>, removes the specified range from the selection.
/// </summary>
/// <param name="begin">The inclusive index of the start of the range to deselect.</param>
/// <param name="end">The inclusive index of the end of the range to deselect.</param>
/// <returns>The number of items selected.</returns>
protected int CommitDeselect(int begin, int end)
{
if (RangesEnabled && _ranges is object)
if (RangesEnabled)
{
return IndexRange.Remove(_ranges, ranges);
_ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, new IndexRange(begin, end));
}
return 0;
}
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// when items are added to the source collection.
/// </summary>
/// <returns>
/// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
/// selection.
/// </returns>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
/// assigning new indexes. Override this method to carry out additional computation when
/// items are added.
/// </remarks>
protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = false;
if (_ranges is object)
if (_ranges is not null)
{
List<IndexRange>? toAdd = null;
@ -150,7 +262,7 @@ namespace Avalonia.Controls.Selection
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
var begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
@ -167,7 +279,7 @@ namespace Avalonia.Controls.Selection
}
}
if (toAdd is object)
if (toAdd is not null)
{
foreach (var range in toAdd)
{
@ -183,14 +295,27 @@ namespace Avalonia.Controls.Selection
};
}
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// when items are removed from the source collection.
/// </summary>
/// <returns>
/// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
/// selection.
/// </returns>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
/// assigning new indexes. Override this method to carry out additional computation when
/// items are removed.
/// </remarks>
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
{
var count = items.Count;
var removedRange = new IndexRange(index, index + count - 1);
bool shifted = false;
var shifted = false;
List<T>? removed = null;
if (_ranges is object)
if (_ranges is not null)
{
var deselected = new List<IndexRange>();
@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection
};
}
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? removed = null;
if (!IsValidCollectionChange(e))
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
break;
}
case NotifyCollectionChangedAction.Remove:
{
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
removed = change.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:
OnSourceReset();
break;
}
if (shiftDelta != 0)
{
OnIndexesChanged(shiftIndex, shiftDelta);
}
if (removed is object)
{
OnSelectionChanged(removed);
}
}
private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
// If the selection is modified in a CollectionChanged handler before the selection
@ -309,11 +380,27 @@ namespace Avalonia.Controls.Selection
return true;
}
private protected struct CollectionChangeState
/// <summary>
/// Details the results of a collection change on the current selection;
/// </summary>
protected class CollectionChangeState
{
public int ShiftIndex;
public int ShiftDelta;
public List<T>? RemovedItems;
/// <summary>
/// Gets or sets the first index that was shifted as a result of the collection
/// changing.
/// </summary>
public int ShiftIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating how the indexes after <see cref="ShiftIndex"/>
/// were shifted.
/// </summary>
public int ShiftDelta { get; set; }
/// <summary>
/// Gets or sets the items removed by the collection change, if any.
/// </summary>
public List<T>? RemovedItems { get; set; }
}
}
}

4
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -172,7 +172,7 @@ namespace Avalonia.Controls
flyout.Opened += Flyout_Opened;
flyout.Closed += Flyout_Closed;
_flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
_flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged);
}
}
@ -432,7 +432,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Called when the <see cref="FlyoutBase.Placement"/> property changes.
/// Called when the <see cref="PopupFlyoutBase.Placement"/> property changes.
/// </summary>
private void Flyout_PlacementPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{

2
src/Avalonia.Controls/TextBox.cs

@ -1736,7 +1736,7 @@ namespace Avalonia.Controls
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
var textLine = textLines[lineIndex];
var textPosition = textLine.FirstTextSourceIndex + textLine.Length;
var textPosition = textLine.FirstTextSourceIndex + textLine.Length - textLine.NewLineLength;
_presenter.MoveCaretToTextPosition(textPosition, true);
}

58
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,11 +1,11 @@
using System;
using System.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -77,7 +77,7 @@ namespace Avalonia.Controls
{
get => _textEditable; set
{
if(_textEditable != null)
if (_textEditable != null)
{
_textEditable.TextChanged -= TextEditable_TextChanged;
_textEditable.SelectionChanged -= TextEditable_SelectionChanged;
@ -86,7 +86,7 @@ namespace Avalonia.Controls
_textEditable = value;
if(_textEditable != null)
if (_textEditable != null)
{
_textEditable.TextChanged += TextEditable_TextChanged;
_textEditable.SelectionChanged += TextEditable_SelectionChanged;
@ -112,7 +112,7 @@ namespace Avalonia.Controls
private void TextEditable_SelectionChanged(object? sender, EventArgs e)
{
if(_parent != null && _textEditable != null)
if (_parent != null && _textEditable != null)
{
_parent.SelectionStart = _textEditable.SelectionStart;
_parent.SelectionEnd = _textEditable.SelectionEnd;
@ -159,14 +159,51 @@ namespace Avalonia.Controls
public event EventHandler? SurroundingTextChanged;
public void SetPreeditText(string? text)
private string? _presenterText;
private int _compositionStart;
public void SetPreeditText(string? preeditText)
{
if (_presenter == null)
if (_presenter == null || _parent == null)
{
return;
}
_presenter.PreeditText = text;
if (_presenterText is null)
{
_presenterText = _parent.Text ?? "";
_compositionStart = _parent.CaretIndex;
}
var text = GetText(preeditText);
_presenter._text = text;
_presenter.PreeditText = preeditText;
_presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false);
if (string.IsNullOrEmpty(preeditText))
{
_presenterText = null;
}
}
private string? GetText(string? preeditText)
{
if (string.IsNullOrEmpty(preeditText))
{
return _presenterText;
}
if (string.IsNullOrEmpty(_presenterText))
{
return preeditText;
}
var text = _presenterText.Substring(0, _compositionStart) + preeditText + _presenterText.Substring(_compositionStart);
return text;
}
public void SetComposingRegion(TextRange? region)
@ -175,6 +212,7 @@ namespace Avalonia.Controls
{
return;
}
_presenter.CompositionRegion = region;
}
@ -256,9 +294,9 @@ namespace Avalonia.Controls
}
}
if(e.Property == TextBox.TextProperty)
if (e.Property == TextBox.TextProperty)
{
if(_textEditable != null)
if (_textEditable != null)
{
_textEditable.Text = (string?)e.NewValue;
}

12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -82,7 +82,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
Popup p => GetPopupHostObservable(p),
Control c => Observable.CombineLatest(
new IObservable<IPopupHostProvider?>[]
new IObservable<object?>[]
{
c.GetObservable(Control.ContextFlyoutProperty),
c.GetObservable(Control.ContextMenuProperty),
@ -93,11 +93,11 @@ namespace Avalonia.Diagnostics.ViewModels
.Select(
items =>
{
var contextFlyout = items[0];
var contextMenu = (ContextMenu?)items[1];
var attachedFlyout = items[2];
var toolTip = items[3];
var buttonFlyout = items[4];
var contextFlyout = items[0] as IPopupHostProvider;
var contextMenu = items[1] as ContextMenu;
var attachedFlyout = items[2] as IPopupHostProvider;
var toolTip = items[3] as IPopupHostProvider;
var buttonFlyout = items[4] as IPopupHostProvider;
if (contextMenu != null)
//Note: ContextMenus are special since all the items are added as visual children.

118
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@ -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);
}
}
}
}
}

11
src/Avalonia.Native/WindowImpl.cs

@ -4,6 +4,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
@ -19,6 +20,7 @@ namespace Avalonia.Native
private double _extendTitleBarHeight = -1;
private DoubleClickHelper _doubleClickHelper;
private readonly ITopLevelNativeMenuExporter _nativeMenuExporter;
private readonly AvaloniaNativeTextInputMethod _inputMethod;
internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts,
AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature)
@ -33,6 +35,8 @@ namespace Avalonia.Native
}
_nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory);
_inputMethod = new AvaloniaNativeTextInputMethod(_native);
}
class WindowEvents : WindowBaseEvents, IAvnWindowEvents
@ -67,7 +71,7 @@ namespace Avalonia.Native
}
}
public IAvnWindow Native => _native;
public new IAvnWindow Native => _native;
public void CanResize(bool value)
{
@ -229,6 +233,11 @@ namespace Avalonia.Native
public override object TryGetFeature(Type featureType)
{
if(featureType == typeof(ITextInputMethodImpl))
{
return _inputMethod;
}
if (featureType == typeof(ITopLevelNativeMenuExporter))
{
return _nativeMenuExporter;

17
src/Avalonia.Native/avn.idl

@ -546,6 +546,7 @@ interface IAvnWindowBase : IUnknown
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle);
HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode);
HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode);
HRESULT GetInputMethod(IAvnTextInputMethod **ppv);
}
[uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits]
@ -612,6 +613,22 @@ interface IAvnWindowEvents : IAvnWindowBaseEvents
void GotInputWhenDisabled();
}
[uuid(f2079145-a2d9-42b8-a85e-2732e3c2b055)]
interface IAvnTextInputMethodClient : IUnknown
{
void SetPreeditText(char* preeditText);
void SelectInSurroundingText(int start, int length);
}
[uuid(1382a29f-e260-4c7a-b83f-c99fc72e27c2)]
interface IAvnTextInputMethod : IUnknown
{
HRESULT SetClient(IAvnTextInputMethodClient* client);
void Reset();
void SetCursorRect(AvnRect rect);
void SetSurroundingText(char* text, int anchorOffset, int cursorOffset);
}
[uuid(e34ae0f8-18b4-48a3-b09d-2e6b19a3cf5e)]
interface IAvnMacOptions : IUnknown
{

2
src/Avalonia.Themes.Simple/Accents/Base.xaml

@ -76,7 +76,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts://Inter#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
<Color x:Key="ThemeAccentColor">#CC119EDA</Color>
<Color x:Key="ThemeAccentColor2">#99119EDA</Color>
<Color x:Key="ThemeAccentColor3">#66119EDA</Color>

2
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -16,7 +16,7 @@ public class BrowserPlatformOptions
public static class BrowserAppBuilder
{
/// <summary>
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see cref="mainDivId"/> parameter.
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see paramref="mainDivId"/> parameter.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>

1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -29,7 +29,6 @@ namespace Avalonia.Direct2D1.Media
/// <summary>
/// Initializes a new instance of the <see cref="DrawingContextImpl"/> class.
/// </summary>
/// <param name="visualBrushRenderer">The visual brush renderer.</param>
/// <param name="renderTarget">The render target to draw to.</param>
/// <param name="layerFactory">
/// An object to use to create layers. May be null, in which case a

22
src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs

@ -31,6 +31,8 @@ namespace Avalonia.Win32.Input
public bool ShowCompositionWindow => false;
public string? Composition { get; internal set; }
public void CreateCaret()
{
_caretManager.TryCreate(Hwnd);
@ -269,28 +271,28 @@ namespace Avalonia.Win32.Input
// we're skipping this. not usable on windows
}
public void CompositionChanged()
public void CompositionChanged(string? composition)
{
if (!IsComposing)
{
return;
}
Composition = composition;
if(!IsActive || !Client.SupportsPreedit)
if (!IsActive || !Client.SupportsPreedit)
{
return;
}
var composition = GetCompositionString();
Client.SetPreeditText(composition);
}
private string? GetCompositionString()
public string? GetCompositionString(GCS flag)
{
if (!IsComposing)
{
return null;
}
var himc = ImmGetContext(Hwnd);
return ImmGetCompositionString(himc, GCS.GCS_COMPSTR);
return ImmGetCompositionString(himc, flag);
}
~Imm32InputMethod()

75
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Avalonia.Automation.Peers;
@ -10,6 +11,7 @@ using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.Win32.Automation;
using Avalonia.Win32.Input;
using Avalonia.Win32.Interop;
using Avalonia.Win32.Interop.Automation;
using static Avalonia.Win32.Interop.UnmanagedMethods;
@ -181,11 +183,17 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_CHAR:
{
if (Imm32InputMethod.Current.IsComposing)
{
break;
}
// Ignore control chars and chars that were handled in WM_KEYDOWN.
if (ToInt32(wParam) >= 32 && !_ignoreWmChar)
{
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner,
new string((char)ToInt32(wParam), 1));
var text = new string((char)ToInt32(wParam), 1);
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text);
}
break;
@ -709,25 +717,80 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_IME_COMPOSITION:
{
Imm32InputMethod.Current.CompositionChanged();
var previousComposition = Imm32InputMethod.Current.Composition;
var flags = (GCS)ToInt32(lParam);
var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR);
Imm32InputMethod.Current.CompositionChanged(currentComposition);
switch (flags)
{
case GCS.GCS_RESULTSTR:
{
if(ToInt32(wParam) >= 32)
{
Imm32InputMethod.Current.Composition = previousComposition;
_ignoreWmChar = true;
}
break;
}
case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
{
// Chinese IME sends WM_CHAR after composition has finished.
break;
}
case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
{
// Japanese IME sends WM_CHAR after composition has finished.
break;
}
}
break;
}
case WindowsMessage.WM_IME_SELECT:
break;
case WindowsMessage.WM_IME_CHAR:
case WindowsMessage.WM_IME_COMPOSITIONFULL:
case WindowsMessage.WM_IME_CONTROL:
case WindowsMessage.WM_IME_KEYDOWN:
case WindowsMessage.WM_IME_KEYUP:
case WindowsMessage.WM_IME_NOTIFY:
case WindowsMessage.WM_IME_SELECT:
break;
case WindowsMessage.WM_IME_STARTCOMPOSITION:
Imm32InputMethod.Current.Composition = null;
if (Imm32InputMethod.Current.IsActive)
{
Imm32InputMethod.Current.Client.SetPreeditText(null);
}
Imm32InputMethod.Current.IsComposing = true;
return IntPtr.Zero;
case WindowsMessage.WM_IME_ENDCOMPOSITION:
Imm32InputMethod.Current.IsComposing = false;
break;
{
var currentComposition = Imm32InputMethod.Current.Composition;
//In case composition has not been comitted yet we need to do that here.
if (!string.IsNullOrEmpty(currentComposition))
{
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, currentComposition);
}
//Cleanup composition state.
Imm32InputMethod.Current.IsComposing = false;
Imm32InputMethod.Current.Composition = null;
if (Imm32InputMethod.Current.IsActive)
{
Imm32InputMethod.Current.Client.SetPreeditText(null);
}
break;
}
case WindowsMessage.WM_GETOBJECT:
if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control)
{

1
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -15,6 +15,7 @@
<ItemGroup>
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" />
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/obj/**/*.cs" />
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" />
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>

15
tests/Avalonia.Benchmarks/Properties/launchSettings.json

@ -0,0 +1,15 @@
{
"profiles": {
"Avalonia.Benchmarks": {
"commandName": "Project"
},
"Avalonia.Benchmarks (in-process)": {
"commandName": "Project",
"commandLineArgs": "--inprocess"
},
"Avalonia.Benchmarks (debug)": {
"commandName": "Project",
"commandLineArgs": "--debug"
}
}
}

48
tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs

@ -1,4 +1,5 @@
using Avalonia.Controls;
using System;
using Avalonia.Controls;
using Avalonia.Styling;
using BenchmarkDotNet.Attributes;
@ -11,6 +12,8 @@ namespace Avalonia.Benchmarks.Styling
private readonly Calendar _matchingControl;
private readonly Selector _isCalendarSelector;
private readonly Selector _classSelector;
private readonly Selector _orSelectorTwo;
private readonly Selector _orSelectorFive;
public SelectorBenchmark()
{
@ -23,6 +26,14 @@ namespace Avalonia.Benchmarks.Styling
_isCalendarSelector = Selectors.Is<Calendar>(null);
_classSelector = Selectors.Class(null, className);
_orSelectorTwo = Selectors.Or(new AlwaysMatchSelector(), new AlwaysMatchSelector());
_orSelectorFive = Selectors.Or(
new AlwaysMatchSelector(),
new AlwaysMatchSelector(),
new AlwaysMatchSelector(),
new AlwaysMatchSelector(),
new AlwaysMatchSelector());
}
[Benchmark]
@ -48,5 +59,40 @@ namespace Avalonia.Benchmarks.Styling
{
return _classSelector.Match(_matchingControl);
}
[Benchmark]
public SelectorMatch OrSelector_One_Match()
{
return _orSelectorTwo.Match(_matchingControl);
}
[Benchmark]
public SelectorMatch OrSelector_Five_Match()
{
return _orSelectorFive.Match(_matchingControl);
}
}
internal class AlwaysMatchSelector : Selector
{
public override bool InTemplate => false;
public override bool IsCombinator => false;
public override Type TargetType => null;
public override string ToString(Style owner)
{
return "Always";
}
protected override SelectorMatch Evaluate(StyledElement control, IStyle parent, bool subscribe)
{
return SelectorMatch.AlwaysThisType;
}
protected override Selector MovePrevious() => null;
protected override Selector MovePreviousOrParent() => null;
}
}

19
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1058,6 +1058,25 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Should_Move_Caret_To_EndOfLine()
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "AB\nAB"
};
tb.Measure(Size.Infinity);
RaiseKeyEvent(tb, Key.End, KeyModifiers.Shift);
Assert.Equal(2, tb.CaretIndex);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),

11
tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

@ -76,5 +76,16 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Throws<InvalidOperationException>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
}
}
[Fact]
public void Should_Return_False_For_Unregistered_FontCollection_Uri()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var result = FontManager.Current.TryGetGlyphTypeface(new Typeface("fonts:invalid#Something"), out _);
Assert.False(result);
}
}
}
}

Loading…
Cancel
Save