From decf863bc9020eb6534ac95234c03dc72f4777af Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Feb 2023 16:38:35 +0100 Subject: [PATCH 01/34] Initial macOS IME integration --- .../project.pbxproj | 12 ++ .../src/OSX/AvnTextInputMethod.h | 46 +++++++ .../src/OSX/AvnTextInputMethod.mm | 41 ++++++ .../src/OSX/AvnTextInputMethodDelegate.h | 20 +++ native/Avalonia.Native/src/OSX/AvnView.h | 6 +- native/Avalonia.Native/src/OSX/AvnView.mm | 53 ++++++-- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 4 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 10 ++ .../AvaloniaNativeTextInputMethod.cs | 118 ++++++++++++++++++ src/Avalonia.Native/WindowImpl.cs | 5 + src/Avalonia.Native/avn.idl | 17 +++ 11 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AvnTextInputMethod.h create mode 100644 native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm create mode 100644 native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h create mode 100644 src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 41d1534f8d..048cc9a6b0 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -43,6 +43,9 @@ 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.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 */; }; @@ -95,6 +98,9 @@ 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; + 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethodDelegate.h; sourceTree = ""; }; + 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethod.h; sourceTree = ""; }; + 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnTextInputMethod.mm; sourceTree = ""; }; AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; 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; }; @@ -140,6 +146,9 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, + 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, + 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */, BC11A5BC2608D58F0017BAD0 /* automation.h */, BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, @@ -210,6 +219,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 */, @@ -289,6 +300,7 @@ BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.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 */, diff --git a/native/Avalonia.Native/src/OSX/AvnTextInputMethod.h b/native/Avalonia.Native/src/OSX/AvnTextInputMethod.h new file mode 100644 index 0000000000..4e5116ee71 --- /dev/null +++ b/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 + +#include "com.h" +#include "comimpl.h" +#include "avalonia-native.h" +#import "AvnTextInputMethodDelegate.h" + +class AvnTextInputMethod: public virtual ComObject, public virtual IAvnTextInputMethod{ +private: + id _inputMethodDelegate; +public: + FORWARD_IUNKNOWN() + + BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnTextInputMethod, IID_IAvnTextInputMethod) + END_INTERFACE_MAP() + + virtual ~AvnTextInputMethod(); + + AvnTextInputMethod(id 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 Client; +}; +#endif /* AvnTextInputMethod_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm b/native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm new file mode 100644 index 0000000000..8c3ae080fa --- /dev/null +++ b/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 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]; +} diff --git a/native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h b/native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h new file mode 100644 index 0000000000..9f321ca595 --- /dev/null +++ b/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 */ diff --git a/native/Avalonia.Native/src/OSX/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h index 86a68d34c5..256caa70e9 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.h +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -5,8 +5,6 @@ #pragma once #import - -#import #import #include "common.h" #include "WindowImpl.h" @@ -14,7 +12,7 @@ @class AvnAccessibilityElement; -@interface AvnView : NSView +@interface AvnView : NSView -(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 \ No newline at end of file +@end diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 4ae6ad5a00..11155afb2b 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -12,6 +12,7 @@ { ComPtr _parent; NSTrackingArea* _area; + NSMutableAttributedString* _markedText; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; @@ -20,6 +21,9 @@ NSObject* _renderTarget; AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; + AvnRect _cursorRect; + NSString* _text; + NSRange _selection; } - (void)onClosed @@ -560,11 +564,13 @@ - (BOOL)hasMarkedText { - return _lastKeyHandled; + return [_markedText length] > 0; } - (NSRange)markedRange { + if([_markedText length] > 0) + return NSMakeRange(0, [_markedText length] - 1); return NSMakeRange(NSNotFound, 0); } @@ -575,12 +581,31 @@ - (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); } - (NSArray *)validAttributesForMarkedText @@ -606,14 +631,16 @@ - (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 ToNSRect(_cursorRect); } - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info @@ -718,4 +745,16 @@ return [[self accessibilityChild] accessibilityFocusedUIElement]; } +- (void) setText:(NSString *)text{ + _text = text; +} + +- (void) setSelection:(int)start :(int)end{ + _selection = NSMakeRange(start, end - start); +} + +- (void) setCursorRect:(AvnRect)rect{ + _cursorRect = rect; +} + @end diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..b2a9d32480 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/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,6 +104,8 @@ BEGIN_INTERFACE_MAP() id GetWindowProtocol (); virtual void BringToFront (); + + virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override; protected: virtual NSWindowStyleMask GetStyle(); @@ -131,6 +134,7 @@ public: NSObject *renderTarget; NSWindow * Window; ComPtr BaseEvents; + ComPtr InputMethod; AvnView *View; }; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..9a6aa6ed2c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -14,6 +14,7 @@ #import "WindowProtocol.h" #import "WindowInterfaces.h" #include "WindowBaseImpl.h" +#include "AvnTextInputMethod.h" WindowBaseImpl::~WindowBaseImpl() { @@ -28,6 +29,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 }; @@ -612,6 +614,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 diff --git a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs new file mode 100644 index 0000000000..b216970af3 --- /dev/null +++ b/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); + } + } + } + } +} diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f27d94b61a..7de3523e5f 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -19,6 +19,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 +34,8 @@ namespace Avalonia.Native } _nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); + + _inputMethod = new AvaloniaNativeTextInputMethod(_native); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents @@ -89,6 +92,8 @@ namespace Avalonia.Native _native.SetTitle(title ?? ""); } + public ITextInputMethodImpl TextInputMethod => _inputMethod; + public WindowState WindowState { get => (WindowState)_native.WindowState; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index a062fdc61d..b4e5fe683c 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -545,6 +545,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] @@ -611,6 +612,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 { From d69f4a86dc6e6b3dc3031eca000e6488a8a03a19 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Feb 2023 16:40:57 +0100 Subject: [PATCH 02/34] Add missing using --- src/Avalonia.Native/WindowImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 7de3523e5f..d086799b6f 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/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; From aa2ad652e34afea5a9ed9af1ae8395f3f935102f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 11:26:51 -0500 Subject: [PATCH 03/34] Remove FlyoutPlacementMode and improve PlacementMode instead --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 90 +------------------ .../Flyouts/FlyoutPlacementMode.cs | 77 ---------------- src/Avalonia.Controls/PlacementMode.cs | 53 +++++++++-- .../PopupPositioning/IPopupPositioner.cs | 45 ++++------ 4 files changed, 70 insertions(+), 195 deletions(-) delete mode 100644 src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 9d4abec549..373386e259 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -29,8 +29,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - public static readonly StyledProperty PlacementProperty = - AvaloniaProperty.Register(nameof(Placement)); + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement)); /// /// Defines the property @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the desired placement /// - public FlyoutPlacementMode Placement + public PlacementMode Placement { get => GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); @@ -452,93 +452,11 @@ namespace Avalonia.Controls.Primitives } else { - Popup.PlacementMode = PlacementMode.AnchorAndGravity; + Popup.PlacementMode = Placement; 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) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs b/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs deleted file mode 100644 index 2e77de3b3b..0000000000 --- a/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace Avalonia.Controls -{ - public enum FlyoutPlacementMode - { - /// - /// Preferred location is above the target element - /// - Top = 0, - - /// - /// Preferred location is below the target element - /// - Bottom = 1, - - /// - /// Preferred location is to the left of the target element - /// - Left = 2, - - /// - /// Preferred location is to the right of the target element - /// - Right = 3, - - //TODO - // - // Preferred location is centered on the screen - // - //Full = 4, - - /// - /// Preferred location is above the target element, with the left edge of the flyout - /// aligned with the left edge of the target element - /// - TopEdgeAlignedLeft = 5, - - /// - /// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element. - /// - TopEdgeAlignedRight = 6, - - /// - /// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element. - /// - BottomEdgeAlignedLeft = 7, - - /// - /// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element. - /// - BottomEdgeAlignedRight = 8, - - /// - /// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element. - /// - LeftEdgeAlignedTop = 9, - - /// - /// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element. - /// - LeftEdgeAlignedBottom = 10, - - /// - /// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element. - /// - RightEdgeAlignedTop = 11, - - /// - /// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element. - /// - RightEdgeAlignedBottom = 12, - - /// - /// Preferred location is determined automatically. - /// - Auto = 13 - } -} diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs index 68a4b9eecb..fa4f029cf3 100644 --- a/src/Avalonia.Controls/PlacementMode.cs +++ b/src/Avalonia.Controls/PlacementMode.cs @@ -13,28 +13,69 @@ namespace Avalonia.Controls Pointer, /// - /// The popup is placed at the bottom left of its target. + /// Preferred location is below the target element. /// Bottom, /// - /// The popup is placed at the top right of its target. + /// Preferred location is to the right of the target element. /// Right, /// - /// The popup is placed at the top left of its target. + /// Preferred location is to the left of the target element. /// Left, /// - /// The popup is placed at the top left of its target. + /// Preferred location is above the target element. /// Top, + + /// + /// The popup is placed according to and rules. + /// + AnchorAndGravity, /// - /// The popup is placed according to anchor and gravity rules + /// Preferred location is above the target element, with the left edge of the popup + /// aligned with the left edge of the target element. + /// + TopEdgeAlignedLeft, + + /// + /// Preferred location is above the target element, with the right edge of popup aligned with right edge of the target element. + /// + TopEdgeAlignedRight, + + /// + /// Preferred location is below the target element, with the left edge of popup aligned with left edge of the target element. + /// + BottomEdgeAlignedLeft, + + /// + /// Preferred location is below the target element, with the right edge of popup aligned with right edge of the target element. + /// + BottomEdgeAlignedRight, + + /// + /// Preferred location is to the left of the target element, with the top edge of popup aligned with top edge of the target element. + /// + LeftEdgeAlignedTop, + + /// + /// Preferred location is to the left of the target element, with the bottom edge of popup aligned with bottom edge of the target element. + /// + LeftEdgeAlignedBottom, + + /// + /// Preferred location is to the right of the target element, with the top edge of popup aligned with top edge of the target element. + /// + RightEdgeAlignedTop, + + /// + /// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element. /// - AnchorAndGravity + RightEdgeAlignedBottom } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 2e70947457..0bb596d778 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -480,33 +480,26 @@ namespace Avalonia.Controls.Primitives.PopupPositioning var anchorRect = rect ?? bounds; 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.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 From 733238a8afe68ea128874eec0f86b1510c9cdc5d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 11:48:42 -0500 Subject: [PATCH 04/34] Implement PlacementMode.Center --- .../ControlCatalog/Pages/FlyoutsPage.axaml | 9 ++++ src/Avalonia.Controls/PlacementMode.cs | 5 +++ .../PopupPositioning/IPopupPositioner.cs | 45 ++++++++++++++----- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 54aa9d1b67..8dbc6d283f 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -136,6 +136,15 @@ + Top, + + /// + /// Preferred location is centered over the target element. + /// + Center, /// /// The popup is placed according to and rules. diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 0bb596d778..3f1b8008b1 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -464,21 +464,25 @@ namespace Avalonia.Controls.Primitives.PopupPositioning positionerParameters.Anchor = PopupAnchor.TopLeft; positionerParameters.Gravity = PopupGravity.BottomRight; } - else + else if (placement == PlacementMode.Center) { - if (target == null) - throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); - var matrix = target.TransformToVisual(topLevel); - if (matrix == null) + // Start with top left position, and add offset to move it to the center + var anchorRectangle = GetAnchorRectangle(topLevel, target, rect); + positionerParameters.AnchorRectangle = anchorRectangle; + positionerParameters.Anchor = PopupAnchor.TopLeft; + positionerParameters.Gravity = PopupGravity.BottomRight; + + var targetSize = positionerParameters.Size; + if (targetSize != default) { - if (target.GetVisualRoot() == null) - throw new InvalidOperationException("Target control is not attached to the visual tree"); - throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); + positionerParameters.Offset += new Point( + (anchorRectangle.Width - targetSize.Width) / 2, + (anchorRectangle.Height - targetSize.Height) / 2); } - - var bounds = new Rect(default, target.Bounds.Size); - var anchorRect = rect ?? bounds; - positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); + } + else + { + positionerParameters.AnchorRectangle = GetAnchorRectangle(topLevel, target, rect); var parameters = placement switch { @@ -528,6 +532,23 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } } } + + private static Rect GetAnchorRectangle(TopLevel topLevel, Visual target, Rect? rect) + { + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) + { + if (target.GetVisualRoot() == null) + throw new InvalidOperationException("Target control is not attached to the visual tree"); + throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); + } + + var bounds = new Rect(default, target.Bounds.Size); + var anchorRect = rect ?? bounds; + return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); + } } } From 8281c6e14f4e6288270ab89769433aa1789cffa0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 12:05:07 -0500 Subject: [PATCH 05/34] Split FlyoutBase and PopupFlyoutBase --- src/Avalonia.Controls/Flyouts/Flyout.cs | 2 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 471 +----------------- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 2 +- .../Flyouts/PopupFlyoutBase.cs | 470 +++++++++++++++++ .../SplitButton/SplitButton.cs | 2 +- 5 files changed, 484 insertions(+), 463 deletions(-) create mode 100644 src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index df3fe28a29..262edbcc14 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -3,7 +3,7 @@ using Avalonia.Metadata; namespace Avalonia.Controls { - public class Flyout : FlyoutBase + public class Flyout : PopupFlyoutBase { /// /// Defines the property diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 373386e259..b5328ccab8 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/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 { /// /// Defines the property @@ -26,80 +17,25 @@ namespace Avalonia.Controls.Primitives public static readonly DirectProperty TargetProperty = AvaloniaProperty.RegisterDirect(nameof(Target), x => x.Target); - /// - /// Defines the property - /// - public static readonly StyledProperty PlacementProperty = - AvaloniaProperty.Register(nameof(Placement)); - - /// - /// Defines the property - /// - public static readonly DirectProperty ShowModeProperty = - AvaloniaProperty.RegisterDirect(nameof(ShowMode), - x => x.ShowMode, (x, v) => x.ShowMode = v); - - /// - /// Defines the property - /// - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - Popup.OverlayInputPassThroughElementProperty.AddOwner( - o => o._overlayInputPassThroughElement, - (o, v) => o._overlayInputPassThroughElement = v); - /// /// Defines the AttachedFlyout property /// public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); - private readonly Lazy _popupLazy; private bool _isOpen; private Control? _target; - private FlyoutShowMode _showMode = FlyoutShowMode.Standard; - private Rect? _enlargedPopupRect; - private PixelRect? _enlargePopupRectScreenPixelRect; - private IDisposable? _transientDisposable; - private Action? _popupHostChangedHandler; - private IInputElement? _overlayInputPassThroughElement; - - static FlyoutBase() - { - Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); - } - - public FlyoutBase() - { - _popupLazy = new Lazy(() => CreatePopup()); - } - - protected Popup Popup => _popupLazy.Value; + public event EventHandler? Opened; + public event EventHandler? Closed; + /// /// Gets whether this Flyout is currently Open /// public bool IsOpen { get => _isOpen; - private set => SetAndRaise(IsOpenProperty, ref _isOpen, value); - } - - /// - /// Gets or sets the desired placement - /// - public PlacementMode Placement - { - get => GetValue(PlacementProperty); - set => SetValue(PlacementProperty, value); - } - - /// - /// Gets or sets the desired ShowMode - /// - public FlyoutShowMode ShowMode - { - get => _showMode; - set => SetAndRaise(ShowModeProperty, ref _showMode, value); + protected set => SetAndRaise(IsOpenProperty, ref _isOpen, value); } /// @@ -108,32 +44,9 @@ namespace Avalonia.Controls.Primitives public Control? Target { get => _target; - private set => SetAndRaise(TargetProperty, ref _target, value); - } - - /// - /// Gets or sets an element that should receive pointer input events even when underneath - /// the flyout's overlay. - /// - public IInputElement? OverlayInputPassThroughElement - { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + protected set => SetAndRaise(TargetProperty, ref _target, value); } - - IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; - remove => _popupHostChangedHandler -= value; - } - - public event EventHandler? Closed; - public event EventHandler? Closing; - public event EventHandler? Opened; - public event EventHandler? Opening; - + public static FlyoutBase? GetAttachedFlyout(Control element) { return element.GetValue(AttachedFlyoutProperty); @@ -150,380 +63,18 @@ namespace Avalonia.Controls.Primitives flyout?.ShowAt(flyoutOwner); } - /// - /// Shows the Flyout at the given Control - /// - /// The control to show the Flyout at - public void ShowAt(Control placementTarget) - { - ShowAtCore(placementTarget); - } - - /// - /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout - /// - /// The target control - /// True to show at pointer - public void ShowAt(Control placementTarget, bool showAtPointer) - { - ShowAtCore(placementTarget, showAtPointer); - } - - /// - /// Hides the Flyout - /// - public void Hide() - { - HideCore(); - } - - /// True, if action was handled - 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; - } - - /// True, if action was handled - 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.SetValue(StyledElement.TemplatedParentProperty, 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); } - - /// - /// Used to create the content the Flyout displays - /// - /// - 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(); - - 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 = 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 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); - } } } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index b028a8f007..79a6cdb313 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -7,7 +7,7 @@ using Avalonia.Styling; namespace Avalonia.Controls { - public class MenuFlyout : FlyoutBase + public class MenuFlyout : PopupFlyoutBase { public MenuFlyout() { diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs new file mode 100644 index 0000000000..4a19c215cf --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -0,0 +1,470 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Diagnostics; +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 + { + /// + /// Defines the property + /// + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement)); + + /// + /// Defines the property + /// + public static readonly DirectProperty ShowModeProperty = + AvaloniaProperty.RegisterDirect(nameof(ShowMode), + x => x.ShowMode, (x, v) => x.ShowMode = v); + + /// + /// Defines the property + /// + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); + + private readonly Lazy _popupLazy; + private FlyoutShowMode _showMode = FlyoutShowMode.Standard; + private Rect? _enlargedPopupRect; + private PixelRect? _enlargePopupRectScreenPixelRect; + private IDisposable? _transientDisposable; + private Action? _popupHostChangedHandler; + private IInputElement? _overlayInputPassThroughElement; + + static PopupFlyoutBase() + { + Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); + } + + public PopupFlyoutBase() + { + _popupLazy = new Lazy(() => CreatePopup()); + } + + protected Popup Popup => _popupLazy.Value; + + /// + /// Gets or sets the desired placement + /// + public PlacementMode Placement + { + get => GetValue(PlacementProperty); + set => SetValue(PlacementProperty, value); + } + + /// + /// Gets or sets the desired ShowMode + /// + public FlyoutShowMode ShowMode + { + get => _showMode; + set => SetAndRaise(ShowModeProperty, ref _showMode, value); + } + + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. + /// + public IInputElement? OverlayInputPassThroughElement + { + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + } + + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + + public event EventHandler? Closing; + public event EventHandler? Opening; + + /// + /// Shows the Flyout at the given Control + /// + /// The control to show the Flyout at + public sealed override void ShowAt(Control placementTarget) + { + ShowAtCore(placementTarget); + } + + /// + /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout + /// + /// The target control + /// True to show at pointer + public void ShowAt(Control placementTarget, bool showAtPointer) + { + ShowAtCore(placementTarget, showAtPointer); + } + + /// + /// Hides the Flyout + /// + public sealed override void Hide() + { + HideCore(); + } + + /// True, if action was handled + 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; + } + + /// True, if action was handled + 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.SetValue(StyledElement.TemplatedParentProperty, 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); + } + + /// + /// Used to create the content the Flyout displays + /// + /// + 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(); + + 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 = 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); + } + } +} diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 31a06d875a..896c8eae31 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -176,7 +176,7 @@ namespace Avalonia.Controls flyout.Opened += Flyout_Opened; flyout.Closed += Flyout_Closed; - _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(PopupFlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); } } From 4e47a273047f4e3b76838d66bd95d8095b9e762e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 12:24:28 -0500 Subject: [PATCH 06/34] Add some popup properties to the PopupFlyoutBase --- .../Pages/ContextFlyoutPage.xaml.cs | 5 +- .../Flyouts/PopupFlyoutBase.cs | 58 +++++++++++++++++-- .../SplitButton/SplitButton.cs | 2 +- .../Diagnostics/ViewModels/VisualTreeNode.cs | 12 ++-- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs index 8bd1f4d85a..0388eee4e2 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -36,8 +36,9 @@ namespace ControlCatalog.Pages customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel); var cancellableContextBorder = this.Get("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; diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 4a19c215cf..052143622c 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -2,6 +2,7 @@ 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; @@ -13,11 +14,25 @@ namespace Avalonia.Controls.Primitives { public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider { - /// - /// Defines the property - /// + /// public static readonly StyledProperty PlacementProperty = - AvaloniaProperty.Register(nameof(Placement)); + Popup.PlacementModeProperty.AddOwner(); + + /// + public static readonly StyledProperty HorizontalOffsetProperty = + Popup.HorizontalOffsetProperty.AddOwner(); + + /// + public static readonly StyledProperty VerticalOffsetProperty = + Popup.VerticalOffsetProperty.AddOwner(); + + /// + public static readonly StyledProperty PlacementAnchorProperty = + Popup.PlacementAnchorProperty.AddOwner(); + + /// + public static readonly StyledProperty PlacementGravityProperty = + Popup.PlacementGravityProperty.AddOwner(); /// /// Defines the property @@ -55,14 +70,43 @@ namespace Avalonia.Controls.Primitives protected Popup Popup => _popupLazy.Value; /// - /// Gets or sets the desired placement + /// Gets or sets the desired placement. /// public PlacementMode Placement { get => GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); } + + /// + public PopupGravity PlacementGravity + { + get { return GetValue(PlacementGravityProperty); } + set { SetValue(PlacementGravityProperty, value); } + } + + /// + public PopupAnchor PlacementAnchor + { + get { return GetValue(PlacementAnchorProperty); } + set { SetValue(PlacementAnchorProperty, value); } + } + + /// + public double HorizontalOffset + { + get { return GetValue(HorizontalOffsetProperty); } + set { SetValue(HorizontalOffsetProperty, value); } + } + /// + public double VerticalOffset + { + get { return GetValue(VerticalOffsetProperty); } + set { SetValue(VerticalOffsetProperty, value); } + } + + /// /// Gets or sets the desired ShowMode /// @@ -379,6 +423,10 @@ namespace Avalonia.Controls.Primitives sz = Popup.Child.DesiredSize; } + Popup.VerticalOffset = VerticalOffset; + Popup.HorizontalOffset = HorizontalOffset; + Popup.PlacementAnchor = PlacementAnchor; + Popup.PlacementGravity = PlacementGravity; if (showAtPointer) { Popup.PlacementMode = PlacementMode.Pointer; diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 896c8eae31..f5b361fa16 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -176,7 +176,7 @@ namespace Avalonia.Controls flyout.Opened += Flyout_Opened; flyout.Closed += Flyout_Closed; - _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(PopupFlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index f9fb0d18ef..c3ebb1beaf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/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[] + new IObservable[] { 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. From d249848d7618f56468dd11522d944deb14012902 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 12:58:18 -0500 Subject: [PATCH 07/34] Simplify PlacementMode.Center --- .../PopupPositioning/IPopupPositioner.cs | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 3f1b8008b1..0c9bb89caa 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -464,25 +464,21 @@ namespace Avalonia.Controls.Primitives.PopupPositioning positionerParameters.Anchor = PopupAnchor.TopLeft; positionerParameters.Gravity = PopupGravity.BottomRight; } - else if (placement == PlacementMode.Center) + else { - // Start with top left position, and add offset to move it to the center - var anchorRectangle = GetAnchorRectangle(topLevel, target, rect); - positionerParameters.AnchorRectangle = anchorRectangle; - positionerParameters.Anchor = PopupAnchor.TopLeft; - positionerParameters.Gravity = PopupGravity.BottomRight; - - var targetSize = positionerParameters.Size; - if (targetSize != default) + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) { - positionerParameters.Offset += new Point( - (anchorRectangle.Width - targetSize.Width) / 2, - (anchorRectangle.Height - targetSize.Height) / 2); + if (target.GetVisualRoot() == null) + throw new InvalidOperationException("Target control is not attached to the visual tree"); + throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); } - } - else - { - positionerParameters.AnchorRectangle = GetAnchorRectangle(topLevel, target, rect); + + var bounds = new Rect(default, target.Bounds.Size); + var anchorRect = rect ?? bounds; + positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); var parameters = placement switch { @@ -490,6 +486,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning 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), @@ -532,23 +529,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } } } - - private static Rect GetAnchorRectangle(TopLevel topLevel, Visual target, Rect? rect) - { - if (target == null) - throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); - var matrix = target.TransformToVisual(topLevel); - if (matrix == null) - { - if (target.GetVisualRoot() == null) - throw new InvalidOperationException("Target control is not attached to the visual tree"); - throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); - } - - var bounds = new Rect(default, target.Bounds.Size); - var anchorRect = rect ?? bounds; - return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); - } } } From 584756043d19c6fe83cf8eff83d13e0d4d593230 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 27 Feb 2023 00:43:55 -0500 Subject: [PATCH 08/34] Try to fix last known position reset --- .../Input/PointerOverPreProcessor.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 7781255d89..7c1fdc7233 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -6,7 +6,8 @@ namespace Avalonia.Input internal class PointerOverPreProcessor : IObserver { 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,9 +42,9 @@ namespace Avalonia.Input } if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown - && _lastPointer is (var lastPointer, var lastPosition)) + && _currentPointer is (var lastPointer, var lastPosition)) { - _lastPointer = null; + _currentPointer = null; ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition), new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), args.InputModifiers.ToKeyModifiers()); @@ -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; } @@ -122,7 +123,7 @@ namespace Avalonia.Input root.PointerOverElement = null; _lastActivePointerDevice = null; - _lastPointer = null; + _currentPointer = null; } private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot) @@ -164,7 +165,9 @@ namespace Avalonia.Input } } - _lastPointer = (pointer, ((Visual)root).PointToScreen(position)); + var screenPosition = ((Visual)root).PointToScreen(position); + _lastKnownPosition = screenPosition; + _currentPointer = (pointer, screenPosition); } private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, From 6496abf14b2419948ad4bd87b5e299f2ab663d62 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 27 Feb 2023 00:44:15 -0500 Subject: [PATCH 09/34] Changes after review, added FlyoutPresenterTheme --- .../ControlCatalog/Pages/FlyoutsPage.axaml | 7 +++- src/Avalonia.Controls/Flyouts/Flyout.cs | 36 +++++++++++++++--- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 37 ++++++++++++++++--- .../Flyouts/PopupFlyoutBase.cs | 27 ++++++-------- 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 8dbc6d283f..35ece516bd 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -16,8 +16,13 @@ + + + + + - + diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index 262edbcc14..c0d3600c1e 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -1,5 +1,7 @@ -using Avalonia.Controls.Primitives; +using System.ComponentModel; +using Avalonia.Controls.Primitives; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -18,6 +20,21 @@ namespace Avalonia.Controls private Classes? _classes; + /// + /// Defines the property. + /// + public static readonly StyledProperty FlyoutPresenterThemeProperty = + AvaloniaProperty.Register(nameof(FlyoutPresenterTheme)); + + /// + /// Gets or sets the that is applied to the container element generated for the flyout presenter. + /// + public ControlTheme? FlyoutPresenterTheme + { + get => GetValue(FlyoutPresenterThemeProperty); + set => SetValue(FlyoutPresenterThemeProperty, value); + } + /// /// Gets or sets the content to display in this flyout /// @@ -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); } } } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index 79a6cdb313..df973debc7 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/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; @@ -34,6 +35,12 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemContainerThemeProperty = ItemsControl.ItemContainerThemeProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FlyoutPresenterThemeProperty = + Flyout.FlyoutPresenterThemeProperty.AddOwner(); + public Classes FlyoutPresenterClasses => _classes ??= new Classes(); /// @@ -60,10 +67,19 @@ namespace Avalonia.Controls /// public ControlTheme? ItemContainerTheme { - get { return GetValue(ItemContainerThemeProperty); } - set { SetValue(ItemContainerThemeProperty, value); } + get => GetValue(ItemContainerThemeProperty); + set => SetValue(ItemContainerThemeProperty, value); } + /// + /// Gets or sets the that is applied to the container element generated for the flyout presenter. + /// + 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); } } } diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 052143622c..f5eb8f4a02 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -37,9 +37,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - public static readonly DirectProperty ShowModeProperty = - AvaloniaProperty.RegisterDirect(nameof(ShowMode), - x => x.ShowMode, (x, v) => x.ShowMode = v); + public static readonly StyledProperty ShowModeProperty = + AvaloniaProperty.Register(nameof(ShowMode)); /// /// Defines the property @@ -50,7 +49,6 @@ namespace Avalonia.Controls.Primitives (o, v) => o._overlayInputPassThroughElement = v); private readonly Lazy _popupLazy; - private FlyoutShowMode _showMode = FlyoutShowMode.Standard; private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; @@ -81,39 +79,38 @@ namespace Avalonia.Controls.Primitives /// public PopupGravity PlacementGravity { - get { return GetValue(PlacementGravityProperty); } - set { SetValue(PlacementGravityProperty, value); } + get => GetValue(PlacementGravityProperty); + set => SetValue(PlacementGravityProperty, value); } /// public PopupAnchor PlacementAnchor { - get { return GetValue(PlacementAnchorProperty); } - set { SetValue(PlacementAnchorProperty, value); } + get => GetValue(PlacementAnchorProperty); + set => SetValue(PlacementAnchorProperty, value); } /// public double HorizontalOffset { - get { return GetValue(HorizontalOffsetProperty); } - set { SetValue(HorizontalOffsetProperty, value); } + get => GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); } /// public double VerticalOffset { - get { return GetValue(VerticalOffsetProperty); } - set { SetValue(VerticalOffsetProperty, value); } + get => GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); } - /// /// Gets or sets the desired ShowMode /// public FlyoutShowMode ShowMode { - get => _showMode; - set => SetAndRaise(ShowModeProperty, ref _showMode, value); + get => GetValue(ShowModeProperty); + set => SetValue(ShowModeProperty, value); } /// From 0fc25da0dc57fadae99169df57c5b04442c52cc8 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 12:35:13 +0100 Subject: [PATCH 10/34] [TextBox] MoveEnd now correctly positions the caret before the line break --- src/Avalonia.Controls/TextBox.cs | 2 +- .../TextBoxTests.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 78caf350b7..ac1b5cd417 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/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); } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 531a2869cd..3d8f823621 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/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(), From cb1d30648295fbd55bcbdc577bc8dbfe834b92ee Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 15:35:43 +0100 Subject: [PATCH 11/34] Rework preedit handling --- .../Presenters/TextPresenter.cs | 33 ++++--------------- .../TextBoxTextInputMethodClient.cs | 6 ++-- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bb6b03d59a..14d8270647 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -564,7 +564,7 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if(_compositionRegion != null) + if (_compositionRegion != null) { var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, new GenericTextRunProperties(typeface, FontSize, @@ -851,7 +851,7 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true) + private void UpdateCaret(CharacterHit characterHit, bool notify = true) { _lastCharacterHit = characterHit; @@ -879,10 +879,14 @@ namespace Avalonia.Controls.Presenters CaretBoundsChanged?.Invoke(this, EventArgs.Empty); } - if (updateCaretIndex) + if (notify) { SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } + else + { + _caretIndex = caretIndex; + } } internal Rect GetCursorRectangle() @@ -899,24 +903,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); @@ -935,11 +921,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): diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 3a28836a99..347cac13ef 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -4,8 +4,6 @@ using Avalonia.Input.TextInput; using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.VisualTree; -using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -161,11 +159,13 @@ namespace Avalonia.Controls public void SetPreeditText(string? text) { - if (_presenter == null) + if (_presenter == null || _parent == null) { return; } + _presenter.CaretIndex = _parent.CaretIndex; + _presenter.PreeditText = text; } From 397867602e4d718c0012fe613f4d005a9e2e1b56 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 11:29:27 +0100 Subject: [PATCH 12/34] Properly sync preedit --- .../Presenters/TextPresenter.cs | 50 ++++++----------- .../TextBoxTextInputMethodClient.cs | 56 ++++++++++++++++--- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 3 +- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 14d8270647..2e6f1a0f2d 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/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( nameof(Text), o => o.Text, - (o, v) => o.Text = v); + (o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay); /// /// Defines the 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,22 +527,22 @@ namespace Avalonia.Controls.Presenters } } - private string? GetText() - { - if (!string.IsNullOrEmpty(_preeditText)) - { - if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length) - { - return _preeditText; - } + //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); + // var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex); - return text; - } + // return text; + // } - return _text; - } + // return _text; + //} /// /// Creates the used to render the text. @@ -551,7 +552,7 @@ namespace Avalonia.Controls.Presenters { TextLayout result; - var text = GetText(); + var text = _text; var typeface = new Typeface(FontFamily, FontStyle, FontWeight); @@ -851,7 +852,7 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - private void UpdateCaret(CharacterHit characterHit, bool notify = true) + internal void UpdateCaret(CharacterHit characterHit, bool notify = true) { _lastCharacterHit = characterHit; @@ -883,10 +884,6 @@ namespace Avalonia.Controls.Presenters { SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } - else - { - _caretIndex = caretIndex; - } } internal Rect GetCursorRectangle() @@ -903,17 +900,6 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - 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); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 347cac13ef..6f8f3f3cab 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,6 +1,8 @@ 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; @@ -75,7 +77,7 @@ namespace Avalonia.Controls { get => _textEditable; set { - if(_textEditable != null) + if (_textEditable != null) { _textEditable.TextChanged -= TextEditable_TextChanged; _textEditable.SelectionChanged -= TextEditable_SelectionChanged; @@ -84,7 +86,7 @@ namespace Avalonia.Controls _textEditable = value; - if(_textEditable != null) + if (_textEditable != null) { _textEditable.TextChanged += TextEditable_TextChanged; _textEditable.SelectionChanged += TextEditable_SelectionChanged; @@ -110,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; @@ -157,16 +159,53 @@ 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 || _parent == null) { return; } - _presenter.CaretIndex = _parent.CaretIndex; + if (_presenterText is null) + { + _presenterText = _parent.Text ?? ""; + _compositionStart = _parent.CaretIndex; + } + + var text = GetText(preeditText); + + Debug.WriteLine(text); + + _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); - _presenter.PreeditText = text; + return text; } public void SetComposingRegion(TextRange? region) @@ -175,6 +214,7 @@ namespace Avalonia.Controls { return; } + _presenter.CompositionRegion = region; } @@ -256,9 +296,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; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index e8d2d8f0c5..1cc9d1fa2f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -713,13 +713,14 @@ namespace Avalonia.Win32 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.IsComposing = true; From 117631d0ef0554c752fa0837639debef21353254 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Mar 2023 13:41:29 +0100 Subject: [PATCH 13/34] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bbf358b8f4..ee778ed4e2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ csx AppPackages/ # NCrunch +.NCrunch_*/ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* From 16c1dc7a506629c487f953eb372176202d7e7d03 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Mar 2023 15:13:52 +0100 Subject: [PATCH 14/34] Ported SelectionNodeBase from TreeDataGrid. `TreeDataGrid` has an updated version of `SelectionNodeBase` that fixes a few problems, and works as a base also for hierarchical selection models. Ported that here and tidied up a few other selection-related classes (change `is object` to `is not null`, fix nullability annotations). --- src/Avalonia.Controls/ItemsSourceView.cs | 16 + .../Selection/InternalSelectionModel.cs | 2 +- .../Selection/SelectedItems.cs | 17 +- .../Selection/SelectionModel.cs | 91 +++--- ...SelectionModelSelectionChangedEventArgs.cs | 8 +- .../Selection/SelectionNodeBase.cs | 291 ++++++++++++------ 6 files changed, 267 insertions(+), 158 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index c8fc76255c..1fa8f6a5cf 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -241,6 +241,22 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } + internal void AddListener(ICollectionChangedListener listener) + { + if (Inner is INotifyCollectionChanged incc) + { + CollectionChangedEventManager.Instance.AddListener(incc, listener); + } + } + + internal void RemoveListener(ICollectionChangedListener listener) + { + if (Inner is INotifyCollectionChanged incc) + { + CollectionChangedEventManager.Instance.RemoveListener(incc, listener); + } + } + /// /// Retrieves the index of the item that has the specified unique identifier (key). /// diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d0715e402d..d0e6144f59 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/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) { diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index ef642b7bdc..74007805cd 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Avalonia.Controls.Selection { - internal class SelectedItems : IReadOnlyList + internal class SelectedItems : IReadOnlyList { private readonly SelectionModel? _owner; private readonly ItemsSourceView? _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? Items => _items ?? _owner?.ItemsView; private IReadOnlyList? Ranges => _ranges ?? _owner!.Ranges; - public IEnumerator GetEnumerator() + public IEnumerator 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 { - private readonly IReadOnlyList _source; - public Untyped(IReadOnlyList source) => _source = source; + private readonly IReadOnlyList _source; + public Untyped(IReadOnlyList source) => _source = source; public object? this[int index] => _source[index]; public int Count => _source.Count; IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index affe762ea7..d4c2b32974 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection private SelectedItems.Untyped? _selectedItemsUntyped; private EventHandler? _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 SelectedItems + public IReadOnlyList 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 deselectedItems) + protected override void OnSelectionRemoved(int index, int count, IReadOnlyList 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? deselected = operation.DeselectedRanges; IReadOnlyList? 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?)operation.DeselectedItems ?? SelectedItems.Create(deselected, deselectedSource); var e = new SelectionModelSelectionChangedEventArgs( SelectedIndexes.Create(deselected), SelectedIndexes.Create(selected), deselectedItems, - SelectedItems.Create(selected, ItemsView)); + SelectedItems.Create(selected, Source is not null ? ItemsView : null)); SelectionChanged?.Invoke(this, e); _untypedSelectionChanged?.Invoke(this, e); } diff --git a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs index 64c1b14253..8f6d256847 100644 --- a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs +++ b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs @@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection public SelectionModelSelectionChangedEventArgs( IReadOnlyList? deselectedIndices = null, IReadOnlyList? selectedIndices = null, - IReadOnlyList? deselectedItems = null, - IReadOnlyList? selectedItems = null) + IReadOnlyList? deselectedItems = null, + IReadOnlyList? selectedItems = null) { DeselectedIndexes = deselectedIndices ?? Array.Empty(); SelectedIndexes = selectedIndices ?? Array.Empty(); @@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection /// /// Gets the items that were removed from the selection. /// - public new IReadOnlyList DeselectedItems { get; } + public new IReadOnlyList DeselectedItems { get; } /// /// Gets the items that were added to the selection. /// - public new IReadOnlyList SelectedItems { get; } + public new IReadOnlyList SelectedItems { get; } protected override IReadOnlyList GetUntypedDeselectedItems() { diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 69a651aca6..22db0cbb6c 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,18 +2,23 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Controls.Utils; namespace Avalonia.Controls.Selection { + /// + /// Base class for selection models. + /// + /// The type of the element being selected. public abstract class SelectionNodeBase : ICollectionChangedListener { private IEnumerable? _source; private bool _rangesEnabled; private List? _ranges; - private int _collectionChanging; + /// + /// Gets or sets the source collection. + /// protected IEnumerable? Source { get => _source; @@ -21,18 +26,23 @@ namespace Avalonia.Controls.Selection { if (_source != value) { - if (ItemsView?.Inner is INotifyCollectionChanged inccOld) - CollectionChangedEventManager.Instance.RemoveListener(inccOld, this); + ItemsView?.RemoveListener(this); _source = value; - ItemsView = value is object ? ItemsSourceView.GetOrCreate(value) : null; - if (ItemsView?.Inner is INotifyCollectionChanged inccNew) - CollectionChangedEventManager.Instance.AddListener(inccNew, this); + ItemsView = value is not null ? ItemsSourceView.GetOrCreate(value) : null; + ItemsView?.AddListener(this); } } } - protected bool IsSourceCollectionChanging => _collectionChanging > 0; + /// + /// Gets an of the . + /// + protected internal ItemsSourceView? ItemsView { get; set; } + /// + /// Gets or sets a value indicating whether range selection is currently enabled for + /// the selection node. + /// protected bool RangesEnabled { get => _rangesEnabled; @@ -50,8 +60,6 @@ namespace Avalonia.Controls.Selection } } - internal ItemsSourceView? ItemsView { get; set; } - internal IReadOnlyList Ranges { get @@ -67,7 +75,7 @@ namespace Avalonia.Controls.Selection void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - ++_collectionChanging; + OnSourceCollectionChangeStarted(); } void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -77,69 +85,173 @@ namespace Avalonia.Controls.Selection void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - if (--_collectionChanging == 0) - { - OnSourceCollectionChangeFinished(); - } + OnSourceCollectionChangeFinished(); } - protected abstract void OnSourceCollectionChangeFinished(); - - private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta); + /// + /// Called when the source collection starts changing. + /// + protected virtual void OnSourceCollectionChangeStarted() + { + } - private protected abstract void OnSourceReset(); + /// + /// Called when the collection changes. + /// + /// The details of the collection change. + /// + /// The implementation in calls + /// and + /// in order to calculate how the collection change affects the currently selected items. + /// It then calls and + /// if necessary, according + /// to the returned by those methods. + /// + /// Override this method and to provide + /// custom handling of source collection changes. + /// + protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + { + var shiftDelta = 0; + var shiftIndex = -1; + List? removed = null; - private protected abstract void OnSelectionChanged(IReadOnlyList deselectedItems); + if (!IsValidCollectionChange(e)) + { + return; + } - private protected int CommitSelect(IndexRange range) - { - if (RangesEnabled) + switch (e.Action) { - _ranges ??= new List(); - 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 ranges) + /// + /// Called when the source collection has finished changing, and all CollectionChanged + /// handlers have run. + /// + /// + /// Override this method to respond to the end of a collection change instead of acting at + /// the end of + /// in order to ensure that all UI subscribers to the source collection change event have + /// had chance to run. + /// + protected virtual void OnSourceCollectionChangeFinished() { - if (RangesEnabled) - { - _ranges ??= new List(); - return IndexRange.Add(_ranges, ranges); - } + } - return 0; + /// + /// Called by , + /// detailing the indexes changed by the collection changing. + /// + /// The first index that was shifted. + /// + /// If positive, the number of items inserted, or if negative the number of items removed. + /// + protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta) + { } - private protected int CommitDeselect(IndexRange range) + /// + /// Called by , + /// on collection reset. + /// + protected abstract void OnSourceReset(); + + /// + /// Called by , + /// detailing the items removed by a collection change. + /// + protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList deselectedItems) + { + } + + /// + /// If , adds the specified range to the selection. + /// + /// The inclusive index of the start of the range to select. + /// The inclusive index of the end of the range to select. + /// The number of items selected. + protected int CommitSelect(int begin, int end) { if (RangesEnabled) { _ranges ??= new List(); - return IndexRange.Remove(_ranges, range); + return IndexRange.Add(_ranges, new IndexRange(begin, end)); } return 0; } - private protected int CommitDeselect(IReadOnlyList ranges) + /// + /// If , removes the specified range from the selection. + /// + /// The inclusive index of the start of the range to deselect. + /// The inclusive index of the end of the range to deselect. + /// The number of items selected. + protected int CommitDeselect(int begin, int end) { - if (RangesEnabled && _ranges is object) + if (RangesEnabled) { - return IndexRange.Remove(_ranges, ranges); + _ranges ??= new List(); + return IndexRange.Remove(_ranges, new IndexRange(begin, end)); } return 0; } - private protected virtual CollectionChangeState OnItemsAdded(int index, IList items) + /// + /// Called by + /// when items are added to the source collection. + /// + /// + /// A struct containing the details of the adjusted + /// selection. + /// + /// + /// The implementation in adjusts the selected ranges, + /// assigning new indexes. Override this method to carry out additional computation when + /// items are added. + /// + 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? 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 }; } + /// + /// Called by + /// when items are removed from the source collection. + /// + /// + /// A struct containing the details of the adjusted + /// selection. + /// + /// + /// The implementation in adjusts the selected ranges, + /// assigning new indexes. Override this method to carry out additional computation when + /// items are removed. + /// 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? removed = null; - if (_ranges is object) + if (_ranges is not null) { var deselected = new List(); @@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection }; } - private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) - { - var shiftDelta = 0; - var shiftIndex = -1; - List? 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 + /// + /// Details the results of a collection change on the current selection; + /// + protected class CollectionChangeState { - public int ShiftIndex; - public int ShiftDelta; - public List? RemovedItems; + /// + /// Gets or sets the first index that was shifted as a result of the collection + /// changing. + /// + public int ShiftIndex { get; set; } + + /// + /// Gets or sets a value indicating how the indexes after + /// were shifted. + /// + public int ShiftDelta { get; set; } + + /// + /// Gets or sets the items removed by the collection change, if any. + /// + public List? RemovedItems { get; set; } } } } From 56727d00a0a4f018a2409e90863b627ddb36492a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 16:02:46 +0100 Subject: [PATCH 15/34] More adjustments --- .../Avalonia.Win32/Input/Imm32InputMethod.cs | 4 ++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 44 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 41417dd950..a777aa81b7 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/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); @@ -283,6 +285,8 @@ namespace Avalonia.Win32.Input var composition = GetCompositionString(); + Composition = composition; + Client.SetPreeditText(composition); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 1cc9d1fa2f..6e12b9c4fd 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/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,8 +717,18 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_IME_COMPOSITION: { + var previousComposition = Imm32InputMethod.Current.Composition; + Imm32InputMethod.Current.CompositionChanged(); + //For Korean IME we need preserve commited text. + if (ToInt32(lParam) == (uint)GCS.GCS_RESULTSTR && ToInt32(wParam) >= 32) + { + Imm32InputMethod.Current.Composition = previousComposition; + + _ignoreWmChar = true; + } + break; } case WindowsMessage.WM_IME_SELECT: @@ -726,9 +744,27 @@ namespace Avalonia.Win32 Imm32InputMethod.Current.IsComposing = true; return IntPtr.Zero; case WindowsMessage.WM_IME_ENDCOMPOSITION: - Imm32InputMethod.Current.IsComposing = false; - break; + { + //In case composition has not been comitted yet we need to do that here. + if (!string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) + { + var text = Imm32InputMethod.Current.Composition; + + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); + + Imm32InputMethod.Current.Composition = null; + } + + //Cleanup composition state. + Imm32InputMethod.Current.IsComposing = false; + if (Imm32InputMethod.Current.IsActive) + { + Imm32InputMethod.Current.Client.SetPreeditText(null); + } + + break; + } case WindowsMessage.WM_GETOBJECT: if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control) { From d5cfc06f72f5baca45a1f6448d822e956e21683c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 16:04:58 +0100 Subject: [PATCH 16/34] Remove redundant code --- .../Presenters/TextPresenter.cs | 17 ----------------- .../TextBoxTextInputMethodClient.cs | 2 -- 2 files changed, 19 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 2e6f1a0f2d..089a2c1168 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -527,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; - //} - /// /// Creates the used to render the text. /// diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 6f8f3f3cab..deace4084c 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -177,8 +177,6 @@ namespace Avalonia.Controls var text = GetText(preeditText); - Debug.WriteLine(text); - _presenter._text = text; _presenter.PreeditText = preeditText; From 8457fba5a315ba18bffd27c74f8122e28c14ea8e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Mar 2023 06:11:39 +0100 Subject: [PATCH 17/34] More adjustments --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 6e12b9c4fd..eaab0d5559 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -721,12 +721,40 @@ namespace Avalonia.Win32 Imm32InputMethod.Current.CompositionChanged(); - //For Korean IME we need preserve commited text. - if (ToInt32(lParam) == (uint)GCS.GCS_RESULTSTR && ToInt32(wParam) >= 32) + var gcs = (GCS)ToInt32(lParam); + + switch (gcs) { - Imm32InputMethod.Current.Composition = previousComposition; + 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: + { + if (string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) + { + var c = (char)ToInt32(wParam); + + Imm32InputMethod.Current.Composition = new string(c, 1); - _ignoreWmChar = true; + _ignoreWmChar = true; + } + break; + } + case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: + { + Imm32InputMethod.Current.Composition = previousComposition; + + _ignoreWmChar = true; + + break; + } } break; @@ -741,6 +769,13 @@ namespace Avalonia.Win32 case WindowsMessage.WM_IME_NOTIFY: 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: @@ -751,12 +786,11 @@ namespace Avalonia.Win32 var text = Imm32InputMethod.Current.Composition; e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); - - Imm32InputMethod.Current.Composition = null; } //Cleanup composition state. Imm32InputMethod.Current.IsComposing = false; + Imm32InputMethod.Current.Composition = null; if (Imm32InputMethod.Current.IsActive) { From 26263cd7adb4224990fc8c3ad2496524c03714b5 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Mar 2023 12:20:51 +0100 Subject: [PATCH 18/34] Fix cursor rect --- native/Avalonia.Native/src/OSX/AvnView.mm | 42 +++++++++++++++++------ src/Avalonia.Native/WindowImpl.cs | 9 +++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 11155afb2b..ee8dbcedc1 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -21,8 +21,8 @@ NSObject* _renderTarget; AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; - AvnRect _cursorRect; - NSString* _text; + NSRect _cursorRect; + NSMutableString* _text; NSRange _selection; } @@ -525,7 +525,7 @@ - (void)keyDown:(NSEvent *)event { [self keyboardEvent:event withType:KeyDown]; - [[self inputContext] handleEvent:event]; + _lastKeyHandled = [[self inputContext] handleEvent:event]; [super keyDown:event]; } @@ -576,7 +576,7 @@ - (NSRange)selectedRange { - return NSMakeRange(NSNotFound, 0); + return _selection; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange @@ -606,6 +606,8 @@ } _parent->InputMethod->Client->SetPreeditText(nullptr); + + [[self inputContext] discardMarkedText]; } - (NSArray *)validAttributesForMarkedText @@ -615,18 +617,24 @@ - (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 @@ -640,7 +648,7 @@ return NSZeroRect; } - return ToNSRect(_cursorRect); + return _cursorRect; } - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info @@ -746,15 +754,27 @@ } - (void) setText:(NSString *)text{ - _text = 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{ - _cursorRect = 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 diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index cbfebbbec4..5d0e6a2d18 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -71,7 +71,7 @@ namespace Avalonia.Native } } - public IAvnWindow Native => _native; + public new IAvnWindow Native => _native; public void CanResize(bool value) { @@ -93,8 +93,6 @@ namespace Avalonia.Native _native.SetTitle(title ?? ""); } - public ITextInputMethodImpl TextInputMethod => _inputMethod; - public WindowState WindowState { get => (WindowState)_native.WindowState; @@ -235,6 +233,11 @@ namespace Avalonia.Native public override object TryGetFeature(Type featureType) { + if(featureType == typeof(ITextInputMethodImpl)) + { + return _inputMethod; + } + if (featureType == typeof(ITopLevelNativeMenuExporter)) { return _nativeMenuExporter; From 21574f5607d687c24d1c831bd808733d71650439 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Mar 2023 13:59:00 +0100 Subject: [PATCH 19/34] Expose pre/post collection changed events. Instead of implementing `ICollectionChangedListener` on `SelectionNodeBase`. We may want to expose this publicly at some point. --- src/Avalonia.Controls/ItemsSourceView.cs | 39 +++++++++++-------- .../Selection/SelectionNodeBase.cs | 38 +++++++++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 1fa8f6a5cf..416b909219 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/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. /// /// - /// TODO: Not yet implemented in Avalonia. + /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// internal bool HasKeyIndexMapping => false; @@ -92,6 +93,25 @@ namespace Avalonia.Controls } } + /// + /// Occurs when a collection has finished changing and all + /// event handlers have been notified. + /// + internal event NotifyCollectionChangedEventHandler? PreCollectionChanged + { + add + { + AddListenerIfNecessary(); + _preCollectionChanged += value; + } + + remove + { + _preCollectionChanged -= value; + RemoveListenerIfNecessary(); + } + } + /// /// Occurs when a collection has finished changing and all /// 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) @@ -241,22 +262,6 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } - internal void AddListener(ICollectionChangedListener listener) - { - if (Inner is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.AddListener(incc, listener); - } - } - - internal void RemoveListener(ICollectionChangedListener listener) - { - if (Inner is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.RemoveListener(incc, listener); - } - } - /// /// Retrieves the index of the item that has the specified unique identifier (key). /// diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 22db0cbb6c..caeff61f07 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using Avalonia.Controls.Utils; namespace Avalonia.Controls.Selection { @@ -10,7 +9,7 @@ namespace Avalonia.Controls.Selection /// Base class for selection models. /// /// The type of the element being selected. - public abstract class SelectionNodeBase : ICollectionChangedListener + public abstract class SelectionNodeBase { private IEnumerable? _source; private bool _rangesEnabled; @@ -24,12 +23,28 @@ namespace Avalonia.Controls.Selection 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) { - ItemsView?.RemoveListener(this); + if (ItemsView is not null) + { + ItemsView.PreCollectionChanged -= OnPreChanged; + ItemsView.CollectionChanged -= OnChanged; + ItemsView.PostCollectionChanged -= OnPostChanged; + } + _source = value; ItemsView = value is not null ? ItemsSourceView.GetOrCreate(value) : null; - ItemsView?.AddListener(this); + + if (ItemsView is not null) + { + ItemsView.PreCollectionChanged += OnPreChanged; + ItemsView.CollectionChanged += OnChanged; + ItemsView.PostCollectionChanged += OnPostChanged; + } } } } @@ -73,21 +88,6 @@ namespace Avalonia.Controls.Selection } } - void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChangeStarted(); - } - - void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChanged(e); - } - - void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChangeFinished(); - } - /// /// Called when the source collection starts changing. /// From 87e0ff2a51a884d47eb888885b1cb3731149b7c7 Mon Sep 17 00:00:00 2001 From: tkefauver Date: Sat, 4 Mar 2023 14:58:19 -0500 Subject: [PATCH 20/34] Flyout sample label fix --- samples/ControlCatalog/Pages/FlyoutsPage.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 54aa9d1b67..36e2e8f4f8 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -190,7 +190,7 @@ - - internal class TypeNameAndClassSelector : Selector + internal sealed class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; private List? _classes; @@ -85,12 +85,7 @@ namespace Avalonia.Styling /// public override string ToString(Style? owner) { - if (_selectorString == null) - { - _selectorString = BuildSelectorString(owner); - } - - return _selectorString; + return _selectorString ??= BuildSelectorString(owner); } /// diff --git a/tests/Avalonia.Benchmarks/Properties/launchSettings.json b/tests/Avalonia.Benchmarks/Properties/launchSettings.json new file mode 100644 index 0000000000..619d635ebf --- /dev/null +++ b/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" + } + } +} diff --git a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs index 0ac96c1103..11bc5ce35f 100644 --- a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs +++ b/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(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; } } From 708c8a1c294fb54918dc5875071ab49fffe3c16c Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 6 Mar 2023 12:32:18 +0100 Subject: [PATCH 23/34] Add missing ClipToBounds for inner Grid --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 4 +++- src/Avalonia.Controls.DataGrid/Themes/Simple.xaml | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index ca516c8918..e4642c1453 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -516,7 +516,9 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> - + + RowDefinitions="Auto,*,Auto,Auto" + ClipToBounds="True"> Date: Mon, 6 Mar 2023 17:10:14 +0100 Subject: [PATCH 24/34] Trz to fix Japanese IME --- .../Avalonia.Win32/Input/Imm32InputMethod.cs | 22 +++++++++---------- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 21 +++++++++--------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index a777aa81b7..db650db4b0 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -271,30 +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(); - - Composition = composition; - 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() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index eaab0d5559..0ffbed0ee7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -719,11 +719,13 @@ namespace Avalonia.Win32 { var previousComposition = Imm32InputMethod.Current.Composition; - Imm32InputMethod.Current.CompositionChanged(); + var flags = (GCS)ToInt32(lParam); - var gcs = (GCS)ToInt32(lParam); + var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR); - switch (gcs) + Imm32InputMethod.Current.CompositionChanged(currentComposition); + + switch (flags) { case GCS.GCS_RESULTSTR: { @@ -749,10 +751,7 @@ namespace Avalonia.Win32 } case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: { - Imm32InputMethod.Current.Composition = previousComposition; - - _ignoreWmChar = true; - + // Japanese IME sends WM_CHAR after composition has finished. break; } } @@ -780,12 +779,12 @@ namespace Avalonia.Win32 return IntPtr.Zero; case WindowsMessage.WM_IME_ENDCOMPOSITION: { + var currentComposition = Imm32InputMethod.Current.Composition; + //In case composition has not been comitted yet we need to do that here. - if (!string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) + if (!string.IsNullOrEmpty(currentComposition)) { - var text = Imm32InputMethod.Current.Composition; - - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, currentComposition); } //Cleanup composition state. From 5ea3027f98c7ccf5456c31c1d51afa14f1c4ab82 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Mar 2023 17:14:13 +0100 Subject: [PATCH 25/34] Do the same for Chinese IME --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0ffbed0ee7..2a40eb9f5b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -739,14 +739,7 @@ namespace Avalonia.Win32 } case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: { - if (string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) - { - var c = (char)ToInt32(wParam); - - Imm32InputMethod.Current.Composition = new string(c, 1); - - _ignoreWmChar = true; - } + // Chinese IME sends WM_CHAR after composition has finished. break; } case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: From 714f4a5b06bad73d0e4d62d4e29b062bb01112ac Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Mar 2023 17:53:12 +0100 Subject: [PATCH 26/34] Correctly use transformed bounds --- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 381c63f430..5b975e29e1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => GlyphRun.Item.Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); public override void Dispose() { From 989e45428a9fd9443751dd59ca689b05729629cc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Mar 2023 18:34:04 +0000 Subject: [PATCH 27/34] always kill testmanagerd --- azure-pipelines-integrationtests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index dec94a44d5..939d09c959 100644 --- a/azure-pipelines-integrationtests.yml +++ b/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 From 67f52011816eb3d3929491ebd44209cbc6af8c81 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 7 Mar 2023 12:36:20 +0900 Subject: [PATCH 28/34] Fix tests --- src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index d827251cf3..a3d05a34b7 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -225,7 +225,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); - Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); + Popup.TemplatedParent = placementTarget.TemplatedParent; } if (Popup.Child == null) From 7452980c39165e545798b97d8351850a8554fbcd Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 7 Mar 2023 14:32:25 +0900 Subject: [PATCH 29/34] Move _lastKnownPosition --- src/Avalonia.Base/Input/PointerOverPreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 7c1fdc7233..3967c5abe0 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -152,6 +152,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) { @@ -165,8 +167,6 @@ namespace Avalonia.Input } } - var screenPosition = ((Visual)root).PointToScreen(position); - _lastKnownPosition = screenPosition; _currentPointer = (pointer, screenPosition); } From 2473bb8408e2f8882beecbd0b23ab354551e19c8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 7 Mar 2023 14:32:31 +0900 Subject: [PATCH 30/34] Fix some warnings --- src/Avalonia.Base/Input/MouseDevice.cs | 1 + src/Avalonia.Base/Input/PenDevice.cs | 3 ++- src/Avalonia.Base/Input/PointerEventArgs.cs | 2 ++ src/Avalonia.Base/Input/PointerOverPreProcessor.cs | 14 +++++++++----- src/Avalonia.Base/Input/TouchDevice.cs | 1 + 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 50980f1c3d..44412cd152 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/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 { diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 285249a5f8..b3cd39212b 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/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; diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 50d7cc5dc5..28a3c3aefb 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -42,7 +42,9 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers modifiers, Lazy?>? previousPoints) +#pragma warning disable CS0618 : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) +#pragma warning restore CS0618 { _previousPoints = previousPoints; } diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 3967c5abe0..0afdb8e080 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -42,14 +42,14 @@ namespace Avalonia.Input } if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown - && _currentPointer is (var lastPointer, var lastPosition)) + && _currentPointer is var (lastPointer, lastPosition)) { _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; @@ -101,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) { @@ -130,11 +132,11 @@ namespace Avalonia.Input { 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; } } @@ -189,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); diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 125d5fc813..bab1b9f784 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/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 { From 037ff6d265884f0e9499724b1a6139de07faf183 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Feb 2023 17:02:23 +0100 Subject: [PATCH 31/34] Don't allow window zoom when CanResize=false. Previously, even though the zoom button was disabled the user could still double-click on the title bar to zoom the window. Prevent that by returning the appropriate value from `NSWindow windowShouldZoom` and move the `CanZoom` logic into a central place for use by this method and `UpdateStyle`. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 ++ native/Avalonia.Native/src/OSX/WindowImpl.h | 2 ++ native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index b1fb915e04..16e1486acc 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/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 diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 93decef136..d00dffa65a 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -104,6 +104,8 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); + virtual bool CanZoom() { return false; } + protected: virtual NSWindowStyleMask CalculateStyleMask() = 0; virtual void UpdateStyle(); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 29bb659039..5140124a17 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/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; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 840f2c9e88..104611eabc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/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()]; } From ef2a47bc9573f3f94710b22477b93145e7b77407 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 7 Mar 2023 13:16:51 +0100 Subject: [PATCH 32/34] Only try to create embedded font collection for valid assets location --- src/Avalonia.Base/Media/FontManager.cs | 2 +- src/Avalonia.Themes.Simple/Accents/Base.xaml | 2 +- .../Media/FontManagerImplTests.cs | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 595a2f3474..5890b90954 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/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); diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 38b122d8b2..0640fe9a4a 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -76,7 +76,7 @@ - fonts://Inter#Inter, $Default + fonts:Inter#Inter, $Default #CC119EDA #99119EDA #66119EDA diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index 21c46b836d..859726e871 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -76,5 +76,16 @@ namespace Avalonia.Skia.UnitTests.Media Assert.Throws(() => 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); + } + } } } From 02983e3c1e096a0f8b87d5afbd06a9860df2799d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 7 Mar 2023 16:12:50 +0100 Subject: [PATCH 33/34] fix: Misc XML Document issue --- src/Avalonia.Base/AvaloniaProperty.cs | 13 ++++++++++++- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- src/Avalonia.Base/Platform/IDrawingContextImpl.cs | 1 + src/Avalonia.Base/Rendering/DisplayDirtyRect.cs | 2 +- .../Rendering/SceneGraph/GeometryNode.cs | 1 - src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs | 1 - .../Rendering/SceneGraph/OpacityMaskNode.cs | 1 - .../Rendering/SceneGraph/RectangleNode.cs | 1 - src/Avalonia.Controls/Platform/IInsetsManager.cs | 2 +- src/Avalonia.Controls/SplitButton/SplitButton.cs | 2 +- src/Browser/Avalonia.Browser/BrowserAppBuilder.cs | 2 +- .../Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 1 - 13 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 24244c5068..bda30c08fb 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,7 +257,18 @@ namespace Avalonia return result; } - /// + /// + /// Registers an attached . + /// + /// The type of the class that is registering the property. + /// The type of the property's value. + /// The name of the property. + /// The default value of the property. + /// Whether the property inherits its value. + /// The default binding mode for the property. + /// A value validation callback. + /// A value coercion callback. + /// if is set to true enable data validation. /// /// 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 diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index fc0ca2323e..8e6f7b0983 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -364,7 +364,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void Register(Type type, AvaloniaProperty property) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 12efb3c383..a40cbf95ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting /// Performs text wrapping returns a list of text lines. /// /// - /// Whether can be reused to store the split runs. + /// Whether can be reused to store the split runs. /// The first text source index. /// The paragraph width. /// The text paragraph properties. diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8962bc1586..ffdfa9aac1 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,6 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. + /// where to apply the opacity. void PushOpacity(double opacity, Rect bounds); /// diff --git a/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs b/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs index 7e6c3062cd..7a89e5b3cc 100644 --- a/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs +++ b/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs @@ -3,7 +3,7 @@ namespace Avalonia.Rendering { /// - /// Holds the state for a dirty rect rendered when is set. + /// Holds the state for a dirty rect rendered when is set. /// internal class DisplayDirtyRect { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 3ab535897a..f64a3e845d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The geometry. - /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, IImmutableBrush? brush, IPen? pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index f21791d038..61bffc3260 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The start point of the line. /// The end point of the line. - /// Auxiliary data required to draw the brush. public LineNode( Matrix transform, IPen pen, diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index e10d712c2d..b0584038a8 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -17,7 +17,6 @@ namespace Avalonia.Rendering.SceneGraph /// /// The opacity mask to push. /// The bounds of the mask. - /// Auxiliary data required to draw the brush. public OpacityMaskNode(IImmutableBrush mask, Rect bounds) : base(default, Matrix.Identity, mask) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index cee9ce9df7..94f61df47d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -20,7 +20,6 @@ namespace Avalonia.Rendering.SceneGraph /// The stroke pen. /// The rectangle to draw. /// The box shadow parameters - /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, IImmutableBrush? brush, diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 6288142805..072bace154 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls.Platform SafeAreaPadding = safeArePadding; } - /// + /// public Thickness SafeAreaPadding { get; } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 19d2b1c5da..e790578675 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -432,7 +432,7 @@ namespace Avalonia.Controls } /// - /// Called when the property changes. + /// Called when the property changes. /// private void Flyout_PlacementPropertyChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 32637b6d1b..9bb471005b 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -16,7 +16,7 @@ public class BrowserPlatformOptions public static class BrowserAppBuilder { /// - /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. + /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. /// /// Application builder. /// ID of the html element where avalonia content should be rendered. diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 87fa963871..318b0fe9ae 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -29,7 +29,6 @@ namespace Avalonia.Direct2D1.Media /// /// Initializes a new instance of the class. /// - /// The visual brush renderer. /// The render target to draw to. /// /// An object to use to create layers. May be null, in which case a From 8c01795ef8e9f288d8e0a2688cb8f956b24e18ed Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 7 Mar 2023 17:15:47 +0100 Subject: [PATCH 34/34] fix: Warning CS0628 new protected member declared in sealed type ```bash Warning CS0628 'DrawingGroup.DrawingGroupDrawingContext._rootDrawing': new protected member declared in sealed type Avalonia.Base (net6.0), Avalonia.Base (netstandard2.0) .\src\Avalonia.Base\Media\DrawingGroup.cs 120 Active Warning CS0628 'DrawingGroup.DrawingGroupDrawingContext._currentDrawingGroup': new protected member declared in sealed type Avalonia.Base (net6.0), Avalonia.Base (netstandard2.0) .\src\Avalonia.Base\Media\DrawingGroup.cs 123 Active ``` --- src/Avalonia.Base/Media/DrawingGroup.cs | 4 ++-- src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index a41054202e..5f87da9dcd 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/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? _previousDrawingGroupStack; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 4e0d37479b..a10b3eb3ea 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -52,7 +52,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector? previous) + TypeNameAndClassSelector(Selector? previous) { _previous = previous; }