diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 5bbc3d6915..875161d336 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -84,11 +84,11 @@ "GenerateCppHeaders", "Package", "RunCoreLibsTests", - "RunDesignerTests", "RunHtmlPreviewerTests", "RunLeakTests", "RunRenderTests", "RunTests", + "RunToolsTests", "ZipFiles" ] } @@ -123,11 +123,11 @@ "GenerateCppHeaders", "Package", "RunCoreLibsTests", - "RunDesignerTests", "RunHtmlPreviewerTests", "RunLeakTests", "RunRenderTests", "RunTests", + "RunToolsTests", "ZipFiles" ] } diff --git a/Avalonia.sln b/Avalonia.sln index e66b73de0e..b21df07628 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -244,8 +244,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -573,10 +579,22 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU + {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU + {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -643,7 +661,10 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 7221fe4657..939d09c959 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -15,28 +15,41 @@ jobs: version: 7.0.101 - script: system_profiler SPDisplaysDataType |grep Resolution + displayName: 'Get Resolution' - script: | + arch="x64" + if [[ $(uname -m) == 'arm64' ]]; then + arch="arm64" + fi sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node - appium & + pkill testmanagerd + appium > appium.out & pkill IntegrationTestApp ./build.sh CompileNative rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh - open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app + open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-$arch/publish/IntegrationTestApp.app pkill IntegrationTestApp + displayName: 'Build IntegrationTestApp' - task: DotNetCoreCLI@2 + displayName: 'Run Integration Tests' inputs: command: 'test' projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' + arguments: '-l "console;verbosity=detailed"' - script: | pkill IntegrationTestApp pkill node + displayName: 'Stop Appium' + - publish: appium.out + displayName: 'Publish appium logs on failure' + condition: failed() - job: Windows pool: @@ -60,11 +73,13 @@ jobs: displayName: 'Start WinAppDriver' - task: DotNetCoreCLI@2 + displayName: 'Build IntegrationTestApp' inputs: command: 'build' projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 + displayName: 'Run Integration Tests' retryCountOnTaskFailure: 3 inputs: command: 'test' diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props index 4929578b60..a66bff4999 100644 --- a/build/SourceGenerators.props +++ b/build/SourceGenerators.props @@ -1,5 +1,10 @@ - + + true + false + + + + + + + + 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 c49290314d..eb0b528fd3 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 @@ -44,6 +44,9 @@ 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; 855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */; }; + 8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */; }; + 8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */; }; + 8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; @@ -97,6 +100,9 @@ 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; 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; }; @@ -143,6 +149,9 @@ isa = PBXGroup; children = ( 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, + 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, + 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, + 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */, BC11A5BC2608D58F0017BAD0 /* automation.h */, BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, @@ -213,6 +222,8 @@ 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, + 8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */, + 8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */, 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, @@ -293,6 +304,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, + 8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, 1A465D10246AB61600C5858B /* dnd.mm in Sources */, 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..5440b76bfe 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; + NSRect _cursorRect; + NSMutableString* _text; + NSRange _selection; } - (void)onClosed @@ -127,11 +131,8 @@ [self updateRenderTarget]; auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - - if(_parent->IsShown()) - { - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); - } + + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); } } @@ -521,7 +522,7 @@ - (void)keyDown:(NSEvent *)event { [self keyboardEvent:event withType:KeyDown]; - [[self inputContext] handleEvent:event]; + _lastKeyHandled = [[self inputContext] handleEvent:event]; [super keyDown:event]; } @@ -560,27 +561,50 @@ - (BOOL)hasMarkedText { - return _lastKeyHandled; + return [_markedText length] > 0; } - (NSRange)markedRange { + if([_markedText length] > 0) + return NSMakeRange(0, [_markedText length] - 1); return NSMakeRange(NSNotFound, 0); } - (NSRange)selectedRange { - return NSMakeRange(NSNotFound, 0); + return _selection; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { - + if([string isKindOfClass:[NSAttributedString class]]) + { + _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + } + else + { + _markedText = [[NSMutableAttributedString alloc] initWithString:string]; + } + + if(!_parent->InputMethod->IsActive()){ + return; + } + + _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]); } - (void)unmarkText { - + [[_markedText mutableString] setString:@""]; + + if(!_parent->InputMethod->IsActive()){ + return; + } + + _parent->InputMethod->Client->SetPreeditText(nullptr); + + [[self inputContext] discardMarkedText]; } - (NSArray *)validAttributesForMarkedText @@ -590,30 +614,38 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - return [NSAttributedString new]; + return nullptr; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - if(!_lastKeyHandled) - { + //[_text replaceCharactersInRange:replacementRange withString:string]; + + [self unmarkText]; + + //if(!_lastKeyHandled) + //{ if(_parent != nullptr) { _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); } - } + //} + + [[self inputContext] invalidateCharacterCoordinates]; } - (NSUInteger)characterIndexForPoint:(NSPoint)point { - return 0; + return NSNotFound; } - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - CGRect result = { 0 }; - - return result; + if(!_parent->InputMethod->IsActive()){ + return NSZeroRect; + } + + return _cursorRect; } - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info @@ -718,4 +750,28 @@ return [[self accessibilityChild] accessibilityFocusedUIElement]; } +- (void) setText:(NSString *)text{ + [_text setString:text]; + + [[self inputContext] discardMarkedText]; +} + +- (void) setSelection:(int)start :(int)end{ + _selection = NSMakeRange(start, end - start); + + [[self inputContext] invalidateCharacterCoordinates]; +} + +- (void) setCursorRect:(AvnRect)rect{ + NSRect cursorRect = ToNSRect(rect); + NSRect windowRectOnScreen = [[self window] convertRectToScreen:self.frame]; + + windowRectOnScreen.size = cursorRect.size; + windowRectOnScreen.origin = NSMakePoint(windowRectOnScreen.origin.x + cursorRect.origin.x, windowRectOnScreen.origin.y + self.frame.size.height - cursorRect.origin.y - cursorRect.size.height); + + _cursorRect = windowRectOnScreen; + + [[self inputContext] invalidateCharacterCoordinates]; +} + @end 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/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 83ab1bfd01..85f4b7c50a 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -41,7 +41,7 @@ public: ret->WorkingArea.X = [screen visibleFrame].origin.x; ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height; - ret->Scaling = [screen backingScaleFactor]; + ret->Scaling = 1; ret->IsPrimary = index == 0; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 93decef136..bc35ca670f 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,7 +104,11 @@ BEGIN_INTERFACE_MAP() id GetWindowProtocol (); virtual void BringToFront (); + + virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override; + virtual bool CanZoom() { return false; } + protected: virtual NSWindowStyleMask CalculateStyleMask() = 0; virtual void UpdateStyle(); @@ -130,6 +135,7 @@ public: NSObject *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 59102e15a6..edf6bcf508 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -4,6 +4,7 @@ // #import +#import #include "common.h" #include "AvnView.h" #include "menu.h" @@ -14,6 +15,7 @@ #import "WindowProtocol.h" #import "WindowInterfaces.h" #include "WindowBaseImpl.h" +#include "AvnTextInputMethod.h" WindowBaseImpl::~WindowBaseImpl() { @@ -28,6 +30,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, _glContext = gl; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl]; View = [[AvnView alloc] initWithParent:this]; + InputMethod = new AvnTextInputMethod(View); StandardContainer = [[AutoFitContentView new] initWithContent:View]; lastPositionSet = { 0, 0 }; @@ -293,15 +296,24 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } @try { - if(x != lastSize.width || y != lastSize.height) { - lastSize = NSSize{x, y}; - + if(x != lastSize.width || y != lastSize.height) + { if (!_shown) { - BaseEvents->Resized(AvnSize{x, y}, reason); - } else if (Window != nullptr) { - [Window setContentSize:lastSize]; - [Window invalidateShadow]; + auto screenSize = [Window screen].visibleFrame.size; + + if (x > screenSize.width) { + x = screenSize.width; + } + + if (y > screenSize.height) { + y = screenSize.height; + } } + + lastSize = NSSize{x, y}; + + [Window setContentSize:lastSize]; + [Window invalidateShadow]; } } @finally { @@ -595,6 +607,14 @@ void WindowBaseImpl::BringToFront() // do nothing. } +HRESULT WindowBaseImpl::GetInputMethod(IAvnTextInputMethod **retOut) { + START_COM_CALL; + + *retOut = InputMethod; + + return S_OK; +} + extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool 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 cf1ee6943d..104611eabc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -54,6 +54,11 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); + + if(IsZoomed()) { + _lastWindowState = _actualWindowState; + } + return SetWindowState(_lastWindowState); } } @@ -617,5 +622,5 @@ void WindowImpl::UpdateStyle() { [miniaturizeButton setHidden:!hasTrafficLights]; [miniaturizeButton setEnabled:_isEnabled]; [zoomButton setHidden:!hasTrafficLights]; - [zoomButton setEnabled:_isEnabled && _canResize]; + [zoomButton setEnabled:CanZoom()]; } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 3704cee890..40232947d9 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -165,10 +165,10 @@ partial class Build : NukeBuild foreach (var fw in targetFrameworks) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on *nix - https://github.com/mono/mono/issues/13969"); continue; } @@ -220,16 +220,18 @@ partial class Build : NukeBuild .Executes(() => { RunCoreTest("Avalonia.Skia.RenderTests"); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (Parameters.IsRunningOnWindows) RunCoreTest("Avalonia.Direct2D1.RenderTests"); }); - Target RunDesignerTests => _ => _ - .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + Target RunToolsTests => _ => _ + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { - RunCoreTest("Avalonia.DesignerSupport.Tests"); + RunCoreTest("Avalonia.Generators.Tests"); + if (Parameters.IsRunningOnWindows) + RunCoreTest("Avalonia.DesignerSupport.Tests"); }); Target RunLeakTests => _ => _ @@ -276,7 +278,7 @@ partial class Build : NukeBuild Target RunTests => _ => _ .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) - .DependsOn(RunDesignerTests) + .DependsOn(RunToolsTests) .DependsOn(RunHtmlPreviewerTests) .DependsOn(RunLeakTests); diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config index d1c0408241..09f22ec527 100644 --- a/nukebuild/numerge.config +++ b/nukebuild/numerge.config @@ -11,6 +11,11 @@ "Id": "Avalonia.Build.Tasks", "IgnoreMissingFrameworkBinaries": true, "DoNotMergeDependencies": true + }, + { + "Id": "Avalonia.Generators", + "IgnoreMissingFrameworkBinaries": true, + "DoNotMergeDependencies": true } ] } diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4d0ed866a3..1d210172f0 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -6,11 +6,15 @@ - + all true TargetFramework=netstandard2.0 + diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index 056d3bf552..f094c0081c 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index 49e079a719..3e1270256d 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -4,7 +4,7 @@ - diff --git a/samples/ControlCatalog.Browser/app.css b/samples/ControlCatalog.Browser/app.css index 27680f6e1a..0e6ab12461 100644 --- a/samples/ControlCatalog.Browser/app.css +++ b/samples/ControlCatalog.Browser/app.css @@ -1,4 +1,11 @@ -#out { +:root { + --sat: env(safe-area-inset-top); + --sar: env(safe-area-inset-right); + --sab: env(safe-area-inset-bottom); + --sal: env(safe-area-inset-left); +} + +#out { height: 100vh; width: 100vw } diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index e55f003133..85c159467b 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Fonts.Inter; using Avalonia.Headless; using Avalonia.LogicalTree; using Avalonia.Threading; @@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore EnableIme = true }) .UseSkia() + .WithInterFont() .AfterSetup(builder => { builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 74d5b2fd8c..b4dac5399c 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -3,7 +3,7 @@ Exe manual net6.0-ios - 10.0 + 13.0 True iossimulator-x64 @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 6ffe3ba662..1dd4416c28 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 10.0 + 13.0 UIDeviceFamily 1 @@ -39,9 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIStatusBarHidden - - UIViewControllerBasedStatusBarAppearance - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index d71d51f068..246fe4385f 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -44,11 +44,11 @@ namespace ControlCatalog { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { - desktopLifetime.MainWindow = new MainWindow(); + desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) { - singleViewLifetime.MainView = new MainView(); + singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() }; } base.OnFrameworkInitializationCompleted(); diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 0e84b3d182..5125b42426 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -2,7 +2,8 @@ netstandard2.0;net6.0 true - enable + enable + true @@ -35,14 +36,5 @@ - - - - - - - - - - + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 6f31d22677..9c439c874f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -12,6 +13,7 @@ using Avalonia.VisualTree; using Avalonia.Styling; using ControlCatalog.Models; using ControlCatalog.Pages; +using ControlCatalog.ViewModels; namespace ControlCatalog { @@ -99,13 +101,47 @@ namespace ControlCatalog }; } + internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); + var decorations = this.Get("Decorations"); if (VisualRoot is Window window) decorations.SelectedIndex = (int)window.SystemDecorations; - + + var insets = TopLevel.GetTopLevel(this)!.InsetsManager; + if (insets != null) + { + // In real life application these events should be unsubscribed to avoid memory leaks. + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + insets.SafeAreaChanged += (sender, args) => + { + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + }; + + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; + + ViewModel.PropertyChanged += async (sender, args) => + { + if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge)) + { + insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge; + } + else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible)) + { + insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible; + } + + // Give the OS some time to apply new values and refresh the view model. + await Task.Delay(100); + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; + }; + } + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); } diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index c589f41442..10ff94d25c 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,6 @@ namespace ControlCatalog { this.InitializeComponent(); - DataContext = new MainWindowViewModel(); _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu; } diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..6d759597b5 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.Items = FontManager.Current.SystemFonts; fontComboBox.SelectedIndex = 0; } } 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/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 54aa9d1b67..c0521cd3ba 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -16,8 +16,13 @@ + + + + + - + @@ -136,6 +141,15 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + Normal + Minimized + Maximized + FullScreen + + + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 43875dd990..f0be34fdaa 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -7,6 +7,25 @@ using Avalonia.Threading; namespace IntegrationTestApp { + public class MeasureBorder : Border + { + protected override Size MeasureOverride(Size availableSize) + { + MeasuredWith = availableSize; + + return base.MeasureOverride(availableSize); + } + + public static readonly StyledProperty MeasuredWithProperty = AvaloniaProperty.Register( + nameof(MeasuredWith)); + + public Size MeasuredWith + { + get => GetValue(MeasuredWithProperty); + set => SetValue(MeasuredWithProperty, value); + } + } + public class ShowWindowTest : Window { private readonly DispatcherTimer? _timer; @@ -16,11 +35,11 @@ namespace IntegrationTestApp { InitializeComponent(); DataContext = this; - PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; + PositionChanged += (s, e) => this.GetControl("CurrentPosition").Text = $"{Position}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - _orderTextBox = this.GetControl("Order"); + _orderTextBox = this.GetControl("CurrentOrder"); _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += TimerOnTick; _timer.Start(); @@ -36,13 +55,13 @@ namespace IntegrationTestApp { base.OnOpened(e); var scaling = PlatformImpl!.DesktopScaling; - this.GetControl("Position").Text = $"{Position}"; - this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{scaling}"; + this.GetControl("CurrentPosition").Text = $"{Position}"; + this.GetControl("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + this.GetControl("CurrentScaling").Text = $"{scaling}"; if (Owner is not null) { - var ownerRect = this.GetControl("OwnerRect"); + var ownerRect = this.GetControl("CurrentOwnerRect"); var owner = (Window)Owner; ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } diff --git a/samples/IntegrationTestApp/bundle.sh b/samples/IntegrationTestApp/bundle.sh index 505991582e..e52968215b 100755 --- a/samples/IntegrationTestApp/bundle.sh +++ b/samples/IntegrationTestApp/bundle.sh @@ -1,5 +1,12 @@ #!/usr/bin/env bash cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -dotnet restore -r osx-arm64 -dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file + +arch="x64" + +if [[ $(uname -m) == 'arm64' ]]; then +arch="arm64" +fi + +dotnet restore -r osx-$arch +dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-$arch -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file diff --git a/samples/MobileSandbox/MobileSandbox.csproj b/samples/MobileSandbox/MobileSandbox.csproj index 02e4c43960..0d7c7859f8 100644 --- a/samples/MobileSandbox/MobileSandbox.csproj +++ b/samples/MobileSandbox/MobileSandbox.csproj @@ -28,6 +28,7 @@ + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 5f61a08f3c..40efe818d8 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 76c1ba7b69..b572c3e4f8 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj index 9650068434..0e054d6c36 100644 --- a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 3c62af1eaf..b37ecf01b9 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -12,6 +12,7 @@ + diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index eab654acb6..f23e391a2a 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index b27cfe77e8..81b30c6cbe 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 247008c503..eb4b6bf6a0 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Android.App; using Android.Content; using Android.Content.PM; @@ -32,6 +33,9 @@ namespace Avalonia.Android lifetime.View = View; } + Window?.ClearFlags(WindowManagerFlags.TranslucentStatus); + Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + base.OnCreate(savedInstanceState); SetContentView(View); @@ -55,6 +59,17 @@ namespace Avalonia.Android } } + protected override void OnResume() + { + base.OnResume(); + + // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible. + if (Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; + } + } + public event EventHandler BackRequested; public override void OnBackPressed() diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 2a345a857c..27481a598e 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -8,6 +8,7 @@ using Avalonia.Android.Platform; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; using Avalonia.Platform; using Avalonia.Rendering; @@ -67,6 +68,11 @@ namespace Avalonia.Android } _root.Renderer.Start(); + + if (_view.TryGetFeature(out var insetsManager) == true) + { + (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); + } } else { diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs new file mode 100644 index 0000000000..35d1b06e6a --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using Android.OS; +using Android.Views; +using AndroidX.AppCompat.App; +using AndroidX.Core.View; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Android.Platform +{ + internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener + { + private readonly AvaloniaMainActivity _activity; + private readonly TopLevelImpl _topLevel; + private readonly InsetsAnimationCallback _callback; + private bool _displayEdgeToEdge; + private bool _usesLegacyLayouts; + private bool? _systemUiVisibility; + private SystemBarTheme? _statusBarTheme; + private bool? _isDefaultSystemBarLightTheme; + + public event EventHandler SafeAreaChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + _displayEdgeToEdge = value; + + if(Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + _activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default; + } + + WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + } + } + + public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) + { + _activity = activity; + _topLevel = topLevel; + _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop); + + _callback.InsetsManager = this; + + ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this); + + ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback); + + if(Build.VERSION.SdkInt < BuildVersionCodes.R) + { + _usesLegacyLayouts = true; + _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); + } + + DisplayEdgeToEdge = false; + } + + public Thickness SafeAreaPadding + { + get + { + var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); + + if (insets != null) + { + var renderScaling = _topLevel.RenderScaling; + + var inset = insets.GetInsets( + (DisplayEdgeToEdge ? + WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | + WindowInsetsCompat.Type.DisplayCutout() : + 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + return new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? + imeInset.Bottom - navBarInset.Bottom : + inset.Bottom) / renderScaling); + } + + return default; + } + } + + public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) + { + NotifySafeAreaChanged(SafeAreaPadding); + return insets; + } + + private void NotifySafeAreaChanged(Thickness safeAreaPadding) + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); + } + + public void OnGlobalLayout() + { + NotifySafeAreaChanged(SafeAreaPadding); + } + + public SystemBarTheme? SystemBarTheme + { + get + { + try + { + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + catch (Exception _) + { + return Controls.Platform.SystemBarTheme.Light; + } + } + set + { + _statusBarTheme = value; + + var isDefault = _statusBarTheme == null; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + if (_isDefaultSystemBarLightTheme == null) + { + _isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars; + } + + if (value == null && _isDefaultSystemBarLightTheme != null) + { + value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + + compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; + compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; + + AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; + } + } + + public bool? IsSystemBarVisible + { + get + { + if(_activity.Window == null) + { + return true; + } + var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); + + return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); + } + set + { + _systemUiVisibility = value; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View); + + if (value == null || value.Value) + { + compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); + } + else + { + compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); + + if (compat != null) + { + compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe; + } + } + } + } + + internal void ApplyStatusBarState() + { + IsSystemBarVisible = _systemUiVisibility; + SystemBarTheme = _statusBarTheme; + } + + private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback + { + public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode) + { + } + + public AndroidInsetsManager InsetsManager { get; set; } + + public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList runningAnimations) + { + foreach (var anim in runningAnimations) + { + if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) + { + var renderScaling = InsetsManager._topLevel.RenderScaling; + + var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + + var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom); + bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction); + + var padding = new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + bottomPadding / renderScaling); + InsetsManager?.NotifySafeAreaChanged(padding); + break; + } + } + return insets; + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index e511ed9a8b..b8d80a50ff 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using Android.App; using Android.Content; using Android.Graphics; -using Android.OS; using Android.Runtime; -using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.Specific; @@ -24,11 +22,13 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; +using Java.Util; using Math = System.Math; using AndroidRect = Android.Graphics.Rect; using Window = Android.Views.Window; using Android.Graphics.Drawables; -using Java.Util; +using Android.OS; +using Android.Text; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -43,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly AndroidInsetsManager _insetsManager; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) @@ -59,6 +60,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + if (avaloniaView.Context is AvaloniaMainActivity mainActivity) + { + _insetsManager = new AndroidInsetsManager(mainActivity, this); + } + _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); @@ -70,21 +76,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IInputRoot InputRoot { get; private set; } - public virtual Size ClientSize - { - get - { - AndroidRect rect = new AndroidRect(); - AndroidRect intersection = new AndroidRect(); - - _view.GetWindowVisibleDisplayFrame(intersection); - _view.GetGlobalVisibleRect(rect); - - intersection.Intersect(rect); - - return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling); - } - } + public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size? FrameSize => null; @@ -285,7 +277,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { - // TODO adjust status bar depending on full screen mode. + if(_insetsManager != null) + { + _insetsManager.SystemBarTheme = themeVariant switch + { + PlatformThemeVariant.Light => SystemBarTheme.Light, + PlatformThemeVariant.Dark => SystemBarTheme.Dark, + _ => null, + }; + } } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); @@ -403,6 +403,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 9d6dd46d0e..0a34e6077c 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -138,27 +138,43 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return Array.Empty(); } - using var javaFile = new JavaFile(Uri.Path!); + List files = new List(); - // Java file represents files AND directories. Don't be confused. - var files = await javaFile.ListFilesAsync().ConfigureAwait(false); - if (files is null) + var contentResolver = Activity.ContentResolver; + if (contentResolver == null) { - return Array.Empty(); + return files; } - return files - .Select(f => (file: f, uri: AndroidUri.FromFile(f))) - .Where(t => t.uri is not null) - .Select(t => t.file switch - { - { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!), - { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false), - _ => null - }) - .Where(i => i is not null) - .ToArray()!; - } + var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri)); + + var projection = new[] + { + DocumentsContract.Document.ColumnDocumentId, + DocumentsContract.Document.ColumnMimeType + }; + if (childrenUri != null) + { + using var cursor = contentResolver.Query(childrenUri, projection, null, null, null); + + if (cursor != null) + while (cursor.MoveToNext()) + { + var mime = cursor.GetString(1); + var id = cursor.GetString(0); + var uri = DocumentsContract.BuildDocumentUriUsingTree(Uri!, id); + if (uri == null) + { + continue; + } + + files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : + new AndroidStorageFile(Activity, uri)); + } + } + + return files; + } } internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index 31b6cad8ab..4a09f2a80a 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -32,9 +32,14 @@ namespace Avalonia /// /// The owner type. /// The property. - public new AttachedProperty AddOwner() where TOwner : AvaloniaObject + public new AttachedProperty AddOwner(StyledPropertyMetadata? metadata = null) where TOwner : AvaloniaObject { AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + if (metadata != null) + { + OverrideMetadata(metadata); + } + return this; } } 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/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs index a202d6b5bc..3b9b2d0de6 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.LogicalTree; -using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Input.GestureRecognizers { @@ -11,13 +11,13 @@ namespace Avalonia.Input.GestureRecognizers private readonly IInputElement _inputElement; private List? _recognizers; private Dictionary? _pointerGrabs; - - + + public GestureRecognizerCollection(IInputElement inputElement) { _inputElement = inputElement; } - + public void Add(IGestureRecognizer recognizer) { if (_recognizers == null) @@ -31,14 +31,13 @@ namespace Avalonia.Input.GestureRecognizers recognizer.Initialize(_inputElement, this); // Hacks to make bindings work - + if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) { logical.SetParent(logicalParent); if (recognizer is StyledElement styleableRecognizer && _inputElement is StyledElement styleableParent) - styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, - styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); + styleableParent.GetObservable(StyledElement.TemplatedParentProperty).Subscribe(parent => styleableRecognizer.TemplatedParent = parent); } } @@ -58,7 +57,7 @@ namespace Avalonia.Input.GestureRecognizers return false; foreach (var r in _recognizers) { - if(e.Handled) + if (e.Handled) break; r.PointerPressed(e); } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index 991694cc60..6784677520 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -13,22 +13,18 @@ namespace Avalonia.Input private Point _initialPosition; private int _gestureId; private IPointer? _tracking; - private PullDirection _pullDirection; private bool _pullInProgress; /// /// Defines the property. /// - public static readonly DirectProperty PullDirectionProperty = - AvaloniaProperty.RegisterDirect( - nameof(PullDirection), - o => o.PullDirection, - (o, v) => o.PullDirection = v); + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection)); public PullDirection PullDirection { - get => _pullDirection; - set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); } public PullGestureRecognizer(PullDirection pullDirection) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 7c1ee13eed..1ad2f292ca 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -17,61 +17,45 @@ namespace Avalonia.Input.GestureRecognizers private IPointer? _tracking; private IInputElement? _target; private IGestureRecognizerActionsDispatcher? _actions; - private bool _canHorizontallyScroll; - private bool _canVerticallyScroll; private int _gestureId; - private int _scrollStartDistance = 30; private Point _pointerPressedPoint; private VelocityTracker? _velocityTracker; // Movement per second private Vector _inertia; private ulong? _lastMoveTimestamp; - private bool _isScrollInertiaEnabled; /// /// Defines the property. /// - public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanHorizontallyScroll), - o => o.CanHorizontallyScroll, - (o, v) => o.CanHorizontallyScroll = v); + public static readonly StyledProperty CanHorizontallyScrollProperty = + AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty CanVerticallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanVerticallyScroll), - o => o.CanVerticallyScroll, - (o, v) => o.CanVerticallyScroll = v); + public static readonly StyledProperty CanVerticallyScrollProperty = + AvaloniaProperty.Register(nameof(CanVerticallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty IsScrollInertiaEnabledProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsScrollInertiaEnabled), - o => o.IsScrollInertiaEnabled, - (o, v) => o.IsScrollInertiaEnabled = v); + public static readonly StyledProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.Register(nameof(IsScrollInertiaEnabled)); /// /// Defines the property. /// - public static readonly DirectProperty ScrollStartDistanceProperty = - AvaloniaProperty.RegisterDirect( - nameof(ScrollStartDistance), - o => o.ScrollStartDistance, - (o, v) => o.ScrollStartDistance = v); + public static readonly StyledProperty ScrollStartDistanceProperty = + AvaloniaProperty.Register(nameof(ScrollStartDistance), 30); /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// public bool CanHorizontallyScroll { - get => _canHorizontallyScroll; - set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); + get => GetValue(CanHorizontallyScrollProperty); + set => SetValue(CanHorizontallyScrollProperty, value); } /// @@ -79,8 +63,8 @@ namespace Avalonia.Input.GestureRecognizers /// public bool CanVerticallyScroll { - get => _canVerticallyScroll; - set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); + get => GetValue(CanVerticallyScrollProperty); + set => SetValue(CanVerticallyScrollProperty, value); } /// @@ -88,8 +72,8 @@ namespace Avalonia.Input.GestureRecognizers /// public bool IsScrollInertiaEnabled { - get => _isScrollInertiaEnabled; - set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); + get => GetValue(IsScrollInertiaEnabledProperty); + set => SetValue(IsScrollInertiaEnabledProperty, value); } /// @@ -97,8 +81,8 @@ namespace Avalonia.Input.GestureRecognizers /// public int ScrollStartDistance { - get => _scrollStartDistance; - set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); + get => GetValue(ScrollStartDistanceProperty); + set => SetValue(ScrollStartDistanceProperty, value); } @@ -137,8 +121,8 @@ namespace Avalonia.Input.GestureRecognizers // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance _trackedRootPoint = new Point( - _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance), - _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance)); + _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance), + _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance)); _actions!.Capture(e.Pointer, this); } 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 7781255d89..0afdb8e080 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,14 +42,14 @@ namespace Avalonia.Input } if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown - && _lastPointer is (var lastPointer, var lastPosition)) + && _currentPointer is var (lastPointer, lastPosition)) { - _lastPointer = null; + _currentPointer = null; ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition), new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), args.InputModifiers.ToKeyModifiers()); } - else if (pointerDevice.TryGetPointer(args) is IPointer pointer + else if (pointerDevice.TryGetPointer(args) is { } pointer && pointer.Type != PointerType.Touch) { var element = pointer.Captured ?? args.InputHitTestResult; @@ -62,7 +63,7 @@ namespace Avalonia.Input public void SceneInvalidated(Rect dirtyRect) { - if (_lastPointer is (var pointer, var position)) + if (_currentPointer is (var pointer, var position)) { var clientPoint = PointToClient(_inputRoot, position); @@ -80,12 +81,12 @@ namespace Avalonia.Input private void ClearPointerOver() { - if (_lastPointer is (var pointer, var position)) + if (_currentPointer is (var pointer, var position)) { var clientPoint = PointToClient(_inputRoot, position); ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } - _lastPointer = null; + _currentPointer = null; _lastActivePointerDevice = null; } @@ -100,9 +101,11 @@ namespace Avalonia.Input // Do not pass rootVisual, when we have unknown position, // so GetPosition won't return invalid values. +#pragma warning disable CS0618 var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer, position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default, timestamp, properties, inputModifiers); +#pragma warning restore CS0618 if (element is Visual v && !v.IsAttachedToVisualTree) { @@ -122,18 +125,18 @@ namespace Avalonia.Input root.PointerOverElement = null; _lastActivePointerDevice = null; - _lastPointer = null; + _currentPointer = null; } private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot) { if (element is Visual v) { - foreach (IInputElement el in v.VisualChildren) + foreach (var el in v.VisualChildren) { - if (el.IsPointerOver) + if (el is IInputElement { IsPointerOver: true } child) { - ClearChildrenPointerOver(e, el, true); + ClearChildrenPointerOver(e, child, true); break; } } @@ -151,6 +154,8 @@ namespace Avalonia.Input ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) { var pointerOverElement = root.PointerOverElement; + var screenPosition = ((Visual)root).PointToScreen(position); + _lastKnownPosition = screenPosition; if (element != pointerOverElement) { @@ -164,7 +169,7 @@ namespace Avalonia.Input } } - _lastPointer = (pointer, ((Visual)root).PointToScreen(position)); + _currentPointer = (pointer, screenPosition); } private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, @@ -186,8 +191,10 @@ namespace Avalonia.Input el = root.PointerOverElement; +#pragma warning disable CS0618 var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position, timestamp, properties, inputModifiers); +#pragma warning restore CS0618 if (el is Visual v && branch != null && !v.IsAttachedToVisualTree) { ClearChildrenPointerOver(e, branch, false); 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 { 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/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2dabb29e76..5890b90954 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia.Media.Fonts; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -13,9 +15,11 @@ namespace Avalonia.Media /// public sealed class FontManager { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly FontFamily _defaultFontFamily; + internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts"); + + public const string FontCollectionScheme = "fonts"; + + private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _fontFallbacks; public FontManager(IFontManagerImpl platformImpl) @@ -33,9 +37,12 @@ namespace Avalonia.Media throw new InvalidOperationException("Default font family name can't be null or empty."); } - _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + AddFontCollection(new SystemFontCollection(this)); } + /// + /// Get the current font manager instance. + /// public static FontManager Current { get @@ -57,11 +64,6 @@ namespace Avalonia.Media } } - /// - /// - /// - public IFontManagerImpl PlatformImpl { get; } - /// /// Gets the system's default font family's name. /// @@ -71,41 +73,109 @@ namespace Avalonia.Media } /// - /// Get all installed font family names. + /// Get all system fonts. /// - /// If true the font collection is updated. - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); + public IFontCollection SystemFonts => _fontCollections[SystemFontsKey]; + + internal IFontManagerImpl PlatformImpl { get; } /// - /// Returns a new , or an existing one if a matching exists. + /// Tries to get a glyph typeface for specified typeface. /// /// The typeface. + /// The created glyphTypeface /// - /// The . + /// True, if the could create the glyph typeface, False otherwise. /// - public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface) + public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - while (true) + glyphTypeface = null; + + var fontFamily = typeface.FontFamily; + + if (fontFamily.Key is FontFamilyKey key) { - if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface)) + var source = key.Source; + + if (!source.IsAbsoluteUri) { - return glyphTypeface; + if (key.BaseUri == null) + { + throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null."); + } + + source = new Uri(key.BaseUri, source); } - glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface); + if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares())) + { + var embeddedFonts = new EmbeddedFontCollection(source, source); + + embeddedFonts.Initialize(PlatformImpl); - if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface)) + if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) + { + fontCollection = embeddedFonts; + } + } + + if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, + typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return glyphTypeface; + return true; } - if (typeface.FontFamily == _defaultFontFamily) + if (!fontFamily.FamilyNames.HasFallbacks) { - throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + return false; } + } - typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight); + foreach (var familyName in fontFamily.FamilyNames) + { + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + { + return true; + } + } + + return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); + } + + /// + /// Add a font collection to the manager. + /// + /// The font collection. + /// + /// If a font collection's key is already present the collection is replaced. + public void AddFontCollection(IFontCollection fontCollection) + { + var key = fontCollection.Key; + + if (!fontCollection.Key.IsFontCollection()) + { + throw new ArgumentException("Font collection Key should follow the fonts: scheme.", nameof(fontCollection)); + } + + _fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) => + { + oldCollection.Dispose(); + + return fontCollection; + }); + + fontCollection.Initialize(PlatformImpl); + } + + /// + /// Removes the font collection that corresponds to specified key. + /// + /// The font collection's key. + public void RemoveFontCollection(Uri key) + { + if (_fontCollections.TryRemove(key, out var fontCollection)) + { + fontCollection.Dispose(); } } @@ -123,18 +193,16 @@ namespace Avalonia.Media /// True, if the could match the character to specified parameters, False otherwise. /// public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) + FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { - if(_fontFallbacks != null) + if (_fontFallbacks != null) { foreach (var fallback in _fontFallbacks) { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = GetOrAddGlyphTypeface(typeface); - - if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { return true; } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs new file mode 100644 index 0000000000..f2fb490592 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public class EmbeddedFontCollection : IFontCollection + { + private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + private readonly List _fontFamilies = new List(1); + + private readonly Uri _key; + + private readonly Uri _source; + + public EmbeddedFontCollection(Uri key, Uri source) + { + _key = key; + + _source = source; + } + + public Uri Key => _key; + + public FontFamily this[int index] => _fontFamilies[index]; + + public int Count => _fontFamilies.Count; + + public void Initialize(IFontManagerImpl fontManager) + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = FontFamilyLoader.LoadFontAssets(_source); + + foreach (var fontAsset in fontAssets) + { + var stream = assetLoader.Open(fontAsset); + + if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) + { + if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) + { + glyphTypefaces = new ConcurrentDictionary(); + + if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) + { + _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); + } + } + + var key = new FontCollectionKey( + glyphTypeface.Style, + glyphTypeface.Weight, + glyphTypeface.Stretch); + + glyphTypefaces.TryAdd(key, glyphTypeface); + } + } + } + + public void Dispose() + { + foreach (var fontFamily in _fontFamilies) + { + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces)) + { + foreach (var glyphTypeface in glyphTypefaces.Values) + { + glyphTypeface.Dispose(); + } + } + } + + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var key = new FontCollectionKey(style, weight, stretch); + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + + //Try to find a partially matching font + for (var i = 0; i < Count; i++) + { + var fontFamily = _fontFamilies[i]; + + if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture))) + { + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && + TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + } + + glyphTypeface = null; + + return false; + } + + private static bool TryGetNearestMatch( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + + if (key.Style != FontStyle.Normal) + { + key = key with { Style = FontStyle.Normal }; + } + + if (key.Stretch != FontStretch.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + { + return true; + } + } + + key = key with { Stretch = FontStretch.Normal }; + } + + if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + //Take the first glyph typeface we can find. + foreach (var typeface in glyphTypefaces.Values) + { + glyphTypeface = typeface; + + return true; + } + + return false; + } + + private static bool TryFindStretchFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface)) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface)) + { + return true; + } + } + } + + return false; + } + + private static bool TryFindWeightFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? typeface) + { + typeface = null; + var weight = (int)key.Weight; + + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + } + + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) + { + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + } + + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs new file mode 100644 index 0000000000..0d0dc3016e --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs @@ -0,0 +1,4 @@ +namespace Avalonia.Media.Fonts +{ + public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch); +} diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs index 365fb6e412..37992c895e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs @@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts /// /// Loads all font assets that belong to the specified /// - /// + /// /// - public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) => - IsFontTtfOrOtf(fontFamilyKey.Source) ? - GetFontAssetsByExpression(fontFamilyKey) : - GetFontAssetsBySource(fontFamilyKey); + public static IEnumerable LoadFontAssets(Uri source) + { + if (source.IsAvares() || source.IsAbsoluteResm()) + { + return IsFontTtfOrOtf(source) ? + GetFontAssetsByExpression(source) : + GetFontAssetsBySource(source); + } + + return Enumerable.Empty(); + } + /// /// Searches for font assets at a given location and returns a quantity of found assets /// - /// + /// /// - private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsBySource(Uri source) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var availableAssets = assetLoader.GetAssets(source, null); return availableAssets.Where(x => IsFontTtfOrOtf(x)); } @@ -34,60 +42,50 @@ namespace Avalonia.Media.Fonts /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression. /// File names can target multiple files with * wildcard. For example "FontFile*.ttf" /// - /// + /// /// - private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsByExpression(Uri source) { - var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location); - var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension); + var (fileNameWithoutExtension, extension) = GetFileName(source, out var location); + var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension); var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, null); return availableResources.Where(x => IsContainsFile(x, filePattern, extension)); } private static (string fileNameWithoutExtension, string extension) GetFileName( - FontFamilyKey fontFamilyKey, out Uri location) + Uri source, out Uri location) { - if (fontFamilyKey.Source.IsAbsoluteResm()) + if (source.IsAbsoluteResm()) { - var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.'); + var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.'); - var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri() + var uriLocation = source.GetUnescapeAbsoluteUri() .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty); location = new Uri(uriLocation, UriKind.RelativeOrAbsolute); return fileName; } - var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString); + var filename = GetFileNameAndExtension(source.OriginalString); var fullFilename = filename.fileNameWithoutExtension + filename.extension; - if (fontFamilyKey.BaseUri != null) - { - var relativePath = fontFamilyKey.Source.OriginalString - .Replace(fullFilename, string.Empty); - - location = new Uri(fontFamilyKey.BaseUri, relativePath); - } - else - { - var uriString = fontFamilyKey.Source - .GetUnescapeAbsoluteUri() - .Replace(fullFilename, string.Empty); - location = new Uri(uriString); - } + var uriString = source + .GetUnescapeAbsoluteUri() + .Replace(fullFilename, string.Empty); + location = new Uri(uriString); return filename; } private static string CreateFilePattern( - FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension) + Uri source, Uri location, string fileNameWithoutExtension) { var path = location.GetUnescapeAbsolutePath(); var file = GetSubString(fileNameWithoutExtension, '*'); - return fontFamilyKey.Source.IsAbsoluteResm() + return source.IsAbsoluteResm() ? path + "." + file : path + file; } diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs new file mode 100644 index 0000000000..814230bcf3 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public interface IFontCollection : IReadOnlyList, IDisposable + { + /// + /// Get the font collection's key. + /// + Uri Key { get; } + + /// + /// Initializes the font collection. + /// + /// The font manager the collection is registered with. + void Initialize(IFontManagerImpl fontManager); + + /// + /// Try to get a glyph typeface for given parameters. + /// + /// The family name. + /// The font style. + /// The font weight. + /// The font stretch. + /// The glyph typeface. + /// Returns true if a glyph typface can be found; otherwise, false + bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs new file mode 100644 index 0000000000..fd332c6ebe --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + internal class SystemFontCollection : IFontCollection + { + private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + private readonly FontManager _fontManager; + private readonly string[] _familyNames; + + public SystemFontCollection(FontManager fontManager) + { + _fontManager = fontManager; + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); + } + + public Uri Key => FontManager.SystemFontsKey; + + public FontFamily this[int index] + { + get + { + var familyName = _familyNames[index]; + + return new FontFamily(familyName); + } + } + + public int Count => _familyNames.Length; + + public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (familyName == FontFamily.DefaultFontFamilyName) + { + familyName = _fontManager.DefaultFontFamilyName; + } + + var key = new FontCollectionKey(style, weight, stretch); + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + else + { + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && + glyphTypefaces.TryAdd(key, glyphTypeface)) + { + return true; + } + } + } + + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + { + glyphTypefaces = new ConcurrentDictionary(); + + if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + { + return true; + } + } + + return false; + } + + public void Initialize(IFontManagerImpl fontManager) + { + //We initialize the system font collection during construction. + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + foreach (var familyName in _familyNames) + { + yield return new FontFamily(familyName); + } + } + + void IDisposable.Dispose() + { + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + { + foreach (var pair in glyphTypefaces) + { + pair.Value.Dispose(); + } + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2966ceee8d..d795cca894 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -151,9 +151,9 @@ namespace Avalonia.Media } /// - /// Gets or sets the conservative bounding box of the . + /// Gets the conservative bounding box of the . /// - public Size Size => PlatformImpl.Item.Size; + public Rect Bounds => PlatformImpl.Item.Bounds; /// /// @@ -252,7 +252,7 @@ namespace Avalonia.Media if (characterIndex > Metrics.LastCluster) { - return Size.Width; + return Bounds.Width; } var glyphIndex = FindGlyphIndex(characterIndex); @@ -287,7 +287,7 @@ namespace Avalonia.Media if (characterIndex <= Metrics.FirstCluster) { - return Size.Width; + return Bounds.Width; } for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) @@ -295,7 +295,7 @@ namespace Avalonia.Media distance += _glyphInfos[i].GlyphAdvance; } - return Size.Width - distance; + return Bounds.Width - distance; } } @@ -321,7 +321,7 @@ namespace Avalonia.Media } //After - if (distance >= Size.Width) + if (distance >= Bounds.Width) { isInside = false; @@ -354,7 +354,7 @@ namespace Avalonia.Media } else { - currentX = Size.Width; + currentX = Bounds.Width; for (var index = _glyphInfos.Count - 1; index >= 0; index--) { diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 06d92fd81c..961203e30e 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : default; + return GlyphRun != null ? GlyphRun.Bounds : default; } } } diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs index 9e1e52cb73..09740aac81 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs @@ -6,6 +6,26 @@ namespace Avalonia.Media [Unstable] public interface IGlyphTypeface : IDisposable { + /// + /// Gets the family name for the object. + /// + string FamilyName { get; } + + /// + /// Gets the designed weight of the font represented by the object. + /// + FontWeight Weight { get; } + + /// + /// Gets the style for the object. + /// + FontStyle Style { get; } + + /// + /// Gets the value for the object. + /// + FontStretch Stretch { get; } + /// /// Gets the number of glyphs held by this glyph typeface. /// diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 525a543b70..8e57f9a902 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -48,8 +48,6 @@ namespace Avalonia.Media.Imaging public CroppedBitmap() { - Source = null; - SourceRect = default; } public CroppedBitmap(IImage source, PixelRect sourceRect) diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index b74b7df9c5..e89a7d8826 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -223,7 +223,7 @@ namespace Avalonia.Media if (intersections.Count > 0) { var last = baselineOrigin.X; - var finalPos = last + glyphRun.Size.Width; + var finalPos = last + glyphRun.Bounds.Width; var end = last; var points = new List(); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index c5dd30b620..2f28c3f954 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -38,14 +38,14 @@ namespace Avalonia.Media.TextFormatting public override double Baseline => -TextMetrics.Ascent; - public override Size Size => GlyphRun.Size; + public override Size Size => GlyphRun.Bounds.Size; public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphInfos.Count == 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index b4734d702b..253c7075fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting if (matchFound) { // Fallback found - var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) - { - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), - biDiLevel); - } + if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface)) + { + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } + } } // no fallback found diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 6422f23dcd..8b6d576c6e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -19,7 +19,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLength = 0; var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); - if (properties.Width < shapedSymbol.GlyphRun.Size.Width) + if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width) { //Not enough space to fit in the symbol return Array.Empty(); 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/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index a21a5d45e9..41d451b9e3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -60,7 +60,7 @@ namespace Avalonia.Media.TextFormatting var currentWidth = 0.0; var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight); - if (Width < shapedSymbol.GlyphRun.Size.Width) + if (Width < shapedSymbol.GlyphRun.Bounds.Width) { return Array.Empty(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b3321d4d9f..f426a20b2c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -423,7 +423,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - currentDistance -= currentGlyphRun.Size.Width; + currentDistance -= currentGlyphRun.Bounds.Width; } return currentDistance + distance; @@ -477,7 +477,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - distance = currentGlyphRun.Size.Width; + distance = currentGlyphRun.Bounds.Width; } return true; @@ -1483,7 +1483,7 @@ namespace Avalonia.Media.TextFormatting trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; - var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + var whitespaceWidth = glyphRun.Bounds.Width - glyphRunMetrics.Width; width -= whitespaceWidth; } diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1e744c30c8..e2729c9158 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -80,7 +80,18 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this); + public IGlyphTypeface GlyphTypeface + { + get + { + if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface)) + { + return glyphTypeface; + } + + throw new InvalidOperationException("Could not create glyphTypeface."); + } + } public static bool operator !=(Typeface a, Typeface b) { 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/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index cd6e64abaf..116f7cd6e2 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Metadata; @@ -17,7 +18,7 @@ namespace Avalonia.Platform /// Get all installed fonts in the system. /// If true the font collection is updated. /// - IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + string[] GetInstalledFontFamilyNames(bool checkForUpdates = false); /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -37,12 +38,27 @@ namespace Avalonia.Platform FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); /// - /// Creates a glyph typeface. + /// Tries to get a glyph typeface for specified parameters. /// - /// The typeface. - /// 0 - /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. + /// The family name. + /// The font style. + /// The font weiht. + /// The font stretch. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. + /// + bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to create a glyph typeface from specified stream. + /// + /// A stream that holds the font's data. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. /// - IGlyphTypeface CreateGlyphTypeface(Typeface typeface); + bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); } } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 46b065b04e..fccea27c43 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,11 +10,10 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - /// /// Gets the conservative bounding box of the glyph run./>. /// - Size Size { get; } + Rect Bounds { get; } /// /// Gets the baseline origin of the glyph run./>. diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index b01fb46aa3..04e40e8744 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server for (var c = FirstChar; c <= LastChar; c++) { - var height = _runs[c - FirstChar].Size.Height; + var height = _runs[c - FirstChar].Bounds.Height; if (height > maxHeight) { maxHeight = height; @@ -51,8 +51,8 @@ namespace Avalonia.Rendering.Composition.Server { var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var run = _runs[effectiveChar - FirstChar]; - width += run.Size.Width; - height = Math.Max(height, run.Size.Height); + width += run.Bounds.Width; + height = Math.Max(height, run.Bounds.Height); } return new Size(width, height); @@ -69,7 +69,7 @@ namespace Avalonia.Rendering.Composition.Server var run = _runs[effectiveChar - FirstChar]; context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); context.DrawGlyphRun(foreground, run.PlatformImpl); - offset += run.Size.Width; + offset += run.Bounds.Width; } context.Transform = originalTransform; 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/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 4b09bc9280..5b975e29e1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -16,12 +16,11 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IImmutableBrush foreground, IRef glyphRun) - : base(new Rect(glyphRun.Item.BaselineOrigin, glyphRun.Item.Size), transform, foreground) + : base(glyphRun.Item.Bounds, transform, foreground) { GlyphRun = glyphRun.Clone(); } @@ -54,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); + public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); public override void Dispose() { 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.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index cbdf3c3c1e..b51093b40c 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -67,8 +67,7 @@ namespace Avalonia public static readonly DirectProperty TemplatedParentProperty = AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), - o => o.TemplatedParent, - (o ,v) => o.TemplatedParent = v); + o => o.TemplatedParent); /// /// Defines the property. diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 8695918c18..5052840013 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -56,9 +56,14 @@ namespace Avalonia /// /// The type of the additional owner. /// The property. - public StyledProperty AddOwner() where TOwner : AvaloniaObject + public StyledProperty AddOwner(StyledPropertyMetadata? metadata = null) where TOwner : AvaloniaObject { AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + if (metadata != null) + { + OverrideMetadata(metadata); + } + return this; } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index a056f3ba8b..77ae0f2877 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -13,7 +13,7 @@ namespace Avalonia.Styling public DescendantSelector(Selector? parent) { - _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); + _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceded by a selector."); } /// diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 3b0aa03492..53e4baa2c4 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -10,7 +10,7 @@ namespace Avalonia.Styling /// /// The OR style selector. /// - internal class OrSelector : Selector + internal sealed class OrSelector : Selector { private readonly IReadOnlyList _selectors; private string? _selectorString; @@ -42,18 +42,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type? TargetType - { - get - { - if (_targetType == null) - { - _targetType = EvaluateTargetType(); - } - - return _targetType; - } - } + public override Type? TargetType => _targetType ??= EvaluateTargetType(); /// public override string ToString(Style? owner) @@ -71,7 +60,9 @@ namespace Avalonia.Styling var activators = new OrActivatorBuilder(); var neverThisInstance = false; - for (var i = 0; i < _selectors.Count; i++) + var count = _selectors.Count; + + for (var i = 0; i < count; i++) { var match = _selectors[i].Match(control, parent, subscribe); @@ -108,7 +99,9 @@ namespace Avalonia.Styling internal override void ValidateNestingSelector(bool inControlTheme) { - for (var i = 0; i < _selectors.Count; i++) + var count = _selectors.Count; + + for (var i = 0; i < count; i++) { _selectors[i].ValidateNestingSelector(inControlTheme); } @@ -118,7 +111,9 @@ namespace Avalonia.Styling { Type? result = null; - for (var i = 0; i < _selectors.Count; i++) + var count = _selectors.Count; + + for (var i = 0; i < count; i++) { var selector = _selectors[i]; if (selector.TargetType == null) diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index f8670cfdb3..a10b3eb3ea 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -11,7 +11,7 @@ namespace Avalonia.Styling /// A selector that matches the common case of a type and/or name followed by a collection of /// style classes and pseudoclasses. /// - internal class TypeNameAndClassSelector : Selector + internal sealed class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; private List? _classes; @@ -52,7 +52,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector? previous) + TypeNameAndClassSelector(Selector? previous) { _previous = previous; } @@ -85,12 +85,7 @@ namespace Avalonia.Styling /// public override string ToString(Style? owner) { - if (_selectorString == null) - { - _selectorString = BuildSelectorString(owner); - } - - return _selectorString; + return _selectorString ??= BuildSelectorString(owner); } /// diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs index c706f72a63..1f9c694eab 100644 --- a/src/Avalonia.Base/Utilities/UriExtensions.cs +++ b/src/Avalonia.Base/Utilities/UriExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; namespace Avalonia.Utilities; @@ -10,7 +11,9 @@ internal static class UriExtensions public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; - + + public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme; + public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri) { if (uri.IsAbsoluteUri) 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"> + /// Registers an action that is executed with the current font manager. + /// + /// The action. + /// An instance. + public AppBuilder ConfigureFonts(Action action) + { + return AfterSetup(appBuilder => + { + action?.Invoke(FontManager.Current); + }); + } + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs new file mode 100644 index 0000000000..86cd0d5b14 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs @@ -0,0 +1,34 @@ +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Automation.Peers +{ + public class LabelAutomationPeer : ControlAutomationPeer + { + public LabelAutomationPeer(Label owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Text"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Text; + } + + override protected string? GetNameCore() + { + var content = ((Label)Owner).Content as string; + + if (string.IsNullOrEmpty(content)) + { + return base.GetNameCore(); + } + + return AccessText.RemoveAccessKeyMarker(content) ?? string.Empty; + } + } +} diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 1ec6f8dabc..f48d7a7cc1 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,11 +1,9 @@ using System; -using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -48,9 +46,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - AvaloniaProperty.RegisterDirect(nameof(Command), - button => button.Command, (button, command) => button.Command = command, enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command), enableDataValidation: true); /// /// Defines the property. @@ -85,8 +82,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty IsPressedProperty = - AvaloniaProperty.Register(nameof(IsPressed)); + public static readonly DirectProperty IsPressedProperty = + AvaloniaProperty.RegisterDirect(nameof(IsPressed), b => b.IsPressed); /// /// Defines the property @@ -94,10 +91,10 @@ namespace Avalonia.Controls public static readonly StyledProperty FlyoutProperty = AvaloniaProperty.Register(nameof(Flyout)); - private ICommand? _command; private bool _commandCanExecute = true; private KeyGesture? _hotkey; private bool _isFlyoutOpen = false; + private bool _isPressed = false; /// /// Initializes static members of the class. @@ -138,8 +135,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _command; - set => SetAndRaise(CommandProperty, ref _command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -185,8 +182,8 @@ namespace Avalonia.Controls /// public bool IsPressed { - get => GetValue(IsPressedProperty); - private set => SetValue(IsPressedProperty, value); + get => _isPressed; + private set => SetAndRaise(IsPressedProperty, ref _isPressed, value); } /// @@ -248,7 +245,7 @@ namespace Avalonia.Controls { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control { - HotKey = _hotkey; + SetCurrentValue(HotKeyProperty, _hotkey); } base.OnAttachedToLogicalTree(e); @@ -267,7 +264,7 @@ namespace Avalonia.Controls if (HotKey != null) { _hotkey = HotKey; - HotKey = null; + SetCurrentValue(HotKeyProperty, null); } base.OnDetachedFromLogicalTree(e); @@ -291,17 +288,17 @@ namespace Avalonia.Controls break; case Key.Space: - { - if (ClickMode == ClickMode.Press) { - OnClick(); + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - break; - } - case Key.Escape when Flyout != null: // If Flyout doesn't have focusable content, close the flyout here CloseFlyout(); @@ -592,7 +589,7 @@ namespace Avalonia.Controls { flyout.Opened -= Flyout_Opened; flyout.Closed -= Flyout_Closed; - } + } } /// @@ -671,7 +668,7 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); void IClickableControl.RaiseClick() => OnClick(); - + /// /// Event handler for when the button's flyout is opened. /// diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 3300292857..10aadfa759 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -232,14 +232,9 @@ namespace Avalonia.Controls internal const int RowsPerYear = 3; internal const int ColumnsPerYear = 4; - private DateTime? _selectedDate; private DateTime _selectedMonth; private DateTime _selectedYear; - private DateTime _displayDate = DateTime.Today; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isShiftPressed; private bool _displayDateIsChanging; @@ -396,13 +391,13 @@ namespace Avalonia.Controls } case CalendarMode.Year: { - DisplayDate = SelectedMonth; + SetCurrentValue(DisplayDateProperty, SelectedMonth); SelectedYear = SelectedMonth; break; } case CalendarMode.Decade: { - DisplayDate = SelectedYear; + SetCurrentValue(DisplayDateProperty, SelectedYear); SelectedMonth = SelectedYear; break; } @@ -472,7 +467,7 @@ namespace Avalonia.Controls if (IsValidSelectionMode(e.NewValue!)) { _displayDateIsChanging = true; - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); _displayDateIsChanging = false; SelectedDates.Clear(); } @@ -497,11 +492,8 @@ namespace Avalonia.Controls || mode == CalendarSelectionMode.None; } - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register(nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay); /// @@ -529,8 +521,8 @@ namespace Avalonia.Controls /// public DateTime? SelectedDate { - get { return _selectedDate; } - set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); } + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } private void OnSelectedDateChanged(AvaloniaPropertyChangedEventArgs e) { @@ -726,11 +718,8 @@ namespace Avalonia.Controls } } - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v, + public static readonly StyledProperty DisplayDateProperty = + AvaloniaProperty.Register(nameof(DisplayDate), defaultBindingMode: BindingMode.TwoWay); /// @@ -760,8 +749,8 @@ namespace Avalonia.Controls /// public DateTime DisplayDate { - get { return _displayDate; } - set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); } + get => GetValue(DisplayDateProperty); + set => SetValue(DisplayDateProperty, value); } internal DateTime DisplayDateInternal { get; private set; } @@ -796,11 +785,8 @@ namespace Avalonia.Controls DisplayDateChanged?.Invoke(this, e); } - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v, + public static readonly StyledProperty DisplayDateStartProperty = + AvaloniaProperty.Register(nameof(DisplayDateStart), defaultBindingMode: BindingMode.TwoWay); /// /// Gets or sets the first date to be displayed. @@ -814,8 +800,8 @@ namespace Avalonia.Controls /// public DateTime? DisplayDateStart { - get { return _displayDateStart; } - set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); } + get => GetValue(DisplayDateStartProperty); + set => SetValue(DisplayDateStartProperty, value); } private void OnDisplayDateStartChanged(AvaloniaPropertyChangedEventArgs e) { @@ -831,7 +817,7 @@ namespace Avalonia.Controls if (selectedDateMin.HasValue && DateTime.Compare(selectedDateMin.Value, newValue.Value) < 0) { - DisplayDateStart = selectedDateMin.Value; + SetCurrentValue(DisplayDateStartProperty, selectedDateMin.Value); return; } @@ -839,14 +825,14 @@ namespace Avalonia.Controls // DisplayDateEnd = DisplayDateStart if (DateTime.Compare(newValue.Value, DisplayDateRangeEnd) > 0) { - DisplayDateEnd = DisplayDateStart; + SetCurrentValue(DisplayDateEndProperty, DisplayDateStart); } // If DisplayDate < DisplayDateStart, // DisplayDate = DisplayDateStart if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) > 0) { - DisplayDate = newValue.Value; + SetCurrentValue(DisplayDateProperty, newValue.Value); } } UpdateMonths(); @@ -905,11 +891,8 @@ namespace Avalonia.Controls get { return DisplayDateStart.GetValueOrDefault(DateTime.MinValue); } } - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v, + public static readonly StyledProperty DisplayDateEndProperty = + AvaloniaProperty.Register(nameof(DisplayDateEnd), defaultBindingMode: BindingMode.TwoWay); /// @@ -924,8 +907,8 @@ namespace Avalonia.Controls /// public DateTime? DisplayDateEnd { - get { return _displayDateEnd; } - set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); } + get => GetValue(DisplayDateEndProperty); + set => SetValue(DisplayDateEndProperty, value); } private void OnDisplayDateEndChanged(AvaloniaPropertyChangedEventArgs e) @@ -942,7 +925,7 @@ namespace Avalonia.Controls if (selectedDateMax.HasValue && DateTime.Compare(selectedDateMax.Value, newValue.Value) > 0) { - DisplayDateEnd = selectedDateMax.Value; + SetCurrentValue(DisplayDateEndProperty, selectedDateMax.Value); return; } @@ -950,7 +933,7 @@ namespace Avalonia.Controls // DisplayDateEnd = DisplayDateStart if (DateTime.Compare(newValue.Value, DisplayDateRangeStart) < 0) { - DisplayDateEnd = DisplayDateStart; + SetCurrentValue(DisplayDateEndProperty, DisplayDateStart); return; } @@ -958,7 +941,7 @@ namespace Avalonia.Controls // DisplayDate = DisplayDateEnd if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) < 0) { - DisplayDate = newValue.Value; + SetCurrentValue(DisplayDateProperty, newValue.Value); } } UpdateMonths(); @@ -1284,7 +1267,7 @@ namespace Avalonia.Controls { LastSelectedDate = d.Value; } - DisplayDate = d.Value; + SetCurrentValue(DisplayDateProperty, d.Value); } } else @@ -1332,7 +1315,7 @@ namespace Avalonia.Controls { LastSelectedDate = d.Value; } - DisplayDate = d.Value; + SetCurrentValue(DisplayDateProperty, d.Value); } } else @@ -1719,7 +1702,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedMonth = DisplayDateInternal; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); } else { @@ -1733,7 +1716,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedYear = SelectedMonth; - DisplayMode = CalendarMode.Decade; + SetCurrentValue(DisplayModeProperty, CalendarMode.Decade); } else { @@ -1770,8 +1753,8 @@ namespace Avalonia.Controls { if (ctrl) { - DisplayDate = SelectedMonth; - DisplayMode = CalendarMode.Month; + SetCurrentValue(DisplayDateProperty, SelectedMonth); + SetCurrentValue(DisplayModeProperty, CalendarMode.Month); } else { @@ -1785,7 +1768,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedMonth = SelectedYear; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); } else { @@ -1850,14 +1833,14 @@ namespace Avalonia.Controls { case CalendarMode.Year: { - DisplayDate = SelectedMonth; - DisplayMode = CalendarMode.Month; + SetCurrentValue(DisplayDateProperty, SelectedMonth); + SetCurrentValue(DisplayModeProperty, CalendarMode.Month); return true; } case CalendarMode.Decade: { SelectedMonth = SelectedYear; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); return true; } } @@ -2103,7 +2086,8 @@ namespace Avalonia.Controls /// public Calendar() { - UpdateDisplayDate(this, this.DisplayDate, DateTime.MinValue); + SetCurrentValue(DisplayDateProperty, DateTime.Today); + UpdateDisplayDate(this, DisplayDate, DateTime.MinValue); BlackoutDates = new CalendarBlackoutDatesCollection(this); SelectedDates = new SelectedDatesCollection(this); RemovedItems = new Collection(); diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 3d436b4485..2e3f1f96ce 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -41,7 +41,6 @@ namespace Avalonia.Controls.Primitives private Button? _headerButton; private Button? _nextButton; private Button? _previousButton; - private ITemplate? _dayTitleTemplate; private DateTime _currentMonth; private bool _isMouseLeftButtonDown; @@ -61,17 +60,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderBackgroundProperty, value); } } - public static readonly DirectProperty?> DayTitleTemplateProperty = - AvaloniaProperty.RegisterDirect?>( + public static readonly StyledProperty?> DayTitleTemplateProperty = + AvaloniaProperty.Register?>( nameof(DayTitleTemplate), - o => o.DayTitleTemplate, - (o,v) => o.DayTitleTemplate = v, defaultBindingMode: BindingMode.OneTime); public ITemplate? DayTitleTemplate { - get { return _dayTitleTemplate; } - set { SetAndRaise(DayTitleTemplateProperty, ref _dayTitleTemplate, value); } + get => GetValue(DayTitleTemplateProperty); + set => SetValue(DayTitleTemplateProperty, value); } /// @@ -176,9 +173,8 @@ namespace Avalonia.Controls.Primitives for (int i = 0; i < Calendar.RowsPerMonth; i++) { - if (_dayTitleTemplate != null) + if (DayTitleTemplate?.Build() is Control cell) { - var cell = _dayTitleTemplate.Build(); cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs index 6c2356b411..1454b4ab6c 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs @@ -11,29 +11,22 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v); + public static readonly StyledProperty DisplayDateProperty = + AvaloniaProperty.Register(nameof(DisplayDate)); /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v); + public static readonly StyledProperty DisplayDateStartProperty = + AvaloniaProperty.Register( + nameof(DisplayDateStart)); /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v); + public static readonly StyledProperty DisplayDateEndProperty = + AvaloniaProperty.Register( + nameof(DisplayDateEnd)); /// /// Defines the property. @@ -44,11 +37,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); + public static readonly StyledProperty IsDropDownOpenProperty = + AvaloniaProperty.Register( + nameof(IsDropDownOpen)); /// /// Defines the property. @@ -59,11 +50,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register( nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, enableDataValidation: true, defaultBindingMode:BindingMode.TwoWay); @@ -88,11 +77,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), - o => o.Text, - (o, v) => o.Text = v); + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); /// /// Defines the property. @@ -141,8 +127,8 @@ namespace Avalonia.Controls /// public DateTime DisplayDate { - get => _displayDate; - set => SetAndRaise(DisplayDateProperty, ref _displayDate, value); + get => GetValue(DisplayDateProperty); + set => SetValue(DisplayDateProperty, value); } /// @@ -151,8 +137,8 @@ namespace Avalonia.Controls /// The first date to display. public DateTime? DisplayDateStart { - get => _displayDateStart; - set => SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); + get => GetValue(DisplayDateStartProperty); + set => SetValue(DisplayDateStartProperty, value); } /// @@ -161,8 +147,8 @@ namespace Avalonia.Controls /// The last date to display. public DateTime? DisplayDateEnd { - get => _displayDateEnd; - set => SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); + get => GetValue(DisplayDateEndProperty); + set => SetValue(DisplayDateEndProperty, value); } /// @@ -188,8 +174,8 @@ namespace Avalonia.Controls /// public bool IsDropDownOpen { - get => _isDropDownOpen; - set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); } /// @@ -223,8 +209,8 @@ namespace Avalonia.Controls /// public DateTime? SelectedDate { - get => _selectedDate; - set => SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } /// @@ -264,8 +250,8 @@ namespace Avalonia.Controls /// public string? Text { - get => _text; - set => SetAndRaise(TextProperty, ref _text, value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 869bdeabea..c091d07632 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -45,12 +45,6 @@ namespace Avalonia.Controls private DateTime? _onOpenSelectedDate; private bool _settingSelectedDate; - private DateTime _displayDate; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isDropDownOpen; - private DateTime? _selectedDate; - private string? _text; private bool _suspendTextChangeHandler; private bool _isPopupClosing; private bool _ignoreButtonClick; @@ -92,9 +86,9 @@ namespace Avalonia.Controls /// public CalendarDatePicker() { - FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; + SetCurrentValue(FirstDayOfWeekProperty, DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek); _defaultText = string.Empty; - DisplayDate = DateTime.Today; + SetCurrentValue(DisplayDateProperty, DateTime.Today); } /// @@ -257,7 +251,7 @@ namespace Avalonia.Controls Threading.Dispatcher.UIThread.InvokeAsync(() => { _settingSelectedDate = true; - Text = DateTimeToString(day); + SetCurrentValue(TextProperty, DateTimeToString(day)); _settingSelectedDate = false; OnDateSelected(addedDate, removedDate); }); @@ -268,7 +262,7 @@ namespace Avalonia.Controls // be changed by the Calendar if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.CalendarDatePickerDisplayDateFlag)) { - DisplayDate = day; + SetCurrentValue(DisplayDateProperty, day); } if(_calendar != null) @@ -317,7 +311,7 @@ namespace Avalonia.Controls if (!_settingSelectedDate) { _settingSelectedDate = true; - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); _settingSelectedDate = false; } } @@ -400,7 +394,7 @@ namespace Avalonia.Controls DateTime? newDate = DateTimeHelper.AddDays(selectedDate, e.Delta.Y > 0 ? -1 : 1); if (newDate.HasValue && Calendar.IsValidDateSelection(_calendar, newDate.Value)) { - SelectedDate = newDate; + SetCurrentValue(SelectedDateProperty, newDate); e.Handled = true; } } @@ -478,7 +472,7 @@ namespace Avalonia.Controls { if (SelectedDate.HasValue) { - Text = DateTimeToString(SelectedDate.Value); + SetCurrentValue(TextProperty, DateTimeToString(SelectedDate.Value)); } else if (string.IsNullOrEmpty(_textBox.Text)) { @@ -491,7 +485,7 @@ namespace Avalonia.Controls if (date != null) { string? s = DateTimeToString((DateTime)date); - Text = s; + SetCurrentValue(TextProperty, s); } } } @@ -547,7 +541,7 @@ namespace Avalonia.Controls private void Calendar_DayButtonMouseUp(object? sender, PointerReleasedEventArgs e) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } private void Calendar_DisplayDateChanged(object? sender, CalendarDateChangedEventArgs e) @@ -564,13 +558,13 @@ namespace Avalonia.Controls if (e.AddedItems.Count > 0 && SelectedDate.HasValue && DateTime.Compare((DateTime)e.AddedItems[0]!, SelectedDate.Value) != 0) { - SelectedDate = (DateTime?)e.AddedItems[0]; + SetCurrentValue(SelectedDateProperty, (DateTime?)e.AddedItems[0]); } else { if (e.AddedItems.Count == 0) { - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); return; } @@ -578,7 +572,7 @@ namespace Avalonia.Controls { if (e.AddedItems.Count > 0) { - SelectedDate = (DateTime?)e.AddedItems[0]; + SetCurrentValue(SelectedDateProperty, (DateTime?)e.AddedItems[0]); } } } @@ -600,18 +594,18 @@ namespace Avalonia.Controls && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape)) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); if (e.Key == Key.Escape) { - SelectedDate = _onOpenSelectedDate; + SetCurrentValue(SelectedDateProperty, _onOpenSelectedDate); } } } private void TextBox_GotFocus(object? sender, RoutedEventArgs e) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } private void TextBox_KeyDown(object? sender, KeyEventArgs e) @@ -627,7 +621,7 @@ namespace Avalonia.Controls if (_textBox != null) { _suspendTextChangeHandler = true; - Text = _textBox.Text; + SetCurrentValue(TextProperty, _textBox.Text); _suspendTextChangeHandler = false; } } @@ -660,7 +654,7 @@ namespace Avalonia.Controls private void PopUp_Closed(object? sender, EventArgs e) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); if(!_isPopupClosing) { @@ -678,12 +672,12 @@ namespace Avalonia.Controls if (IsDropDownOpen) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } else { SetSelectedDate(); - IsDropDownOpen = true; + SetCurrentValue(IsDropDownOpenProperty, true); _calendar!.Focus(); } } @@ -821,14 +815,14 @@ namespace Avalonia.Controls if (SelectedDate != d) { - SelectedDate = d; + SetCurrentValue(SelectedDateProperty, d); } } else { if (SelectedDate != null) { - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); } } } @@ -838,7 +832,7 @@ namespace Avalonia.Controls if (SelectedDate != d) { - SelectedDate = d; + SetCurrentValue(SelectedDateProperty, d); } } } @@ -884,7 +878,7 @@ namespace Avalonia.Controls if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark) { DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat(); - Text = string.Empty; + SetCurrentValue(TextProperty, string.Empty); _defaultText = string.Empty; var watermarkFormat = "<{0}>"; string watermarkText; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 17a6ad7a09..f6e4b32d6b 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -35,11 +35,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); + public static readonly StyledProperty IsDropDownOpenProperty = + AvaloniaProperty.Register(nameof(IsDropDownOpen)); /// /// Defines the property. @@ -77,7 +74,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - private bool _isDropDownOpen; private Popup? _popup; private object? _selectionBoxItem; private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); @@ -107,8 +103,8 @@ namespace Avalonia.Controls /// public bool IsDropDownOpen { - get => _isDropDownOpen; - set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); } /// @@ -123,10 +119,10 @@ namespace Avalonia.Controls /// /// Gets or sets the item to display as the control's content. /// - protected object? SelectionBoxItem + public object? SelectionBoxItem { get => _selectionBoxItem; - set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); + protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } /// @@ -191,23 +187,23 @@ namespace Avalonia.Controls if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) || ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt))) { - IsDropDownOpen = !IsDropDownOpen; + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; } else if (IsDropDownOpen && e.Key == Key.Escape) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); e.Handled = true; } else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { - IsDropDownOpen = true; + SetCurrentValue(IsDropDownOpenProperty, true); e.Handled = true; } else if (IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { SelectFocusedItem(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); e.Handled = true; } else if (!IsDropDownOpen) @@ -291,7 +287,7 @@ namespace Avalonia.Controls } else { - IsDropDownOpen = !IsDropDownOpen; + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; } } @@ -390,7 +386,7 @@ namespace Avalonia.Controls { if (!isVisible && IsDropDownOpen) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index bb05cd1b1f..118183102a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,7 +1,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Layout; @@ -29,65 +28,56 @@ namespace Avalonia.Controls /// /// Define the Property /// - public static readonly DirectProperty DayFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(DayFormat), - x => x.DayFormat, (x, v) => x.DayFormat = v); + public static readonly StyledProperty DayFormatProperty = + AvaloniaProperty.Register(nameof(DayFormat), "%d"); /// /// Defines the Property /// - public static readonly DirectProperty DayVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(DayVisible), - x => x.DayVisible, (x, v) => x.DayVisible = v); + public static readonly StyledProperty DayVisibleProperty = + AvaloniaProperty.Register(nameof(DayVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty MaxYearProperty = - AvaloniaProperty.RegisterDirect(nameof(MaxYear), - x => x.MaxYear, (x, v) => x.MaxYear = v); + public static readonly StyledProperty MaxYearProperty = + AvaloniaProperty.Register(nameof(MaxYear), DateTimeOffset.MaxValue, coerce: CoerceMaxYear); /// /// Defines the Property /// - public static readonly DirectProperty MinYearProperty = - AvaloniaProperty.RegisterDirect(nameof(MinYear), - x => x.MinYear, (x, v) => x.MinYear = v); + public static readonly StyledProperty MinYearProperty = + AvaloniaProperty.Register(nameof(MinYear), DateTimeOffset.MinValue, coerce: CoerceMinYear); /// /// Defines the Property /// - public static readonly DirectProperty MonthFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(MonthFormat), - x => x.MonthFormat, (x, v) => x.MonthFormat = v); + public static readonly StyledProperty MonthFormatProperty = + AvaloniaProperty.Register(nameof(MonthFormat), "MMMM"); /// /// Defines the Property /// - public static readonly DirectProperty MonthVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(MonthVisible), - x => x.MonthVisible, (x, v) => x.MonthVisible = v); + public static readonly StyledProperty MonthVisibleProperty = + AvaloniaProperty.Register(nameof(MonthVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty YearFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(YearFormat), - x => x.YearFormat, (x, v) => x.YearFormat = v); + public static readonly StyledProperty YearFormatProperty = + AvaloniaProperty.Register(nameof(YearFormat), "yyyy"); /// /// Defines the Property /// - public static readonly DirectProperty YearVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(YearVisible), - x => x.YearVisible, (x, v) => x.YearVisible = v); + public static readonly StyledProperty YearVisibleProperty = + AvaloniaProperty.Register(nameof(YearVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedDate), - x => x.SelectedDate, (x, v) => x.SelectedDate = v, + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register(nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay); // Template Items @@ -103,28 +93,20 @@ namespace Avalonia.Controls private bool _areControlsAvailable; - private string _dayFormat = "%d"; - private bool _dayVisible = true; - private DateTimeOffset _maxYear; - private DateTimeOffset _minYear; - private string _monthFormat = "MMMM"; - private bool _monthVisible = true; - private string _yearFormat = "yyyy"; - private bool _yearVisible = true; - private DateTimeOffset? _selectedDate; - public DatePicker() { PseudoClasses.Set(":hasnodate", true); var now = DateTimeOffset.Now; - _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset); - _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset); + SetCurrentValue(MinYearProperty, new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset)); + SetCurrentValue(MaxYearProperty, new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset)); } + private static void OnGridVisibilityChanged(DatePicker sender, AvaloniaPropertyChangedEventArgs e) => sender.SetGrid(); + public string DayFormat { - get => _dayFormat; - set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + get => GetValue(DayFormatProperty); + set => SetValue(DayFormatProperty, value); } /// @@ -132,12 +114,8 @@ namespace Avalonia.Controls /// public bool DayVisible { - get => _dayVisible; - set - { - SetAndRaise(DayVisibleProperty, ref _dayVisible, value); - SetGrid(); - } + get => GetValue(DayVisibleProperty); + set => SetValue(DayVisibleProperty, value); } /// @@ -145,16 +123,24 @@ namespace Avalonia.Controls /// public DateTimeOffset MaxYear { - get => _maxYear; - set - { - if (value < MinYear) - throw new InvalidOperationException("MaxDate cannot be less than MinDate"); - SetAndRaise(MaxYearProperty, ref _maxYear, value); + get => GetValue(MaxYearProperty); + set => SetValue(MaxYearProperty, value); + } - if (SelectedDate.HasValue && SelectedDate.Value > value) - SelectedDate = value; + private static DateTimeOffset CoerceMaxYear(AvaloniaObject sender, DateTimeOffset value) + { + if (value < sender.GetValue(MinYearProperty)) + { + throw new InvalidOperationException($"{MaxYearProperty.Name} cannot be less than {MinYearProperty.Name}"); } + + return value; + } + + private void OnMaxYearChanged(DateTimeOffset? value) + { + if (SelectedDate.HasValue && SelectedDate.Value > value) + SetCurrentValue(SelectedDateProperty, value); } /// @@ -162,16 +148,24 @@ namespace Avalonia.Controls /// public DateTimeOffset MinYear { - get => _minYear; - set - { - if (value > MaxYear) - throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); - SetAndRaise(MinYearProperty, ref _minYear, value); + get => GetValue(MinYearProperty); + set => SetValue(MinYearProperty, value); + } - if (SelectedDate.HasValue && SelectedDate.Value < value) - SelectedDate = value; + private static DateTimeOffset CoerceMinYear(AvaloniaObject sender, DateTimeOffset value) + { + if (value > sender.GetValue(MaxYearProperty)) + { + throw new InvalidOperationException($"{MinYearProperty.Name} cannot be greater than {MaxYearProperty.Name}"); } + + return value; + } + + private void OnMinYearChanged(DateTimeOffset? value) + { + if (SelectedDate.HasValue && SelectedDate.Value < value) + SetCurrentValue(SelectedDateProperty, value); } /// @@ -179,8 +173,8 @@ namespace Avalonia.Controls /// public string MonthFormat { - get => _monthFormat; - set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + get => GetValue(MonthFormatProperty); + set => SetValue(MonthFormatProperty, value); } /// @@ -188,12 +182,8 @@ namespace Avalonia.Controls /// public bool MonthVisible { - get => _monthVisible; - set - { - SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); - SetGrid(); - } + get => GetValue(MonthVisibleProperty); + set => SetValue(MonthVisibleProperty, value); } /// @@ -201,8 +191,8 @@ namespace Avalonia.Controls /// public string YearFormat { - get => _yearFormat; - set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + get => GetValue(YearFormatProperty); + set => SetValue(YearFormatProperty, value); } /// @@ -210,12 +200,8 @@ namespace Avalonia.Controls /// public bool YearVisible { - get => _yearVisible; - set - { - SetAndRaise(YearVisibleProperty, ref _yearVisible, value); - SetGrid(); - } + get => GetValue(YearVisibleProperty); + set => SetValue(YearVisibleProperty, value); } /// @@ -223,14 +209,8 @@ namespace Avalonia.Controls /// public DateTimeOffset? SelectedDate { - get => _selectedDate; - set - { - var old = _selectedDate; - SetAndRaise(SelectedDateProperty, ref _selectedDate, value); - SetSelectedDateText(); - OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value)); - } + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } /// @@ -287,6 +267,31 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DayVisibleProperty || change.Property == MonthVisibleProperty || change.Property == YearVisibleProperty) + { + SetGrid(); + } + else if (change.Property == MaxYearProperty) + { + OnMaxYearChanged(change.GetNewValue()); + } + else if (change.Property == MinYearProperty) + { + OnMinYearChanged(change.GetNewValue()); + } + else if (change.Property == SelectedDateProperty) + { + SetSelectedDateText(); + + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(oldValue, newValue)); + } + } + private void OnDismissPicker(object? sender, EventArgs e) { _popup!.Close(); @@ -296,7 +301,7 @@ namespace Avalonia.Controls private void OnConfirmed(object? sender, EventArgs e) { _popup!.Close(); - SelectedDate = _presenter!.Date; + SetCurrentValue(SelectedDateProperty, _presenter!.Date); } private void SetGrid() diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index 2057480490..0ae743f30a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -35,65 +35,72 @@ namespace Avalonia.Controls /// /// Defines the Property /// - public static readonly DirectProperty DateProperty = - AvaloniaProperty.RegisterDirect(nameof(Date), - x => x.Date, (x, v) => x.Date = v); + public static readonly StyledProperty DateProperty = + AvaloniaProperty.Register(nameof(Date), coerce: CoerceDate); + + private static DateTimeOffset CoerceDate(AvaloniaObject sender, DateTimeOffset value) + { + var max = sender.GetValue(MaxYearProperty); + if (value > max) + { + return max; + } + var min = sender.GetValue(MinYearProperty); + if (value < min) + { + return min; + } + + return value; + } /// /// Defines the Property /// - public static readonly DirectProperty DayFormatProperty = - DatePicker.DayFormatProperty.AddOwner(x => - x.DayFormat, (x, v) => x.DayFormat = v); + public static readonly StyledProperty DayFormatProperty = + DatePicker.DayFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty DayVisibleProperty = - DatePicker.DayVisibleProperty.AddOwner(x => - x.DayVisible, (x, v) => x.DayVisible = v); + public static readonly StyledProperty DayVisibleProperty = + DatePicker.DayVisibleProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MaxYearProperty = - DatePicker.MaxYearProperty.AddOwner(x => - x.MaxYear, (x, v) => x.MaxYear = v); + public static readonly StyledProperty MaxYearProperty = + DatePicker.MaxYearProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MinYearProperty = - DatePicker.MinYearProperty.AddOwner(x => - x.MinYear, (x, v) => x.MinYear = v); + public static readonly StyledProperty MinYearProperty = + DatePicker.MinYearProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MonthFormatProperty = - DatePicker.MonthFormatProperty.AddOwner(x => - x.MonthFormat, (x, v) => x.MonthFormat = v); + public static readonly StyledProperty MonthFormatProperty = + DatePicker.MonthFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MonthVisibleProperty = - DatePicker.MonthVisibleProperty.AddOwner(x => - x.MonthVisible, (x, v) => x.MonthVisible = v); + public static readonly StyledProperty MonthVisibleProperty = + DatePicker.MonthVisibleProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty YearFormatProperty = - DatePicker.YearFormatProperty.AddOwner(x => - x.YearFormat, (x, v) => x.YearFormat = v); + public static readonly StyledProperty YearFormatProperty = + DatePicker.YearFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty YearVisibleProperty = - DatePicker.YearVisibleProperty.AddOwner(x => - x.YearVisible, (x, v) => x.YearVisible = v); + public static readonly StyledProperty YearVisibleProperty = + DatePicker.YearVisibleProperty.AddOwner(); // Template Items private Grid? _pickerContainer; @@ -114,15 +121,6 @@ namespace Avalonia.Controls private Button? _dayDownButton; private Button? _yearDownButton; - private DateTimeOffset _date; - private string _dayFormat = "%d"; - private bool _dayVisible = true; - private DateTimeOffset _maxYear; - private DateTimeOffset _minYear; - private string _monthFormat = "MMMM"; - private bool _monthVisible = true; - private string _yearFormat = "yyyy"; - private bool _yearVisible = true; private DateTimeOffset _syncDate; private readonly GregorianCalendar _calendar; @@ -131,11 +129,20 @@ namespace Avalonia.Controls public DatePickerPresenter() { var now = DateTimeOffset.Now; - _minYear = new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset); - _maxYear = new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset); - _date = now; + SetCurrentValue(MinYearProperty, new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset)); + SetCurrentValue(MaxYearProperty, new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset)); + SetCurrentValue(DateProperty, now); _calendar = new GregorianCalendar(); - KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + } + + static DatePickerPresenter() + { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); + } + + private static void OnDateRangeChanged(DatePickerPresenter sender, AvaloniaPropertyChangedEventArgs e) + { + sender.CoerceValue(DateProperty); } /// @@ -143,13 +150,14 @@ namespace Avalonia.Controls /// public DateTimeOffset Date { - get => _date; - set - { - SetAndRaise(DateProperty, ref _date, value); - _syncDate = Date; - InitPicker(); - } + get => GetValue(DateProperty); + set => SetValue(DateProperty, value); + } + + private void OnDateChanged(DateTimeOffset newValue) + { + _syncDate = newValue; + InitPicker(); } /// @@ -157,8 +165,8 @@ namespace Avalonia.Controls /// public string DayFormat { - get => _dayFormat; - set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + get => GetValue(DayFormatProperty); + set => SetValue(DayFormatProperty, value); } /// @@ -166,11 +174,8 @@ namespace Avalonia.Controls /// public bool DayVisible { - get => _dayVisible; - set - { - SetAndRaise(DayVisibleProperty, ref _dayVisible, value); - } + get => GetValue(DayVisibleProperty); + set => SetValue(DayVisibleProperty, value); } /// @@ -178,16 +183,8 @@ namespace Avalonia.Controls /// public DateTimeOffset MaxYear { - get => _maxYear; - set - { - if (value < MinYear) - throw new InvalidOperationException("MaxDate cannot be less than MinDate"); - SetAndRaise(MaxYearProperty, ref _maxYear, value); - - if (Date > value) - Date = value; - } + get => GetValue(MaxYearProperty); + set => SetValue(MaxYearProperty, value); } /// @@ -195,16 +192,8 @@ namespace Avalonia.Controls /// public DateTimeOffset MinYear { - get => _minYear; - set - { - if (value > MaxYear) - throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); - SetAndRaise(MinYearProperty, ref _minYear, value); - - if (Date < value) - Date = value; - } + get => GetValue(MinYearProperty); + set => SetValue(MinYearProperty, value); } /// @@ -212,8 +201,8 @@ namespace Avalonia.Controls /// public string MonthFormat { - get => _monthFormat; - set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + get => GetValue(MonthFormatProperty); + set => SetValue(MonthFormatProperty, value); } /// @@ -221,11 +210,8 @@ namespace Avalonia.Controls /// public bool MonthVisible { - get => _monthVisible; - set - { - SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); - } + get => GetValue(MonthVisibleProperty); + set => SetValue(MonthVisibleProperty, value); } /// @@ -233,8 +219,8 @@ namespace Avalonia.Controls /// public string YearFormat { - get => _yearFormat; - set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + get => GetValue(YearFormatProperty); + set => SetValue(YearFormatProperty, value); } /// @@ -242,11 +228,8 @@ namespace Avalonia.Controls /// public bool YearVisible { - get => _yearVisible; - set - { - SetAndRaise(YearVisibleProperty, ref _yearVisible, value); - } + get => GetValue(YearVisibleProperty); + set => SetValue(YearVisibleProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -317,6 +300,20 @@ namespace Avalonia.Controls InitPicker(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DateProperty) + { + OnDateChanged(change.GetNewValue()); + } + else if (change.Property == MaxYearProperty || change.Property == MinYearProperty) + { + OnDateRangeChanged(this, change); + } + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -334,7 +331,7 @@ namespace Avalonia.Controls } break; case Key.Enter: - Date = _syncDate; + SetCurrentValue(DateProperty, _syncDate); OnConfirmed(); e.Handled = true; break; @@ -381,13 +378,13 @@ namespace Avalonia.Controls _monthSelector.SelectedValue = dt.Month; _monthSelector.FormatDate = dt.Date; } - + if (YearVisible) { _yearSelector.SelectedValue = dt.Year; _yearSelector.FormatDate = dt.Date; } - + _suppressUpdateSelection = false; SetInitialFocus(); @@ -471,7 +468,7 @@ namespace Avalonia.Controls private void OnAcceptButtonClicked(object? sender, RoutedEventArgs e) { - Date = _syncDate; + SetCurrentValue(DateProperty, _syncDate); OnConfirmed(); } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index a7a6881fe5..2f49a44b8c 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,7 +1,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using System; @@ -30,23 +29,20 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty MinuteIncrementProperty = - AvaloniaProperty.RegisterDirect(nameof(MinuteIncrement), - x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v); + public static readonly StyledProperty MinuteIncrementProperty = + AvaloniaProperty.Register(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement); /// /// Defines the property /// - public static readonly DirectProperty ClockIdentifierProperty = - AvaloniaProperty.RegisterDirect(nameof(ClockIdentifier), - x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v); + public static readonly StyledProperty ClockIdentifierProperty = + AvaloniaProperty.Register(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier); /// /// Defines the property /// - public static readonly DirectProperty SelectedTimeProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedTime), - x => x.SelectedTime, (x, v) => x.SelectedTime = v, + public static readonly StyledProperty SelectedTimeProperty = + AvaloniaProperty.Register(nameof(SelectedTime), defaultBindingMode: BindingMode.TwoWay); // Template Items @@ -63,17 +59,13 @@ namespace Avalonia.Controls private Grid? _contentGrid; private Popup? _popup; - private TimeSpan? _selectedTime; - private int _minuteIncrement = 1; - private string _clockIdentifier = "12HourClock"; - public TimePicker() { PseudoClasses.Set(":hasnotime", true); var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; if (timePattern.IndexOf("H") != -1) - _clockIdentifier = "24HourClock"; + SetCurrentValue(ClockIdentifierProperty, "24HourClock"); } /// @@ -81,14 +73,16 @@ namespace Avalonia.Controls /// public int MinuteIncrement { - get => _minuteIncrement; - set - { - if (value < 1 || value > 59) - throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59"); - SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value); - SetSelectedTimeText(); - } + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); + } + + private static int CoerceMinuteIncrement(AvaloniaObject sender, int value) + { + if (value < 1 || value > 59) + throw new ArgumentOutOfRangeException(null, "1 >= MinuteIncrement <= 59"); + + return value; } /// @@ -96,15 +90,17 @@ namespace Avalonia.Controls /// public string ClockIdentifier { - get => _clockIdentifier; - set - { - if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) - throw new ArgumentException("Invalid ClockIdentifier"); - SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); - SetGrid(); - SetSelectedTimeText(); - } + + get => GetValue(ClockIdentifierProperty); + set => SetValue(ClockIdentifierProperty, value); + } + + private static string CoerceClockIdentifier(AvaloniaObject sender, string value) + { + if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) + throw new ArgumentException("Invalid ClockIdentifier", default(string)); + + return value; } /// @@ -112,14 +108,8 @@ namespace Avalonia.Controls /// public TimeSpan? SelectedTime { - get => _selectedTime; - set - { - var old = _selectedTime; - SetAndRaise(SelectedTimeProperty, ref _selectedTime, value); - OnSelectedTimeChanged(old, value); - SetSelectedTimeText(); - } + get => GetValue(SelectedTimeProperty); + set => SetValue(SelectedTimeProperty, value); } /// @@ -173,6 +163,27 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MinuteIncrementProperty) + { + SetSelectedTimeText(); + } + else if (change.Property == ClockIdentifierProperty) + { + SetGrid(); + SetSelectedTimeText(); + } + else if (change.Property == SelectedTimeProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnSelectedTimeChanged(oldValue, newValue); + SetSelectedTimeText(); + } + } + private void SetGrid() { if (_contentGrid == null) @@ -270,7 +281,7 @@ namespace Avalonia.Controls private void OnConfirmed(object? sender, EventArgs e) { _popup!.Close(); - SelectedTime = _presenter!.Time; + SetCurrentValue(SelectedTimeProperty, _presenter!.Time); } } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index d6599c9f18..ba06e1b5e6 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -30,28 +30,29 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty MinuteIncrementProperty = - TimePicker.MinuteIncrementProperty.AddOwner(x => x.MinuteIncrement, - (x, v) => x.MinuteIncrement = v); + public static readonly StyledProperty MinuteIncrementProperty = + TimePicker.MinuteIncrementProperty.AddOwner(); /// /// Defines the property /// - public static readonly DirectProperty ClockIdentifierProperty = - TimePicker.ClockIdentifierProperty.AddOwner(x => x.ClockIdentifier, - (x, v) => x.ClockIdentifier = v); + public static readonly StyledProperty ClockIdentifierProperty = + TimePicker.ClockIdentifierProperty.AddOwner(); /// /// Defines the property /// - public static readonly DirectProperty TimeProperty = - AvaloniaProperty.RegisterDirect(nameof(Time), - x => x.Time, (x, v) => x.Time = v); + public static readonly StyledProperty TimeProperty = + AvaloniaProperty.Register(nameof(Time)); public TimePickerPresenter() { - Time = DateTime.Now.TimeOfDay; - KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay); + } + + static TimePickerPresenter() + { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); } // TemplateItems @@ -70,24 +71,13 @@ namespace Avalonia.Controls private Button? _minuteDownButton; private Button? _periodDownButton; - // Backing Fields - private TimeSpan _time; - private int _minuteIncrement = 1; - private string _clockIdentifier = "12HourClock"; - /// /// Gets or sets the minute increment in the selector /// public int MinuteIncrement { - get => _minuteIncrement; - set - { - if (value < 1 || value > 59) - throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59"); - SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value); - InitPicker(); - } + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); } /// @@ -95,14 +85,8 @@ namespace Avalonia.Controls /// public string ClockIdentifier { - get => _clockIdentifier; - set - { - if (string.IsNullOrEmpty(value) || !(value == "12HourClock" || value == "24HourClock")) - throw new ArgumentException("Invalid ClockIdentifier"); - SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); - InitPicker(); - } + get => GetValue(ClockIdentifierProperty); + set => SetValue(ClockIdentifierProperty, value); } /// @@ -110,12 +94,8 @@ namespace Avalonia.Controls /// public TimeSpan Time { - get => _time; - set - { - SetAndRaise(TimeProperty, ref _time, value); - InitPicker(); - } + get => GetValue(TimeProperty); + set => SetValue(TimeProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -162,6 +142,16 @@ namespace Avalonia.Controls InitPicker(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MinuteIncrementProperty || change.Property == ClockIdentifierProperty || change.Property == TimeProperty) + { + InitPicker(); + } + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -197,7 +187,7 @@ namespace Avalonia.Controls hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr; } - Time = new TimeSpan(hr, min, 0); + SetCurrentValue(TimeProperty, new TimeSpan(hr, min, 0)); base.OnConfirmed(); } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 12a096b105..fe9f5e64a8 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Documents this.ForEachItem( x => - { + { x.InlineHost = InlineHost; LogicalChildren?.Add(x); Invalidate(); @@ -92,10 +92,10 @@ namespace Avalonia.Controls.Documents public override void Add(Inline inline) { if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text)) - { + { base.Add(new Run(textBlock._text)); - textBlock._text = null; + textBlock._text = null; } base.Add(inline); @@ -159,7 +159,7 @@ namespace Avalonia.Controls.Documents oldParent.Remove(child); } - if(newParent != null) + if (newParent != null) { newParent.Add(child); } diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index df3fe28a29..c0d3600c1e 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -1,9 +1,11 @@ -using Avalonia.Controls.Primitives; +using System.ComponentModel; +using Avalonia.Controls.Primitives; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { - public class Flyout : FlyoutBase + public class Flyout : PopupFlyoutBase { /// /// Defines the property @@ -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/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 9d4abec549..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 FlyoutPlacementMode 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); + protected 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); - } - - 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,462 +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 = PlacementMode.AnchorAndGravity; - Popup.PlacementConstraintAdjustment = - PopupPositioning.PopupPositionerConstraintAdjustment.SlideX | - PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; - } - - var trgtBnds = Target?.Bounds ?? default; - - switch (Placement) - { - case FlyoutPlacementMode.Top: //Above & centered - Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.Top; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top; - break; - - case FlyoutPlacementMode.TopEdgeAlignedLeft: - Popup.PlacementRect = new Rect(0, 0, 0, 0); - Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight; - break; - - case FlyoutPlacementMode.TopEdgeAlignedRight: - Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft; - break; - - case FlyoutPlacementMode.RightEdgeAlignedTop: - Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right; - break; - - case FlyoutPlacementMode.Right: //Right & centered - Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height); - Popup.PlacementGravity = PopupPositioning.PopupGravity.Right; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right; - break; - - case FlyoutPlacementMode.RightEdgeAlignedBottom: - Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right; - break; - - case FlyoutPlacementMode.Bottom: //Below & centered - Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom; - break; - - case FlyoutPlacementMode.BottomEdgeAlignedLeft: - Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom; - break; - - case FlyoutPlacementMode.BottomEdgeAlignedRight: - Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom; - break; - - case FlyoutPlacementMode.LeftEdgeAlignedTop: - Popup.PlacementRect = new Rect(0, 0, 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left; - break; - - case FlyoutPlacementMode.Left: //Left & centered - Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height); - Popup.PlacementGravity = PopupPositioning.PopupGravity.Left; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left; - break; - - case FlyoutPlacementMode.LeftEdgeAlignedBottom: - Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft; - Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft; - break; - - //includes Auto (not sure what determines that)... - default: - //This is just FlyoutPlacementMode.Top behavior (above & centered) - Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1); - Popup.PlacementGravity = PopupPositioning.PopupGravity.Top; - break; - } - } - - private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args) - { - if (args.Sender is Control c) - { - if (args.OldValue is FlyoutBase) - { - c.ContextRequested -= OnControlContextRequested; - } - if (args.NewValue is FlyoutBase) - { - c.ContextRequested += OnControlContextRequested; - } - } - } - - private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e) - { - if (!e.Handled - && sender is Control control - && control.ContextFlyout is FlyoutBase flyout) - { - if (control.ContextMenu != null) - { - Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu"); - return; - } - - // We do not support absolute popup positioning yet, so we ignore "point" at this moment. - var triggeredByPointerInput = e.TryGetPosition(null, out _); - e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput); - } - } - - private bool CancelClosing() - { - var eventArgs = new CancelEventArgs(); - OnClosing(eventArgs); - return eventArgs.Cancel; - } - - private bool CancelOpening() - { - var eventArgs = new CancelEventArgs(); - OnOpening(eventArgs); - return eventArgs.Cancel; - } - - internal static void SetPresenterClasses(Control? presenter, Classes classes) - { - if(presenter is null) - { - return; - } - //Remove any classes no longer in use, ignoring pseudo classes - for (int i = presenter.Classes.Count - 1; i >= 0; i--) - { - if (!classes.Contains(presenter.Classes[i]) && - !presenter.Classes[i].Contains(':')) - { - presenter.Classes.RemoveAt(i); - } - } - - //Add new classes - presenter.Classes.AddRange(classes); - } } } 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/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index b028a8f007..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; @@ -7,7 +8,7 @@ using Avalonia.Styling; namespace Avalonia.Controls { - public class MenuFlyout : FlyoutBase + public class MenuFlyout : PopupFlyoutBase { public MenuFlyout() { @@ -34,6 +35,12 @@ namespace Avalonia.Controls public static readonly StyledProperty 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 new file mode 100644 index 0000000000..a3d05a34b7 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -0,0 +1,512 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Diagnostics; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Input.Raw; +using Avalonia.Layout; +using Avalonia.Logging; +using Avalonia.Reactive; + +namespace Avalonia.Controls.Primitives +{ + public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider + { + /// + public static readonly StyledProperty PlacementProperty = + 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 + /// + public static readonly StyledProperty ShowModeProperty = + AvaloniaProperty.Register(nameof(ShowMode)); + + /// + /// Defines the property + /// + public static readonly StyledProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner(); + + private readonly Lazy _popupLazy; + private Rect? _enlargedPopupRect; + private PixelRect? _enlargePopupRectScreenPixelRect; + private IDisposable? _transientDisposable; + private Action? _popupHostChangedHandler; + + 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); + } + + /// + public PopupGravity PlacementGravity + { + get => GetValue(PlacementGravityProperty); + set => SetValue(PlacementGravityProperty, value); + } + + /// + public PopupAnchor PlacementAnchor + { + get => GetValue(PlacementAnchorProperty); + set => SetValue(PlacementAnchorProperty, value); + } + + /// + public double HorizontalOffset + { + get => GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + public double VerticalOffset + { + get => GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + + /// + /// Gets or sets the desired ShowMode + /// + public FlyoutShowMode ShowMode + { + get => GetValue(ShowModeProperty); + set => SetValue(ShowModeProperty, value); + } + + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. + /// + public IInputElement? OverlayInputPassThroughElement + { + get => GetValue(OverlayInputPassThroughElementProperty); + set => SetValue(OverlayInputPassThroughElementProperty, 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.TemplatedParent = placementTarget.TemplatedParent; + } + + if (Popup.Child == null) + { + Popup.Child = CreatePresenter(); + } + + Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; + + if (CancelOpening()) + { + return false; + } + + PositionPopup(showAtPointer); + IsOpen = Popup.IsOpen = true; + OnOpened(); + + placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree; + placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp; + + if (ShowMode == FlyoutShowMode.Standard) + { + // Try and focus content inside Flyout + if (Popup.Child.Focusable) + { + FocusManager.Instance?.Focus(Popup.Child); + } + else + { + var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next); + if (nextFocus != null) + { + FocusManager.Instance?.Focus(nextFocus); + } + } + } + else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) + { + _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); + } + + return true; + } + + private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + _ = HideCore(false); + } + + private void HandleTransientDismiss(RawInputEventArgs args) + { + if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move) + { + // In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept + // shown as long as the pointer is within a certain px distance from the + // flyout itself. I'm not sure what WinUI uses, but I'm defaulting to + // 100px, which seems about right + // enlargedPopupRect is the Flyout bounds enlarged 100px + // For windowed popups, enlargedPopupRect is in screen coordinates, + // for overlay popups, its in OverlayLayer coordinates + + if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null) + { + // Only do this once when the Flyout opens & cache the result + if (Popup?.Host is PopupRoot root) + { + // Get the popup root bounds and convert to screen coordinates + + var tmp = root.Bounds.Inflate(100); + _enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight)); + } + else if (Popup?.Host is OverlayPopupHost host) + { + // Overlay popups are in OverlayLayer coordinates, just use that + _enlargedPopupRect = host.Bounds.Inflate(100); + } + + return; + } + + if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot) + { + // As long as the pointer stays within the enlargedPopupRect + // the flyout stays open. If it leaves, close it + // Despite working in screen coordinates, leaving the TopLevel + // window will not close this (as pointer events stop), which + // does match UWP + var pt = eventRoot.PointToScreen(pArgs.Position); + if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false) + { + HideCore(false); + } + } + else if (Popup?.Host is OverlayPopupHost) + { + // Same as above here, but just different coordinate space + // so we don't need to translate + if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false) + { + HideCore(false); + } + } + } + } + + protected virtual void OnOpening(CancelEventArgs args) + { + Opening?.Invoke(this, args); + } + + protected virtual void OnClosing(CancelEventArgs args) + { + Closing?.Invoke(this, args); + } + + /// + /// 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; + } + + Popup.VerticalOffset = VerticalOffset; + Popup.HorizontalOffset = HorizontalOffset; + Popup.PlacementAnchor = PlacementAnchor; + Popup.PlacementGravity = PlacementGravity; + if (showAtPointer) + { + Popup.PlacementMode = PlacementMode.Pointer; + } + else + { + Popup.PlacementMode = Placement; + Popup.PlacementConstraintAdjustment = + PopupPositioning.PopupPositionerConstraintAdjustment.SlideX | + PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; + } + } + + private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Sender is Control c) + { + if (args.OldValue is FlyoutBase) + { + c.ContextRequested -= OnControlContextRequested; + } + if (args.NewValue is FlyoutBase) + { + c.ContextRequested += OnControlContextRequested; + } + } + } + + private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (!e.Handled + && sender is Control control + && control.ContextFlyout is { } flyout) + { + if (control.ContextMenu != null) + { + Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu"); + return; + } + + if (flyout is PopupFlyoutBase popupFlyout) + { + // We do not support absolute popup positioning yet, so we ignore "point" at this moment. + var triggeredByPointerInput = e.TryGetPosition(null, out _); + e.Handled = popupFlyout.ShowAtCore(control, triggeredByPointerInput); + } + else + { + flyout.ShowAt(control); + e.Handled = true; + } + } + } + + private bool CancelClosing() + { + var eventArgs = new CancelEventArgs(); + OnClosing(eventArgs); + return eventArgs.Cancel; + } + + private bool CancelOpening() + { + var eventArgs = new CancelEventArgs(); + OnOpening(eventArgs); + return eventArgs.Cancel; + } + + internal static void SetPresenterClasses(Control? presenter, Classes classes) + { + if(presenter is null) + { + return; + } + //Remove any classes no longer in use, ignoring pseudo classes + for (int i = presenter.Classes.Count - 1; i >= 0; i--) + { + if (!classes.Contains(presenter.Classes[i]) && + !presenter.Classes[i].Contains(':')) + { + presenter.Classes.RemoveAt(i); + } + } + + //Add new classes + presenter.Classes.AddRange(classes); + } + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9483f98881..bebf4a38f6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -383,14 +383,7 @@ namespace Avalonia.Controls { hic.Header = item; hic.HeaderTemplate = itemTemplate; - - itemTemplate ??= hic.FindDataTemplate(item) ?? this.FindDataTemplate(item); - - if (itemTemplate is ITreeDataTemplate treeTemplate) - { - if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding) - BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null); - } + hic.PrepareItemContainer(); } } diff --git a/src/Avalonia.Controls/Label.cs b/src/Avalonia.Controls/Label.cs index 5c8a6e0a5b..94ea66c4c1 100644 --- a/src/Avalonia.Controls/Label.cs +++ b/src/Avalonia.Controls/Label.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Data; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,13 +13,8 @@ namespace Avalonia.Controls /// /// Defines the Direct property /// - public static readonly DirectProperty TargetProperty = - AvaloniaProperty.RegisterDirect(nameof(Target), lbl => lbl.Target, (lbl, inp) => lbl.Target = inp); - - /// - /// Label focus target storage field - /// - private IInputElement? _target; + public static readonly StyledProperty TargetProperty = + AvaloniaProperty.Register(nameof(Target)); /// /// Label focus Target @@ -32,8 +22,8 @@ namespace Avalonia.Controls [ResolveByName] public IInputElement? Target { - get => _target; - set => SetAndRaise(TargetProperty, ref _target, value); + get => GetValue(TargetProperty); + set => SetValue(TargetProperty, value); } static Label() @@ -71,5 +61,10 @@ namespace Avalonia.Controls } base.OnPointerPressed(e); } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new LabelAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 1670e496b4..03e3444d71 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -27,11 +27,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -113,7 +110,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - private ICommand? _command; private bool _commandCanExecute = true; private bool _commandBindingError; private Popup? _popup; @@ -217,8 +213,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get { return _command; } - set { SetAndRaise(CommandProperty, ref _command, value); } + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -337,7 +333,7 @@ namespace Avalonia.Controls /// /// This has the same effect as setting to true. /// - public void Open() => IsSubMenuOpen = true; + public void Open() => SetCurrentValue(IsSubMenuOpenProperty, true); /// /// Closes the submenu. @@ -345,7 +341,7 @@ namespace Avalonia.Controls /// /// This has the same effect as setting to false. /// - public void Close() => IsSubMenuOpen = false; + public void Close() => SetCurrentValue(IsSubMenuOpenProperty, false); /// void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); @@ -369,7 +365,7 @@ namespace Avalonia.Controls { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control { - HotKey = _hotkey; + SetCurrentValue(HotKeyProperty, _hotkey); } base.OnAttachedToLogicalTree(e); @@ -397,7 +393,7 @@ namespace Avalonia.Controls if (HotKey != null) { _hotkey = HotKey; - HotKey = null; + SetCurrentValue(HotKeyProperty, null); } base.OnDetachedFromLogicalTree(e); @@ -663,7 +659,7 @@ namespace Avalonia.Controls } RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent)); - IsSelected = true; + SetCurrentValue(IsSelectedProperty, true); PseudoClasses.Add(":open"); } else diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 5ff4148e5a..7b03c607b6 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -79,12 +79,12 @@ namespace Avalonia.Controls } public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); public NativeMenuItem? Parent { get => _parent; - set => SetAndRaise(ParentProperty, ref _parent, value); + internal set => SetAndRaise(ParentProperty, ref _parent, value); } public void Add(NativeMenuItemBase item) => _items.Add(item); diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 32fa574ee6..9b5f756887 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -4,36 +4,13 @@ using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Utilities; -using Avalonia.Reactive; namespace Avalonia.Controls { public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge { - private string? _header; - private KeyGesture? _gesture; - private bool _isEnabled = true; - private ICommand? _command; - private bool _isChecked = false; - private NativeMenuItemToggleType _toggleType; - private IBitmap? _icon; private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; - private NativeMenu? _menu; - - static NativeMenuItem() - { - MenuProperty.Changed.Subscribe(args => - { - var item = (NativeMenuItem)args.Sender; - var value = args.NewValue.GetValueOrDefault()!; - if (value.Parent != null && value.Parent != item) - throw new InvalidOperationException("NativeMenu already has a parent"); - value.Parent = item; - }); - } - - class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -60,78 +37,70 @@ namespace Avalonia.Controls Header = header; } - public static readonly DirectProperty MenuProperty = - AvaloniaProperty.RegisterDirect(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v); + public static readonly StyledProperty MenuProperty = + AvaloniaProperty.Register(nameof(Menu), coerce: CoerceMenu); [Content] public NativeMenu? Menu { - get => _menu; - set - { - if (value != null && value.Parent != null && value.Parent != this) - throw new InvalidOperationException("NativeMenu already has a parent"); - SetAndRaise(MenuProperty, ref _menu, value); - } + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); } - public static readonly DirectProperty IconProperty = - AvaloniaProperty.RegisterDirect(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v); + private static NativeMenu? CoerceMenu(AvaloniaObject sender, NativeMenu? value) + { + if (value != null && value.Parent != null && value.Parent != sender) + throw new InvalidOperationException("NativeMenu already has a parent"); + return value; + } + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); public IBitmap? Icon { - get => _icon; - set => SetAndRaise(IconProperty, ref _icon, value); + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); } - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); public string? Header { - get => _header; - set => SetAndRaise(HeaderProperty, ref _header, value); + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } - public static readonly DirectProperty GestureProperty = - AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v); + public static readonly StyledProperty GestureProperty = + AvaloniaProperty.Register(nameof(Gesture)); public KeyGesture? Gesture { - get => _gesture; - set => SetAndRaise(GestureProperty, ref _gesture, value); + get => GetValue(GestureProperty); + set => SetValue(GestureProperty, value); } - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsChecked), - o => o.IsChecked, - (o, v) => o.IsChecked = v); + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register(nameof(IsChecked)); public bool IsChecked { - get => _isChecked; - set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } - public static readonly DirectProperty ToggleTypeProperty = - AvaloniaProperty.RegisterDirect( - nameof(ToggleType), - o => o.ToggleType, - (o, v) => o.ToggleType = v); + public static readonly StyledProperty ToggleTypeProperty = + AvaloniaProperty.Register(nameof(ToggleType)); public NativeMenuItemToggleType ToggleType { - get => _toggleType; - set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value); + get => GetValue(ToggleTypeProperty); + set => SetValue(ToggleTypeProperty, value); } - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -139,37 +108,26 @@ namespace Avalonia.Controls public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - public static readonly DirectProperty IsEnabledProperty = - AvaloniaProperty.RegisterDirect(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true); + public static readonly StyledProperty IsEnabledProperty = + AvaloniaProperty.Register(nameof(IsEnabled), true); public bool IsEnabled { - get => _isEnabled; - set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value); + get => GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); } void CanExecuteChanged() { - IsEnabled = _command?.CanExecute(CommandParameter) ?? true; + SetCurrentValue(IsEnabledProperty, Command?.CanExecute(CommandParameter) ?? true); } public bool HasClickHandlers => Click != null; public ICommand? Command { - get => _command; - set - { - if (_command != null) - WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); - - SetAndRaise(CommandProperty, ref _command, value); - - if (_command != null) - WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); - - CanExecuteChanged(); - } + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -196,8 +154,28 @@ namespace Avalonia.Controls Command.Execute(CommandParameter); } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MenuProperty && change.NewValue is NativeMenu newMenu) + { + if (newMenu.Parent != null && newMenu.Parent != this) + throw new InvalidOperationException("NativeMenu already has a parent"); + newMenu.Parent = this; + } + else if (change.Property == CommandProperty) + { + if (change.OldValue is ICommand oldCommand) + WeakEvents.CommandCanExecuteChanged.Unsubscribe(oldCommand, _canExecuteChangedSubscriber); + if (change.NewValue is ICommand newCommand) + WeakEvents.CommandCanExecuteChanged.Subscribe(newCommand, _canExecuteChangedSubscriber); + CanExecuteChanged(); + } + } } - + public enum NativeMenuItemToggleType { None, diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index 4946d16f01..70cb2b806e 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -12,12 +12,12 @@ namespace Avalonia.Controls } public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); public NativeMenu? Parent { get => _parent; - set => SetAndRaise(ParentProperty, ref _parent, value); + internal set => SetAndRaise(ParentProperty, ref _parent, value); } } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 663bd3358a..705d40380e 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -13,7 +13,6 @@ namespace Avalonia.Controls.Notifications [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { - private bool _isClosed; private bool _isClosing; static NotificationCard() @@ -84,15 +83,15 @@ namespace Avalonia.Controls.Notifications /// public bool IsClosed { - get { return _isClosed; } - set { SetAndRaise(IsClosedProperty, ref _isClosed, value); } + get => GetValue(IsClosedProperty); + set => SetValue(IsClosedProperty, value); } /// /// Defines the property. /// - public static readonly DirectProperty IsClosedProperty = - AvaloniaProperty.RegisterDirect(nameof(IsClosed), o => o.IsClosed, (o, v) => o.IsClosed = v); + public static readonly StyledProperty IsClosedProperty = + AvaloniaProperty.Register(nameof(IsClosed)); /// /// Defines the event. diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index ac4f699313..885a8af5d1 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -43,16 +43,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ClipValueToMinMaxProperty = - AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax), - updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + public static readonly StyledProperty ClipValueToMinMaxProperty = + AvaloniaProperty.Register(nameof(ClipValueToMinMax)); /// /// Defines the property. /// - public static readonly DirectProperty NumberFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(NumberFormat), o => o.NumberFormat, - (o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo); + public static readonly StyledProperty NumberFormatProperty = + AvaloniaProperty.Register(nameof(NumberFormat), NumberFormatInfo.CurrentInfo); /// /// Defines the property. @@ -87,30 +85,28 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ParsingNumberStyleProperty = - AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle), - updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + public static readonly StyledProperty ParsingNumberStyleProperty = + AvaloniaProperty.Register(nameof(ParsingNumberStyle), NumberStyles.Any); /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text), defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. /// - public static readonly DirectProperty TextConverterProperty = - AvaloniaProperty.RegisterDirect(nameof(TextConverter), - updown => updown.TextConverter, (o, v) => o.TextConverter = v, null, BindingMode.OneWay, false); + public static readonly StyledProperty TextConverterProperty = + AvaloniaProperty.Register(nameof(TextConverter), defaultBindingMode: BindingMode.OneWay); /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, - (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value), coerce: (s,v) => ((NumericUpDown)s).OnCoerceValue(v), + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -132,15 +128,9 @@ namespace Avalonia.Controls private IDisposable? _textBoxTextChangedSubscription; - private decimal? _value; - private string? _text; - private IValueConverter? _textConverter; private bool _internalValueSet; - private bool _clipValueToMinMax; private bool _isSyncingTextAndValueProperties; private bool _isTextChangedFromUI; - private NumberStyles _parsingNumberStyle = NumberStyles.Any; - private NumberFormatInfo? _numberFormat; /// /// Gets the Spinner template part. @@ -184,8 +174,8 @@ namespace Avalonia.Controls /// public bool ClipValueToMinMax { - get { return _clipValueToMinMax; } - set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + get => GetValue(ClipValueToMinMaxProperty); + set => SetValue(ClipValueToMinMaxProperty, value); } /// @@ -193,8 +183,8 @@ namespace Avalonia.Controls /// public NumberFormatInfo? NumberFormat { - get { return _numberFormat; } - set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); } + get => GetValue(NumberFormatProperty); + set => SetValue(NumberFormatProperty, value); } /// @@ -249,8 +239,8 @@ namespace Avalonia.Controls /// public NumberStyles ParsingNumberStyle { - get { return _parsingNumberStyle; } - set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + get => GetValue(ParsingNumberStyleProperty); + set => SetValue(ParsingNumberStyleProperty, value); } /// @@ -258,8 +248,8 @@ namespace Avalonia.Controls /// public string? Text { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// @@ -269,8 +259,8 @@ namespace Avalonia.Controls /// public IValueConverter? TextConverter { - get { return _textConverter; } - set { SetAndRaise(TextConverterProperty, ref _textConverter, value); } + get => GetValue(TextConverterProperty); + set => SetValue(TextConverterProperty, value); } /// @@ -278,12 +268,8 @@ namespace Avalonia.Controls /// public decimal? Value { - get { return _value; } - set - { - value = OnCoerceValue(value); - SetAndRaise(ValueProperty, ref _value, value); - } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); } /// @@ -475,7 +461,7 @@ namespace Avalonia.Controls } if (ClipValueToMinMax && Value.HasValue) { - Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(Value.Value, Minimum, Maximum)); } } @@ -492,7 +478,7 @@ namespace Avalonia.Controls } if (ClipValueToMinMax && Value.HasValue) { - Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(Value.Value, Minimum, Maximum)); } } @@ -508,7 +494,7 @@ namespace Avalonia.Controls SyncTextAndValueProperties(true, Text); } } - + /// /// Called when the property value changed. /// @@ -675,8 +661,8 @@ namespace Avalonia.Controls { result = Minimum; } - - Value = MathUtilities.Clamp(result, Minimum, Maximum); + + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } /// @@ -685,7 +671,7 @@ namespace Avalonia.Controls private void OnDecrement() { decimal result; - + if (Value.HasValue) { result = Value.Value - Increment; @@ -694,8 +680,8 @@ namespace Avalonia.Controls { result = Maximum; } - - Value = MathUtilities.Clamp(result, Minimum, Maximum); + + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } /// @@ -712,7 +698,7 @@ namespace Avalonia.Controls { validDirections = ValidSpinDirections.Increase | ValidSpinDirections.Decrease; } - + if (Value < Maximum) { validDirections = validDirections | ValidSpinDirections.Increase; @@ -862,7 +848,7 @@ namespace Avalonia.Controls _internalValueSet = true; try { - Value = value; + SetCurrentValue(ValueProperty, value); } finally { @@ -907,7 +893,7 @@ namespace Avalonia.Controls _isTextChangedFromUI = true; if (TextBox != null) { - Text = TextBox.Text; + SetCurrentValue(TextProperty, TextBox.Text); } } finally @@ -1026,7 +1012,7 @@ namespace Avalonia.Controls var newText = ConvertValueToText(); if (!Equals(Text, newText)) { - Text = newText; + SetCurrentValue(TextProperty, newText); } } @@ -1066,7 +1052,7 @@ namespace Avalonia.Controls { return null; } - + if (TextConverter != null) { var valueFromText = TextConverter.Convert(text, typeof(decimal?), null, CultureInfo.CurrentCulture); diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs index 68a4b9eecb..20e4e5470d 100644 --- a/src/Avalonia.Controls/PlacementMode.cs +++ b/src/Avalonia.Controls/PlacementMode.cs @@ -13,28 +13,74 @@ 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 anchor and gravity rules + /// Preferred location is centered over the target element. /// - AnchorAndGravity + Center, + + /// + /// The popup is placed according to and rules. + /// + AnchorAndGravity, + + /// + /// 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. + /// + RightEdgeAlignedBottom } } diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs new file mode 100644 index 0000000000..072bace154 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Metadata; + +#nullable enable +namespace Avalonia.Controls.Platform +{ + [Unstable] + [NotClientImplementable] + public interface IInsetsManager + { + /// + /// Gets or sets whether the system bars are visible. + /// + bool? IsSystemBarVisible { get; set; } + + /// + /// Gets or sets whether the window draws edge to edge. behind any visibile system bars. + /// + bool DisplayEdgeToEdge { get; set; } + + /// + /// Gets the current safe area padding. + /// + Thickness SafeAreaPadding { get; } + + /// + /// Occurs when safe area for the current window changes. + /// + event EventHandler? SafeAreaChanged; + } + + public class SafeAreaChangedArgs : EventArgs + { + public SafeAreaChangedArgs(Thickness safeArePadding) + { + SafeAreaPadding = safeArePadding; + } + + /// + public Thickness SafeAreaPadding { get; } + } + + public enum SystemBarTheme + { + /// + /// Light system bar theme, with light background and a dark foreground + /// + Light, + + /// + /// Bark system bar theme, with dark background and a light foreground + /// + Dark + } +} diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index be61bb18a1..329a0fa6ab 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -156,16 +156,13 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property /// - public static readonly DirectProperty RecognizesAccessKeyProperty = - AvaloniaProperty.RegisterDirect( - nameof(RecognizesAccessKey), - cp => cp.RecognizesAccessKey, (cp, value) => cp.RecognizesAccessKey = value); + public static readonly StyledProperty RecognizesAccessKeyProperty = + AvaloniaProperty.Register(nameof(RecognizesAccessKey)); private Control? _child; private bool _createdChild; private IRecyclingDataTemplate? _recyclingDataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); - private bool _recognizesAccessKey; /// /// Initializes static members of the class. @@ -386,8 +383,8 @@ namespace Avalonia.Controls.Presenters /// public bool RecognizesAccessKey { - get => _recognizesAccessKey; - set => SetAndRaise(RecognizesAccessKeyProperty, ref _recognizesAccessKey, value); + get => GetValue(RecognizesAccessKeyProperty); + set => SetValue(RecognizesAccessKeyProperty, value); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 0a762c438d..a0020a0b6e 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -166,7 +166,7 @@ namespace Avalonia.Controls.Presenters } Panel = ItemsPanel.Build(); - Panel.SetValue(TemplatedParentProperty, TemplatedParent); + Panel.TemplatedParent = TemplatedParent; Panel.IsItemsHost = true; _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bb6b03d59a..089a2c1168 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,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. /// @@ -551,7 +535,7 @@ namespace Avalonia.Controls.Presenters { TextLayout result; - var text = GetText(); + var text = _text; var typeface = new Typeface(FontFamily, FontStyle, FontWeight); @@ -564,7 +548,7 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if(_compositionRegion != null) + if (_compositionRegion != null) { var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, new GenericTextRunProperties(typeface, FontSize, @@ -851,7 +835,7 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true) + internal void UpdateCaret(CharacterHit characterHit, bool notify = true) { _lastCharacterHit = characterHit; @@ -879,7 +863,7 @@ namespace Avalonia.Controls.Presenters CaretBoundsChanged?.Invoke(this, EventArgs.Empty); } - if (updateCaretIndex) + if (notify) { SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } @@ -899,35 +883,6 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - protected void OnPreeditTextChanged(string? oldValue, string? newValue) - { - InvalidateTextLayout(); - - if (string.IsNullOrEmpty(newValue)) - { - UpdateCaret(_lastCharacterHit); - } - else - { - var textPosition = _caretIndex + newValue?.Length ?? 0; - - var characterHit = GetCharacterHitFromTextPosition(textPosition); - - UpdateCaret(characterHit, false); - } - } - - private CharacterHit GetCharacterHitFromTextPosition(int textPosition) - { - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true); - - var textLine = TextLayout.TextLines[lineIndex]; - - var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1)); - - return characterHit; - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -935,11 +890,6 @@ namespace Avalonia.Controls.Presenters switch (change.Property.Name) { case nameof(PreeditText): - { - OnPreeditTextChanged(change.OldValue as string, change.NewValue as string); - break; - } - case nameof(CompositionRegion): case nameof(Foreground): case nameof(FontSize): diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 71ae7a5bf6..55d2ec7506 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,6 +1,8 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -10,6 +12,9 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private bool _prepareItemContainerOnAttach; + /// /// Defines the property. /// @@ -60,6 +65,17 @@ namespace Avalonia.Controls.Primitives /// IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach) + { + PrepareItemContainer(); + _prepareItemContainerOnAttach = false; + } + } + /// bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { @@ -81,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer() + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = false; + return; + } + + var headerTemplate = HeaderTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = true; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 9d443d9289..0c6c434713 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel; using Avalonia.Reactive; using Avalonia.Automation.Peers; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; @@ -41,11 +40,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsOpen), - o => o.IsOpen, - (o, v) => o.IsOpen = v); + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); /// /// Defines the property. @@ -90,11 +86,8 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OverlayDismissEventPassThroughProperty = AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - AvaloniaProperty.RegisterDirect( - nameof(OverlayInputPassThroughElement), - o => o.OverlayInputPassThroughElement, - (o, v) => o.OverlayInputPassThroughElement = v); + public static readonly StyledProperty OverlayInputPassThroughElementProperty = + AvaloniaProperty.Register(nameof(OverlayInputPassThroughElement)); /// /// Defines the property. @@ -121,10 +114,8 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(Topmost)); private bool _isOpenRequested; - private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; - private IInputElement? _overlayInputPassThroughElement; private Action? _popupHostChangedHandler; /// @@ -209,8 +200,8 @@ namespace Avalonia.Controls.Primitives /// public bool IsOpen { - get { return _isOpen; } - set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); } /// @@ -301,8 +292,8 @@ namespace Avalonia.Controls.Primitives /// public IInputElement? OverlayInputPassThroughElement { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + get => GetValue(OverlayInputPassThroughElementProperty); + set => SetValue(OverlayInputPassThroughElementProperty, value); } /// @@ -486,7 +477,7 @@ namespace Avalonia.Controls.Primitives using (BeginIgnoringIsOpen()) { - IsOpen = true; + SetCurrentValue(IsOpenProperty, true); } Opened?.Invoke(this, EventArgs.Empty); @@ -704,7 +695,7 @@ namespace Avalonia.Controls.Primitives { using (BeginIgnoringIsOpen()) { - IsOpen = false; + SetCurrentValue(IsOpenProperty, false); } return; @@ -717,7 +708,7 @@ namespace Avalonia.Controls.Primitives using (BeginIgnoringIsOpen()) { - IsOpen = false; + SetCurrentValue(IsOpenProperty, false); } Closed?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 2e70947457..0c9bb89caa 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -478,35 +478,29 @@ namespace Avalonia.Controls.Primitives.PopupPositioning var bounds = new Rect(default, target.Bounds.Size); var anchorRect = rect ?? bounds; - positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); + positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); - if (placement == PlacementMode.Right) + var parameters = placement switch { - positionerParameters.Anchor = PopupAnchor.TopRight; - positionerParameters.Gravity = PopupGravity.BottomRight; - } - else if (placement == PlacementMode.Bottom) - { - positionerParameters.Anchor = PopupAnchor.BottomLeft; - positionerParameters.Gravity = PopupGravity.BottomRight; - } - else if (placement == PlacementMode.Left) - { - positionerParameters.Anchor = PopupAnchor.TopLeft; - positionerParameters.Gravity = PopupGravity.BottomLeft; - } - else if (placement == PlacementMode.Top) - { - positionerParameters.Anchor = PopupAnchor.TopLeft; - positionerParameters.Gravity = PopupGravity.TopRight; - } - else if (placement == PlacementMode.AnchorAndGravity) - { - positionerParameters.Anchor = anchor; - positionerParameters.Gravity = gravity; - } - else - throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); + PlacementMode.Bottom => (PopupAnchor.Bottom, PopupGravity.Bottom), + PlacementMode.Right => (PopupAnchor.Right, PopupGravity.Right), + PlacementMode.Left => (PopupAnchor.Left, PopupGravity.Left), + PlacementMode.Top => (PopupAnchor.Top, PopupGravity.Top), + PlacementMode.Center => (PopupAnchor.None, PopupGravity.None), + PlacementMode.AnchorAndGravity => (anchor, gravity), + PlacementMode.TopEdgeAlignedRight => (PopupAnchor.TopRight, PopupGravity.TopLeft), + PlacementMode.TopEdgeAlignedLeft => (PopupAnchor.TopLeft, PopupGravity.TopRight), + PlacementMode.BottomEdgeAlignedLeft => (PopupAnchor.BottomLeft, PopupGravity.BottomRight), + PlacementMode.BottomEdgeAlignedRight => (PopupAnchor.BottomRight, PopupGravity.BottomLeft), + PlacementMode.LeftEdgeAlignedTop => (PopupAnchor.TopLeft, PopupGravity.BottomLeft), + PlacementMode.LeftEdgeAlignedBottom => (PopupAnchor.BottomLeft, PopupGravity.TopLeft), + PlacementMode.RightEdgeAlignedTop => (PopupAnchor.TopRight, PopupGravity.BottomRight), + PlacementMode.RightEdgeAlignedBottom => (PopupAnchor.BottomRight, PopupGravity.TopRight), + _ => throw new ArgumentOutOfRangeException(nameof(placement), placement, + "Invalid value for Popup.PlacementMode") + }; + positionerParameters.Anchor = parameters.Item1; + positionerParameters.Gravity = parameters.Item2; } // Invert coordinate system if FlowDirection is RTL diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 8253342782..e1cf25d89f 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Primitives { foreach (var child in this.GetTemplateChildren()) { - child.SetValue(TemplatedParentProperty, null); + child.TemplatedParent = null; ((ISetLogicalParent)child).SetParent(null); } @@ -377,7 +377,7 @@ namespace Avalonia.Controls.Primitives /// The templated parent to apply. internal static void ApplyTemplatedParent(StyledElement control, AvaloniaObject? templatedParent) { - control.SetValue(TemplatedParentProperty, templatedParent); + control.TemplatedParent = templatedParent; var children = control.LogicalChildren; var count = children.Count; diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 158c5d875b..dfaf7bbc45 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -15,12 +15,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsChecked), - o => o.IsChecked, - (o, v) => o.IsChecked = v, - unsetValue: false, + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay); /// @@ -64,8 +60,6 @@ namespace Avalonia.Controls.Primitives nameof(IsCheckedChanged), RoutingStrategies.Bubble); - private bool? _isChecked = false; - static ToggleButton() { } @@ -119,12 +113,8 @@ namespace Avalonia.Controls.Primitives /// public bool? IsChecked { - get => _isChecked; - set - { - SetAndRaise(IsCheckedProperty, ref _isChecked, value); - UpdatePseudoClasses(IsChecked); - } + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } /// @@ -147,28 +137,31 @@ namespace Avalonia.Controls.Primitives /// protected virtual void Toggle() { + bool? newValue; if (IsChecked.HasValue) { if (IsChecked.Value) { if (IsThreeState) { - IsChecked = null; + newValue = null; } else { - IsChecked = false; + newValue = false; } } else { - IsChecked = true; + newValue = true; } } else { - IsChecked = false; + newValue = false; } + + SetCurrentValue(IsCheckedProperty, newValue); } /// @@ -224,6 +217,8 @@ namespace Avalonia.Controls.Primitives { var newValue = change.GetNewValue(); + UpdatePseudoClasses(newValue); + #pragma warning disable CS0618 // Type or member is obsolete switch (newValue) { diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 87772aced7..d4528fdb1c 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -98,31 +98,22 @@ namespace Avalonia.Controls } } - public static readonly DirectProperty GroupNameProperty = - AvaloniaProperty.RegisterDirect( - nameof(GroupName), - o => o.GroupName, - (o, v) => o.GroupName = v); + public static readonly StyledProperty GroupNameProperty = + AvaloniaProperty.Register(nameof(GroupName)); - private string? _groupName; private RadioButtonGroupManager? _groupManager; - public RadioButton() - { - this.GetObservable(IsCheckedProperty).Subscribe(IsCheckedChanged); - } - public string? GroupName { - get { return _groupName; } - set { SetGroupName(value); } + get => GetValue(GroupNameProperty); + set => SetValue(GroupNameProperty, value); } protected override void Toggle() { if (!IsChecked.GetValueOrDefault()) { - IsChecked = true; + SetCurrentValue(IsCheckedProperty, true); } } @@ -154,28 +145,38 @@ namespace Avalonia.Controls return new RadioButtonAutomationPeer(this); } - private void SetGroupName(string? newGroupName) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var oldGroupName = GroupName; - if (newGroupName != oldGroupName) + base.OnPropertyChanged(change); + + if (change.Property == IsCheckedProperty) { - if (!string.IsNullOrEmpty(oldGroupName)) - { - _groupManager?.Remove(this, oldGroupName); - } - _groupName = newGroupName; - if (!string.IsNullOrEmpty(newGroupName)) + IsCheckedChanged(change.GetNewValue()); + } + else if (change.Property == GroupNameProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnGroupNameChanged(oldValue, newValue); + } + } + + private void OnGroupNameChanged(string? oldGroupName, string? newGroupName) + { + if (!string.IsNullOrEmpty(oldGroupName)) + { + _groupManager?.Remove(this, oldGroupName); + } + if (!string.IsNullOrEmpty(newGroupName)) + { + if (_groupManager == null) { - if (_groupManager == null) - { - _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); - } - _groupManager.Add(this); + _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); } + _groupManager.Add(this); } } - private void IsCheckedChanged(bool? value) + private new void IsCheckedChanged(bool? value) { var groupName = GroupName; if (string.IsNullOrEmpty(groupName)) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 31a06d875a..e790578675 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -42,10 +42,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - splitButton => splitButton.Command, - (splitButton, command) => splitButton.Command = command); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(); /// /// Defines the property. @@ -59,8 +57,6 @@ namespace Avalonia.Controls public static readonly StyledProperty FlyoutProperty = Button.FlyoutProperty.AddOwner(); - private ICommand? _Command; - private Button? _primaryButton = null; private Button? _secondaryButton = null; @@ -83,8 +79,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _Command; - set => SetAndRaise(CommandProperty, ref _Command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -176,7 +172,7 @@ namespace Avalonia.Controls flyout.Opened += Flyout_Opened; flyout.Closed += Flyout_Closed; - _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged); } } @@ -436,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/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/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 3a28836a99..deace4084c 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,11 +1,11 @@ using System; +using System.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Input.TextInput; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.VisualTree; -using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -77,7 +77,7 @@ namespace Avalonia.Controls { get => _textEditable; set { - if(_textEditable != null) + if (_textEditable != null) { _textEditable.TextChanged -= TextEditable_TextChanged; _textEditable.SelectionChanged -= TextEditable_SelectionChanged; @@ -86,7 +86,7 @@ namespace Avalonia.Controls _textEditable = value; - if(_textEditable != null) + if (_textEditable != null) { _textEditable.TextChanged += TextEditable_TextChanged; _textEditable.SelectionChanged += TextEditable_SelectionChanged; @@ -112,7 +112,7 @@ namespace Avalonia.Controls private void TextEditable_SelectionChanged(object? sender, EventArgs e) { - if(_parent != null && _textEditable != null) + if (_parent != null && _textEditable != null) { _parent.SelectionStart = _textEditable.SelectionStart; _parent.SelectionEnd = _textEditable.SelectionEnd; @@ -159,14 +159,51 @@ namespace Avalonia.Controls public event EventHandler? SurroundingTextChanged; - public void SetPreeditText(string? text) + private string? _presenterText; + private int _compositionStart; + + public void SetPreeditText(string? preeditText) { - if (_presenter == null) + if (_presenter == null || _parent == null) { return; } - _presenter.PreeditText = text; + if (_presenterText is null) + { + _presenterText = _parent.Text ?? ""; + _compositionStart = _parent.CaretIndex; + } + + var text = GetText(preeditText); + + _presenter._text = text; + + _presenter.PreeditText = preeditText; + + _presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false); + + if (string.IsNullOrEmpty(preeditText)) + { + _presenterText = null; + } + } + + private string? GetText(string? preeditText) + { + if (string.IsNullOrEmpty(preeditText)) + { + return _presenterText; + } + + if (string.IsNullOrEmpty(_presenterText)) + { + return preeditText; + } + + var text = _presenterText.Substring(0, _compositionStart) + preeditText + _presenterText.Substring(_compositionStart); + + return text; } public void SetComposingRegion(TextRange? region) @@ -175,6 +212,7 @@ namespace Avalonia.Controls { return; } + _presenter.CompositionRegion = region; } @@ -256,9 +294,9 @@ namespace Avalonia.Controls } } - if(e.Property == TextBox.TextProperty) + if (e.Property == TextBox.TextProperty) { - if(_textEditable != null) + if (_textEditable != null) { _textEditable.Text = (string?)e.NewValue; } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fdcb8cc537..5c2a8c8a13 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -15,6 +15,7 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Platform.Storage; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; @@ -391,7 +392,9 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - + + public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature(); + /// Point IRenderRoot.PointToClient(PixelPoint p) { diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 5713846b35..73bcb84c69 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -13,13 +13,10 @@ namespace Avalonia.Controls public sealed class TrayIcons : AvaloniaList { } - - public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { private readonly ITrayIconImpl? _impl; - private ICommand? _command; private TrayIcon(ITrayIconImpl? impl) { @@ -85,11 +82,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - trayIcon => trayIcon.Command, - (trayIcon, command) => trayIcon.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -136,8 +130,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _command; - set => SetAndRaise(CommandProperty, ref _command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index e9abfef673..3ac157f727 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -104,12 +104,12 @@ namespace Avalonia.Controls if (ItemTemplate == null && _treeView?.ItemTemplate != null) { - ItemTemplate = _treeView.ItemTemplate; + SetCurrentValue(ItemTemplateProperty, _treeView.ItemTemplate); } if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null) { - ItemContainerTheme = _treeView.ItemContainerTheme; + SetCurrentValue(ItemContainerThemeProperty, _treeView.ItemContainerTheme); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ba1b599421..f9593f1c1b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; @@ -11,6 +9,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Styling; namespace Avalonia.Controls @@ -149,11 +148,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty WindowStartupLocationProperty = - AvaloniaProperty.RegisterDirect( - nameof(WindowStartupLocation), - o => o.WindowStartupLocation, - (o, v) => o.WindowStartupLocation = v); + public static readonly StyledProperty WindowStartupLocationProperty = + AvaloniaProperty.Register(nameof(WindowStartupLocation)); public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); @@ -171,7 +167,6 @@ namespace Avalonia.Controls RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); private object? _dialogResult; private readonly Size _maxPlatformClientSize; - private WindowStartupLocation _windowStartupLocation; private bool _shown; private bool _showingAsDialog; @@ -305,7 +300,7 @@ namespace Avalonia.Controls { get => GetValue(ExtendClientAreaTitleBarHeightHintProperty); set => SetValue(ExtendClientAreaTitleBarHeightHintProperty, value); - } + } /// /// Gets if the ClientArea is Extended into the Window Decorations. @@ -314,7 +309,7 @@ namespace Avalonia.Controls { get => _isExtendedIntoWindowDecorations; private set => SetAndRaise(IsExtendedIntoWindowDecorationsProperty, ref _isExtendedIntoWindowDecorations, value); - } + } /// /// Gets the WindowDecorationMargin. @@ -324,7 +319,7 @@ namespace Avalonia.Controls { get => _windowDecorationMargin; private set => SetAndRaise(WindowDecorationMarginProperty, ref _windowDecorationMargin, value); - } + } /// /// Gets the window margin that is hidden off the screen area. @@ -397,8 +392,8 @@ namespace Avalonia.Controls /// public WindowStartupLocation WindowStartupLocation { - get { return _windowStartupLocation; } - set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } + get => GetValue(WindowStartupLocationProperty); + set => SetValue(WindowStartupLocationProperty, value); } /// @@ -488,7 +483,7 @@ namespace Avalonia.Controls CloseInternal(); return false; } - + return true; } @@ -614,7 +609,7 @@ namespace Avalonia.Controls if (_shown != isVisible) { - if(!_shown) + if (!_shown) { Show(); } @@ -657,7 +652,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } } - + private void EnsureParentStateBeforeShow(Window owner) { if (owner.PlatformImpl == null) @@ -819,7 +814,7 @@ namespace Avalonia.Controls { bool isEnabled = true; - foreach (var (_, isDialog) in _children) + foreach (var (_, isDialog) in _children) { if (isDialog) { @@ -856,7 +851,7 @@ namespace Avalonia.Controls { Window? firstDialogChild = null; - foreach (var (child, isDialog) in _children) + foreach (var (child, isDialog) in _children) { if (isDialog) { @@ -880,7 +875,7 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - (owner is null || + (owner is null || (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { @@ -902,7 +897,7 @@ namespace Avalonia.Controls if (owner is not null) { - screen = Screens.ScreenFromWindow(owner) + screen = Screens.ScreenFromWindow(owner) ?? Screens.ScreenFromPoint(owner.Position); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 26e11f0d4a..814a9b5960 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -27,10 +27,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty OwnerProperty = - AvaloniaProperty.RegisterDirect( - nameof(Owner), - o => o.Owner, - (o, v) => o.Owner = v); + AvaloniaProperty.RegisterDirect(nameof(Owner), o => o.Owner); public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs new file mode 100644 index 0000000000..b7579ed31b --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Diagnostics.Controls +{ + internal sealed class BrushEditor : Control + { + /// + /// Defines the property. + /// + public static readonly DirectProperty BrushProperty = + AvaloniaProperty.RegisterDirect( + nameof(Brush), o => o.Brush, (o, v) => o.Brush = v); + + private IBrush? _brush; + + public IBrush? Brush + { + get => _brush; + set => SetAndRaise(BrushProperty, ref _brush, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BrushProperty) + { + switch (Brush) + { + case ISolidColorBrush scb: + { + var colorView = new ColorView { Color = scb.Color }; + + colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); + + FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView }); + ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})"); + + break; + } + + default: + + FlyoutBase.SetAttachedFlyout(this, null); + ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)"); + + break; + } + + InvalidateVisual(); + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + FlyoutBase.ShowAttachedFlyout(this); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (Brush != null) + { + context.FillRectangle(Brush, Bounds); + } + else + { + context.FillRectangle(Brushes.Black, Bounds); + + var ft = new FormattedText("(null)", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + Typeface.Default, + 10, + Brushes.White); + + context.DrawText(ft, + new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2)); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs new file mode 100644 index 0000000000..7870febd0a --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs @@ -0,0 +1,89 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics.Controls +{ + //TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control. + internal sealed class CommitTextBox : TextBox, IStyleable + { + Type IStyleable.StyleKey => typeof(TextBox); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommittedTextProperty = + AvaloniaProperty.RegisterDirect( + nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v); + + private string? _committedText; + + public string? CommittedText + { + get => _committedText; + set => SetAndRaise(CommittedTextProperty, ref _committedText, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CommittedTextProperty) + { + Text = CommittedText; + } + } + + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + + switch (e.Key) + { + case Key.Enter: + + TryCommit(); + + e.Handled = true; + + break; + + case Key.Escape: + + Cancel(); + + e.Handled = true; + + break; + } + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + TryCommit(); + } + + private void Cancel() + { + Text = CommittedText; + DataValidationErrors.ClearErrors(this); + } + + private void TryCommit() + { + if (!DataValidationErrors.GetHasErrors(this)) + { + CommittedText = Text; + } + else + { + Text = CommittedText; + DataValidationErrors.ClearErrors(this); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index c9189a886d..f765871ee8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -6,84 +6,73 @@ namespace Avalonia.Diagnostics.Controls { internal class ThicknessEditor : ContentControl { - public static readonly DirectProperty ThicknessProperty = - AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness, - (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), + defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, - (o, v) => o.Header = v); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); - public static readonly DirectProperty IsPresentProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPresent), o => o.IsPresent, - (o, v) => o.IsPresent = v); + public static readonly StyledProperty IsPresentProperty = + AvaloniaProperty.Register(nameof(IsPresent), true); - public static readonly DirectProperty LeftProperty = - AvaloniaProperty.RegisterDirect(nameof(Left), o => o.Left, (o, v) => o.Left = v); + public static readonly StyledProperty LeftProperty = + AvaloniaProperty.Register(nameof(Left)); - public static readonly DirectProperty TopProperty = - AvaloniaProperty.RegisterDirect(nameof(Top), o => o.Top, (o, v) => o.Top = v); + public static readonly StyledProperty TopProperty = + AvaloniaProperty.Register(nameof(Top)); - public static readonly DirectProperty RightProperty = - AvaloniaProperty.RegisterDirect(nameof(Right), o => o.Right, - (o, v) => o.Right = v); + public static readonly StyledProperty RightProperty = + AvaloniaProperty.Register(nameof(Right)); - public static readonly DirectProperty BottomProperty = - AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom, - (o, v) => o.Bottom = v); + public static readonly StyledProperty BottomProperty = + AvaloniaProperty.Register(nameof(Bottom)); public static readonly StyledProperty HighlightProperty = AvaloniaProperty.Register(nameof(Highlight)); - private Thickness _thickness; - private string? _header; - private bool _isPresent = true; - private double _left; - private double _top; - private double _right; - private double _bottom; private bool _isUpdatingThickness; public Thickness Thickness { - get => _thickness; - set => SetAndRaise(ThicknessProperty, ref _thickness, value); + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); } public string? Header { - get => _header; - set => SetAndRaise(HeaderProperty, ref _header, value); + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } public bool IsPresent { - get => _isPresent; - set => SetAndRaise(IsPresentProperty, ref _isPresent, value); + get => GetValue(IsPresentProperty); + set => SetValue(IsPresentProperty, value); } public double Left { - get => _left; - set => SetAndRaise(LeftProperty, ref _left, value); + get => GetValue(LeftProperty); + set => SetValue(LeftProperty, value); } public double Top { - get => _top; - set => SetAndRaise(TopProperty, ref _top, value); + get => GetValue(TopProperty); + set => SetValue(TopProperty, value); } public double Right { - get => _right; - set => SetAndRaise(RightProperty, ref _right, value); + get => GetValue(RightProperty); + set => SetValue(RightProperty, value); } public double Bottom { - get => _bottom; - set => SetAndRaise(BottomProperty, ref _bottom, value); + get => GetValue(BottomProperty); + set => SetValue(BottomProperty, value); } public IBrush Highlight @@ -104,10 +93,10 @@ namespace Avalonia.Diagnostics.Controls var value = change.GetNewValue(); - Left = value.Left; - Top = value.Top; - Right = value.Right; - Bottom = value.Bottom; + SetCurrentValue(LeftProperty, value.Left); + SetCurrentValue(TopProperty, value.Top); + SetCurrentValue(RightProperty, value.Right); + SetCurrentValue(BottomProperty, value.Bottom); } finally { @@ -118,7 +107,7 @@ namespace Avalonia.Diagnostics.Controls (change.Property == LeftProperty || change.Property == TopProperty || change.Property == RightProperty || change.Property == BottomProperty)) { - Thickness = new Thickness(Left, Top, Right, Bottom); + SetCurrentValue(ThicknessProperty, new(Left, Top, Right, Bottom)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 0e412a2fa5..2412ea5325 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -35,15 +35,14 @@ namespace Avalonia.Diagnostics.ViewModels public override string Priority => _priority; public override Type AssignedType => _assignedType; - public override string? Value + public override object? Value { - get => ConvertToString(_value); + get => _value; set { try { - var convertedValue = ConvertFromString(value, Property.PropertyType); - _target.SetValue(Property, convertedValue); + _target.SetValue(Property, value); Update(); } catch { } @@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels public override Type? DeclaringType { get; } public override Type PropertyType => _propertyType; + public override bool IsReadonly => Property.IsReadOnly; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 895ff41f7b..b7ee1459f7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -40,16 +40,16 @@ namespace Avalonia.Diagnostics.ViewModels public override Type AssignedType => _assignedType; public override Type PropertyType => _propertyType; + public override bool IsReadonly => !Property.CanWrite; - public override string? Value + public override object? Value { - get => ConvertToString(_value); + get => _value; set { try { - var convertedValue = ConvertFromString(value, Property.PropertyType); - Property.SetValue(_target, convertedValue); + Property.SetValue(_target, value); Update(); } catch { } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index a7faf35769..aa2682e376 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -7,78 +7,21 @@ namespace Avalonia.Diagnostics.ViewModels { internal abstract class PropertyViewModel : ViewModelBase { - private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = { typeof(string) }; - private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; - public abstract object Key { get; } public abstract string Name { get; } public abstract string Group { get; } public abstract Type AssignedType { get; } public abstract Type? DeclaringType { get; } - public abstract string? Value { get; set; } + public abstract object? Value { get; set; } public abstract string Priority { get; } public abstract bool? IsAttached { get; } public abstract void Update(); public abstract Type PropertyType { get; } - public string Type => PropertyType == AssignedType - ? PropertyType.GetTypeName() - : $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; - - - protected static string? ConvertToString(object? value) - { - if (value is null) - { - return "(null)"; - } - - var converter = TypeDescriptor.GetConverter(value); - - //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || - converter.GetType() == typeof(CollectionConverter)) - { - return value.ToString() ?? "(null)"; - } - - return converter.ConvertToString(value); - } - - private static object? InvokeParse(string s, Type targetType) - { - var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); - } - - method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s }); - } - - throw new InvalidCastException("Unable to convert value."); - } - - protected static object? ConvertFromString(string? s, Type targetType) - { - if (s is null) - { - return null; - } - - var converter = TypeDescriptor.GetConverter(targetType); - if (converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); - } + public string Type => PropertyType == AssignedType ? + PropertyType.GetTypeName() : + $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; - return InvokeParse(s, targetType); - } + public abstract bool IsReadonly { get; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs new file mode 100644 index 0000000000..9425989096 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal static class ReactiveExtensions + { + public static IObservable GetObservable( + this TOwner vm, + Expression> property, + bool fireImmediately = true) + where TOwner : INotifyPropertyChanged + { + return Observable.Create(o => + { + var propertyInfo = GetPropertyInfo(property); + + void Fire() + { + o.OnNext((TValue) propertyInfo.GetValue(vm)!); + } + + void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == propertyInfo.Name) + { + Fire(); + } + } + + if (fireImmediately) + { + Fire(); + } + + vm.PropertyChanged += OnPropertyChanged; + + return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged); + }); + } + + private static PropertyInfo GetPropertyInfo(this Expression> property) + { + if (property.Body is UnaryExpression unaryExpression) + { + return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member; + } + + var memExpr = (MemberExpression)property.Body; + + return (PropertyInfo)memExpr.Member; + } + } +} 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. diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 2a69798c6c..97f195c91b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -60,7 +60,11 @@ DoubleTapped="PropertiesGrid_OnDoubleTapped"> - + + + + + ("console"); _consoleSplitter = this.GetControl("consoleSplitter"); _rootGrid = this.GetControl("rootGrid"); @@ -58,7 +57,7 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void PreviewKeyDown(object? sender, KeyEventArgs e) + private void PreviewKeyUp(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 748c2cc313..486df860c3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -15,6 +15,7 @@ + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs new file mode 100644 index 0000000000..6e7729a350 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -0,0 +1,404 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Diagnostics.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.Views +{ + internal class PropertyValueEditorView : UserControl + { + private static readonly Geometry ImageIcon = Geometry.Parse( + "M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z"); + + private static readonly Geometry GeometryIcon = Geometry.Parse( + "M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z"); + + private static readonly ColorToBrushConverter Color2Brush = new(); + + private readonly CompositeDisposable _cleanup = new(); + private PropertyViewModel? Property => (PropertyViewModel?)DataContext; + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + Content = UpdateControl(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _cleanup.Clear(); + } + + private static bool ImplementsInterface(Type type) + { + var interfaceType = typeof(TInterface); + return type == interfaceType || interfaceType.IsAssignableFrom(type); + } + + private Control? UpdateControl() + { + _cleanup.Clear(); + + if (Property?.PropertyType is not { } propertyType) + return null; + + TControl CreateControl(AvaloniaProperty valueProperty, + IValueConverter? converter = null, + Action? init = null, + AvaloniaProperty? readonlyProperty = null) + where TControl : Control, new() + { + var control = new TControl(); + + init?.Invoke(control); + + control.Bind(valueProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, + Converter = converter ?? new ValueConverter(), + ConverterParameter = propertyType + }).DisposeWith(_cleanup); + + if (readonlyProperty != null) + { + control[readonlyProperty] = Property.IsReadonly; + } + else + { + control.IsEnabled = !Property.IsReadonly; + } + + return control; + } + + if (propertyType == typeof(bool)) + return CreateControl(ToggleButton.IsCheckedProperty); + + //TODO: Infinity, NaN not working with NumericUpDown + if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) + return CreateControl( + NumericUpDown.ValueProperty, + new ValueToDecimalConverter(), + init: n => + { + n.Increment = 1; + n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; + n.ParsingNumberStyle = NumberStyles.Integer; + }, + readonlyProperty: NumericUpDown.IsReadOnlyProperty); + + if (propertyType == typeof(Color)) + { + var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center }; + + el.Bind( + Shape.FillProperty, + new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush }) + .DisposeWith(_cleanup); + + var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + + tbl.Bind( + TextBlock.TextProperty, + new Binding(nameof(Property.Value)) { Source = Property }) + .DisposeWith(_cleanup); + + var sp = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 2, + Children = { el, tbl }, + Background = Brushes.Transparent, + Cursor = new Cursor(StandardCursorType.Hand), + IsEnabled = !Property.IsReadonly + }; + + var cv = new ColorView(); + + cv.Bind( + ColorView.ColorProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, Converter = Color2Brush + }) + .DisposeWith(_cleanup); + + FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv }); + + sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp); + + return sp; + } + + if (ImplementsInterface(propertyType)) + return CreateControl(BrushEditor.BrushProperty); + + var isImage = ImplementsInterface(propertyType); + var isGeometry = propertyType == typeof(Geometry); + + if (isImage || isGeometry) + { + var valueObservable = Property.GetObservable(x => x.Value); + var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + + tbl.Bind(TextBlock.TextProperty, + valueObservable.Select( + value => value switch + { + IImage img => $"{img.Size.Width} x {img.Size.Height}", + Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}", + _ => "(null)" + })) + .DisposeWith(_cleanup); + + var sp = new StackPanel + { + Background = Brushes.Transparent, + Orientation = Orientation.Horizontal, + Spacing = 2, + Children = + { + new Path + { + Data = isImage ? ImageIcon : GeometryIcon, + Fill = Brushes.Gray, + Width = 12, + Height = 12, + Stretch = Stretch.Uniform, + VerticalAlignment = VerticalAlignment.Center + }, + tbl + } + }; + + if (isImage) + { + var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 }; + + previewImage + .Bind(Image.SourceProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, previewImage); + } + else + { + var previewShape = new Path + { + Stretch = Stretch.Uniform, + Fill = Brushes.White, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center + }; + + previewShape + .Bind(Path.DataProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 }); + } + + return sp; + } + + if (propertyType.IsEnum) + return CreateControl( + SelectingItemsControl.SelectedItemProperty, init: c => + { + c.Items = Enum.GetValues(propertyType); + }); + + var tb = CreateControl( + CommitTextBox.CommittedTextProperty, + new TextToValueConverter(), + t => + { + t.Watermark = "(null)"; + }, + readonlyProperty: TextBox.IsReadOnlyProperty); + + tb.IsReadOnly |= propertyType == typeof(object) || + !StringConversionHelper.CanConvertFromString(propertyType); + + if (!tb.IsReadOnly) + { + tb.GetObservable(TextBox.TextProperty).Subscribe(t => + { + try + { + if (t != null) + { + StringConversionHelper.FromString(t, propertyType); + } + + DataValidationErrors.ClearErrors(tb); + } + catch (Exception ex) + { + DataValidationErrors.SetError(tb, ex.GetBaseException()); + } + }).DisposeWith(_cleanup); + } + + return tb; + } + + //HACK: ValueConverter that skips first target update + //TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control + //if the first value comes from the source or target + private class ValueConverter : IValueConverter + { + private bool _firstUpdate = true; + + object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Convert(value, targetType, parameter, culture); + } + + object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (_firstUpdate) + { + _firstUpdate = false; + + return BindingOperations.DoNothing; + } + + //Note: targetType provided by Converter is simply "object" + return ConvertBack(value, (Type)parameter!, parameter, culture); + } + + protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } + + protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + return value; + } + } + + private static class StringConversionHelper + { + private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; + private static readonly Type[] StringParameter = { typeof(string) }; + private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; + + public static bool CanConvertFromString(Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + if (converter.CanConvertFrom(typeof(string))) + return true; + + return GetParseMethod(type, out _) != null; + } + + public static string? ToString(object o) + { + var converter = TypeDescriptor.GetConverter(o); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) + return o.ToString(); + + return converter.ConvertToInvariantString(o); + } + + public static object? FromString(string str, Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + return converter.CanConvertFrom(typeof(string)) ? + converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) : + InvokeParse(str, type); + } + + private static object? InvokeParse(string s, Type targetType) + { + var m = GetParseMethod(targetType, out var hasFormat); + + if (m == null) + throw new InvalidOperationException(); + + return m.Invoke(null, + hasFormat ? + new object[] { s, CultureInfo.InvariantCulture } : + new object[] { s }); + } + + private static MethodInfo? GetParseMethod(Type type, out bool hasFormat) + { + var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); + + if (m != null) + { + hasFormat = true; + + return m; + } + + hasFormat = false; + + return type.GetMethod("Parse", PublicStatic, null, StringParameter, null); + } + } + + private sealed class ValueToDecimalConverter : ValueConverter + { + protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return System.Convert.ToDecimal(value); + } + + protected override object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + return System.Convert.ChangeType(value, targetType); + } + } + + private sealed class TextToValueConverter : ValueConverter + { + protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is null ? null : StringConversionHelper.ToString(value); + } + + protected override object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + if (value is not string s) + return null; + + try + { + return StringConversionHelper.FromString(s, targetType); + } + catch + { + return BindingOperations.DoNothing; + } + } + } + } +} diff --git a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs new file mode 100644 index 0000000000..842629c923 --- /dev/null +++ b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Fonts.Inter +{ + public static class AppBuilderExtension + { + public static AppBuilder WithInterFont(this AppBuilder appBuilder) + { + return appBuilder.ConfigureFonts(fontManager => + { + fontManager.AddFontCollection(new InterFontCollection()); + }); + } + } +} diff --git a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj index c81a13558c..c18c07d347 100644 --- a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj +++ b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Fonts.Inter/InterFontCollection.cs b/src/Avalonia.Fonts.Inter/InterFontCollection.cs new file mode 100644 index 0000000000..0ed1779a03 --- /dev/null +++ b/src/Avalonia.Fonts.Inter/InterFontCollection.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Media.Fonts; + +namespace Avalonia.Fonts.Inter +{ + public sealed class InterFontCollection : EmbeddedFontCollection + { + public InterFontCollection() : base( + new Uri("fonts:Inter", UriKind.Absolute), + new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute)) + { + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 31aaebcdc7..f8100d3832 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -118,7 +118,7 @@ namespace Avalonia.Headless public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - return new HeadlessGeometryStub(new Rect(glyphRun.Size)); + return new HeadlessGeometryStub(glyphRun.Bounds); } public IGlyphRunImpl CreateGlyphRun( @@ -132,7 +132,7 @@ namespace Avalonia.Headless class HeadlessGlyphRunStub : IGlyphRunImpl { - public Size Size => new Size(8, 12); + public Rect Bounds => new Rect(new Size(8, 12)); public Point BaselineOrigin => new Point(0, 8); diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ee4cd5af98 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -84,6 +84,14 @@ namespace Avalonia.Headless public FontSimulations FontSimulations { get; } + public string FamilyName => "Arial"; + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + public void Dispose() { } @@ -147,19 +155,28 @@ namespace Avalonia.Headless class HeadlessFontManagerStub : IFontManagerImpl { - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public string GetDefaultFontFamilyName() { - return new HeadlessGlyphTypefaceImpl(); + return "Arial"; } - public string GetDefaultFontFamilyName() + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return "Arial"; + return new string[] { "Arial" }; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface) { - return new List { "Arial" }; + glyphTypeface= new HeadlessGlyphTypefaceImpl(); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + glyphTypeface = new HeadlessGlyphTypefaceImpl(); + + return true; } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, 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 64c1d0da10..5d0e6a2d18 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; @@ -19,6 +20,7 @@ namespace Avalonia.Native private double _extendTitleBarHeight = -1; private DoubleClickHelper _doubleClickHelper; private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; + private readonly AvaloniaNativeTextInputMethod _inputMethod; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) @@ -33,6 +35,8 @@ namespace Avalonia.Native } _nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); + + _inputMethod = new AvaloniaNativeTextInputMethod(_native); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents @@ -67,7 +71,7 @@ namespace Avalonia.Native } } - public IAvnWindow Native => _native; + public new IAvnWindow Native => _native; public void CanResize(bool value) { @@ -229,6 +233,11 @@ namespace Avalonia.Native public override object TryGetFeature(Type featureType) { + if(featureType == typeof(ITextInputMethodImpl)) + { + return _inputMethod; + } + if (featureType == typeof(ITopLevelNativeMenuExporter)) { return _nativeMenuExporter; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index db78873672..b107e83e7a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -546,6 +546,7 @@ interface IAvnWindowBase : IUnknown IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle); HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode); HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode); + HRESULT GetInputMethod(IAvnTextInputMethod **ppv); } [uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits] @@ -612,6 +613,22 @@ interface IAvnWindowEvents : IAvnWindowBaseEvents void GotInputWhenDisabled(); } +[uuid(f2079145-a2d9-42b8-a85e-2732e3c2b055)] +interface IAvnTextInputMethodClient : IUnknown +{ + void SetPreeditText(char* preeditText); + void SelectInSurroundingText(int start, int length); +} + +[uuid(1382a29f-e260-4c7a-b83f-c99fc72e27c2)] +interface IAvnTextInputMethod : IUnknown +{ + HRESULT SetClient(IAvnTextInputMethodClient* client); + void Reset(); + void SetCursorRect(AvnRect rect); + void SetSurroundingText(char* text, int anchorOffset, int cursorOffset); +} + [uuid(e34ae0f8-18b4-48a3-b09d-2e6b19a3cf5e)] interface IAvnMacOptions : IUnknown { diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 82e48851b5..c19a4f5c09 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -3,7 +3,7 @@ xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts:Inter#Inter, $Default 14 diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 0a06927034..0640fe9a4a 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -76,7 +76,7 @@ - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts:Inter#Inter, $Default #CC119EDA #99119EDA #66119EDA diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml index 8428e3aae7..0c7095f2f5 100644 --- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml @@ -92,6 +92,7 @@ + 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/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs new file mode 100644 index 0000000000..30f80ba27c --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Browser +{ + internal class BrowserInsetsManager : IInsetsManager + { + public bool? IsSystemBarVisible + { + get + { + return DomHelper.IsFullscreen(); + } + set + { + DomHelper.SetFullscreen(!value ?? false); + } + } + + public bool DisplayEdgeToEdge { get; set; } + + public event EventHandler? SafeAreaChanged; + + public Thickness SafeAreaPadding + { + get + { + var padding = DomHelper.GetSafeAreaPadding(); + + return new Thickness(padding[0], padding[1], padding[2], padding[3]); + } + } + + public void NotifySafeAreaPaddingChanged() + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 1bf4636f61..7c5418dbeb 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Browser private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly IInsetsManager? _insetsManager; public BrowserTopLevelImpl(AvaloniaView avaloniaView) { @@ -40,9 +41,12 @@ namespace Avalonia.Browser AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); _touchDevice = new TouchDevice(); _penDevice = new PenDevice(); + + _insetsManager = new BrowserInsetsManager(); _nativeControlHost = _avaloniaView.GetNativeControlHostImpl(); _storageProvider = new BrowserStorageProvider(); _systemNavigationManager = new BrowserSystemNavigationManagerImpl(); + } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; @@ -69,6 +73,8 @@ namespace Avalonia.Browser } Resized?.Invoke(newSize, PlatformResizeReason.User); + + (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } } @@ -271,6 +277,11 @@ namespace Avalonia.Browser return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index d1133b8916..c1811bb117 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -11,6 +11,15 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] public static partial JSObject CreateAvaloniaHost(JSObject element); + [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)] + public static partial bool IsFullscreen(); + + [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)] + public static partial JSObject SetFullscreen(bool isFullscreen); + + [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)] + public static partial byte[] GetSafeAreaPadding(); + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index b99f8e7907..d9790f69e9 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -84,4 +84,26 @@ export class AvaloniaDOM { inputElement }; } + + public static isFullscreen(): boolean { + return document.fullscreenElement != null; + } + + public static async setFullscreen(isFullscreen: boolean) { + if (isFullscreen) { + const doc = document.documentElement; + await doc.requestFullscreen(); + } else { + await document.exitFullscreen(); + } + } + + public static getSafeAreaPadding(): number[] { + const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sat")); + const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sab")); + const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sal")); + const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sar")); + + return [left, top, bottom, right]; + } } diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index ba321db144..cfb1b508d9 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -104,9 +104,7 @@ namespace Avalonia.Data CultureInfo.CurrentCulture); } - // Use LocalValue priority here, as TemplatedParent doesn't make sense on controls - // that aren't template children. - templatedParent.SetValue(Property, value, BindingPriority.LocalValue); + templatedParent.SetCurrentValue(Property, value); } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index af0231579c..e3e2f664c3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -516,7 +516,7 @@ namespace Avalonia.Skia return; } - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 90a2f9169b..29e5687423 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -16,14 +17,14 @@ namespace Avalonia.Skia return SKTypeface.Default.FamilyName; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { if (checkForUpdates) { _skFontManager = SKFontManager.CreateDefault(); } - return _skFontManager.FontFamilies; + return _skFontManager.GetFontFamilies(); } [ThreadStatic] private static string[]? t_languageTagBuffer; @@ -95,72 +96,58 @@ namespace Avalonia.Skia return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - SKTypeface? skTypeface = null; + glyphTypeface = null; - if(typeface.FontFamily.Key is not null) - { - var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - - skTypeface = fontCollection.Get(typeface); + var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, + (SKFontStyleSlant)style); - if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) - { - throw new InvalidOperationException( - $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); - } - } + var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); if (skTypeface is null) { - var defaultName = SKTypeface.Default.FamilyName; - - var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch, - (SKFontStyleSlant)typeface.Style); - - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - if(familyName == FontFamily.DefaultFontFamilyName) - { - continue; - } - - skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); - - if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) - { - continue; - } - - break; - } - - // MatchTypeface can return "null" if matched typeface wasn't found for the style - // Fallback to the default typeface and styles instead. - skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) - ?? SKTypeface.Default; + return false; } - - if (skTypeface == null) + + //MatchFamily can return a font other than we requested so we have to verify we got the expected. + if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal)) { - throw new InvalidOperationException( - $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + return false; } var fontSimulations = FontSimulations.None; - if((int)typeface.Weight >= 600 && !skTypeface.IsBold) + if ((int)weight >= 600 && !skTypeface.IsBold) { fontSimulations |= FontSimulations.Bold; } - if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic) + if (style == FontStyle.Italic && !skTypeface.IsItalic) { fontSimulations |= FontSimulations.Oblique; } - return new GlyphTypefaceImpl(skTypeface, fontSimulations); + glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var skTypeface = SKTypeface.FromStream(stream); + + if (skTypeface != null) + { + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } + + glyphTypeface = null; + + return false; } } } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 079eea7bef..0521e238f3 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Skia { TextBlob = textBlob ?? throw new ArgumentNullException(nameof(textBlob)); - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; } @@ -21,7 +21,7 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } - public Size Size { get; } + public Rect Bounds { get; } public Point BaselineOrigin { get; } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 3093455bec..43e10e3e96 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -51,6 +51,12 @@ namespace Avalonia.Skia GlyphCount = Typeface.GlyphCount; FontSimulations = fontSimulations; + + Weight = (FontWeight)Typeface.FontWeight; + + Style = Typeface.FontSlant.ToAvalonia(); + + Stretch = (FontStretch)Typeface.FontStyle.Width; } public Face Face { get; } @@ -67,6 +73,14 @@ namespace Avalonia.Skia public int GlyphCount { get; } + public string FamilyName => Typeface.FamilyName; + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) { metrics = default; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs deleted file mode 100644 index 9ee17a09d6..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class SKTypefaceCollection - { - private readonly ConcurrentDictionary _typefaces = new(); - - public void AddTypeface(Typeface key, SKTypeface typeface) - { - _typefaces.TryAdd(key, typeface); - } - - public SKTypeface? Get(Typeface typeface) - { - return GetNearestMatch(typeface); - } - - private SKTypeface? GetNearestMatch(Typeface key) - { - if (_typefaces.Count == 0) - { - return null; - } - - if (_typefaces.TryGetValue(key, out var typeface)) - { - return typeface; - } - - if(key.Style != FontStyle.Normal) - { - key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch); - } - - if(key.Stretch != FontStretch.Normal) - { - if(TryFindStretchFallback(key, out typeface)) - { - return typeface; - } - - if(key.Weight != FontWeight.Normal) - { - if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface)) - { - return typeface; - } - } - - key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal); - } - - if(TryFindWeightFallback(key, out typeface)) - { - return typeface; - } - - if (TryFindStretchFallback(key, out typeface)) - { - return typeface; - } - - //Nothing was found so we try some regular typeface. - if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) - { - return typeface; - } - - SKTypeface? skTypeface = null; - - foreach(var pair in _typefaces) - { - skTypeface = pair.Value; - - if (skTypeface.FamilyName.Contains(key.FontFamily.Name)) - { - return skTypeface; - } - } - - return skTypeface; - } - - private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) - { - typeface = null; - var stretch = (int)key.Stretch; - - if (stretch < 5) - { - for (var i = 0; stretch + i < 9; i++) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface)) - { - return true; - } - } - } - else - { - for (var i = 0; stretch - i > 1; i++) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface)) - { - return true; - } - } - } - - return false; - } - - private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) - { - typeface = null; - var weight = (int)key.Weight; - - //If the target weight given is between 400 and 500 inclusive - if (weight >= 400 && weight <= 500) - { - //Look for available weights between the target and 500, in ascending order. - for (var i = 0; weight + i <= 500; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights greater than 500, in ascending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - } - - //If a weight less than 400 is given, look for available weights less than the target, in descending order. - if (weight < 400) - { - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - } - - //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. - if (weight > 500) - { - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - } - - return false; - } - } -} diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs deleted file mode 100644 index d064f49ae4..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Media; -using Avalonia.Media.Fonts; -using Avalonia.Platform; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal static class SKTypefaceCollectionCache - { - private static readonly ConcurrentDictionary s_cachedCollections; - - static SKTypefaceCollectionCache() - { - s_cachedCollections = new ConcurrentDictionary(); - } - - /// - /// Gets the or add typeface collection. - /// - /// The font family. - /// - public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) - { - return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); - } - - /// - /// Creates the custom font collection. - /// - /// The font family. - /// - private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) - { - var typeFaceCollection = new SKTypefaceCollection(); - - if (fontFamily.Key is not { } fontFamilyKey) - { - return typeFaceCollection; - } - - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey); - - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - - foreach (var asset in fontAssets) - { - var assetStream = assetLoader.Open(asset); - - if (assetStream == null) - throw new InvalidOperationException("Asset could not be loaded."); - - var typeface = SKTypeface.FromStream(assetStream); - - if (typeface == null) - throw new InvalidOperationException("Typeface could not be loaded."); - - if (!typeface.FamilyName.Contains(fontFamily.Name)) - { - continue; - } - - var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), - (FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth); - - typeFaceCollection.AddTypeface(key, typeface); - } - - return typeFaceCollection; - } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs index 4663a6561f..b60962a091 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; -using Avalonia.Platform; using SharpDX; using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { - using System; + using System.IO; internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader { @@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media /// /// The factory. /// - public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets) + public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets) { var factory1 = factory; - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - foreach (var asset in fontAssets) { - var assetStream = assetLoader.Open(asset); - - var dataStream = new DataStream((int)assetStream.Length, true, true); + var dataStream = new DataStream((int)asset.Length, true, true); - assetStream.CopyTo(dataStream); + asset.CopyTo(dataStream); dataStream.Position = 0; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index 792bf2d0be..ad2ede3a91 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily; using FontStyle = SharpDX.DirectWrite.FontStyle; using FontWeight = SharpDX.DirectWrite.FontWeight; using FontStretch = SharpDX.DirectWrite.FontStretch; +using Avalonia.Platform; +using System.Linq; +using System; namespace Avalonia.Direct2D1.Media { @@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media private static FontCollection CreateFontCollection(FontFamilyKey key) { - var assets = FontFamilyLoader.LoadFontAssets(key); + var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source; - var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); + var assets = FontFamilyLoader.LoadFontAssets(source); + + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray(); + + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets); return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index f9b5953e3f..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 @@ -387,7 +386,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Item.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index b98ed3ffe6..ec2f6385da 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; -using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; using FontStretch = Avalonia.Media.FontStretch; using FontStyle = Avalonia.Media.FontStyle; @@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media return "Segoe UI"; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { - return new GlyphTypefaceImpl(typeface); + var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection; + + if (familyName == FontFamily.DefaultFontFamilyName) + { + familyName = "Segoe UI"; + } + + if (systemFonts.FindFamilyName(familyName, out var index)) + { + var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont( + (SharpDX.DirectWrite.FontWeight)weight, + (SharpDX.DirectWrite.FontStretch)stretch, + (SharpDX.DirectWrite.FontStyle)style); + + glyphTypeface = new GlyphTypefaceImpl(font); + + return true; + } + + glyphTypeface = null; + + return false; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream }); + + var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + + if (fontCollection.FontFamilyCount > 0) + { + var fontFamily = fontCollection.GetFontFamily(0); + + if (fontFamily.FontCount > 0) + { + var font = fontFamily.GetFont(0); + + glyphTypeface = new GlyphTypefaceImpl(font); + + return true; + } + } + + glyphTypeface = null; + + return false; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 446db47d92..2e7a4b67f6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Direct2D1.Media { public GlyphRunImpl(GlyphRun glyphRun, Size size, Point baselineOrigin) { - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; GlyphRun = glyphRun; } - public Size Size { get; } + public Rect Bounds{ get; } public Point BaselineOrigin { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index e4988322e7..01add0f0cb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media { private bool _isDisposed; - public GlyphTypefaceImpl(Typeface typeface) + public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font) { - DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + DWFont = font; FontFace = new FontFace(DWFont).QueryInterface(); @@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media StrikethroughThickness = strikethroughThickness, IsFixedPitch = FontFace.IsMonospacedFont }; + + FamilyName = DWFont.FontFamily.FamilyNames.GetString(0); + + Weight = (Avalonia.Media.FontWeight)DWFont.Weight; + + Style = (Avalonia.Media.FontStyle)DWFont.Style; + + Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; } private Blob GetTable(Face face, Tag tag) @@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media public FontSimulations FontSimulations => FontSimulations.None; + public string FamilyName { get; } + + public Avalonia.Media.FontWeight Weight { get; } + + public Avalonia.Media.FontStyle Style { get; } + + public Avalonia.Media.FontStretch Stretch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 41417dd950..db650db4b0 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); @@ -269,28 +271,28 @@ namespace Avalonia.Win32.Input // we're skipping this. not usable on windows } - public void CompositionChanged() + public void CompositionChanged(string? composition) { - if (!IsComposing) - { - return; - } + Composition = composition; - if(!IsActive || !Client.SupportsPreedit) + if (!IsActive || !Client.SupportsPreedit) { return; } - var composition = GetCompositionString(); - Client.SetPreeditText(composition); } - private string? GetCompositionString() + public string? GetCompositionString(GCS flag) { + if (!IsComposing) + { + return null; + } + var himc = ImmGetContext(Hwnd); - return ImmGetCompositionString(himc, GCS.GCS_COMPSTR); + return ImmGetCompositionString(himc, flag); } ~Imm32InputMethod() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index e8d2d8f0c5..2a40eb9f5b 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,25 +717,80 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_IME_COMPOSITION: { - Imm32InputMethod.Current.CompositionChanged(); + var previousComposition = Imm32InputMethod.Current.Composition; + + var flags = (GCS)ToInt32(lParam); + + var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR); + + Imm32InputMethod.Current.CompositionChanged(currentComposition); + + switch (flags) + { + case GCS.GCS_RESULTSTR: + { + if(ToInt32(wParam) >= 32) + { + Imm32InputMethod.Current.Composition = previousComposition; + + _ignoreWmChar = true; + } + break; + } + case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: + { + // Chinese IME sends WM_CHAR after composition has finished. + break; + } + case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: + { + // Japanese IME sends WM_CHAR after composition has finished. + break; + } + } break; } + case WindowsMessage.WM_IME_SELECT: + break; case WindowsMessage.WM_IME_CHAR: case WindowsMessage.WM_IME_COMPOSITIONFULL: case WindowsMessage.WM_IME_CONTROL: case WindowsMessage.WM_IME_KEYDOWN: case WindowsMessage.WM_IME_KEYUP: case WindowsMessage.WM_IME_NOTIFY: - case WindowsMessage.WM_IME_SELECT: break; case WindowsMessage.WM_IME_STARTCOMPOSITION: + Imm32InputMethod.Current.Composition = null; + + if (Imm32InputMethod.Current.IsActive) + { + Imm32InputMethod.Current.Client.SetPreeditText(null); + } + Imm32InputMethod.Current.IsComposing = true; return IntPtr.Zero; case WindowsMessage.WM_IME_ENDCOMPOSITION: - Imm32InputMethod.Current.IsComposing = false; - break; + { + var currentComposition = Imm32InputMethod.Current.Composition; + + //In case composition has not been comitted yet we need to do that here. + if (!string.IsNullOrEmpty(currentComposition)) + { + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, currentComposition); + } + //Cleanup composition state. + Imm32InputMethod.Current.IsComposing = false; + Imm32InputMethod.Current.Composition = null; + + if (Imm32InputMethod.Current.IsActive) + { + Imm32InputMethod.Current.Client.SetPreeditText(null); + } + + break; + } case WindowsMessage.WM_GETOBJECT: if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control) { diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index b605d82541..18ccbad692 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -40,10 +40,12 @@ namespace Avalonia.iOS var view = new AvaloniaView(); lifetime.View = view; - Window.RootViewController = new UIViewController + var controller = new DefaultAvaloniaViewController { View = view }; + Window.RootViewController = controller; + view.InitWithController(controller); }); builder.SetupWithLifetime(lifetime); diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 2d6b93f818..09721ad181 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -16,6 +16,7 @@ using Foundation; using ObjCRuntime; using OpenGLES; using UIKit; +using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.iOS { @@ -26,6 +27,7 @@ namespace Avalonia.iOS private EmbeddableControlRoot _topLevel; private TouchHandler _touches; private ITextInputMethodClient _client; + private IAvaloniaViewController _controller; public AvaloniaView() { @@ -48,10 +50,13 @@ namespace Avalonia.iOS MultipleTouchEnabled = true; } + /// public override bool CanBecomeFirstResponder => true; + /// public override bool CanResignFirstResponder => true; + /// public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) { base.TraitCollectionDidChange(previousTraitCollection); @@ -60,6 +65,7 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + /// public override void TintColorDidChange() { base.TintColorDidChange(); @@ -68,18 +74,31 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + public void InitWithController(TController controller) + where TController : UIViewController, IAvaloniaViewController + { + _controller = controller; + _topLevelImpl._insetsManager.InitWithController(controller); + } + internal class TopLevelImpl : ITopLevelImpl { private readonly AvaloniaView _view; private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; + internal readonly InsetsManager _insetsManager; public AvaloniaView View => _view; public TopLevelImpl(AvaloniaView view) { _view = view; - _nativeControlHost = new NativeControlHostImpl(_view); + _nativeControlHost = new NativeControlHostImpl(view); _storageProvider = new IOSStorageProvider(view); + _insetsManager = new InsetsManager(view); + _insetsManager.DisplayEdgeToEdgeChanged += (sender, b) => + { + view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding; + }; } public void Dispose() @@ -141,17 +160,14 @@ namespace Avalonia.iOS public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { // TODO adjust status bar depending on full screen mode. - if (OperatingSystem.IsIOSVersionAtLeast(13)) + if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null) { - var uiStatusBarStyle = themeVariant switch + _view._controller.PreferredStatusBarStyle = themeVariant switch { PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent, PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent, - _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + _ => UIStatusBarStyle.Default }; - - // Consider using UIViewController.PreferredStatusBarStyle in the future. - UIApplication.SharedApplication.SetStatusBarStyle(uiStatusBarStyle, true); } } @@ -175,6 +191,11 @@ namespace Avalonia.iOS return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs new file mode 100644 index 0000000000..62e560ddf9 --- /dev/null +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -0,0 +1,83 @@ +using System; +using Avalonia.Controls.Platform; +using UIKit; + +namespace Avalonia.iOS; +#nullable enable + +internal class InsetsManager : IInsetsManager +{ + private readonly AvaloniaView _view; + private IAvaloniaViewController? _controller; + private bool _displayEdgeToEdge; + + public InsetsManager(AvaloniaView view) + { + _view = view; + } + + internal void InitWithController(IAvaloniaViewController controller) + { + _controller = controller; + if (_controller is not null) + { + _controller.SafeAreaPaddingChanged += (_, _) => + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge); + }; + } + } + + public SystemBarTheme? SystemBarTheme + { + get => _controller?.PreferredStatusBarStyle switch + { + UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark, + UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light, + _ => null + }; + set + { + if (_controller != null) + { + _controller.PreferredStatusBarStyle = value switch + { + Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent, + Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent, + null => UIStatusBarStyle.Default, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }; + } + } + } + + public bool? IsSystemBarVisible + { + get => _controller?.PrefersStatusBarHidden == false; + set + { + if (_controller is not null) + { + _controller.PrefersStatusBarHidden = value == false; + } + } + } + public event EventHandler? SafeAreaChanged; + public event EventHandler? DisplayEdgeToEdgeChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + if (_displayEdgeToEdge != value) + { + _displayEdgeToEdge = value; + DisplayEdgeToEdgeChanged?.Invoke(this, value); + } + } + } + + public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; +} diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs new file mode 100644 index 0000000000..42a0949a9c --- /dev/null +++ b/src/iOS/Avalonia.iOS/ViewController.cs @@ -0,0 +1,74 @@ +using System; +using Avalonia.Metadata; +using UIKit; + +namespace Avalonia.iOS; + +[Unstable] +public interface IAvaloniaViewController +{ + UIStatusBarStyle PreferredStatusBarStyle { get; set; } + bool PrefersStatusBarHidden { get; set; } + Thickness SafeAreaPadding { get; } + event EventHandler SafeAreaPaddingChanged; +} + +/// +public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController +{ + private UIStatusBarStyle? _preferredStatusBarStyle; + private bool? _prefersStatusBarHidden; + + /// + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + var size = View?.Frame.Size ?? default; + var frame = View?.SafeAreaLayoutGuide.LayoutFrame ?? default; + var safeArea = new Thickness(frame.Left, frame.Top, size.Width - frame.Right, size.Height - frame.Bottom); + if (SafeAreaPadding != safeArea) + { + SafeAreaPadding = safeArea; + SafeAreaPaddingChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public override bool PrefersStatusBarHidden() + { + return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden(); + } + + /// + public override UIStatusBarStyle PreferredStatusBarStyle() + { + // don't set _preferredStatusBarStyle value if it's null, so we can keep "default" there instead of actual app style. + return _preferredStatusBarStyle ?? base.PreferredStatusBarStyle(); + } + + UIStatusBarStyle IAvaloniaViewController.PreferredStatusBarStyle + { + get => _preferredStatusBarStyle ?? UIStatusBarStyle.Default; + set + { + _preferredStatusBarStyle = value; + SetNeedsStatusBarAppearanceUpdate(); + } + } + + bool IAvaloniaViewController.PrefersStatusBarHidden + { + get => _prefersStatusBarHidden ?? false; // false is default on ios/ipados + set + { + _prefersStatusBarHidden = value; + SetNeedsStatusBarAppearanceUpdate(); + } + } + + /// + public Thickness SafeAreaPadding { get; private set; } + + /// + public event EventHandler SafeAreaPaddingChanged; +} diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj new file mode 100644 index 0000000000..586caee605 --- /dev/null +++ b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj @@ -0,0 +1,33 @@ + + + netstandard2.0 + false + Avalonia.Generators + $(DefineConstants);XAMLX_INTERNAL + true + true + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.props b/src/tools/Avalonia.Generators/Avalonia.Generators.props new file mode 100644 index 0000000000..08cbeff1ba --- /dev/null +++ b/src/tools/Avalonia.Generators/Avalonia.Generators.props @@ -0,0 +1,22 @@ + + + true + InitializeComponent + internal + * + * + + + + + + + + + + + + + + + diff --git a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs new file mode 100644 index 0000000000..4b426172f8 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.Common.Domain; + +internal interface ICodeGenerator +{ + string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names); +} diff --git a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs new file mode 100644 index 0000000000..04dbf9cbb9 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Generators.Common.Domain; + +internal interface IGlobPattern +{ + bool Matches(string str); +} diff --git a/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs new file mode 100644 index 0000000000..cb5488d8a3 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using XamlX.Ast; + +namespace Avalonia.Generators.Common.Domain; + +internal enum NamedFieldModifier +{ + Public = 0, + Private = 1, + Internal = 2, + Protected = 3, +} + +internal interface INameResolver +{ + IReadOnlyList ResolveNames(XamlDocument xaml); +} + +internal record ResolvedName(string TypeName, string Name, string FieldModifier); diff --git a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs new file mode 100644 index 0000000000..c3c219e3f0 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs @@ -0,0 +1,11 @@ +using XamlX.Ast; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.Common.Domain; + +internal interface IViewResolver +{ + ResolvedView ResolveView(string xaml); +} + +internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml); diff --git a/src/tools/Avalonia.Generators/Common/GlobPattern.cs b/src/tools/Avalonia.Generators/Common/GlobPattern.cs new file mode 100644 index 0000000000..484e17d787 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/GlobPattern.cs @@ -0,0 +1,18 @@ +using System.Text.RegularExpressions; +using Avalonia.Generators.Common.Domain; + +namespace Avalonia.Generators.Common; + +internal class GlobPattern : IGlobPattern +{ + private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; + private readonly Regex _regex; + + public GlobPattern(string pattern) + { + var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$"; + _regex = new Regex(expression, GlobOptions); + } + + public bool Matches(string str) => _regex.IsMatch(str); +} diff --git a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs new file mode 100644 index 0000000000..1358ee7920 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Generators.Common.Domain; + +namespace Avalonia.Generators.Common; + +internal class GlobPatternGroup : IGlobPattern +{ + private readonly GlobPattern[] _patterns; + + public GlobPatternGroup(IEnumerable patterns) => + _patterns = patterns + .Select(pattern => new GlobPattern(pattern)) + .ToArray(); + + public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str)); +} diff --git a/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs new file mode 100644 index 0000000000..04352298c8 --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs @@ -0,0 +1,25 @@ +using System.Linq; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.Common; + +internal static class ResolverExtensions +{ + public static bool IsAvaloniaStyledElement(this IXamlType clrType) => + clrType.HasStyledElementBaseType() || + clrType.HasIStyledElementInterface(); + + private static bool HasStyledElementBaseType(this IXamlType clrType) + { + // Check for the base type since IStyledElement interface is removed. + // https://github.com/AvaloniaUI/Avalonia/pull/9553 + if (clrType.FullName == "Avalonia.StyledElement") + return true; + return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType); + } + + private static bool HasIStyledElementInterface(this IXamlType clrType) => + clrType.Interfaces.Any(abstraction => + abstraction.IsInterface && + abstraction.FullName == "Avalonia.IStyledElement"); +} diff --git a/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs new file mode 100644 index 0000000000..7ed19eb84c --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Avalonia.Generators.Common.Domain; +using XamlX; +using XamlX.Ast; + +namespace Avalonia.Generators.Common; + +internal class XamlXNameResolver : INameResolver, IXamlAstVisitor +{ + private readonly List _items = new(); + private readonly string _defaultFieldModifier; + + public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) + { + _defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant(); + } + + public IReadOnlyList ResolveNames(XamlDocument xaml) + { + _items.Clear(); + xaml.Root.Visit(this); + xaml.Root.VisitChildren(this); + return _items; + } + + IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode) + return node; + + var clrType = objectNode.Type.GetClrType(); + if (!clrType.IsAvaloniaStyledElement()) + return node; + + foreach (var child in objectNode.Children) + { + if (child is XamlAstXamlPropertyValueNode propertyValueNode && + propertyValueNode.Property is XamlAstNamePropertyReference namedProperty && + namedProperty.Name == "Name" && + propertyValueNode.Values.Count > 0 && + propertyValueNode.Values[0] is XamlAstTextNode text) + { + var fieldModifier = TryGetFieldModifier(objectNode); + var typeName = $@"{clrType.Namespace}.{clrType.Name}"; + var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList(); + var genericTypeName = typeAgs.Count == 0 + ? $"global::{typeName}" + : $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>"; + + var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier); + if (_items.Contains(resolvedName)) + continue; + _items.Add(resolvedName); + } + } + + return node; + } + + void IXamlAstVisitor.Push(IXamlAstNode node) { } + + void IXamlAstVisitor.Pop() { } + + private string TryGetFieldModifier(XamlAstObjectNode objectNode) + { + // We follow Xamarin.Forms API behavior in terms of x:FieldModifier here: + // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers + // However, by default we use 'internal' field modifier here for generated + // x:Name references for historical purposes and WPF compatibility. + // + var fieldModifierType = objectNode + .Children + .OfType() + .Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006) + .Select(dir => dir.Values[0]) + .OfType() + .Select(txt => txt.Text) + .FirstOrDefault(); + + return fieldModifierType?.ToLowerInvariant() switch + { + "private" => "private", + "public" => "public", + "protected" => "protected", + "internal" => "internal", + "notpublic" => "internal", + _ => _defaultFieldModifier + }; + } +} diff --git a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs new file mode 100644 index 0000000000..5bbe0c060d --- /dev/null +++ b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Generators.Common.Domain; +using Avalonia.Generators.Compiler; +using XamlX; +using XamlX.Ast; +using XamlX.Parsers; + +namespace Avalonia.Generators.Common; + +internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor +{ + private readonly RoslynTypeSystem _typeSystem; + private readonly MiniCompiler _compiler; + private readonly bool _checkTypeValidity; + private readonly Action _onTypeInvalid; + private readonly Action _onUnhandledError; + + private ResolvedView _resolvedClass; + private XamlDocument _xaml; + + public XamlXViewResolver( + RoslynTypeSystem typeSystem, + MiniCompiler compiler, + bool checkTypeValidity = false, + Action onTypeInvalid = null, + Action onUnhandledError = null) + { + _checkTypeValidity = checkTypeValidity; + _onTypeInvalid = onTypeInvalid; + _onUnhandledError = onUnhandledError; + _typeSystem = typeSystem; + _compiler = compiler; + } + + public ResolvedView ResolveView(string xaml) + { + try + { + _resolvedClass = null; + _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary + { + {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} + }); + + _compiler.Transform(_xaml); + _xaml.Root.Visit(this); + _xaml.Root.VisitChildren(this); + return _resolvedClass; + } + catch (Exception exception) + { + _onUnhandledError?.Invoke(exception); + return null; + } + } + + IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode) + return node; + + var clrType = objectNode.Type.GetClrType(); + if (!clrType.IsAvaloniaStyledElement()) + return node; + + foreach (var child in objectNode.Children) + { + if (child is XamlAstXmlDirective directive && + directive.Name == "Class" && + directive.Namespace == XamlNamespaces.Xaml2006 && + directive.Values[0] is XamlAstTextNode text) + { + if (_checkTypeValidity) + { + var existingType = _typeSystem.FindType(text.Text); + if (existingType == null) + { + _onTypeInvalid?.Invoke(text.Text); + return node; + } + } + + var split = text.Text.Split('.'); + var nameSpace = string.Join(".", split.Take(split.Length - 1)); + var className = split.Last(); + + _resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml); + return node; + } + } + + return node; + } + + void IXamlAstVisitor.Push(IXamlAstNode node) { } + + void IXamlAstVisitor.Pop() { } +} diff --git a/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs b/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs new file mode 100644 index 0000000000..e7c60c79ad --- /dev/null +++ b/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs @@ -0,0 +1,17 @@ +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Generators.Compiler; + +internal class DataTemplateTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode objectNode && + objectNode.Type is XamlAstXmlTypeReference typeReference && + (typeReference.Name == "DataTemplate" || + typeReference.Name == "ControlTemplate")) + objectNode.Children.Clear(); + return node; + } +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs new file mode 100644 index 0000000000..71f34d173c --- /dev/null +++ b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using XamlX.Compiler; +using XamlX.Emit; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.Compiler; + +internal sealed class MiniCompiler : XamlCompiler +{ + public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; + + public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) + { + var mappings = new XamlLanguageTypeMappings(typeSystem); + foreach (var additionalType in additionalTypes) + mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType)); + + var configuration = new TransformerConfiguration( + typeSystem, + typeSystem.Assemblies.First(), + mappings); + return new MiniCompiler(configuration); + } + + private MiniCompiler(TransformerConfiguration configuration) + : base(configuration, new XamlLanguageEmitMappings(), false) + { + Transformers.Add(new NameDirectiveTransformer()); + Transformers.Add(new DataTemplateTransformer()); + Transformers.Add(new KnownDirectivesTransformer()); + Transformers.Add(new XamlIntrinsicsTransformer()); + Transformers.Add(new XArgumentsTransformer()); + Transformers.Add(new TypeReferenceResolver()); + } + + protected override XamlEmitContext InitCodeGen( + IFileSource file, + Func> createSubType, + Func, + IXamlTypeBuilder> createDelegateType, + object codeGen, + XamlRuntimeContext context, + bool needContextLocal) => + throw new NotSupportedException(); +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs b/src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs new file mode 100644 index 0000000000..2d4d3225b7 --- /dev/null +++ b/src/tools/Avalonia.Generators/Compiler/NameDirectiveTransformer.cs @@ -0,0 +1,28 @@ +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Generators.Compiler; + +internal class NameDirectiveTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode) + return node; + + for (var index = 0; index < objectNode.Children.Count; index++) + { + var child = objectNode.Children[index]; + if (child is XamlAstXmlDirective directive && + directive.Namespace == XamlNamespaces.Xaml2006 && + directive.Name == "Name") + objectNode.Children[index] = new XamlAstXamlPropertyValueNode( + directive, + new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), + directive.Values); + } + + return node; + } +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs new file mode 100644 index 0000000000..851bc3f29b --- /dev/null +++ b/src/tools/Avalonia.Generators/Compiler/RoslynTypeSystem.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.Compiler; + +internal class RoslynTypeSystem : IXamlTypeSystem +{ + private readonly List _assemblies = new(); + + public RoslynTypeSystem(CSharpCompilation compilation) + { + _assemblies.Add(new RoslynAssembly(compilation.Assembly)); + + var assemblySymbols = compilation + .References + .Select(compilation.GetAssemblyOrModuleSymbol) + .OfType() + .Select(assembly => new RoslynAssembly(assembly)) + .ToList(); + + _assemblies.AddRange(assemblySymbols); + } + + public IEnumerable Assemblies => _assemblies; + + public IXamlAssembly FindAssembly(string name) => + Assemblies + .FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)); + + public IXamlType FindType(string name) => + _assemblies + .Select(assembly => assembly.FindType(name)) + .FirstOrDefault(type => type != null); + + public IXamlType FindType(string name, string assembly) => + _assemblies + .Select(assemblyInstance => assemblyInstance.FindType(name)) + .FirstOrDefault(type => type != null); +} + +internal class RoslynAssembly : IXamlAssembly +{ + private readonly IAssemblySymbol _symbol; + + public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol; + + public bool Equals(IXamlAssembly other) => + other is RoslynAssembly roslynAssembly && + SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol); + + public string Name => _symbol.Name; + + public IReadOnlyList CustomAttributes => + _symbol.GetAttributes() + .Select(data => new RoslynAttribute(data, this)) + .ToList(); + + public IXamlType FindType(string fullName) + { + var type = _symbol.GetTypeByMetadataName(fullName); + return type is null ? null : new RoslynType(type, this); + } +} + +internal class RoslynAttribute : IXamlCustomAttribute +{ + private readonly AttributeData _data; + private readonly RoslynAssembly _assembly; + + public RoslynAttribute(AttributeData data, RoslynAssembly assembly) + { + _data = data; + _assembly = assembly; + } + + public bool Equals(IXamlCustomAttribute other) => + other is RoslynAttribute attribute && + _data == attribute._data; + + public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly); + + public List Parameters => + _data.ConstructorArguments + .Select(argument => argument.Value) + .ToList(); + + public Dictionary Properties => + _data.NamedArguments.ToDictionary( + pair => pair.Key, + pair => pair.Value.Value); +} + +internal class RoslynType : IXamlType +{ + private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeVariance); + + private readonly RoslynAssembly _assembly; + private readonly INamedTypeSymbol _symbol; + + public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlType other) => + other is RoslynType roslynType && + SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol); + + public object Id => _symbol; + + public string Name => _symbol.Name; + + public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); + + public string FullName => $"{Namespace}.{Name}"; + + public IXamlAssembly Assembly => _assembly; + + public IReadOnlyList Properties => + _symbol.GetMembers() + .Where(member => member.Kind == SymbolKind.Property) + .OfType() + .Select(property => new RoslynProperty(property, _assembly)) + .ToList(); + + public IReadOnlyList Events { get; } = new List(); + + public IReadOnlyList Fields { get; } = new List(); + + public IReadOnlyList Methods { get; } = new List(); + + public IReadOnlyList Constructors => + _symbol.Constructors + .Select(method => new RoslynConstructor(method, _assembly)) + .ToList(); + + public IReadOnlyList CustomAttributes { get; } = new List(); + + public IReadOnlyList GenericArguments { get; private set; } = new List(); + + public bool IsAssignableFrom(IXamlType type) => type == this; + + public IXamlType MakeGenericType(IReadOnlyList typeArguments) + { + GenericArguments = typeArguments; + return this; + } + + public IXamlType GenericTypeDefinition => this; + + public bool IsArray => false; + + public IXamlType ArrayElementType { get; } = null; + + public IXamlType MakeArrayType(int dimensions) => null; + + public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly); + + public bool IsValueType { get; } = false; + + public bool IsEnum { get; } = false; + + public IReadOnlyList Interfaces => + _symbol.AllInterfaces + .Select(abstraction => new RoslynType(abstraction, _assembly)) + .ToList(); + + public bool IsInterface => _symbol.IsAbstract; + + public IXamlType GetEnumUnderlyingType() => null; + + public IReadOnlyList GenericParameters { get; } = new List(); +} + +internal class RoslynConstructor : IXamlConstructor +{ + private readonly IMethodSymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlConstructor other) => + other is RoslynConstructor roslynConstructor && + SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol); + + public bool IsPublic => true; + + public bool IsStatic => false; + + public IReadOnlyList Parameters => + _symbol.Parameters + .Select(parameter => parameter.Type) + .OfType() + .Select(type => new RoslynType(type, _assembly)) + .ToList(); +} + +internal class RoslynProperty : IXamlProperty +{ + private readonly IPropertySymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlProperty other) => + other is RoslynProperty roslynProperty && + SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol); + + public string Name => _symbol.Name; + + public IXamlType PropertyType => + _symbol.Type is INamedTypeSymbol namedTypeSymbol + ? new RoslynType(namedTypeSymbol, _assembly) + : null; + + public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly); + + public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly); + + public IReadOnlyList CustomAttributes { get; } = new List(); + + public IReadOnlyList IndexerParameters { get; } = new List(); +} + +internal class RoslynMethod : IXamlMethod +{ + private readonly IMethodSymbol _symbol; + private readonly RoslynAssembly _assembly; + + public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly) + { + _symbol = symbol; + _assembly = assembly; + } + + public bool Equals(IXamlMethod other) => + other is RoslynMethod roslynMethod && + SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol); + + public string Name => _symbol.Name; + + public bool IsPublic => true; + + public bool IsStatic => false; + + public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly); + + public IReadOnlyList Parameters => + _symbol.Parameters.Select(parameter => parameter.Type) + .OfType() + .Select(type => new RoslynType(type, _assembly)) + .ToList(); + + public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly); + + public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => null; + + public IReadOnlyList CustomAttributes { get; } = new List(); +} diff --git a/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs new file mode 100644 index 0000000000..d057e8732e --- /dev/null +++ b/src/tools/Avalonia.Generators/GeneratorContextExtensions.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators; + +internal static class GeneratorContextExtensions +{ + private const string UnhandledErrorDescriptorId = "AXN0002"; + private const string InvalidTypeDescriptorId = "AXN0001"; + + public static string GetMsBuildProperty( + this GeneratorExecutionContext context, + string name, + string defaultValue = "") + { + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); + return value ?? defaultValue; + } + + public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => + context.Report(UnhandledErrorDescriptorId, + "Unhandled exception occured while generating typed Name references. " + + "Please file an issue: https://github.com/avaloniaui/Avalonia.Generators", + error.ToString()); + + public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => + context.Report(InvalidTypeDescriptorId, + $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + + $"The type '{typeName}' does not exist in the assembly."); + + private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) => + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true), + Location.None)); +} diff --git a/src/tools/Avalonia.Generators/GeneratorOptions.cs b/src/tools/Avalonia.Generators/GeneratorOptions.cs new file mode 100644 index 0000000000..bfa91f691c --- /dev/null +++ b/src/tools/Avalonia.Generators/GeneratorOptions.cs @@ -0,0 +1,71 @@ +using System; +using Avalonia.Generators.Common.Domain; +using Avalonia.Generators.NameGenerator; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators; + +// When update these enum values, don't forget to update Avalonia.Generators.props. +internal enum BuildProperties +{ + AvaloniaNameGeneratorIsEnabled = 0, + AvaloniaNameGeneratorBehavior = 1, + AvaloniaNameGeneratorDefaultFieldModifier = 2, + AvaloniaNameGeneratorFilterByPath = 3, + AvaloniaNameGeneratorFilterByNamespace = 4, + AvaloniaNameGeneratorViewFileNamingStrategy = 5, + + // TODO add other generators properties here. +} + +internal class GeneratorOptions +{ + private readonly GeneratorExecutionContext _context; + + public GeneratorOptions(GeneratorExecutionContext context) => _context = context; + + public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty( + BuildProperties.AvaloniaNameGeneratorIsEnabled, + true); + + public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( + BuildProperties.AvaloniaNameGeneratorBehavior, + Behavior.InitializeComponent); + + public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( + BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier, + NamedFieldModifier.Internal); + + public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( + BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy, + ViewFileNamingStrategy.NamespaceAndClassName); + + public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( + BuildProperties.AvaloniaNameGeneratorFilterByPath, + "*"); + + public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( + BuildProperties.AvaloniaNameGeneratorFilterByNamespace, + "*"); + + private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) + { + var key = name.ToString(); + var value = _context.GetMsBuildProperty(key, defaultValue); + return value.Contains(";") ? value.Split(';') : new[] {value}; + } + + private TEnum GetEnumProperty(BuildProperties name, TEnum defaultValue) where TEnum : struct + { + var key = name.ToString(); + var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); + return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue; + } + + private bool GetBoolProperty(BuildProperties name, bool defaultValue) + { + var key = name.ToString(); + var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); + return bool.TryParse(value, out var result) ? result : defaultValue; + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs new file mode 100644 index 0000000000..8d45afafa7 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameGenerator.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Generators.Common.Domain; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal class AvaloniaNameGenerator : INameGenerator +{ + private readonly ViewFileNamingStrategy _naming; + private readonly IGlobPattern _pathPattern; + private readonly IGlobPattern _namespacePattern; + private readonly IViewResolver _classes; + private readonly INameResolver _names; + private readonly ICodeGenerator _code; + + public AvaloniaNameGenerator( + ViewFileNamingStrategy naming, + IGlobPattern pathPattern, + IGlobPattern namespacePattern, + IViewResolver classes, + INameResolver names, + ICodeGenerator code) + { + _naming = naming; + _pathPattern = pathPattern; + _namespacePattern = namespacePattern; + _classes = classes; + _names = names; + _code = code; + } + + public IReadOnlyList GenerateNameReferences(IEnumerable additionalFiles) + { + var resolveViews = + from file in additionalFiles + where (file.Path.EndsWith(".xaml") || + file.Path.EndsWith(".paml") || + file.Path.EndsWith(".axaml")) && + _pathPattern.Matches(file.Path) + let xaml = file.GetText()!.ToString() + let view = _classes.ResolveView(xaml) + where view != null && _namespacePattern.Matches(view.Namespace) + select view; + + var query = + from view in resolveViews + let names = _names.ResolveNames(view.Xaml) + let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names) + let fileName = ResolveViewFileName(view, _naming) + select new GeneratedPartialClass(fileName, code); + + return query.ToList(); + } + + private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch + { + ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", + ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", + _ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!") + }; +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs new file mode 100644 index 0000000000..fa1dcde064 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameSourceGenerator.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Generators.Common; +using Avalonia.Generators.Common.Domain; +using Avalonia.Generators.Compiler; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Avalonia.Generators.NameGenerator; + +[Generator] +public class AvaloniaNameSourceGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) { } + + public void Execute(GeneratorExecutionContext context) + { + try + { + var generator = CreateNameGenerator(context); + if (generator is null) + { + return; + } + + var partials = generator.GenerateNameReferences(context.AdditionalFiles); + foreach (var (fileName, content) in partials) context.AddSource(fileName, content); + } + catch (Exception exception) + { + context.ReportNameGeneratorUnhandledError(exception); + } + } + + private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context) + { + var options = new GeneratorOptions(context); + if (!options.AvaloniaNameGeneratorIsEnabled) + { + return null; + } + + var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation); + ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch { + Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), + Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types), + _ => throw new ArgumentOutOfRangeException() + }; + + var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); + return new AvaloniaNameGenerator( + options.AvaloniaNameGeneratorViewFileNamingStrategy, + new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath), + new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace), + new XamlXViewResolver(types, compiler, true, + type => context.ReportNameGeneratorInvalidType(type), + error => context.ReportNameGeneratorUnhandledError(error)), + new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), + generator); + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs new file mode 100644 index 0000000000..922d800332 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/INameGenerator.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal interface INameGenerator +{ + IReadOnlyList GenerateNameReferences(IEnumerable additionalFiles); +} + +internal record GeneratedPartialClass(string FileName, string Content); diff --git a/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs new file mode 100644 index 0000000000..b84c65f7b5 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/InitializeComponentCodeGenerator.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Avalonia.Generators.Common.Domain; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.NameGenerator; + +internal class InitializeComponentCodeGenerator: ICodeGenerator +{ + private readonly bool _diagnosticsAreConnected; + private const string AttachDevToolsCodeBlock = @" +#if DEBUG + if (attachDevTools) + { + this.AttachDevTools(); + } +#endif +"; + private const string AttachDevToolsParameterDocumentation + = @" /// Should the dev tools be attached. +"; + + public InitializeComponentCodeGenerator(IXamlTypeSystem types) + { + _diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null; + } + + public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + { + var properties = new List(); + var initializations = new List(); + foreach (var resolvedName in names) + { + var (typeName, name, fieldModifier) = resolvedName; + properties.Add($" {fieldModifier} {typeName} {name};"); + initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");"); + } + + var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); + + return $@"// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace {nameSpace} +{{ + partial class {className} + {{ +{string.Join("\n", properties)} + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. +{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)} + public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)}) + {{ + if (loadXaml) + {{ + AvaloniaXamlLoader.Load(this); + }} +{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)} +{string.Join("\n", initializations)} + }} + }} +}} +"; + } + + private static bool IsWindow(IXamlType xamlType) + { + var type = xamlType; + bool isWindow; + do + { + isWindow = type.FullName == "Avalonia.Controls.Window"; + type = type.BaseType; + } while (!isWindow && type != null); + + return isWindow; + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs new file mode 100644 index 0000000000..367874e212 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/OnlyPropertiesCodeGenerator.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Generators.Common.Domain; +using XamlX.TypeSystem; + +namespace Avalonia.Generators.NameGenerator; + +internal class OnlyPropertiesCodeGenerator : ICodeGenerator +{ + public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names) + { + var namedControls = names + .Select(info => " " + + $"{info.FieldModifier} {info.TypeName} {info.Name} => " + + $"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");") + .ToList(); + var lines = string.Join("\n", namedControls); + return $@"// + +using Avalonia.Controls; + +namespace {nameSpace} +{{ + partial class {className} + {{ +{lines} + }} +}} +"; + } +} diff --git a/src/tools/Avalonia.Generators/NameGenerator/Options.cs b/src/tools/Avalonia.Generators/NameGenerator/Options.cs new file mode 100644 index 0000000000..abdaaab72b --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/Options.cs @@ -0,0 +1,21 @@ +namespace Avalonia.Generators.NameGenerator; + +internal enum Options +{ + Public = 0, + Private = 1, + Internal = 2, + Protected = 3, +} + +internal enum Behavior +{ + OnlyProperties = 0, + InitializeComponent = 1, +} + +internal enum ViewFileNamingStrategy +{ + ClassName = 0, + NamespaceAndClassName = 1, +} diff --git a/src/tools/Avalonia.Generators/Properties/launchSettings.json b/src/tools/Avalonia.Generators/Properties/launchSettings.json new file mode 100644 index 0000000000..2066a00fc5 --- /dev/null +++ b/src/tools/Avalonia.Generators/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Profile 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj" + } + } +} \ No newline at end of file diff --git a/src/tools/Avalonia.Generators/README.md b/src/tools/Avalonia.Generators/README.md new file mode 100644 index 0000000000..73e9e71196 --- /dev/null +++ b/src/tools/Avalonia.Generators/README.md @@ -0,0 +1,209 @@ +[![NuGet Stats](https://img.shields.io/nuget/v/XamlNameReferenceGenerator.svg)](https://www.nuget.org/packages/XamlNameReferenceGenerator) [![downloads](https://img.shields.io/nuget/dt/XamlNameReferenceGenerator)](https://www.nuget.org/packages/XamlNameReferenceGenerator) ![Build](https://github.com/avaloniaui/Avalonia.NameGenerator/workflows/Build/badge.svg) ![License](https://img.shields.io/github/license/avaloniaui/Avalonia.NameGenerator.svg) ![Size](https://img.shields.io/github/repo-size/avaloniaui/Avalonia.NameGenerator.svg) + +### C# `SourceGenerator` for Typed Avalonia `x:Name` References + +This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code. + +### Getting Started + +In order to get started, just install the NuGet package: + +``` +dotnet add package XamlNameReferenceGenerator +``` + +Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such: + +```xml + + + + + +``` + +### Usage + +After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup: + +```xml + + + +``` + +A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below: + +```cs +using Avalonia.Controls; + +namespace Sample.App +{ + public partial class SignUpView : Window + { + public SignUpView() + { + // This method is generated. Call it before accessing any + // of the generated properties. The 'UserNameTextBox' + // property is also generated. + InitializeComponent(); + UserNameTextBox.Text = "Joseph"; + } + } +} +``` + + + +### Why do I need this? + +The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/): + +```cs +// UserNameValidation and PasswordValidation are auto generated. +public partial class SignUpView : ReactiveWindow +{ + public SignUpView() + { + InitializeComponent(); + this.WhenActivated(disposables => + { + this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text) + .DisposeWith(disposables); + this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text) + .DisposeWith(disposables); + }); + } +} +``` + +### Advanced Usage + +> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`. + +The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options: + +- `AvaloniaNameGeneratorBehavior` + Possible values: `OnlyProperties`, `InitializeComponent` + Default value: `InitializeComponent` + Determines if the generator should generate get-only properties, or the `InitializeComponent` method. + +- `AvaloniaNameGeneratorDefaultFieldModifier` + Possible values: `internal`, `public`, `private`, `protected` + Default value: `internal` + The default field modifier that should be used when there is no `x:FieldModifier` directive specified. + +- `AvaloniaNameGeneratorFilterByPath` + Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` + Default value: `*` + The generator will process only XAML files with paths matching the specified glob pattern(s). + Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml` + +- `AvaloniaNameGeneratorFilterByNamespace` + Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` + Default value: `*` + The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s). + Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls` + +- `AvaloniaNameGeneratorViewFileNamingStrategy` + Possible values: `ClassName`, `NamespaceAndClassName` + Default value: `NamespaceAndClassName` + Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92). + +The default values are given by: + +```xml + + + InitializeComponent + internal + * + * + NamespaceAndClassName + + + +``` + +![](https://user-images.githubusercontent.com/6759207/107812261-7ddfea00-6d80-11eb-9c7e-67bf95d0f0d4.gif) + +### What do the generated sources look like? + +For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode: + +```cs +// + +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox; + public global::Avalonia.Controls.TextBlock UserNameValidation; + private global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.TextBlock PasswordValidation; + internal global::Avalonia.Controls.ListBox AwesomeListView; + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; + internal global::Avalonia.Controls.Button SignUpButton; + internal global::Avalonia.Controls.TextBlock CompoundValidation; + + public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + +// This will be added only if you install Avalonia.Diagnostics. +#if DEBUG + if (attachDevTools) + { + this.AttachDevTools(); + } +#endif + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + UserNameValidation = this.FindNameScope()?.Find("UserNameValidation"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + PasswordValidation = this.FindNameScope()?.Find("PasswordValidation"); + AwesomeListView = this.FindNameScope()?.Find("AwesomeListView"); + ConfirmPasswordTextBox = this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + ConfirmPasswordValidation = this.FindNameScope()?.Find("ConfirmPasswordValidation"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + CompoundValidation = this.FindNameScope()?.Find("CompoundValidation"); + } + } +} +``` + +If you enable the `OnlyProperties` source generator mode, you get: + +```cs +// + +using Avalonia.Controls; + +namespace Avalonia.NameGenerator.Sandbox.Views +{ + partial class SignUpView + { + internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find("UserNameValidation"); + private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find("PasswordValidation"); + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find("ConfirmPasswordValidation"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find("CompoundValidation"); + } +} +``` diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 11ecac0039..89e609eb10 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media var typeface = new Typeface(fontFamily); - var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)); - Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); + FontManager.Current.TryGetGlyphTypeface(typeface, out var other); + + Assert.Same(glyphTypeface, other); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs index afc25ab88e..82dcd8f4fc 100644 --- a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs @@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset() { var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset_Avares_Without_BaseUri() { var source = new Uri(AssetYourFontAvares); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute); var baseUri = new Uri(AssetLocationAvares); - var key = new FontFamilyKey(source, baseUri); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source)); Assert.Single(fontAssets); } @@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Matching_Assets() { var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); foreach (var fontAsset in fontAssets) { @@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono"); + var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute); - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); Assert.NotEmpty(fontAssets); diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs index 738469bc6f..7528815510 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Styling .Template() .OfType(); - border.SetValue(StyledElement.TemplatedParentProperty, null); + border.TemplatedParent = null; Assert.Equal(SelectorMatchResult.NeverThisInstance, selector.Match(border).Result); } @@ -124,10 +124,10 @@ namespace Avalonia.Base.UnitTests.Styling { VisualChildren.Add(new Border { - [TemplatedParentProperty] = this, + TemplatedParent = this, Child = new TextBlock { - [TemplatedParentProperty] = this, + TemplatedParent = this, }, }); } diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index c4707c78c8..5b584f302d 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -5,7 +5,7 @@ namespace Avalonia.Benchmarks { internal class NullGlyphRun : IGlyphRunImpl { - public Size Size => default; + public Rect Bounds => default; public Point BaselineOrigin => default; 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; } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3aaf62f0bf..7b5aa83b46 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -131,7 +131,7 @@ namespace Avalonia.Controls.UnitTests root.Content = target; var templatedParent = new Button(); - target.SetValue(StyledElement.TemplatedParentProperty, templatedParent); + target.TemplatedParent = templatedParent; target.Template = GetTemplate(); target.Items = new[] { "Foo" }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index d3737de45b..9042e84fa1 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(host.Presenter); - target.SetValue(Control.TemplatedParentProperty, host); + target.TemplatedParent = host; Assert.Same(target, host.Presenter); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 71f803fab7..573ce5834d 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(host.Presenter); - target.SetValue(Control.TemplatedParentProperty, host); + target.TemplatedParent = host; Assert.Same(target, host.Presenter); } diff --git a/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs b/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs index 8789d79742..33593a0631 100644 --- a/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs @@ -18,17 +18,17 @@ namespace Avalonia.Controls.Templates.UnitTests var border1 = new Border { Name = "border1", - [StyledElement.TemplatedParentProperty] = target, + TemplatedParent = target, }; var inner = new TestTemplatedControl { Name = "inner", - [StyledElement.TemplatedParentProperty] = target, + TemplatedParent = target, }; - var border2 = new Border { Name = "border2", [StyledElement.TemplatedParentProperty] = inner }; - var border3 = new Border { Name = "border3", [StyledElement.TemplatedParentProperty] = inner }; - var border4 = new Border { Name = "border4", [StyledElement.TemplatedParentProperty] = target }; - var border5 = new Border { Name = "border5", [StyledElement.TemplatedParentProperty] = null }; + var border2 = new Border { Name = "border2", TemplatedParent = inner }; + var border3 = new Border { Name = "border3", TemplatedParent = inner }; + var border4 = new Border { Name = "border4", TemplatedParent = target }; + var border5 = new Border { Name = "border5", TemplatedParent = null }; target.AddVisualChild(border1); border1.Child = inner; @@ -42,4 +42,4 @@ namespace Avalonia.Controls.Templates.UnitTests Assert.Equal(new[] { "border1", "inner", "border4" }, result); } } -} \ No newline at end of file +} 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(), diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2ca716fa8f..eee6e49617 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -100,6 +100,31 @@ namespace Avalonia.Controls.UnitTests Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme)); } + [Fact] + public void Finds_Correct_DataTemplate_When_Application_DataTemplate_Is_Present() + { + // #10398 + using var app = UnitTestApplication.Start(); + + Avalonia.Application.Current.DataTemplates.Add(new FuncDataTemplate((x, _) => new Canvas())); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Avalonia.Application.Current); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + }; + + var root = new TestRoot(target); + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + } + [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index c6ecc0a7e5..14e48b3b6c 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Direct2D1.Media; +using System; +using Avalonia.Direct2D1.Media; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"))); - - var font = glyphTypeface.DWFont; - - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + var glyphTypeface = + new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface; - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal("Arial", glyphTypeface.FamilyName); } } @@ -38,18 +31,13 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold)); + var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; - var font = glyphTypeface.DWFont; + Assert.Equal("Arial", glyphTypeface.FamilyName); - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal(FontWeight.Bold, glyphTypeface.Weight); - Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(FontStyle.Normal, glyphTypeface.Style); } } @@ -60,20 +48,11 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); + var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("Unknown"))); + var defaultName = FontManager.Current.DefaultFontFamilyName; - var font = glyphTypeface.DWFont; - - var defaultName = fontManager.GetDefaultFontFamilyName(); - - Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(defaultName, glyphTypeface.FamilyName); } } @@ -86,12 +65,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media var fontManager = new FontManagerImpl(); - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri)); + var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface; - var font = glyphTypeface.DWFont; - - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } @@ -102,14 +78,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); - - var font = glyphTypeface.DWFont; + var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface; - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } } diff --git a/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj b/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj new file mode 100644 index 0000000000..ec4cbcafb7 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Avalonia.Generators.Tests.csproj @@ -0,0 +1,26 @@ + + + Exe + net6.0 + Avalonia.Generators.Tests + true + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Generators.Tests/GlobPatternTests.cs b/tests/Avalonia.Generators.Tests/GlobPatternTests.cs new file mode 100644 index 0000000000..5358c2bdda --- /dev/null +++ b/tests/Avalonia.Generators.Tests/GlobPatternTests.cs @@ -0,0 +1,31 @@ +using Avalonia.Generators.Common; +using Xunit; + +namespace Avalonia.Generators.Tests; + +public class GlobPatternTests +{ + [Theory] + [InlineData("*", "anything", true)] + [InlineData("", "anything", false)] + [InlineData("Views/*", "Views/SignUpView.xaml", true)] + [InlineData("Views/*", "Extensions/SignUpView.xaml", false)] + [InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)] + [InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)] + [InlineData("*.xaml", "Extensions/SignUpView.xaml", true)] + public void Should_Match_Glob_Expressions(string pattern, string value, bool matches) + { + Assert.Equal(matches, new GlobPattern(pattern).Matches(value)); + } + + [Theory] + [InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })] + [InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })] + [InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })] + [InlineData("anything", true, new[] { "*", "*" })] + [InlineData("anything", false, new[] { "", "" })] + public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns) + { + Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value)); + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedProps.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt new file mode 100644 index 0000000000..c7ca9f20c1 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/AttachedPropsWithDevTools.txt @@ -0,0 +1,36 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + /// Should the dev tools be attached. + + public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + +#if DEBUG + if (attachDevTools) + { + this.AttachDevTools(); + } +#endif + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/ControlWithoutWindow.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt new file mode 100644 index 0000000000..2e9a534b3a --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/CustomControls.txt @@ -0,0 +1,32 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost; + internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost; + internal global::Controls.CustomTextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + ClrNamespaceRoutedViewHost = this.FindNameScope()?.Find("ClrNamespaceRoutedViewHost"); + UriRoutedViewHost = this.FindNameScope()?.Find("UriRoutedViewHost"); + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt new file mode 100644 index 0000000000..fff718517c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/DataTemplates.txt @@ -0,0 +1,30 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + internal global::Avalonia.Controls.ListBox NamedListBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + NamedListBox = this.FindNameScope()?.Find("NamedListBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt new file mode 100644 index 0000000000..b0ba74ca17 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/FieldModifier.txt @@ -0,0 +1,38 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + public global::Avalonia.Controls.TextBox FirstNameTextBox; + public global::Avalonia.Controls.TextBox LastNameTextBox; + protected global::Avalonia.Controls.TextBox PasswordTextBox; + private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; + internal global::Avalonia.Controls.Button SignUpButton; + internal global::Avalonia.Controls.Button RegisterButton; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + FirstNameTextBox = this.FindNameScope()?.Find("FirstNameTextBox"); + LastNameTextBox = this.FindNameScope()?.Find("LastNameTextBox"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + ConfirmPasswordTextBox = this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + RegisterButton = this.FindNameScope()?.Find("RegisterButton"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs new file mode 100644 index 0000000000..41d2cfaaee --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/InitializeComponentCode.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; + +public static class InitializeComponentCode +{ + public const string NamedControl = "NamedControl.txt"; + public const string NamedControls = "NamedControls.txt"; + public const string XNamedControl = "xNamedControl.txt"; + public const string XNamedControls = "xNamedControls.txt"; + public const string NoNamedControls = "NoNamedControls.txt"; + public const string CustomControls = "CustomControls.txt"; + public const string DataTemplates = "DataTemplates.txt"; + public const string SignUpView = "SignUpView.txt"; + public const string FieldModifier = "FieldModifier.txt"; + public const string AttachedProps = "AttachedProps.txt"; + public const string AttachedPropsWithDevTools = "AttachedPropsWithDevTools.txt"; + public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; + + public static async Task Load(string generatedCodeResourceName) + { + var assembly = typeof(XamlXNameResolverTests).Assembly; + var fullResourceName = assembly + .GetManifestResourceNames() + .First(name => name.Contains("InitializeComponent") && + name.Contains("GeneratedInitializeComponent") && + name.EndsWith(generatedCodeResourceName)); + + await using var stream = assembly.GetManifestResourceStream(fullResourceName); + using var reader = new StreamReader(stream!); + return await reader.ReadToEndAsync(); + } +} \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControl.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt new file mode 100644 index 0000000000..3451718ce5 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NamedControls.txt @@ -0,0 +1,32 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + internal global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.Button SignUpButton; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt new file mode 100644 index 0000000000..b68dce6170 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/NoNamedControls.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt new file mode 100644 index 0000000000..541a6f7106 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/SignUpView.txt @@ -0,0 +1,46 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Controls.CustomTextBox UserNameTextBox; + internal global::Avalonia.Controls.TextBlock UserNameValidation; + internal global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.TextBlock PasswordValidation; + internal global::Avalonia.Controls.ListBox AwesomeListView; + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; + internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription; + internal global::Avalonia.Controls.Button SignUpButton; + internal global::Avalonia.Controls.TextBlock CompoundValidation; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + UserNameValidation = this.FindNameScope()?.Find("UserNameValidation"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + PasswordValidation = this.FindNameScope()?.Find("PasswordValidation"); + AwesomeListView = this.FindNameScope()?.Find("AwesomeListView"); + ConfirmPasswordTextBox = this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + ConfirmPasswordValidation = this.FindNameScope()?.Find("ConfirmPasswordValidation"); + SignUpButtonDescription = this.FindNameScope()?.Find("SignUpButtonDescription"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + CompoundValidation = this.FindNameScope()?.Find("CompoundValidation"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt new file mode 100644 index 0000000000..42f6801af0 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControl.txt @@ -0,0 +1,28 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt new file mode 100644 index 0000000000..3451718ce5 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/GeneratedInitializeComponent/xNamedControls.txt @@ -0,0 +1,32 @@ +// + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox; + internal global::Avalonia.Controls.TextBox PasswordTextBox; + internal global::Avalonia.Controls.Button SignUpButton; + + /// + /// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). + /// + /// Should the XAML be loaded into the component. + + public void InitializeComponent(bool loadXaml = true) + { + if (loadXaml) + { + AvaloniaXamlLoader.Load(this); + } + + UserNameTextBox = this.FindNameScope()?.Find("UserNameTextBox"); + PasswordTextBox = this.FindNameScope()?.Find("PasswordTextBox"); + SignUpButton = this.FindNameScope()?.Find("SignUpButton"); + } + } +} diff --git a/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs new file mode 100644 index 0000000000..7bd9c35577 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/InitializeComponent/InitializeComponentTests.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using Avalonia.Generators.Common; +using Avalonia.Generators.Compiler; +using Avalonia.Generators.NameGenerator; +using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; +using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; +using Avalonia.Generators.Tests.Views; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Avalonia.Generators.Tests.InitializeComponent; + +public class InitializeComponentTests +{ + [Theory] + [InlineData(InitializeComponentCode.NamedControl, View.NamedControl, false)] + [InlineData(InitializeComponentCode.NamedControls, View.NamedControls, false)] + [InlineData(InitializeComponentCode.XNamedControl, View.XNamedControl, false)] + [InlineData(InitializeComponentCode.XNamedControls, View.XNamedControls, false)] + [InlineData(InitializeComponentCode.NoNamedControls, View.NoNamedControls, false)] + [InlineData(InitializeComponentCode.CustomControls, View.CustomControls, false)] + [InlineData(InitializeComponentCode.DataTemplates, View.DataTemplates, false)] + [InlineData(InitializeComponentCode.SignUpView, View.SignUpView, false)] + [InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)] + [InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] + [InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] + [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)] + [InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] + public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( + string expectation, + string markup, + bool devToolsMode) + { + var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; + var compilation = + View.CreateAvaloniaCompilation(excluded) + .WithCustomTextBox(); + + var types = new RoslynTypeSystem(compilation); + var classResolver = new XamlXViewResolver( + types, + MiniCompiler.CreateDefault( + new RoslynTypeSystem(compilation), + MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + + var xaml = await View.Load(markup); + var classInfo = classResolver.ResolveView(xaml); + var nameResolver = new XamlXNameResolver(); + var names = nameResolver.ResolveNames(classInfo.Xaml); + + var generator = new InitializeComponentCodeGenerator(types); + + var code = generator + .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .Replace("\r", string.Empty); + + var expected = await InitializeComponentCode.Load(expectation); + + + CSharpSyntaxTree.ParseText(code); + Assert.Equal(expected.Replace("\r", string.Empty), code); + } +} diff --git a/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs b/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs new file mode 100644 index 0000000000..26ed20f982 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/MiniCompilerTests.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel; +using Avalonia.Generators.Compiler; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Avalonia.Generators.Tests.Views; +using XamlX; +using XamlX.Parsers; +using Xunit; + +namespace Avalonia.Generators.Tests; + +public class MiniCompilerTests +{ + private const string AvaloniaXaml = ""; + private const string MiniClass = "namespace Example { public class Valid { public int Foo() => 21; } }"; + private const string MiniInvalidXaml = ""; + private const string MiniValidXaml = ""; + + [Fact] + public void Should_Resolve_Types_From_Simple_Valid_Xaml_Markup() + { + var xaml = XDocumentXamlParser.Parse(MiniValidXaml); + var compilation = CreateBasicCompilation(MiniClass); + MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + + Assert.NotNull(xaml.Root); + } + + [Fact] + public void Should_Throw_When_Unable_To_Resolve_Types_From_Simple_Invalid_Markup() + { + var xaml = XDocumentXamlParser.Parse(MiniInvalidXaml); + var compilation = CreateBasicCompilation(MiniClass); + var compiler = MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)); + + Assert.Throws(() => compiler.Transform(xaml)); + } + + [Fact] + public void Should_Resolve_Types_From_Simple_Avalonia_Markup() + { + var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); + var compilation = View.CreateAvaloniaCompilation(); + MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); + + Assert.NotNull(xaml.Root); + } + + private static CSharpCompilation CreateBasicCompilation(string source) => + CSharpCompilation + .Create("BasicLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) + .AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)) + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)); +} \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/AttachedProps.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/ControlWithoutWindow.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt new file mode 100644 index 0000000000..d9328b4b0d --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/CustomControls.txt @@ -0,0 +1,13 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost => this.FindNameScope()?.Find("ClrNamespaceRoutedViewHost"); + internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindNameScope()?.Find("UriRoutedViewHost"); + internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt new file mode 100644 index 0000000000..ee73a529e9 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/DataTemplates.txt @@ -0,0 +1,12 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.ListBox NamedListBox => this.FindNameScope()?.Find("NamedListBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt new file mode 100644 index 0000000000..250e8c98f3 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/FieldModifier.txt @@ -0,0 +1,16 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + public global::Avalonia.Controls.TextBox FirstNameTextBox => this.FindNameScope()?.Find("FirstNameTextBox"); + public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindNameScope()?.Find("LastNameTextBox"); + protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + internal global::Avalonia.Controls.Button RegisterButton => this.FindNameScope()?.Find("RegisterButton"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControl.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt new file mode 100644 index 0000000000..1129600cea --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NamedControls.txt @@ -0,0 +1,13 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt new file mode 100644 index 0000000000..7db25c4693 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/NoNamedControls.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs new file mode 100644 index 0000000000..3a69735bb3 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/OnlyPropertiesCode.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; + +public static class OnlyPropertiesCode +{ + public const string NamedControl = "NamedControl.txt"; + public const string NamedControls = "NamedControls.txt"; + public const string XNamedControl = "xNamedControl.txt"; + public const string XNamedControls = "xNamedControls.txt"; + public const string NoNamedControls = "NoNamedControls.txt"; + public const string CustomControls = "CustomControls.txt"; + public const string DataTemplates = "DataTemplates.txt"; + public const string SignUpView = "SignUpView.txt"; + public const string AttachedProps = "AttachedProps.txt"; + public const string FieldModifier = "FieldModifier.txt"; + public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; + + public static async Task Load(string generatedCodeResourceName) + { + var assembly = typeof(XamlXNameResolverTests).Assembly; + var fullResourceName = assembly + .GetManifestResourceNames() + .First(name => name.Contains("OnlyProperties") && name.EndsWith(generatedCodeResourceName)); + + await using var stream = assembly.GetManifestResourceStream(fullResourceName); + using var reader = new StreamReader(stream!); + return await reader.ReadToEndAsync(); + } +} \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt new file mode 100644 index 0000000000..c70abaf6af --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/SignUpView.txt @@ -0,0 +1,20 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find("UserNameValidation"); + internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find("PasswordValidation"); + internal global::Avalonia.Controls.ListBox AwesomeListView => this.FindNameScope()?.Find("AwesomeListView"); + internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find("ConfirmPasswordTextBox"); + internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find("ConfirmPasswordValidation"); + internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription => this.FindNameScope()?.Find("SignUpButtonDescription"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find("CompoundValidation"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt new file mode 100644 index 0000000000..8a3a65773c --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControl.txt @@ -0,0 +1,11 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt new file mode 100644 index 0000000000..1129600cea --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/GeneratedCode/xNamedControls.txt @@ -0,0 +1,13 @@ +// + +using Avalonia.Controls; + +namespace Sample.App +{ + partial class SampleView + { + internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find("UserNameTextBox"); + internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find("PasswordTextBox"); + internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find("SignUpButton"); + } +} diff --git a/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs new file mode 100644 index 0000000000..937fd323b7 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/OnlyProperties/OnlyPropertiesTests.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Avalonia.Generators.Common; +using Avalonia.Generators.Compiler; +using Avalonia.Generators.NameGenerator; +using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; +using Avalonia.Generators.Tests.Views; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Avalonia.Generators.Tests.OnlyProperties; + +public class OnlyPropertiesTests +{ + [Theory] + [InlineData(OnlyPropertiesCode.NamedControl, View.NamedControl)] + [InlineData(OnlyPropertiesCode.NamedControls, View.NamedControls)] + [InlineData(OnlyPropertiesCode.XNamedControl, View.XNamedControl)] + [InlineData(OnlyPropertiesCode.XNamedControls, View.XNamedControls)] + [InlineData(OnlyPropertiesCode.NoNamedControls, View.NoNamedControls)] + [InlineData(OnlyPropertiesCode.CustomControls, View.CustomControls)] + [InlineData(OnlyPropertiesCode.DataTemplates, View.DataTemplates)] + [InlineData(OnlyPropertiesCode.SignUpView, View.SignUpView)] + [InlineData(OnlyPropertiesCode.AttachedProps, View.AttachedProps)] + [InlineData(OnlyPropertiesCode.FieldModifier, View.FieldModifier)] + [InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)] + public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) + { + var compilation = + View.CreateAvaloniaCompilation() + .WithCustomTextBox(); + + var classResolver = new XamlXViewResolver( + new RoslynTypeSystem(compilation), + MiniCompiler.CreateDefault( + new RoslynTypeSystem(compilation), + MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + + var xaml = await View.Load(markup); + var classInfo = classResolver.ResolveView(xaml); + var nameResolver = new XamlXNameResolver(); + var names = nameResolver.ResolveNames(classInfo.Xaml); + + var generator = new OnlyPropertiesCodeGenerator(); + var code = generator + .GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) + .Replace("\r", string.Empty); + + var expected = await OnlyPropertiesCode.Load(expectation); + CSharpSyntaxTree.ParseText(code); + Assert.Equal(expected.Replace("\r", string.Empty), code); + } +} diff --git a/tests/Avalonia.Generators.Tests/Views/AttachedProps.xml b/tests/Avalonia.Generators.Tests/Views/AttachedProps.xml new file mode 100644 index 0000000000..896da6d1cd --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/AttachedProps.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml b/tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml new file mode 100644 index 0000000000..77de06a27e --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/ControlWithoutWindow.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/CustomControls.xml b/tests/Avalonia.Generators.Tests/Views/CustomControls.xml new file mode 100644 index 0000000000..9085e73d4b --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/CustomControls.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/DataTemplates.xml b/tests/Avalonia.Generators.Tests/Views/DataTemplates.xml new file mode 100644 index 0000000000..f7e15644aa --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/DataTemplates.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Generators.Tests/Views/FieldModifier.xml b/tests/Avalonia.Generators.Tests/Views/FieldModifier.xml new file mode 100644 index 0000000000..3ee5e51466 --- /dev/null +++ b/tests/Avalonia.Generators.Tests/Views/FieldModifier.xml @@ -0,0 +1,28 @@ + + + + + + +