Browse Source

Merge branch 'master' into patch-task-after-pack

pull/4146/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
aa75ba67e4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/NativeEmbedSample.v3.ncrunchproject
  2. 31
      Avalonia.sln
  3. 24
      native/Avalonia.Native/inc/avalonia-native.h
  4. 8
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  5. 36
      native/Avalonia.Native/src/OSX/app.mm
  6. 9
      native/Avalonia.Native/src/OSX/common.h
  7. 160
      native/Avalonia.Native/src/OSX/controlhost.mm
  8. 17
      native/Avalonia.Native/src/OSX/deadlock.mm
  9. 61
      native/Avalonia.Native/src/OSX/window.mm
  10. 4
      nukebuild/Build.cs
  11. 2
      nukebuild/Numerge
  12. 4
      packages/Avalonia/Avalonia.csproj
  13. 10
      packages/Avalonia/AvaloniaBuildTasks.props
  14. 22
      packages/Avalonia/AvaloniaBuildTasks.targets
  15. 18
      packages/Avalonia/AvaloniaItemSchema.xaml
  16. 2
      readme.md
  17. 2
      samples/ControlCatalog/MainView.xaml
  18. 1
      samples/ControlCatalog/Pages/ButtonPage.xaml
  19. 7
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  20. 34
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  21. 26
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  22. 35
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  23. 65
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
  24. 97
      samples/ControlCatalog/Pages/SplitViewPage.xaml
  25. 21
      samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
  26. 13
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  27. 46
      samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
  28. 8
      samples/interop/NativeEmbedSample/App.xaml
  29. 22
      samples/interop/NativeEmbedSample/App.xaml.cs
  30. 121
      samples/interop/NativeEmbedSample/EmbedSample.cs
  31. 58
      samples/interop/NativeEmbedSample/GtkHelper.cs
  32. 39
      samples/interop/NativeEmbedSample/MacHelper.cs
  33. 52
      samples/interop/NativeEmbedSample/MainWindow.xaml
  34. 36
      samples/interop/NativeEmbedSample/MainWindow.xaml.cs
  35. 30
      samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
  36. 17
      samples/interop/NativeEmbedSample/Program.cs
  37. 74
      samples/interop/NativeEmbedSample/WinApi.cs
  38. 1
      samples/interop/NativeEmbedSample/nodes-license.md
  39. BIN
      samples/interop/NativeEmbedSample/nodes.mp4
  40. 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  41. 4
      src/Android/Avalonia.Android/AvaloniaView.cs
  42. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  43. 6
      src/Avalonia.Animation/Easing/Easing.cs
  44. 85
      src/Avalonia.Animation/Easing/SplineEasing.cs
  45. 35
      src/Avalonia.Animation/KeySpline.cs
  46. 25
      src/Avalonia.Animation/KeySplineTypeConverter.cs
  47. 1
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  48. 23
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  49. 6
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  50. 33
      src/Avalonia.Base/Utilities/MathUtilities.cs
  51. 4
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  52. 3
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  53. 7
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
  54. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
  55. 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  56. 6
      src/Avalonia.Controls/Grid.cs
  57. 20
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  58. 198
      src/Avalonia.Controls/NativeControlHost.cs
  59. 16
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  60. 12
      src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs
  61. 32
      src/Avalonia.Controls/Platform/INativeControlHostImpl.cs
  62. 5
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  63. 2
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  64. 2
      src/Avalonia.Controls/Platform/PlatformManager.cs
  65. 198
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  66. 27
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  67. 11
      src/Avalonia.Controls/Primitives/Popup.cs
  68. 123
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  69. 164
      src/Avalonia.Controls/ProgressBar.cs
  70. 4
      src/Avalonia.Controls/Remote/RemoteServer.cs
  71. 8
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  72. 7
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  73. 27
      src/Avalonia.Controls/Repeater/ViewManager.cs
  74. 170
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  75. 1
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  76. 115
      src/Avalonia.Controls/ScrollViewer.cs
  77. 2
      src/Avalonia.Controls/SelectionModel.cs
  78. 487
      src/Avalonia.Controls/SplitView.cs
  79. 14
      src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs
  80. 24
      src/Avalonia.Controls/TopLevel.cs
  81. 1
      src/Avalonia.Controls/TreeViewItem.cs
  82. 9
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  83. 4
      src/Avalonia.Controls/Window.cs
  84. 2
      src/Avalonia.Controls/WindowBase.cs
  85. 7
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  86. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  87. 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  88. 10
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  89. 29
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  90. 23
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  91. 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  92. 78
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  93. 10
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  94. 37
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  95. 22
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  96. 33
      src/Avalonia.Input/KeyboardNavigation.cs
  97. 39
      src/Avalonia.Input/Navigation/TabNavigation.cs
  98. 2
      src/Avalonia.Layout/AttachedLayout.cs
  99. 24
      src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs
  100. 4
      src/Avalonia.Layout/ElementManager.cs

5
.ncrunch/NativeEmbedSample.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

31
Avalonia.sln

@ -203,14 +203,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
@ -1896,6 +1898,30 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.Build.0 = Release|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.ActiveCfg = Release|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.Build.0 = Release|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@ -1977,6 +2003,7 @@ Global
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

24
native/Avalonia.Native/inc/avalonia-native.h

@ -26,7 +26,8 @@ struct IAvnStringArray;
struct IAvnDndResultCallback;
struct IAvnGCHandleDeallocatorCallback;
struct IAvnMenuEvents;
struct IAvnNativeControlHost;
struct IAvnNativeControlHostTopLevelAttachment;
enum SystemDecorations {
SystemDecorationsNone = 0,
SystemDecorationsBorderOnly = 1,
@ -256,6 +257,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) = 0;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0;
virtual HRESULT SetBlurEnabled (bool enable) = 0;
@ -276,6 +278,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0;
virtual HRESULT GetWindowState(AvnWindowState*ret) = 0;
virtual HRESULT TakeFocusFromChildren() = 0;
};
AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
@ -295,6 +298,7 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0;
virtual void ScalingChanged(double scaling) = 0;
virtual void RunRenderPriorityJobs() = 0;
virtual void LostFocus() = 0;
virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers, AvnDragDropEffects effects,
IAvnClipboard* clipboard, void* dataObjectHandle) = 0;
@ -478,4 +482,22 @@ AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown
virtual void FreeGCHandle(void* handle) = 0;
};
AVNCOM(IAvnNativeControlHost, 20) : IUnknown
{
virtual HRESULT CreateDefaultChild(void* parent, void** retOut) = 0;
virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() = 0;
virtual void DestroyDefaultChild(void* child) = 0;
};
AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown
{
virtual void* GetParentHandle() = 0;
virtual HRESULT InitializeWithChildHandle(void* child) = 0;
virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0;
virtual void ShowInBounds(float x, float y, float width, float height) = 0;
virtual void HideWithSize(float width, float height) = 0;
virtual void ReleaseChild() = 0;
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();

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

@ -10,6 +10,8 @@
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
@ -32,6 +34,8 @@
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = "<group>"; };
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = "<group>"; };
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = "<group>"; };
1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = "<group>"; };
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = "<group>"; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = "<group>"; };
@ -86,11 +90,13 @@
AB7A61E62147C814003C5833 = {
isa = PBXGroup;
children = (
1A1852DB23E05814008F0DED /* deadlock.mm */,
1A002B9D232135EE00021753 /* app.mm */,
37DDA9B121933371002E132B /* AvnString.h */,
37DDA9AF219330F8002E132B /* AvnString.mm */,
37A4E71A2178846A00EACBCD /* headers */,
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */,
1AFD334023E03C4F0042899B /* controlhost.mm */,
5BF943652167AD1D009CAE35 /* cursor.h */,
5B21A981216530F500CEE36E /* cursor.mm */,
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
@ -191,6 +197,7 @@
files = (
1A002B9E232135EE00021753 /* app.mm in Sources */,
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */,
1A1852DC23E05814008F0DED /* deadlock.mm in Sources */,
5B21A982216530F500CEE36E /* cursor.mm in Sources */,
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
@ -199,6 +206,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,
1AFD334123E03C4F0042899B /* controlhost.mm in Sources */,
1A465D10246AB61600C5858B /* dnd.mm in Sources */,
AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,

36
native/Avalonia.Native/src/OSX/app.mm

@ -29,9 +29,43 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
@end
@interface AvnApplication : NSApplication
@end
@implementation AvnApplication
{
BOOL _isHandlingSendEvent;
}
- (void)sendEvent:(NSEvent *)event
{
bool oldHandling = _isHandlingSendEvent;
_isHandlingSendEvent = true;
@try {
[super sendEvent: event];
} @finally {
_isHandlingSendEvent = oldHandling;
}
}
// This is needed for certain embedded controls
- (BOOL) isHandlingSendEvent
{
return _isHandlingSendEvent;
}
- (void)setHandlingSendEvent:(BOOL)handlingSendEvent
{
_isHandlingSendEvent = handlingSendEvent;
}
@end
extern void InitializeAvnApp()
{
NSApplication* app = [NSApplication sharedApplication];
NSApplication* app = [AvnApplication sharedApplication];
id delegate = [AvnAppDelegate new];
[app setDelegate:delegate];
}

9
native/Avalonia.Native/src/OSX/common.h

@ -24,6 +24,7 @@ extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeperator();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
@ -54,4 +55,12 @@ template<typename T> inline T* objc_cast(id from) {
- (void) action;
@end
class AvnInsidePotentialDeadlock
{
public:
static bool IsInside();
AvnInsidePotentialDeadlock();
~AvnInsidePotentialDeadlock();
};
#endif

160
native/Avalonia.Native/src/OSX/controlhost.mm

@ -0,0 +1,160 @@
#include "common.h"
IAvnNativeControlHostTopLevelAttachment* CreateAttachment();
class AvnNativeControlHost :
public ComSingleObject<IAvnNativeControlHost, &IID_IAvnNativeControlHost>
{
public:
FORWARD_IUNKNOWN();
NSView* View;
AvnNativeControlHost(NSView* view)
{
View = view;
}
virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override
{
NSView* view = [NSView new];
[view setWantsLayer: true];
*retOut = (__bridge_retained void*)view;
return S_OK;
};
virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override
{
return ::CreateAttachment();
};
virtual void DestroyDefaultChild(void* child) override
{
// ARC will release the object for us
(__bridge_transfer NSView*) child;
}
};
class AvnNativeControlHostTopLevelAttachment :
public ComSingleObject<IAvnNativeControlHostTopLevelAttachment, &IID_IAvnNativeControlHostTopLevelAttachment>
{
NSView* _holder;
NSView* _child;
public:
FORWARD_IUNKNOWN();
AvnNativeControlHostTopLevelAttachment()
{
_holder = [NSView new];
[_holder setWantsLayer:true];
}
virtual ~AvnNativeControlHostTopLevelAttachment()
{
if(_child != nil && [_child superview] == _holder)
{
[_child removeFromSuperview];
}
if([_holder superview] != nil)
{
[_holder removeFromSuperview];
}
}
virtual void* GetParentHandle() override
{
return (__bridge void*)_holder;
};
virtual HRESULT InitializeWithChildHandle(void* child) override
{
if(_child != nil)
return E_FAIL;
_child = (__bridge NSView*)child;
if(_child == nil)
return E_FAIL;
[_holder addSubview:_child];
[_child setHidden: false];
return S_OK;
};
virtual HRESULT AttachTo(IAvnNativeControlHost* host) override
{
if(host == nil)
{
[_holder removeFromSuperview];
[_holder setHidden: true];
}
else
{
AvnNativeControlHost* chost = dynamic_cast<AvnNativeControlHost*>(host);
if(chost == nil || chost->View == nil)
return E_FAIL;
[_holder setHidden:true];
[chost->View addSubview:_holder];
}
return S_OK;
};
virtual void ShowInBounds(float x, float y, float width, float height) override
{
if(_child == nil)
return;
if(AvnInsidePotentialDeadlock::IsInside())
{
IAvnNativeControlHostTopLevelAttachment* slf = this;
slf->AddRef();
dispatch_async(dispatch_get_main_queue(), ^{
slf->ShowInBounds(x, y, width, height);
slf->Release();
});
return;
}
NSRect childFrame = {0, 0, width, height};
NSRect holderFrame = {x, y, width, height};
[_child setFrame: childFrame];
[_holder setFrame: holderFrame];
[_holder setHidden: false];
if([_holder superview] != nil)
[[_holder superview] setNeedsDisplay:true];
}
virtual void HideWithSize(float width, float height) override
{
if(_child == nil)
return;
if(AvnInsidePotentialDeadlock::IsInside())
{
IAvnNativeControlHostTopLevelAttachment* slf = this;
slf->AddRef();
dispatch_async(dispatch_get_main_queue(), ^{
slf->HideWithSize(width, height);
slf->Release();
});
return;
}
NSRect frame = {0, 0, width, height};
[_holder setHidden: true];
[_child setFrame: frame];
}
virtual void ReleaseChild() override
{
[_child removeFromSuperview];
_child = nil;
}
};
IAvnNativeControlHostTopLevelAttachment* CreateAttachment()
{
return new AvnNativeControlHostTopLevelAttachment();
}
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent)
{
return new AvnNativeControlHost(parent);
}

17
native/Avalonia.Native/src/OSX/deadlock.mm

@ -0,0 +1,17 @@
#include "common.h"
static int Counter = 0;
AvnInsidePotentialDeadlock::AvnInsidePotentialDeadlock()
{
Counter++;
}
AvnInsidePotentialDeadlock::~AvnInsidePotentialDeadlock()
{
Counter--;
}
bool AvnInsidePotentialDeadlock::IsInside()
{
return Counter!=0;
}

61
native/Avalonia.Native/src/OSX/window.mm

@ -116,10 +116,15 @@ public:
{
SetPosition(lastPositionSet);
UpdateStyle();
[Window makeKeyAndOrderFront:Window];
[NSApp activateIgnoringOtherApps:YES];
if(ShouldTakeFocusOnShow())
{
[Window makeKeyAndOrderFront:Window];
[NSApp activateIgnoringOtherApps:YES];
}
else
{
[Window orderFront: Window];
}
[Window setTitle:_lastTitle];
_shown = true;
@ -128,6 +133,11 @@ public:
}
}
virtual bool ShouldTakeFocusOnShow()
{
return true;
}
virtual HRESULT Hide () override
{
@autoreleasepool
@ -390,6 +400,14 @@ public:
return *ppv == nil ? E_FAIL : S_OK;
}
virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override
{
if(View == NULL)
return E_FAIL;
*retOut = ::CreateNativeControlHost(View);
return S_OK;
}
virtual HRESULT SetBlurEnabled (bool enable) override
{
[Window setContentView: enable ? VisualEffect : View];
@ -766,6 +784,15 @@ private:
}
}
virtual HRESULT TakeFocusFromChildren () override
{
if(Window == nil)
return S_OK;
if([Window isKeyWindow])
[Window makeFirstResponder: View];
return S_OK;
}
void EnterFullScreenMode ()
{
_fullScreenActive = true;
@ -1060,9 +1087,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
}
- (void)updateLayer
{
AvnInsidePotentialDeadlock deadlock;
if (_parent == nullptr)
{
return;
@ -1142,7 +1169,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return;
}
[self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
@ -1169,7 +1195,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto timestamp = [event timestamp] * 1000;
auto modifiers = [self getModifiers:[event modifierFlags]];
[self becomeFirstResponder];
if(type != AvnRawMouseEventType::Move ||
(
[self window] != nil &&
(
[[self window] firstResponder] == nil
|| ![[[self window] firstResponder] isKindOfClass: [NSView class]]
)
)
)
[self becomeFirstResponder];
if(_parent != nullptr)
{
@ -1179,6 +1214,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super mouseMoved:event];
}
- (BOOL) resignFirstResponder
{
_parent->BaseEvents->LostFocus();
return YES;
}
- (void)mouseMoved:(NSEvent *)event
{
[self mouseEvent:event withType:Move];
@ -1836,7 +1877,6 @@ private:
WindowEvents = events;
[Window setLevel:NSPopUpMenuWindowLevel];
}
protected:
virtual NSWindowStyleMask GetStyle() override
{
@ -1854,6 +1894,11 @@ protected:
return S_OK;
}
}
public:
virtual bool ShouldTakeFocusOnShow() override
{
return false;
}
};
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl)

4
nukebuild/Build.cs

@ -101,7 +101,7 @@ partial class Build : NukeBuild
.SetProjectFile(projectFile)
// This is required for VS2019 image on Azure Pipelines
.When(Parameters.IsRunningOnWindows &&
Parameters.IsRunningOnAzure, c => c
Parameters.IsRunningOnAzure, _ => _
.AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_8_X64")))
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", true)
@ -176,7 +176,7 @@ partial class Build : NukeBuild
.SetFramework(fw)
.EnableNoBuild()
.EnableNoRestore()
.When(Parameters.PublishTestResults, c => c
.When(Parameters.PublishTestResults, _ => _
.SetLogger("trx")
.SetResultsDirectory(Parameters.TestResultsRoot)));
}

2
nukebuild/Numerge

@ -1 +1 @@
Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8
Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5

4
packages/Avalonia/Avalonia.csproj

@ -41,6 +41,10 @@
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
</Content>
<Content Include="AvaloniaItemSchema.xaml">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
</Content>
</ItemGroup>
<Import Project="..\..\build\SharedVersion.props" />
<Import Project="..\..\build\NetFX.props" />

10
packages/Avalonia/AvaloniaBuildTasks.props

@ -1,3 +1,11 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AvailableItemName Include="AvaloniaXaml" />
<AvailableItemName Include="AvaloniaResource" />
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)AvaloniaItemSchema.xaml" />
</ItemGroup>
<ItemGroup Condition="'$(EnableDefaultItems)'=='True'">
<AvaloniaXaml Include="**\*.axaml" SubType="Designer" />
<AvaloniaXaml Include="**\*.paml" SubType="Designer" />
</ItemGroup>
</Project>

22
packages/Avalonia/AvaloniaBuildTasks.targets

@ -4,6 +4,20 @@
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
</PropertyGroup>
<!-- Unfortunately we have to update default items in .targets since custom nuget props are improted before Microsoft.NET.Sdk.DefaultItems.props -->
<ItemGroup Condition="'$(EnableDefaultItems)'=='True'">
<Compile Update="**\*.paml.cs">
<DependentUpon>%(Filename)</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="**\*.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
<SubType>Code</SubType>
</Compile>
<None Remove="**\*.axaml" />
<None Remove="**\*.paml" />
</ItemGroup>
<UsingTask TaskName="GenerateAvaloniaResourcesTask"
AssemblyFile="$(AvaloniaBuildTasksLocation)"
@ -31,9 +45,12 @@
<Target Name="GenerateAvaloniaResources"
BeforeTargets="CoreCompile;CoreResGen"
Inputs="@(AvaloniaResource);$(MSBuildAllProjects)"
Inputs="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)"
Outputs="$(AvaloniaResourcesTemporaryFilePath)"
DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)">
DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)">
<ItemGroup>
<AvaloniaResource Include="@(AvaloniaXaml)"/>
</ItemGroup>
<GenerateAvaloniaResourcesTask
Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
Output="$(AvaloniaResourcesTemporaryFilePath)"
@ -79,5 +96,6 @@
<ItemGroup>
<UpToDateCheckInput Include="@(AvaloniaResource)" />
<UpToDateCheckInput Include="@(AvaloniaXaml)" />
</ItemGroup>
</Project>

18
packages/Avalonia/AvaloniaItemSchema.xaml

@ -0,0 +1,18 @@
<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
<ContentType
Name="AvaloniaXaml"
DisplayName="Avalonia XAML"
ItemType="AvaloniaXaml">
<NameValuePair Name="DependentFileExtensions" Value=".cs" />
<NameValuePair Name="DefaultMetadata_SubType" Value="Designer" />
</ContentType>
<ItemType Name="AvaloniaXaml" DisplayName="Avalonia XAML" />
<FileExtension Name=".axaml" ContentType="AvaloniaXaml" />
<FileExtension Name=".paml" ContentType="AvaloniaXaml" />
<ContentType
Name="AvaloniaResource"
DisplayName="Avalonia Resource"
ItemType="AvaloniaResource"
/>
</ProjectSchemaDefinitions>

2
readme.md

@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md).
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)].
This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers

2
samples/ControlCatalog/MainView.xaml

@ -52,7 +52,9 @@
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="ScrollViewer"><pages:ScrollViewerPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>
<TabItem Header="SplitView"><pages:SplitViewPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>

1
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -35,6 +35,7 @@
<Button BorderBrush="{DynamicResource ThemeAccentBrush}">Border Color</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="4">Thick Border</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="4" IsEnabled="False">Disabled</Button>
<Button BorderBrush="{DynamicResource ThemeAccentBrush}" KeyboardNavigation.IsTabStop="False">IsTabStop=False</Button>
</StackPanel>
</StackPanel>
</StackPanel>

7
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -16,6 +16,8 @@
<Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
<Button Command="{Binding ResetItems}">Reset items</Button>
<Button x:Name="scrollToLast">Scroll to Last</Button>
<Button x:Name="scrollToRandom">Scroll to Random</Button>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
@ -24,7 +26,10 @@
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}"/>
<TextBlock Focusable="True"
Background="{Binding Background}"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>

34
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -5,23 +5,32 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class ItemsRepeaterPage : UserControl
{
private readonly ItemsRepeaterPageViewModel _viewModel;
private ItemsRepeater _repeater;
private ScrollViewer _scroller;
private Button _scrollToLast;
private Button _scrollToRandom;
private Random _random = new Random(0);
public ItemsRepeaterPage()
{
this.InitializeComponent();
_repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller");
_scrollToLast = this.FindControl<Button>("scrollToLast");
_scrollToRandom = this.FindControl<Button>("scrollToRandom");
_repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown;
DataContext = new ItemsRepeaterPageViewModel();
_scrollToLast.Click += scrollToLast_Click;
_scrollToRandom.Click += scrollToRandom_Click;
DataContext = _viewModel = new ItemsRepeaterPageViewModel();
}
private void InitializeComponent()
@ -73,18 +82,37 @@ namespace ControlCatalog.Pages
}
}
private void ScrollTo(int index)
{
System.Diagnostics.Debug.WriteLine("Scroll to " + index);
var layoutManager = ((Window)this.GetVisualRoot()).LayoutManager;
var element = _repeater.GetOrCreateElement(index);
layoutManager.ExecuteLayoutPass();
element.BringIntoView();
}
private void RepeaterClick(object sender, PointerPressedEventArgs e)
{
var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
_viewModel.SelectedItem = item;
}
private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F5)
{
((ItemsRepeaterPageViewModel)DataContext).ResetItems();
_viewModel.ResetItems();
}
}
private void scrollToLast_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_viewModel.Items.Count - 1);
}
private void scrollToRandom_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_random.Next(_viewModel.Items.Count - 1));
}
}
}

26
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,29 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ProgressBarPage">
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ProgressBar</TextBlock>
<TextBlock Classes="h2">A progress bar control</TextBlock>
<StackPanel>
<CheckBox
x:Name="showProgress"
Margin="10,16,0,0"
Content="Show Progress Text" />
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
<CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
<StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
<StackPanel Spacing="16">
<ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="True"/>
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
</StackPanel>
<ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
<ProgressBar Orientation="Vertical" IsIndeterminate="True" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
</StackPanel>
<StackPanel Margin="16">
<Slider Name="hprogress" Maximum="100" Value="40"/>
<Slider Name="vprogress" Maximum="100" Value="60"/>
<Slider Name="hprogress" Maximum="100" Value="40" />
<Slider Name="vprogress" Maximum="100" Value="60" />
</StackPanel>
</StackPanel>
</StackPanel>

35
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@ -0,0 +1,35 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ScrollViewerPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ScrollViewer</TextBlock>
<TextBlock Classes="h2">Allows for horizontal and vertical content scrolling.</TextBlock>
<Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical" Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Vertical Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel>
</StackPanel>
<ScrollViewer x:Name="ScrollViewer"
Grid.Column="1"
Width="400" Height="400"
AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
<Image Width="800" Height="800" Stretch="UniformToFill"
Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer>
</Grid>
</StackPanel>
</UserControl>

65
samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs

@ -0,0 +1,65 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using ReactiveUI;
namespace ControlCatalog.Pages
{
public class ScrollViewerPageViewModel : ReactiveObject
{
private bool _allowAutoHide;
private ScrollBarVisibility _horizontalScrollVisibility;
private ScrollBarVisibility _verticalScrollVisibility;
public ScrollViewerPageViewModel()
{
AvailableVisibility = new List<ScrollBarVisibility>
{
ScrollBarVisibility.Auto,
ScrollBarVisibility.Visible,
ScrollBarVisibility.Hidden,
ScrollBarVisibility.Disabled,
};
HorizontalScrollVisibility = ScrollBarVisibility.Auto;
VerticalScrollVisibility = ScrollBarVisibility.Auto;
AllowAutoHide = true;
}
public bool AllowAutoHide
{
get => _allowAutoHide;
set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value);
}
public ScrollBarVisibility HorizontalScrollVisibility
{
get => _horizontalScrollVisibility;
set => this.RaiseAndSetIfChanged(ref _horizontalScrollVisibility, value);
}
public ScrollBarVisibility VerticalScrollVisibility
{
get => _verticalScrollVisibility;
set => this.RaiseAndSetIfChanged(ref _verticalScrollVisibility, value);
}
public List<ScrollBarVisibility> AvailableVisibility { get; }
}
public class ScrollViewerPage : UserControl
{
public ScrollViewerPage()
{
InitializeComponent();
DataContext = new ScrollViewerPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

97
samples/ControlCatalog/Pages/SplitViewPage.xaml

@ -0,0 +1,97 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.SplitViewPage">
<Border>
<Grid ColumnDefinitions="*,225">
<StackPanel Grid.Column="1" Orientation="Vertical" Spacing="4" Margin="5">
<ToggleButton Name="PaneOpenButton"
Content="IsPaneOpen"
IsChecked="{Binding IsPaneOpen, ElementName=SplitView}" />
<ToggleButton Name="UseLightDismissOverlayModeButton"
Content="UseLightDismissOverlayMode"
IsChecked="{Binding UseLightDismissOverlayMode, ElementName=SplitView}" />
<ToggleSwitch OffContent="Left" OnContent="Right" Content="Placement" IsChecked="{Binding !IsLeft}" />
<TextBlock Text="DisplayMode" />
<ComboBox Name="DisplayModeSelector" Width="170" Margin="10" SelectedIndex="{Binding DisplayMode}">
<ComboBoxItem>Inline</ComboBoxItem>
<ComboBoxItem>CompactInline</ComboBoxItem>
<ComboBoxItem>Overlay</ComboBoxItem>
<ComboBoxItem>CompactOverlay</ComboBoxItem>
</ComboBox>
<TextBlock Text="PaneBackground" />
<ComboBox Name="PaneBackgroundSelector" SelectedIndex="0" Width="170" Margin="10">
<ComboBoxItem Tag="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}">SystemControlBackgroundChromeMediumLowBrush</ComboBoxItem>
<ComboBoxItem Tag="Red">Red</ComboBoxItem>
<ComboBoxItem Tag="Blue">Blue</ComboBoxItem>
<ComboBoxItem Tag="Green">Green</ComboBoxItem>
</ComboBox>
<TextBlock Text="{Binding Value, ElementName=OpenPaneLengthSlider, StringFormat='{}OpenPaneLength: {0}'}" />
<Slider Name="OpenPaneLengthSlider" Value="256" Minimum="128" Maximum="500"
Width="150" />
<TextBlock Text="{Binding Value, ElementName=CompactPaneLengthSlider, StringFormat='{}CompactPaneLength: {0}'}" />
<Slider Name="CompactPaneLengthSlider" Value="48" Minimum="24" Maximum="128"
Width="150" />
</StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1">
<!--{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}-->
<SplitView Name="SplitView"
PanePlacement="{Binding PanePlacement}"
PaneBackground="{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}"
OpenPaneLength="{Binding Value, ElementName=OpenPaneLengthSlider}"
CompactPaneLength="{Binding Value, ElementName=CompactPaneLengthSlider}"
DisplayMode="{Binding CurrentDisplayMode}">
<SplitView.Pane>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="PANE CONTENT" FontWeight="Bold" Name="PaneHeader" Margin="5,12,0,0" />
<ListBoxItem Grid.Row="1" VerticalAlignment="Top" Margin="0 10">
<StackPanel Orientation="Horizontal">
<!--Path glyph from materialdesignicons.com-->
<Border Width="48">
<Viewbox Width="24" Height="24" HorizontalAlignment="Left">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource SystemControlForegroundBaseHighBrush}" Data="M16 17V19H2V17S2 13 9 13 16 17 16 17M12.5 7.5A3.5 3.5 0 1 0 9 11A3.5 3.5 0 0 0 12.5 7.5M15.94 13A5.32 5.32 0 0 1 18 17V19H22V17S22 13.37 15.94 13M15 4A3.39 3.39 0 0 0 13.07 4.59A5 5 0 0 1 13.07 10.41A3.39 3.39 0 0 0 15 11A3.5 3.5 0 0 0 15 4Z" />
</Canvas>
</Viewbox>
</Border>
<TextBlock Text="People" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<TextBlock Grid.Row="2" Text="Item at bottom" Margin="60,12" />
</Grid>
</SplitView.Pane>
<Grid>
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Right" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" HorizontalAlignment="Right" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
</Grid>
</SplitView>
</Border>
</Grid>
</Border>
</UserControl>

21
samples/ControlCatalog/Pages/SplitViewPage.xaml.cs

@ -0,0 +1,21 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class SplitViewPage : UserControl
{
public SplitViewPage()
{
this.InitializeComponent();
DataContext = new SplitViewPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

13
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Media;
using ReactiveUI;
namespace ControlCatalog.ViewModels
@ -27,7 +28,7 @@ namespace ControlCatalog.ViewModels
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" });
Items.Insert(index + 1, new Item(index + 1) { Text = $"New Item {_newItemIndex++}" });
}
public void RandomizeHeights()
@ -52,7 +53,7 @@ namespace ControlCatalog.ViewModels
_newGenerationIndex++;
return new ObservableCollection<Item>(
Enumerable.Range(1, 100000).Select(i => new Item
Enumerable.Range(1, 100000).Select(i => new Item(i)
{
Text = $"Item {i.ToString()} {suffix}"
}));
@ -61,6 +62,12 @@ namespace ControlCatalog.ViewModels
public class Item : ReactiveObject
{
private double _height = double.NaN;
private int _index;
public Item(int index)
{
_index = index;
}
public string Text { get; set; }
@ -69,6 +76,8 @@ namespace ControlCatalog.ViewModels
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat;
}
}
}

46
samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs

@ -0,0 +1,46 @@
using System;
using Avalonia.Controls;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
public class SplitViewPageViewModel : ReactiveObject
{
private bool _isLeft = true;
private int _displayMode = 3; //CompactOverlay
public bool IsLeft
{
get => _isLeft;
set
{
this.RaiseAndSetIfChanged(ref _isLeft, value);
this.RaisePropertyChanged(nameof(PanePlacement));
}
}
public int DisplayMode
{
get => _displayMode;
set
{
this.RaiseAndSetIfChanged(ref _displayMode, value);
this.RaisePropertyChanged(nameof(CurrentDisplayMode));
}
}
public SplitViewPanePlacement PanePlacement => _isLeft ? SplitViewPanePlacement.Left : SplitViewPanePlacement.Right;
public SplitViewDisplayMode CurrentDisplayMode
{
get
{
if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode))
{
return (SplitViewDisplayMode)_displayMode;
}
return SplitViewDisplayMode.CompactOverlay;
}
}
}
}

8
samples/interop/NativeEmbedSample/App.xaml

@ -0,0 +1,8 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NativeEmbedSample.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
</Application.Styles>
</Application>

22
samples/interop/NativeEmbedSample/App.xaml.cs

@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace NativeEmbedSample
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
desktopLifetime.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
}
}

121
samples/interop/NativeEmbedSample/EmbedSample.cs

@ -0,0 +1,121 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Threading;
using MonoMac.AppKit;
using MonoMac.Foundation;
using MonoMac.WebKit;
using Encoding = SharpDX.Text.Encoding;
namespace NativeEmbedSample
{
public class EmbedSample : NativeControlHost
{
public bool IsSecond { get; set; }
private Process _mplayer;
IPlatformHandle CreateLinux(IPlatformHandle parent)
{
if (IsSecond)
{
var chooser = GtkHelper.CreateGtkFileChooser(parent.Handle);
if (chooser != null)
return chooser;
}
var control = base.CreateNativeControlCore(parent);
var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
"..",
"nodes.mp4"));
_mplayer = Process.Start(new ProcessStartInfo("mplayer",
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
{
UseShellExecute = false,
});
return control;
}
void DestroyLinux(IPlatformHandle handle)
{
_mplayer?.Kill();
_mplayer = null;
base.DestroyNativeControlCore(handle);
}
private const string RichText =
@"{\rtf1\ansi\ansicpg1251\deff0\nouicompat\deflang1049{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\colortbl ;\red255\green0\blue0;\red0\green77\blue187;\red0\green176\blue80;\red155\green0\blue211;\red247\green150\blue70;\red75\green172\blue198;}
{\*\generator Riched20 6.3.9600}\viewkind4\uc1
\pard\sa200\sl276\slmult1\f0\fs22\lang9 <PREFIX>I \i am\i0 a \cf1\b Rich Text \cf0\b0\fs24 control\cf2\fs28 !\cf3\fs32 !\cf4\fs36 !\cf1\fs40 !\cf5\fs44 !\cf6\fs48 !\cf0\fs44\par
}";
IPlatformHandle CreateWin32(IPlatformHandle parent)
{
WinApi.LoadLibrary("Msftedit.dll");
var handle = WinApi.CreateWindowEx(0, "RICHEDIT50W",
@"Rich Edit",
0x800000 | 0x10000000 | 0x40000000 | 0x800000 | 0x10000 | 0x0004, 0, 0, 1, 1, parent.Handle,
IntPtr.Zero, WinApi.GetModuleHandle(null), IntPtr.Zero);
var st = new WinApi.SETTEXTEX { Codepage = 65001, Flags = 0x00000008 };
var text = RichText.Replace("<PREFIX>", IsSecond ? "\\qr " : "");
var bytes = Encoding.UTF8.GetBytes(text);
WinApi.SendMessage(handle, 0x0400 + 97, ref st, bytes);
return new PlatformHandle(handle, "HWND");
}
void DestroyWin32(IPlatformHandle handle)
{
WinApi.DestroyWindow(handle.Handle);
}
IPlatformHandle CreateOSX(IPlatformHandle parent)
{
// Note: We are using MonoMac for example purposes
// It shouldn't be used in production apps
MacHelper.EnsureInitialized();
var webView = new WebView();
Dispatcher.UIThread.Post(() =>
{
webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(
IsSecond ? "https://bing.com": "https://google.com/")));
});
return new MacOSViewHandle(webView);
}
void DestroyOSX(IPlatformHandle handle)
{
((MacOSViewHandle)handle).Dispose();
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return CreateLinux(parent);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return CreateWin32(parent);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return CreateOSX(parent);
return base.CreateNativeControlCore(parent);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
DestroyLinux(control);
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
DestroyWin32(control);
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
DestroyOSX(control);
else
base.DestroyNativeControlCore(control);
}
}
}

58
samples/interop/NativeEmbedSample/GtkHelper.cs

@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.NativeDialogs.Gtk;
using static Avalonia.X11.NativeDialogs.Glib;
namespace NativeEmbedSample
{
public class GtkHelper
{
private static Task<bool> s_gtkTask;
class FileChooser : INativeControlHostDestroyableControlHandle
{
private readonly IntPtr _widget;
public FileChooser(IntPtr widget, IntPtr xid)
{
_widget = widget;
Handle = xid;
}
public IntPtr Handle { get; }
public string HandleDescriptor => "XID";
public void Destroy()
{
RunOnGlibThread(() =>
{
gtk_widget_destroy(_widget);
return 0;
}).Wait();
}
}
public static IPlatformHandle CreateGtkFileChooser(IntPtr parentXid)
{
if (s_gtkTask == null)
s_gtkTask = StartGtk();
if (!s_gtkTask.Result)
return null;
return RunOnGlibThread(() =>
{
using (var title = new Utf8Buffer("Embedded"))
{
var widget = gtk_file_chooser_dialog_new(title, IntPtr.Zero, GtkFileChooserAction.SelectFolder,
IntPtr.Zero);
gtk_widget_realize(widget);
var xid = gdk_x11_window_get_xid(gtk_widget_get_window(widget));
gtk_window_present(widget);
return new FileChooser(widget, xid);
}
}).Result;
}
}
}

39
samples/interop/NativeEmbedSample/MacHelper.cs

@ -0,0 +1,39 @@
using System;
using Avalonia.Platform;
using MonoMac.AppKit;
namespace NativeEmbedSample
{
public class MacHelper
{
private static bool _isInitialized;
public static void EnsureInitialized()
{
if (_isInitialized)
return;
_isInitialized = true;
NSApplication.Init();
}
}
class MacOSViewHandle : IPlatformHandle, IDisposable
{
private NSView _view;
public MacOSViewHandle(NSView view)
{
_view = view;
}
public IntPtr Handle => _view?.Handle ?? IntPtr.Zero;
public string HandleDescriptor => "NSView";
public void Dispose()
{
_view.Dispose();
_view = null;
}
}
}

52
samples/interop/NativeEmbedSample/MainWindow.xaml

@ -0,0 +1,52 @@
<Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
Width="1024" Height="800"
Title="Native embedding sample"
xmlns:local="clr-namespace:NativeEmbedSample"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NativeEmbedSample.MainWindow">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="Test">
<MenuItem Header="SubMenu">
<MenuItem Header="Item 1"/>
<MenuItem Header="Item 2"/>
<MenuItem Header="Item 3"/>
</MenuItem>
<MenuItem Header="Item 1"/>
<MenuItem Header="Item 2"/>
<MenuItem Header="Item 3"/>
</MenuItem>
</Menu>
<DockPanel DockPanel.Dock="Top">
<Button DockPanel.Dock="Right" Click="ShowPopupDelay">Show popup (delay)</Button>
<Button DockPanel.Dock="Right" Click="ShowPopup">Show popup</Button>
<Border DockPanel.Dock="Right" Background="#c0c0c0">
<ToolTip.Tip>
<ToolTip>
<TextBlock>Text</TextBlock>
</ToolTip>
</ToolTip.Tip>
<TextBlock>Tooltip</TextBlock>
</Border>
<TextBox Text="Lorem ipsum dolor sit amet"/>
</DockPanel>
<Grid ColumnDefinitions="*,5,*">
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<CheckBox x:Name="firstVisible" IsChecked="True"/>
<TextBlock>Visible</TextBlock>
</StackPanel>
<local:EmbedSample IsVisible="{Binding #firstVisible.IsChecked}"/>
</DockPanel>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<DockPanel Grid.Column="2">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<CheckBox x:Name="secondVisible" IsChecked="True"/>
<TextBlock>Visible</TextBlock>
</StackPanel>
<local:EmbedSample IsSecond="True" IsVisible="{Binding #secondVisible.IsChecked}"/>
</DockPanel>
</Grid>
</DockPanel>
</Window>

36
samples/interop/NativeEmbedSample/MainWindow.xaml.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace NativeEmbedSample
{
public class MainWindow : Window
{
public MainWindow()
{
AvaloniaXamlLoader.Load(this);
this.AttachDevTools();
}
public async void ShowPopupDelay(object sender, RoutedEventArgs args)
{
await Task.Delay(3000);
ShowPopup(sender, args);
}
public void ShowPopup(object sender, RoutedEventArgs args)
{
new ContextMenu()
{
Items = new List<MenuItem>
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
}.Open((Control)sender);
}
}
}

30
samples/interop/NativeEmbedSample/NativeEmbedSample.csproj

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
<ProjectReference Include="..\..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
<ProjectReference Include="..\..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2019013001" />
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<None Remove="nodes.mp4" />
<Content Include="nodes.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Compile Include="..\..\..\src\Avalonia.X11\NativeDialogs\Gtk.cs" />
</ItemGroup>
<Import Project="..\..\..\build\SampleApp.props" />
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\ReferenceCoreLibraries.props" />
</Project>

17
samples/interop/NativeEmbedSample/Program.cs

@ -0,0 +1,17 @@
using Avalonia;
namespace NativeEmbedSample
{
class Program
{
static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.With(new AvaloniaNativePlatformOptions()
{
})
.UsePlatformDetect();
}
}

74
samples/interop/NativeEmbedSample/WinApi.cs

@ -0,0 +1,74 @@
using System;
using System.Runtime.InteropServices;
namespace NativeEmbedSample
{
public unsafe class WinApi
{
public enum CommonControls : uint
{
ICC_LISTVIEW_CLASSES = 0x00000001, // listview, header
ICC_TREEVIEW_CLASSES = 0x00000002, // treeview, tooltips
ICC_BAR_CLASSES = 0x00000004, // toolbar, statusbar, trackbar, tooltips
ICC_TAB_CLASSES = 0x00000008, // tab, tooltips
ICC_UPDOWN_CLASS = 0x00000010, // updown
ICC_PROGRESS_CLASS = 0x00000020, // progress
ICC_HOTKEY_CLASS = 0x00000040, // hotkey
ICC_ANIMATE_CLASS = 0x00000080, // animate
ICC_WIN95_CLASSES = 0x000000FF,
ICC_DATE_CLASSES = 0x00000100, // month picker, date picker, time picker, updown
ICC_USEREX_CLASSES = 0x00000200, // comboex
ICC_COOL_CLASSES = 0x00000400, // rebar (coolbar) control
ICC_INTERNET_CLASSES = 0x00000800,
ICC_PAGESCROLLER_CLASS = 0x00001000, // page scroller
ICC_NATIVEFNTCTL_CLASS = 0x00002000, // native font control
ICC_STANDARD_CLASSES = 0x00004000,
ICC_LINK_CLASS = 0x00008000
}
[StructLayout(LayoutKind.Sequential)]
public struct INITCOMMONCONTROLSEX
{
public int dwSize;
public uint dwICC;
}
[DllImport("Comctl32.dll")]
public static extern void InitCommonControlsEx(ref INITCOMMONCONTROLSEX init);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool DestroyWindow(IntPtr hwnd);
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lib);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr CreateWindowEx(
int dwExStyle,
string lpClassName,
string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[StructLayout(LayoutKind.Sequential)]
public struct SETTEXTEX
{
public uint Flags;
public uint Codepage;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, ref SETTEXTEX wParam, byte[] lParam);
}
}

1
samples/interop/NativeEmbedSample/nodes-license.md

@ -0,0 +1 @@
nodes.mp4 by beeple is licensed under the creative commons license, downloaded from https://vimeo.com/9936271

BIN
samples/interop/NativeEmbedSample/nodes.mp4

Binary file not shown.

2
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -67,7 +67,7 @@ namespace Avalonia.Android
throw new NotSupportedException();
}
public IEmbeddableWindowImpl CreateEmbeddableWindow()
public IWindowImpl CreateEmbeddableWindow()
{
throw new NotSupportedException();
}

4
src/Android/Avalonia.Android/AvaloniaView.cs

@ -33,10 +33,8 @@ namespace Avalonia.Android
return _view.View.DispatchKeyEvent(e);
}
class ViewImpl : TopLevelImpl, IEmbeddableWindowImpl
class ViewImpl : TopLevelImpl
{
public event Action LostFocus;
public ViewImpl(Context context) : base(context)
{
View.Focusable = true;

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -194,6 +194,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
public IPopupImpl CreatePopup() => null;
public Action LostFocus { get; set; }
ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
}

6
src/Avalonia.Animation/Easing/Easing.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
namespace Avalonia.Animation.Easings
@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings
/// <returns>Returns the instance of the parsed type.</returns>
public static Easing Parse(string e)
{
if (e.Contains(','))
{
return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
}
if (_easingTypes == null)
{
_easingTypes = new Dictionary<string, Type>();

85
src/Avalonia.Animation/Easing/SplineEasing.cs

@ -0,0 +1,85 @@
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a user-defined cubic bezier curve.
/// Good for custom easing functions that doesn't quite
/// fit with the built-in ones.
/// </summary>
public class SplineEasing : Easing
{
/// <summary>
/// X coordinate of the first control point
/// </summary>
public double X1
{
get => _internalKeySpline.ControlPointX1;
set
{
_internalKeySpline.ControlPointX1 = value;
}
}
/// <summary>
/// Y coordinate of the first control point
/// </summary>
public double Y1
{
get => _internalKeySpline.ControlPointY1;
set
{
_internalKeySpline.ControlPointY1 = value;
}
}
/// <summary>
/// X coordinate of the second control point
/// </summary>
public double X2
{
get => _internalKeySpline.ControlPointX2;
set
{
_internalKeySpline.ControlPointX2 = value;
}
}
/// <summary>
/// Y coordinate of the second control point
/// </summary>
public double Y2
{
get => _internalKeySpline.ControlPointY2;
set
{
_internalKeySpline.ControlPointY2 = value;
}
}
private readonly KeySpline _internalKeySpline;
public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d)
{
_internalKeySpline = new KeySpline();
this.X1 = x1;
this.Y1 = y1;
this.X2 = x2;
this.Y1 = y2;
}
public SplineEasing(KeySpline keySpline)
{
_internalKeySpline = keySpline;
}
public SplineEasing()
{
_internalKeySpline = new KeySpline();
}
/// <inheritdoc/>
public override double Ease(double progress) =>
_internalKeySpline.GetSplineProgress(progress);
}
}

35
src/Avalonia.Animation/KeySpline.cs

@ -81,7 +81,10 @@ namespace Avalonia.Animation
/// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
public static KeySpline Parse(string value, CultureInfo culture)
{
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
if (culture is null)
culture = CultureInfo.InvariantCulture;
using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
@ -98,6 +101,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX1 = value;
_isDirty = true;
}
else
{
@ -112,7 +116,11 @@ namespace Avalonia.Animation
public double ControlPointY1
{
get => _controlPointY1;
set => _controlPointY1 = value;
set
{
_controlPointY1 = value;
_isDirty = true;
}
}
/// <summary>
@ -126,6 +134,7 @@ namespace Avalonia.Animation
if (IsValidXValue(value))
{
_controlPointX2 = value;
_isDirty = true;
}
else
{
@ -140,7 +149,11 @@ namespace Avalonia.Animation
public double ControlPointY2
{
get => _controlPointY2;
set => _controlPointY2 = value;
set
{
_controlPointY2 = value;
_isDirty = true;
}
}
/// <summary>
@ -330,20 +343,4 @@ namespace Avalonia.Animation
}
}
}
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

25
src/Avalonia.Animation/KeySplineTypeConverter.cs

@ -0,0 +1,25 @@
using System;
using System.ComponentModel;
using System.Globalization;
// Ported from WPF open-source code.
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
namespace Avalonia.Animation
{
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

1
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@ -140,6 +140,7 @@ namespace Avalonia.Collections
}
}
[Obsolete("Causes memory leaks. Use DynamicData or similar instead.")]
public static IAvaloniaReadOnlyList<TDerived> CreateDerivedList<TSource, TDerived>(
this IAvaloniaReadOnlyList<TSource> collection,
Func<TSource, TDerived> select)

23
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
@ -49,6 +50,18 @@ namespace Avalonia.Data.Core
var accessor = plugin?.Start(reference, PropertyName);
// We need to handle accessor fallback before handling validation. Validators do not support null accessors.
if (accessor == null)
{
reference.TryGetTarget(out object instance);
var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
if (_enableValidation && Next == null)
{
foreach (var validator in ExpressionObserver.DataValidators)
@ -60,15 +73,9 @@ namespace Avalonia.Data.Core
}
}
if (accessor == null)
if (accessor is null)
{
reference.TryGetTarget(out object instance);
var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
throw new AvaloniaInternalException("Data validators must return non-null accessor.");
}
_accessor = accessor;

6
src/Avalonia.Base/Threading/DispatcherTimer.cs

@ -14,7 +14,7 @@ namespace Avalonia.Threading
private readonly DispatcherPriority _priority;
private TimeSpan _interval;
/// <summary>
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
/// </summary>
@ -154,6 +154,8 @@ namespace Avalonia.Threading
TimeSpan interval,
DispatcherPriority priority = DispatcherPriority.Normal)
{
interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1);
var timer = new DispatcherTimer(priority) { Interval = interval };
timer.Tick += (s, e) =>
@ -197,7 +199,7 @@ namespace Avalonia.Threading
}
}
/// <summary>
/// Raises the <see cref="Tick"/> event on the dispatcher thread.

33
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -206,39 +206,6 @@ namespace Avalonia.Utilities
}
}
/// <summary>
/// Calculates the value to be used for layout rounding at high DPI.
/// </summary>
/// <param name="value">Input value to be rounded.</param>
/// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
/// <returns>Adjusted value that will produce layout rounding on screen at high dpi.</returns>
/// <remarks>This is a layout helper method. It takes DPI into account and also does not return
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with
/// UseLayoutRounding property and should not be used as a general rounding utility.</remarks>
public static double RoundLayoutValue(double value, double dpiScale)
{
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.IsOne(dpiScale))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
if (double.IsNaN(newValue) ||
double.IsInfinity(newValue) ||
MathUtilities.AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Round(value);
}
return newValue;
}
/// <summary>
/// Clamps a value between a minimum and maximum value.
/// </summary>

4
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -107,7 +107,7 @@ namespace Avalonia.Build.Tasks
foreach (var s in sources.ToList())
{
if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml"))
if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml"))
{
XamlFileInfo info;
try
@ -150,7 +150,7 @@ namespace Avalonia.Build.Tasks
BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml")))
foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml")))
BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources();

3
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -25,7 +25,8 @@ namespace Avalonia.Build.Tasks
public static partial class XamlCompilerTaskExecutor
{
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
|| r.Name.ToLowerInvariant().EndsWith(".paml");
|| r.Name.ToLowerInvariant().EndsWith(".paml")
|| r.Name.ToLowerInvariant().EndsWith(".axaml");
public class CompileResult
{

7
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@ -10,7 +10,7 @@ namespace Avalonia.Controls.Embedding
{
public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable
{
public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl)
public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl)
{
}
@ -19,16 +19,13 @@ namespace Avalonia.Controls.Embedding
{
}
[CanBeNull]
public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl;
protected bool EnforceClientSize { get; set; } = true;
public void Prepare()
{
EnsureInitialized();
ApplyTemplate();
LayoutManager.ExecuteInitialLayoutPass(this);
LayoutManager.ExecuteInitialLayoutPass();
}
private void EnsureInitialized()

2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
{
EnsureInitialized();
ApplyTemplate();
LayoutManager.ExecuteInitialLayoutPass(this);
LayoutManager.ExecuteInitialLayoutPass();
}
private void EnsureInitialized()

1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -63,6 +63,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
}
public Action Closed { get; set; }
public Action LostFocus { get; set; }
public abstract IMouseDevice MouseDevice { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { }

6
src/Avalonia.Controls/Grid.cs

@ -8,10 +8,8 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -2103,7 +2101,7 @@ namespace Avalonia.Controls
for (int i = 0; i < definitions.Count; ++i)
{
DefinitionBase def = definitions[i];
double roundedSize = MathUtilities.RoundLayoutValue(def.SizeCache, dpi);
double roundedSize = LayoutHelper.RoundLayoutValue(def.SizeCache, dpi);
roundingErrors[i] = (roundedSize - def.SizeCache);
def.SizeCache = roundedSize;
roundedTakenSize += roundedSize;

20
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -1,9 +1,29 @@
namespace Avalonia.Controls
{
/// <summary>
/// Specifies a contract for a scrolling control that supports scroll anchoring.
/// </summary>
public interface IScrollAnchorProvider
{
/// <summary>
/// The currently chosen anchor element to use for scroll anchoring.
/// </summary>
IControl CurrentAnchor { get; }
/// <summary>
/// Registers a control as a potential scroll anchor candidate.
/// </summary>
/// <param name="element">
/// A control within the subtree of the <see cref="IScrollAnchorProvider"/>.
/// </param>
void RegisterAnchorCandidate(IControl element);
/// <summary>
/// Unregisters a control as a potential scroll anchor candidate.
/// </summary>
/// <param name="element">
/// A control within the subtree of the <see cref="IScrollAnchorProvider"/>.
/// </param>
void UnregisterAnchorCandidate(IControl element);
}
}

198
src/Avalonia.Controls/NativeControlHost.cs

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
public class NativeControlHost : Control
{
private TopLevel _currentRoot;
private INativeControlHostImpl _currentHost;
private INativeControlHostControlTopLevelAttachment _attachment;
private IPlatformHandle _nativeControlHandle;
private bool _queuedForDestruction;
private bool _queuedForMoveResize;
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
static NativeControlHost()
{
IsVisibleProperty.Changed.AddClassHandler<NativeControlHost>(OnVisibleChanged);
}
public NativeControlHost()
{
_propertyChangedHandler = PropertyChangedHandler;
}
private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2)
=> host.UpdateHost();
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_currentRoot = e.Root as TopLevel;
var visual = (IVisual)this;
while (visual != _currentRoot)
{
if (visual is Visual v)
{
v.PropertyChanged += _propertyChangedHandler;
_propertyChangedSubscriptions.Add(v);
}
visual = visual.GetVisualParent();
}
UpdateHost();
}
private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.IsEffectiveValueChange && e.Property == BoundsProperty)
EnqueueForMoveResize();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_currentRoot = null;
if (_propertyChangedSubscriptions != null)
{
foreach (var v in _propertyChangedSubscriptions)
v.PropertyChanged -= _propertyChangedHandler;
_propertyChangedSubscriptions.Clear();
}
UpdateHost();
}
private void UpdateHost()
{
_queuedForMoveResize = false;
_currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost;
var needsAttachment = _currentHost != null;
if (needsAttachment)
{
// If there is an existing attachment, ensure that we are attached to the proper host or destroy the attachment
if (_attachment != null && _attachment.AttachedTo != _currentHost)
{
if (_attachment != null)
{
if (_attachment.IsCompatibleWith(_currentHost))
{
_attachment.AttachedTo = _currentHost;
}
else
{
_attachment.Dispose();
_attachment = null;
}
}
}
// If there is no attachment, but the control exists,
// attempt to attach to to the current toplevel or destroy the control if it's incompatible
if (_attachment == null && _nativeControlHandle != null)
{
if (_currentHost.IsCompatibleWith(_nativeControlHandle))
_attachment = _currentHost.CreateNewAttachment(_nativeControlHandle);
else
DestroyNativeControl();
}
// There is no control handle an no attachment, create both
if (_nativeControlHandle == null)
{
_attachment = _currentHost.CreateNewAttachment(parent =>
_nativeControlHandle = CreateNativeControlCore(parent));
}
}
else
{
// Immediately detach the control from the current toplevel if there is an existing attachment
if (_attachment != null)
_attachment.AttachedTo = null;
// Don't destroy the control immediately, it might be just being reparented to another TopLevel
if (_nativeControlHandle != null && !_queuedForDestruction)
{
_queuedForDestruction = true;
Dispatcher.UIThread.Post(CheckDestruction, DispatcherPriority.Background);
}
}
if (_attachment?.AttachedTo != _currentHost)
return;
TryUpdateNativeControlPosition();
}
private Rect? GetAbsoluteBounds()
{
var bounds = Bounds;
var position = this.TranslatePoint(bounds.Position, _currentRoot);
if (position == null)
return null;
return new Rect(position.Value, bounds.Size);
}
void EnqueueForMoveResize()
{
if(_queuedForMoveResize)
return;
_queuedForMoveResize = true;
Dispatcher.UIThread.Post(UpdateHost, DispatcherPriority.Render);
}
public bool TryUpdateNativeControlPosition()
{
if (_currentHost == null)
return false;
var bounds = GetAbsoluteBounds();
var needsShow = IsEffectivelyVisible && bounds.HasValue;
if (needsShow)
_attachment?.ShowInBounds(bounds.Value);
else
_attachment?.HideWithSize(Bounds.Size);
return false;
}
private void CheckDestruction()
{
_queuedForDestruction = false;
if (_currentRoot == null)
DestroyNativeControl();
}
protected virtual IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
if (_currentHost == null)
throw new InvalidOperationException();
return _currentHost.CreateDefaultChild(parent);
}
private void DestroyNativeControl()
{
if (_nativeControlHandle != null)
{
_attachment?.Dispose();
_attachment = null;
DestroyNativeControlCore(_nativeControlHandle);
_nativeControlHandle = null;
}
}
protected virtual void DestroyNativeControlCore(IPlatformHandle control)
{
((INativeControlHostDestroyableControlHandle)control).Destroy();
}
}
}

16
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -3,6 +3,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -67,6 +68,9 @@ namespace Avalonia.Controls.Platform
window.Deactivated += WindowDeactivated;
}
if (_root is TopLevel tl)
tl.PlatformImpl.LostFocus += TopLevelLostPlatformFocus;
_inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
}
@ -96,6 +100,9 @@ namespace Avalonia.Controls.Platform
{
root.Deactivated -= WindowDeactivated;
}
if (_root is TopLevel tl)
tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
_inputManagerSubscription?.Dispose();
@ -333,6 +340,10 @@ namespace Avalonia.Controls.Platform
{
item.Parent.SelectedItem = null;
}
else if (!item.IsPointerOverSubMenu)
{
item.IsSubMenuOpen = false;
}
}
}
@ -405,6 +416,11 @@ namespace Avalonia.Controls.Platform
{
Menu?.Close();
}
private void TopLevelLostPlatformFocus()
{
Menu?.Close();
}
protected void Click(IMenuItem item)
{

12
src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs

@ -1,12 +0,0 @@
using System;
namespace Avalonia.Platform
{
/// <summary>
/// Defines a platform-specific embeddable window implementation.
/// </summary>
public interface IEmbeddableWindowImpl : ITopLevelImpl
{
event Action LostFocus;
}
}

32
src/Avalonia.Controls/Platform/INativeControlHostImpl.cs

@ -0,0 +1,32 @@
using System;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Platform
{
public interface INativeControlHostImpl
{
INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent);
INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create);
INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle);
bool IsCompatibleWith(IPlatformHandle handle);
}
public interface INativeControlHostDestroyableControlHandle : IPlatformHandle
{
void Destroy();
}
public interface INativeControlHostControlTopLevelAttachment : IDisposable
{
INativeControlHostImpl AttachedTo { get; set; }
bool IsCompatibleWith(INativeControlHostImpl host);
void HideWithSize(Size size);
void ShowInBounds(Rect rect);
}
public interface ITopLevelImplWithNativeControlHost
{
INativeControlHostImpl NativeControlHost { get; }
}
}

5
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -104,6 +104,11 @@ namespace Avalonia.Platform
/// Gets or sets a method called when the underlying implementation is destroyed.
/// </summary>
Action Closed { get; set; }
/// <summary>
/// Gets or sets a method called when the input focus is lost.
/// </summary>
Action LostFocus { get; set; }
/// <summary>
/// Gets a mouse device associated with toplevel

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

@ -3,6 +3,6 @@ namespace Avalonia.Platform
public interface IWindowingPlatform
{
IWindowImpl CreateWindow();
IEmbeddableWindowImpl CreateEmbeddableWindow();
IWindowImpl CreateEmbeddableWindow();
}
}

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

@ -34,7 +34,7 @@ namespace Avalonia.Controls.Platform
return s_designerMode ? (IWindowImpl)platform.CreateEmbeddableWindow() : platform.CreateWindow();
}
public static IEmbeddableWindowImpl CreateEmbeddableWindow()
public static IWindowImpl CreateEmbeddableWindow()
{
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
if (platform == null)

198
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices.ComTypes;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
@ -14,7 +12,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -58,13 +56,19 @@ namespace Avalonia.Controls.Presenters
o => o.Viewport,
(o, v) => o.Viewport = v);
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _arranging;
private Size _extent;
private Vector _offset;
private IDisposable _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls;
private List<IControl> _anchorCandidates;
private (IControl control, Rect bounds) _anchor;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -73,7 +77,6 @@ namespace Avalonia.Controls.Presenters
{
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>((x, e) => x.ChildChanged(e));
AffectsArrange<ScrollContentPresenter>(OffsetProperty);
}
/// <summary>
@ -87,6 +90,8 @@ namespace Avalonia.Controls.Presenters
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
internal event EventHandler<VectorEventArgs> PreArrange;
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
@ -120,7 +125,7 @@ namespace Avalonia.Controls.Presenters
public Vector Offset
{
get { return _offset; }
set { SetAndRaise(OffsetProperty, ref _offset, value); }
set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); }
}
/// <summary>
@ -132,6 +137,9 @@ namespace Avalonia.Controls.Presenters
private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
}
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control;
/// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content.
/// </summary>
@ -196,6 +204,30 @@ namespace Avalonia.Controls.Presenters
return result;
}
/// <inheritdoc/>
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
{
if (!this.IsVisualAncestorOf(element))
{
throw new InvalidOperationException(
"An anchor control must be a visual descendent of the ScrollContentPresenter.");
}
_anchorCandidates ??= new List<IControl>();
_anchorCandidates.Add(element);
}
/// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
_anchorCandidates?.Remove(element);
if (_anchor.control == element)
{
_anchor = default;
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@ -215,22 +247,85 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
PreArrange?.Invoke(this, new VectorEventArgs
{
Vector = new Vector(finalSize.Width, finalSize.Height),
});
if (_logicalScrollSubscription != null || Child == null)
{
return base.ArrangeOverride(finalSize);
}
try
{
_arranging = true;
return ArrangeWithAnchoring(finalSize);
}
finally
{
_arranging = false;
}
}
private Size ArrangeWithAnchoring(Size finalSize)
{
var size = new Size(
CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width,
CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height);
Vector TrackAnchor()
{
// If we have an anchor and its position relative to Child has changed during the
// arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust
// relative positions within Child).
if (_anchor.control != null &&
TranslateBounds(_anchor.control, Child, out var updatedBounds) &&
updatedBounds.Position != _anchor.bounds.Position)
{
var offset = updatedBounds.Position - _anchor.bounds.Position;
return offset;
}
return default;
}
// Calculate the new anchor element.
_anchor = CalculateCurrentAnchor();
// Do the arrange.
ArrangeOverrideImpl(size, -Offset);
// If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
var anchorShift = TrackAnchor();
if (anchorShift != default)
{
var newOffset = Offset + anchorShift;
var newExtent = Extent;
var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height);
if (newOffset.X > maxOffset.X)
{
newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width);
}
if (newOffset.Y > maxOffset.Y)
{
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height);
}
Extent = newExtent;
Offset = newOffset;
ArrangeOverrideImpl(size, -Offset);
}
Viewport = finalSize;
Extent = Child.Bounds.Size.Inflate(Child.Margin);
return finalSize;
}
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
{
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
@ -327,6 +422,16 @@ namespace Avalonia.Controls.Presenters
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (change.Property == OffsetProperty && !_arranging)
{
InvalidateArrange();
}
base.OnPropertyChanged(change);
}
private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect);
@ -390,5 +495,84 @@ namespace Avalonia.Controls.Presenters
Offset = scrollable.Offset;
}
}
private (IControl, Rect) CalculateCurrentAnchor()
{
if (_anchorCandidates == null)
{
return default;
}
var bestCandidate = default(IControl);
var bestCandidateDistance = double.MaxValue;
// Find the anchor candidate that is scrolled closest to the top-left of this
// ScrollContentPresenter.
foreach (var element in _anchorCandidates)
{
if (element.IsVisible && GetViewportBounds(element, out var bounds))
{
var distance = (Vector)bounds.Position;
var candidateDistance = Math.Abs(distance.Length);
if (candidateDistance < bestCandidateDistance)
{
bestCandidate = element;
bestCandidateDistance = candidateDistance;
}
}
}
if (bestCandidate != null)
{
// We have a candidate, calculate its bounds relative to Child. Because these
// bounds aren't relative to the ScrollContentPresenter itself, if they change
// then we know it wasn't just due to scrolling.
var unscrolledBounds = TranslateBounds(bestCandidate, Child);
return (bestCandidate, unscrolledBounds);
}
return default;
}
private bool GetViewportBounds(IControl element, out Rect bounds)
{
if (TranslateBounds(element, Child, out var childBounds))
{
// We want the bounds relative to the new Offset, regardless of whether the child
// control has actually been arranged to this offset yet, so translate first to the
// child control and then apply Offset rather than translating directly to this
// control.
var thisBounds = new Rect(Bounds.Size);
bounds = new Rect(childBounds.Position - Offset, childBounds.Size);
return bounds.Intersects(thisBounds);
}
bounds = default;
return false;
}
private Rect TranslateBounds(IControl control, IControl to)
{
if (TranslateBounds(control, to, out var bounds))
{
return bounds;
}
throw new InvalidOperationException("The control's bounds could not be translated to the requested control.");
}
private bool TranslateBounds(IControl control, IControl to, out Rect bounds)
{
if (!control.IsVisible)
{
bounds = default;
return false;
}
var p = control.TranslatePoint(default, to);
bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default;
return p.HasValue;
}
}
}

27
src/Avalonia.Controls/Primitives/HeaderedContentControl.cs

@ -1,8 +1,9 @@
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
@ -13,36 +14,36 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly StyledProperty<object> HeaderProperty =
AvaloniaProperty.Register<HeaderedContentControl, object>(nameof(Header));
public static readonly StyledProperty<object?> HeaderProperty =
AvaloniaProperty.Register<HeaderedContentControl, object?>(nameof(Header));
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> HeaderTemplateProperty =
AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate>(nameof(HeaderTemplate));
public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate?>(nameof(HeaderTemplate));
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static HeaderedContentControl()
{
ContentProperty.Changed.AddClassHandler<HeaderedContentControl>((x, e) => x.HeaderChanged(e));
HeaderProperty.Changed.AddClassHandler<HeaderedContentControl>((x, e) => x.HeaderChanged(e));
}
/// <summary>
/// Gets or sets the header content.
/// </summary>
public object Header
public object? Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public IContentPresenter HeaderPresenter
public IContentPresenter? HeaderPresenter
{
get;
private set;
@ -51,10 +52,10 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets or sets the data template used to display the header content of the control.
/// </summary>
public IDataTemplate HeaderTemplate
public IDataTemplate? HeaderTemplate
{
get { return GetValue(HeaderTemplateProperty); }
set { SetValue(HeaderTemplateProperty, value); }
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <inheritdoc/>

11
src/Avalonia.Controls/Primitives/Popup.cs

@ -9,6 +9,7 @@ using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.VisualTree;
#nullable enable
@ -351,6 +352,10 @@ namespace Avalonia.Controls.Primitives
DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler));
DeferCleanup(SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler));
}
else
{
@ -610,6 +615,12 @@ namespace Avalonia.Controls.Primitives
Close();
}
}
private void WindowLostFocus()
{
if(!StaysOpen)
Close();
}
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{

123
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -3,6 +3,7 @@ using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Threading;
namespace Avalonia.Controls.Primitives
{
@ -40,10 +41,26 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ScrollBar, Orientation>(nameof(Orientation), Orientation.Vertical);
/// <summary>
/// Defines the <see cref="IsExpandedProperty"/> property.
/// </summary>
public static readonly DirectProperty<ScrollBar, bool> IsExpandedProperty =
AvaloniaProperty.RegisterDirect<ScrollBar, bool>(
nameof(IsExpanded),
o => o.IsExpanded);
/// <summary>
/// Defines the <see cref="AllowAutoHide"/> property.
/// </summary>
public static readonly StyledProperty<bool> AllowAutoHideProperty =
AvaloniaProperty.Register<ScrollBar, bool>(nameof(AllowAutoHide), true);
private Button _lineUpButton;
private Button _lineDownButton;
private Button _pageUpButton;
private Button _pageDownButton;
private DispatcherTimer _timer;
private bool _isExpanded;
/// <summary>
/// Initializes static members of the <see cref="ScrollBar"/> class.
@ -90,6 +107,24 @@ namespace Avalonia.Controls.Primitives
set { SetValue(OrientationProperty, value); }
}
/// <summary>
/// Gets a value that indicates whether the scrollbar is expanded.
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
private set => SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
}
/// <summary>
/// Gets a value that indicates whether the scrollbar can hide itself when user is not interacting with it.
/// </summary>
public bool AllowAutoHide
{
get => GetValue(AllowAutoHideProperty);
set => SetValue(AllowAutoHideProperty, value);
}
public event EventHandler<ScrollEventArgs> Scroll;
/// <summary>
@ -131,6 +166,10 @@ namespace Avalonia.Controls.Primitives
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Orientation>());
}
else if (change.Property == AllowAutoHideProperty)
{
UpdateIsExpandedState();
}
else
{
if (change.Property == MinimumProperty ||
@ -143,6 +182,26 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnPointerEnter(PointerEventArgs e)
{
base.OnPointerEnter(e);
if (AllowAutoHide)
{
ExpandAfterDelay();
}
}
protected override void OnPointerLeave(PointerEventArgs e)
{
base.OnPointerLeave(e);
if (AllowAutoHide)
{
CollapseAfterDelay();
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_lineUpButton != null)
@ -170,8 +229,6 @@ namespace Avalonia.Controls.Primitives
_pageUpButton = e.NameScope.Find<Button>("PART_PageUpButton");
_pageDownButton = e.NameScope.Find<Button>("PART_PageDownButton");
if (_lineUpButton != null)
{
_lineUpButton.Click += LineUpClick;
@ -193,6 +250,68 @@ namespace Avalonia.Controls.Primitives
}
}
private void InvokeAfterDelay(Action handler, TimeSpan delay)
{
if (_timer != null)
{
_timer.Stop();
}
else
{
_timer = new DispatcherTimer(DispatcherPriority.Normal);
_timer.Tick += (sender, args) =>
{
var senderTimer = (DispatcherTimer)sender;
if (senderTimer.Tag is Action action)
{
action();
}
senderTimer.Stop();
};
}
_timer.Tag = handler;
_timer.Interval = delay;
_timer.Start();
}
private void UpdateIsExpandedState()
{
if (!AllowAutoHide)
{
_timer?.Stop();
IsExpanded = true;
}
else
{
IsExpanded = IsPointerOver;
}
}
private void CollapseAfterDelay()
{
InvokeAfterDelay(Collapse, TimeSpan.FromSeconds(2));
}
private void ExpandAfterDelay()
{
InvokeAfterDelay(Expand, TimeSpan.FromMilliseconds(400));
}
private void Collapse()
{
IsExpanded = false;
}
private void Expand()
{
IsExpanded = true;
}
private void LineUpClick(object sender, RoutedEventArgs e)
{
SmallDecrement();

164
src/Avalonia.Controls/ProgressBar.cs

@ -1,8 +1,7 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
namespace Avalonia.Controls
{
@ -11,6 +10,92 @@ namespace Avalonia.Controls
/// </summary>
public class ProgressBar : RangeBase
{
public class ProgressBarTemplateProperties : AvaloniaObject
{
private double _container2Width;
private double _containerWidth;
private double _containerAnimationStartPosition;
private double _containerAnimationEndPosition;
private double _container2AnimationStartPosition;
private double _container2AnimationEndPosition;
public static readonly DirectProperty<ProgressBarTemplateProperties, double> ContainerAnimationStartPositionProperty =
AvaloniaProperty.RegisterDirect<ProgressBarTemplateProperties, double>(
nameof(ContainerAnimationStartPosition),
p => p.ContainerAnimationStartPosition,
(p, o) => p.ContainerAnimationStartPosition = o, 0d);
public static readonly DirectProperty<ProgressBarTemplateProperties, double> ContainerAnimationEndPositionProperty =
AvaloniaProperty.RegisterDirect<ProgressBarTemplateProperties, double>(
nameof(ContainerAnimationEndPosition),
p => p.ContainerAnimationEndPosition,
(p, o) => p.ContainerAnimationEndPosition = o, 0d);
public static readonly DirectProperty<ProgressBarTemplateProperties, double> Container2AnimationStartPositionProperty =
AvaloniaProperty.RegisterDirect<ProgressBarTemplateProperties, double>(
nameof(Container2AnimationStartPosition),
p => p.Container2AnimationStartPosition,
(p, o) => p.Container2AnimationStartPosition = o, 0d);
public static readonly DirectProperty<ProgressBarTemplateProperties, double> Container2AnimationEndPositionProperty =
AvaloniaProperty.RegisterDirect<ProgressBarTemplateProperties, double>(
nameof(Container2AnimationEndPosition),
p => p.Container2AnimationEndPosition,
(p, o) => p.Container2AnimationEndPosition = o);
public static readonly DirectProperty<ProgressBarTemplateProperties, double> Container2WidthProperty =
AvaloniaProperty.RegisterDirect<ProgressBarTemplateProperties, double>(
nameof(Container2Width),
p => p.Container2Width,
(p, o) => p.Container2Width = o);
public static readonly DirectProperty<ProgressBarTemplateProperties, double> ContainerWidthProperty =
AvaloniaProperty.RegisterDirect<ProgressBarTemplateProperties, double>(
nameof(ContainerWidth),
p => p.ContainerWidth,
(p, o) => p.ContainerWidth = o);
public double ContainerAnimationStartPosition
{
get => _containerAnimationStartPosition;
set => SetAndRaise(ContainerAnimationStartPositionProperty, ref _containerAnimationStartPosition, value);
}
public double ContainerAnimationEndPosition
{
get => _containerAnimationEndPosition;
set => SetAndRaise(ContainerAnimationEndPositionProperty, ref _containerAnimationEndPosition, value);
}
public double Container2AnimationStartPosition
{
get => _container2AnimationStartPosition;
set => SetAndRaise(Container2AnimationStartPositionProperty, ref _container2AnimationStartPosition, value);
}
public double Container2Width
{
get => _container2Width;
set => SetAndRaise(Container2WidthProperty, ref _container2Width, value);
}
public double ContainerWidth
{
get => _containerWidth;
set => SetAndRaise(ContainerWidthProperty, ref _containerWidth, value);
}
public double Container2AnimationEndPosition
{
get => _container2AnimationEndPosition;
set => SetAndRaise(Container2AnimationEndPositionProperty, ref _container2AnimationEndPosition, value);
}
}
private double _indeterminateStartingOffset;
private double _indeterminateEndingOffset;
private Border _indicator;
public static readonly StyledProperty<bool> IsIndeterminateProperty =
AvaloniaProperty.Register<ProgressBar, bool>(nameof(IsIndeterminate));
@ -20,19 +105,33 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
private static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
private static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
private Border _indicator;
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public double IndeterminateStartingOffset
{
get => _indeterminateStartingOffset;
set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
}
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public double IndeterminateEndingOffset
{
get => _indeterminateEndingOffset;
set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
}
static ProgressBar()
{
@ -45,6 +144,8 @@ namespace Avalonia.Controls
UpdatePseudoClasses(IsIndeterminate, Orientation);
}
public ProgressBarTemplateProperties TemplateProperties { get; } = new ProgressBarTemplateProperties();
public bool IsIndeterminate
{
get => GetValue(IsIndeterminateProperty);
@ -62,19 +163,6 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
private double _indeterminateStartingOffset;
private double IndeterminateStartingOffset
{
get => _indeterminateStartingOffset;
set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
}
private double _indeterminateEndingOffset;
private double IndeterminateEndingOffset
{
get => _indeterminateEndingOffset;
set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
@ -111,21 +199,33 @@ namespace Avalonia.Controls
{
if (IsIndeterminate)
{
if (Orientation == Orientation.Horizontal)
{
var width = bounds.Width / 5.0;
IndeterminateStartingOffset = -width;
_indicator.Width = width;
IndeterminateEndingOffset = bounds.Width;
// Pulled from ModernWPF.
}
else
{
var height = bounds.Height / 5.0;
IndeterminateStartingOffset = -bounds.Height;
_indicator.Height = height;
IndeterminateEndingOffset = height;
}
var dim = Orientation == Orientation.Horizontal ? bounds.Width : bounds.Height;
var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar
var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar
TemplateProperties.ContainerWidth = barIndicatorWidth;
TemplateProperties.Container2Width = barIndicatorWidth2;
TemplateProperties.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180%
TemplateProperties.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300%
TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150%
TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166%
// Remove these properties when we switch to fluent as default and removed the old one.
IndeterminateStartingOffset = -(dim / 5d);
IndeterminateEndingOffset = dim;
var padding = Padding;
var rectangle = new RectangleGeometry(
new Rect(
padding.Left,
padding.Top,
bounds.Width - (padding.Right + padding.Left),
bounds.Height - (padding.Bottom + padding.Top)
));
}
else
{

4
src/Avalonia.Controls/Remote/RemoteServer.cs

@ -10,13 +10,13 @@ namespace Avalonia.Controls.Remote
{
private EmbeddableControlRoot _topLevel;
class EmbeddableRemoteServerTopLevelImpl : RemoteServerTopLevelImpl, IEmbeddableWindowImpl
class EmbeddableRemoteServerTopLevelImpl : RemoteServerTopLevelImpl
{
public EmbeddableRemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) : base(transport)
{
}
#pragma warning disable 67
public event Action LostFocus;
public Action LostFocus { get; set; }
}

8
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -80,6 +81,7 @@ namespace Avalonia.Controls
static ItemsRepeater()
{
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
RequestBringIntoViewEvent.AddClassHandler<ItemsRepeater>((x, e) => x.OnRequestBringIntoView(e));
}
/// <summary>
@ -305,6 +307,7 @@ namespace Avalonia.Controls
virtInfo.AutoRecycleCandidate &&
!virtInfo.KeepAlive)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "AutoClear - {Index}", virtInfo.Index);
ClearElementImpl(element);
}
}
@ -743,6 +746,11 @@ namespace Avalonia.Controls
}
}
private void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
_viewportManager.OnBringIntoViewRequested(e);
}
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();

7
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Layout;
using Avalonia.Logging;
namespace Avalonia.Controls
{
@ -58,7 +59,11 @@ namespace Avalonia.Controls
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element);
protected override void RecycleElementCore(ILayoutable element)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "RepeaterLayout - RecycleElement: {Index}", _owner.GetElementIndex((IControl)element));
_owner.ClearElementImpl((IControl)element);
}
protected override Rect RealizationRectCore() => _owner.RealizationWindow;
}

27
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -60,11 +61,13 @@ namespace Avalonia.Controls
if (suppressAutoRecycle)
{
virtInfo.AutoRecycleCandidate = false;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index);
}
else
{
virtInfo.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index);
}
return element;
@ -107,10 +110,28 @@ namespace Avalonia.Controls
}
}
// We need to clear the datacontext to prevent crashes from happening,
// however we only do that if we were the ones setting it.
// That is when one of the following is the case (numbering taken from line ~642):
// 1.2 No ItemTemplate, data is not a UIElement
// 2.1 ItemTemplate, data is not FrameworkElement
// 2.2.2 Itemtemplate, data is FrameworkElement, ElementFactory returned Element different to data
//
// In all of those three cases, we the ItemTemplateShim is NOT null.
// Luckily when we create the items, we store whether we were the once setting the DataContext.
public void ClearElementToElementFactory(IControl element)
{
_owner.OnElementClearing(element);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToElementFactory();
// During creation of this object, we were the one setting the DataContext, so clear it now.
if (virtInfo.MustClearDataContext)
{
element.DataContext = null;
}
if (_owner.ItemTemplateShim != null)
{
_owner.ItemTemplateShim.RecycleElement(_owner, element);
@ -124,9 +145,6 @@ namespace Avalonia.Controls
}
}
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToElementFactory();
if (_lastFocusedElement == element)
{
// Focused element is going away. Remove the tracked last focused element
@ -594,11 +612,14 @@ namespace Avalonia.Controls
{
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
}
// Clear flag
virtInfo.MustClearDataContext = false;
if (data != element)
{
// Prepare the element
element.DataContext = data;
virtInfo.MustClearDataContext = true;
}
virtInfo.MoveOwnershipToLayoutFromElementFactory(

170
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -5,9 +5,15 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -35,16 +41,16 @@ namespace Avalonia.Controls
// actually happened. This can happen in cases where no scrollviewer
// in the parent chain can scroll in the shift direction.
private Point _unshiftableShift;
private double _maximumHorizontalCacheLength = 0.0;
private double _maximumVerticalCacheLength = 0.0;
private double _maximumHorizontalCacheLength = 2.0;
private double _maximumVerticalCacheLength = 2.0;
private double _horizontalCacheBufferPerSide;
private double _verticalCacheBufferPerSide;
private bool _isBringIntoViewInProgress;
// For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work.
bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker;
private bool _managingViewportDisabled;
private bool _effectiveViewportChangedSubscribed;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
@ -184,6 +190,9 @@ namespace Avalonia.Controls
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})",
_owner.Layout.LayoutId, _expectedViewportShift);
// There are cases where we might be expecting a shift but not get it. We will
// be waiting for the effective viewport event but if the scroll viewer is not able
// to perform the shift (perhaps because it cannot scroll in negative offset),
@ -219,24 +228,26 @@ namespace Avalonia.Controls
_pendingViewportShift = default;
_unshiftableShift = default;
_effectiveViewportChangedRevoker?.Dispose();
if (!_managingViewportDisabled)
if (_managingViewportDisabled && _effectiveViewportChangedSubscribed)
{
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
_owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
}
else if (!_managingViewportDisabled && !_effectiveViewportChangedSubscribed)
{
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
}
}
public void OnElementPrepared(IControl element)
{
// If we have an anchor element, we do not want the
// scroll anchor provider to start anchoring some other element.
////element.CanBeScrollAnchor(true);
_scroller?.RegisterAnchorCandidate(element);
}
public void OnElementCleared(ILayoutable element)
public void OnElementCleared(IControl element)
{
////element.CanBeScrollAnchor(false);
_scroller?.UnregisterAnchorCandidate(element);
}
public void OnOwnerMeasuring()
@ -282,6 +293,7 @@ namespace Avalonia.Controls
private void OnLayoutUpdated(object sender, EventArgs args)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
_layoutUpdatedSubscribed = false;
if (_managingViewportDisabled)
{
return;
@ -293,6 +305,10 @@ namespace Avalonia.Controls
// that can scroll in the direction where the shift is expected.
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout Updated with pending shift {Shift}- invalidating measure",
_owner.Layout.LayoutId,
_pendingViewportShift);
// Assume this is never going to come.
_unshiftableShift = new Point(
_unshiftableShift.X + _pendingViewportShift.X,
@ -306,8 +322,11 @@ namespace Avalonia.Controls
public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
{
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
if (_makeAnchorElement != anchor)
{
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
}
}
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
@ -325,29 +344,36 @@ namespace Avalonia.Controls
// Note that the element being brought into view could be a descendant.
var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
if (targetChild is null)
{
return;
}
// Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children)
{
////if (child.CanBeScrollAnchor && child != targetChild)
////{
//// child.CanBeScrollAnchor = false;
////}
if (child != targetChild)
{
_scroller.UnregisterAnchorCandidate(child);
}
}
// Register to rendering event to go back to how things were before where any child can be the anchor.
_isBringIntoViewInProgress = true;
////if (!m_renderingToken)
////{
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
////}
// Register action to go back to how things were before where any child can be the anchor. Here,
// WinUI uses CompositionTarget.Rendering but we don't currently have that, so post an action to
// run *after* rendering has completed (priority needs to be lower than Render as Transformed
// bounds must have been set in order for OnEffectiveViewportChanged to trigger).
if (!_isBringIntoViewInProgress)
{
_isBringIntoViewInProgress = true;
Dispatcher.UIThread.Post(OnCompositionTargetRendering, DispatcherPriority.Loaded);
}
}
}
private IControl GetImmediateChildOfRepeater(IControl descendant)
{
var targetChild = descendant;
var parent = descendant.Parent;
var parent = (IControl)descendant.VisualParent;
while (parent != null && parent != _owner)
{
targetChild = parent;
@ -356,33 +382,57 @@ namespace Avalonia.Controls
if (parent == null)
{
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
return null;
}
return targetChild;
}
public void ResetScrollers()
private void OnCompositionTargetRendering()
{
_scroller = null;
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = null;
_ensuredScroller = false;
_isBringIntoViewInProgress = false;
_makeAnchorElement = null;
if (_scroller is object)
{
foreach (var child in _owner.Children)
{
var info = ItemsRepeater.GetVirtualizationInfo(child);
if (info.IsRealized && info.IsHeldByLayout)
{
_scroller.RegisterAnchorCandidate(child);
}
}
}
// HACK: Invalidate measure now that the anchor has been removed so that a layout can be
// done with a proper realization rect. This is a hack not present upstream to try to fix
// https://github.com/microsoft/microsoft-ui-xaml/issues/1422
TryInvalidateMeasure();
}
private void OnEffectiveViewportChanged(TransformedBounds? bounds)
public void ResetScrollers()
{
if (!bounds.HasValue)
if (_scroller is object)
{
return;
foreach (var child in _owner.Children)
{
_scroller.UnregisterAnchorCandidate(child);
}
_scroller = null;
}
var globalClip = bounds.Value.Clip;
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
var clip = globalClip.TransformToAABB(transform);
var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
_owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
_ensuredScroller = false;
}
UpdateViewport(effectiveViewport);
private void OnEffectiveViewportChanged(object sender, EffectiveViewportChangedEventArgs e)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId);
UpdateViewport(e.EffectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
@ -397,6 +447,7 @@ namespace Avalonia.Controls
if (_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
_layoutUpdatedSubscribed = false;
}
}
@ -426,8 +477,8 @@ namespace Avalonia.Controls
}
else if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
}
_ensuredScroller = true;
@ -437,10 +488,17 @@ namespace Avalonia.Controls
private void UpdateViewport(Rect viewport)
{
var currentVisibleWindow = viewport;
var previousVisibleWindow = _visibleWindow;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Effective Viewport: ({Before})->({After})",
_owner.Layout.LayoutId,
previousVisibleWindow,
viewport);
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout.LayoutId);
// We got cleared.
_visibleWindow = default;
}
@ -449,7 +507,14 @@ namespace Avalonia.Controls
_visibleWindow = currentVisibleWindow;
}
TryInvalidateMeasure();
if (_visibleWindow != previousVisibleWindow)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Used Viewport: ({Before})->({After})",
_owner.Layout.LayoutId,
previousVisibleWindow,
currentVisibleWindow);
TryInvalidateMeasure();
}
}
private static void ValidateCacheLength(double cacheLength)
@ -468,26 +533,11 @@ namespace Avalonia.Controls
// We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to
// avoid layout cycles.
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout.LayoutId);
_owner.InvalidateMeasure();
}
}
private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
{
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has
// run, resulting in the UI being updated too late when scrolling quickly. We can
// partially remedey this by triggering also on Bounds changes, but this won't work so
// well for nested ItemsRepeaters.
//
// UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia.
return control.GetObservable(Visual.TransformedBoundsProperty)
.Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds))
.Skip(1)
.Subscribe(OnEffectiveViewportChanged);
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)

1
src/Avalonia.Controls/Repeater/VirtualizationInfo.cs

@ -36,6 +36,7 @@ namespace Avalonia.Controls
public bool IsHeldByLayout => Owner == ElementOwner.Layout;
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool MustClearDataContext { get; set; }
public bool KeepAlive { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; }

115
src/Avalonia.Controls/ScrollViewer.cs

@ -1,4 +1,5 @@
using System;
using System.Reactive.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
@ -166,6 +167,18 @@ namespace Avalonia.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
/// <summary>
/// Defines the <see cref="IsExpandedProperty"/> property.
/// </summary>
public static readonly DirectProperty<ScrollViewer, bool> IsExpandedProperty =
ScrollBar.IsExpandedProperty.AddOwner<ScrollViewer>(o => o.IsExpanded);
/// <summary>
/// Defines the <see cref="AllowAutoHide"/> property.
/// </summary>
public static readonly StyledProperty<bool> AllowAutoHideProperty =
ScrollBar.AllowAutoHideProperty.AddOwner<ScrollViewer>();
/// <summary>
/// Defines the <see cref="ScrollChanged"/> event.
/// </summary>
@ -186,6 +199,8 @@ namespace Avalonia.Controls
private Size _oldViewport;
private Size _largeChange;
private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange);
private bool _isExpanded;
private IDisposable _scrollBarExpandSubscription;
/// <summary>
/// Initializes static members of the <see cref="ScrollViewer"/> class.
@ -244,9 +259,7 @@ namespace Avalonia.Controls
set
{
value = ValidateOffset(this, value);
if (SetAndRaise(OffsetProperty, ref _offset, value))
if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value)))
{
CalculatedPropertiesChanged();
}
@ -316,6 +329,9 @@ namespace Avalonia.Controls
get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; }
}
/// <inheritdoc/>
public IControl CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor;
/// <summary>
/// Gets the maximum horizontal scrollbar value.
/// </summary>
@ -382,8 +398,23 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary>
/// Gets a value that indicates whether any scrollbar is expanded.
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value);
}
/// <summary>
/// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it.
/// </summary>
public bool AllowAutoHide
{
get => GetValue(AllowAutoHideProperty);
set => SetValue(AllowAutoHideProperty, value);
}
/// <summary>
/// Scrolls the content up one line.
@ -473,14 +504,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
/// <inheritdoc/>
public void RegisterAnchorCandidate(IControl element)
{
// TODO: Implement
(Presenter as IScrollAnchorProvider)?.RegisterAnchorCandidate(element);
}
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
/// <inheritdoc/>
public void UnregisterAnchorCandidate(IControl element)
{
// TODO: Implement
(Presenter as IScrollAnchorProvider)?.UnregisterAnchorCandidate(element);
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
@ -517,22 +550,6 @@ namespace Avalonia.Controls
return double.IsNaN(result) ? 0 : result;
}
private static Vector ValidateOffset(AvaloniaObject o, Vector value)
{
ScrollViewer scrollViewer = o as ScrollViewer;
if (scrollViewer != null)
{
var extent = scrollViewer.Extent;
var viewport = scrollViewer.Viewport;
return CoerceOffset(extent, viewport, value);
}
else
{
return value;
}
}
private void ChildChanged(IControl child)
{
if (_logicalScrollable is object)
@ -630,6 +647,54 @@ namespace Avalonia.Controls
RaiseEvent(e);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scrollBarExpandSubscription?.Dispose();
_scrollBarExpandSubscription = SubscribeToScrollBars(e);
}
private IDisposable SubscribeToScrollBars(TemplateAppliedEventArgs e)
{
static IObservable<bool> GetExpandedObservable(ScrollBar scrollBar)
{
return scrollBar?.GetObservable(ScrollBar.IsExpandedProperty);
}
var horizontalScrollBar = e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar");
var verticalScrollBar = e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar");
var horizontalExpanded = GetExpandedObservable(horizontalScrollBar);
var verticalExpanded = GetExpandedObservable(verticalScrollBar);
IObservable<bool> actualExpanded = null;
if (horizontalExpanded != null && verticalExpanded != null)
{
actualExpanded = horizontalExpanded.CombineLatest(verticalExpanded, (h, v) => h || v);
}
else
{
if (horizontalExpanded != null)
{
actualExpanded = horizontalExpanded;
}
else if (verticalExpanded != null)
{
actualExpanded = verticalExpanded;
}
}
return actualExpanded?.Subscribe(OnScrollBarExpandedChanged);
}
private void OnScrollBarExpandedChanged(bool isExpanded)
{
IsExpanded = isExpanded;
}
private void OnLayoutUpdated(object sender, EventArgs e) => RaiseScrollChanged();
private void RaiseScrollChanged()

2
src/Avalonia.Controls/SelectionModel.cs

@ -189,8 +189,6 @@ namespace Avalonia.Controls
}
set
{
var isSelected = IsSelectedWithPartialAt(value);
if (!IsSelectedAt(value) || SelectedItems.Count > 1)
{
using var operation = new Operation(this);

487
src/Avalonia.Controls/SplitView.cs

@ -0,0 +1,487 @@
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.VisualTree;
using System;
using System.Reactive.Disposables;
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants for how the SplitView Pane should display
/// </summary>
public enum SplitViewDisplayMode
{
/// <summary>
/// Pane is displayed next to content, and does not auto collapse
/// when tapped outside
/// </summary>
Inline,
/// <summary>
/// Pane is displayed next to content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane does not auto collapse
/// when tapped outside
/// </summary>
CompactInline,
/// <summary>
/// Pane is displayed above content. Pane collapses when tapped outside
/// </summary>
Overlay,
/// <summary>
/// Pane is displayed above content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane collapses when tapped outside
/// </summary>
CompactOverlay
}
/// <summary>
/// Defines constants for where the Pane should appear
/// </summary>
public enum SplitViewPanePlacement
{
Left,
Right
}
public class SplitViewTemplateSettings : AvaloniaObject
{
internal SplitViewTemplateSettings() { }
public static readonly StyledProperty<double> ClosedPaneWidthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, double>(nameof(ClosedPaneWidth), 0d);
public static readonly StyledProperty<GridLength> PaneColumnGridLengthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, GridLength>(nameof(PaneColumnGridLength));
public double ClosedPaneWidth
{
get => GetValue(ClosedPaneWidthProperty);
internal set => SetValue(ClosedPaneWidthProperty, value);
}
public GridLength PaneColumnGridLength
{
get => GetValue(PaneColumnGridLengthProperty);
internal set => SetValue(PaneColumnGridLengthProperty, value);
}
}
/// <summary>
/// A control with two views: A collapsible pane and an area for content
/// </summary>
public class SplitView : TemplatedControl
{
/*
Pseudo classes & combos
:open / :closed
:compactoverlay :compactinline :overlay :inline
:left :right
*/
/// <summary>
/// Defines the <see cref="Content"/> property
/// </summary>
public static readonly StyledProperty<IControl> ContentProperty =
AvaloniaProperty.Register<SplitView, IControl>(nameof(Content));
/// <summary>
/// Defines the <see cref="CompactPaneLength"/> property
/// </summary>
public static readonly StyledProperty<double> CompactPaneLengthProperty =
AvaloniaProperty.Register<SplitView, double>(nameof(CompactPaneLength), defaultValue: 48);
/// <summary>
/// Defines the <see cref="DisplayMode"/> property
/// </summary>
public static readonly StyledProperty<SplitViewDisplayMode> DisplayModeProperty =
AvaloniaProperty.Register<SplitView, SplitViewDisplayMode>(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay);
/// <summary>
/// Defines the <see cref="IsPaneOpen"/> property
/// </summary>
public static readonly DirectProperty<SplitView, bool> IsPaneOpenProperty =
AvaloniaProperty.RegisterDirect<SplitView, bool>(nameof(IsPaneOpen),
x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v);
/// <summary>
/// Defines the <see cref="OpenPaneLength"/> property
/// </summary>
public static readonly StyledProperty<double> OpenPaneLengthProperty =
AvaloniaProperty.Register<SplitView, double>(nameof(OpenPaneLength), defaultValue: 320);
/// <summary>
/// Defines the <see cref="PaneBackground"/> property
/// </summary>
public static readonly StyledProperty<IBrush> PaneBackgroundProperty =
AvaloniaProperty.Register<SplitView, IBrush>(nameof(PaneBackground));
/// <summary>
/// Defines the <see cref="PanePlacement"/> property
/// </summary>
public static readonly StyledProperty<SplitViewPanePlacement> PanePlacementProperty =
AvaloniaProperty.Register<SplitView, SplitViewPanePlacement>(nameof(PanePlacement));
/// <summary>
/// Defines the <see cref="Pane"/> property
/// </summary>
public static readonly StyledProperty<IControl> PaneProperty =
AvaloniaProperty.Register<SplitView, IControl>(nameof(Pane));
/// <summary>
/// Defines the <see cref="UseLightDismissOverlayMode"/> property
/// </summary>
public static readonly StyledProperty<bool> UseLightDismissOverlayModeProperty =
AvaloniaProperty.Register<SplitView, bool>(nameof(UseLightDismissOverlayMode));
/// <summary>
/// Defines the <see cref="TemplateSettings"/> property
/// </summary>
public static readonly StyledProperty<SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.Register<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings));
private bool _isPaneOpen;
private Panel _pane;
private CompositeDisposable _pointerDisposables;
public SplitView()
{
PseudoClasses.Add(":overlay");
PseudoClasses.Add(":left");
TemplateSettings = new SplitViewTemplateSettings();
}
static SplitView()
{
UseLightDismissOverlayModeProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnUseLightDismissChanged(v));
CompactPaneLengthProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnCompactPaneLengthChanged(v));
PanePlacementProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnPanePlacementChanged(v));
DisplayModeProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnDisplayModeChanged(v));
}
/// <summary>
/// Gets or sets the content of the SplitView
/// </summary>
[Content]
public IControl Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
/// <summary>
/// Gets or sets the length of the pane when in <see cref="SplitViewDisplayMode.CompactOverlay"/>
/// or <see cref="SplitViewDisplayMode.CompactInline"/> mode
/// </summary>
public double CompactPaneLength
{
get => GetValue(CompactPaneLengthProperty);
set => SetValue(CompactPaneLengthProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="SplitViewDisplayMode"/> for the SplitView
/// </summary>
public SplitViewDisplayMode DisplayMode
{
get => GetValue(DisplayModeProperty);
set => SetValue(DisplayModeProperty, value);
}
/// <summary>
/// Gets or sets whether the pane is open or closed
/// </summary>
public bool IsPaneOpen
{
get => _isPaneOpen;
set
{
if (value == _isPaneOpen)
{
return;
}
if (value)
{
OnPaneOpening(this, null);
SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
PseudoClasses.Add(":open");
PseudoClasses.Remove(":closed");
OnPaneOpened(this, null);
}
else
{
SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false);
OnPaneClosing(this, args);
if (!args.Cancel)
{
SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
PseudoClasses.Add(":closed");
PseudoClasses.Remove(":open");
OnPaneClosed(this, null);
}
}
}
}
/// <summary>
/// Gets or sets the length of the pane when open
/// </summary>
public double OpenPaneLength
{
get => GetValue(OpenPaneLengthProperty);
set => SetValue(OpenPaneLengthProperty, value);
}
/// <summary>
/// Gets or sets the background of the pane
/// </summary>
public IBrush PaneBackground
{
get => GetValue(PaneBackgroundProperty);
set => SetValue(PaneBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="SplitViewPanePlacement"/> for the SplitView
/// </summary>
public SplitViewPanePlacement PanePlacement
{
get => GetValue(PanePlacementProperty);
set => SetValue(PanePlacementProperty, value);
}
/// <summary>
/// Gets or sets the Pane for the SplitView
/// </summary>
public IControl Pane
{
get => GetValue(PaneProperty);
set => SetValue(PaneProperty, value);
}
/// <summary>
/// Gets or sets whether WinUI equivalent LightDismissOverlayMode is enabled
/// <para>When enabled, and the pane is open in Overlay or CompactOverlay mode,
/// the contents of the splitview are darkened to visually separate the open pane
/// and the rest of the SplitView</para>
/// </summary>
public bool UseLightDismissOverlayMode
{
get => GetValue(UseLightDismissOverlayModeProperty);
set => SetValue(UseLightDismissOverlayModeProperty, value);
}
/// <summary>
/// Gets or sets the TemplateSettings for the SplitView
/// </summary>
public SplitViewTemplateSettings TemplateSettings
{
get => GetValue(TemplateSettingsProperty);
set => SetValue(TemplateSettingsProperty, value);
}
/// <summary>
/// Fired when the pane is closed
/// </summary>
public event EventHandler<EventArgs> PaneClosed;
/// <summary>
/// Fired when the pane is closing
/// </summary>
public event EventHandler<SplitViewPaneClosingEventArgs> PaneClosing;
/// <summary>
/// Fired when the pane is opened
/// </summary>
public event EventHandler<EventArgs> PaneOpened;
/// <summary>
/// Fired when the pane is opening
/// </summary>
public event EventHandler<EventArgs> PaneOpening;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_pane = e.NameScope.Find<Panel>("PART_PaneRoot");
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var topLevel = this.VisualRoot;
if (topLevel is Window window)
{
//Logic adapted from Popup
//Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane
IDisposable subscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
_pointerDisposables = new CompositeDisposable(
window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel),
InputManager.Instance?.Process.Subscribe(OnNonClientClick),
subscribeToEventHandler<Window, EventHandler>(window, Window_Deactivated,
(x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler),
subscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, OnWindowLostFocus,
(x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler));
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposables?.Dispose();
}
private void OnWindowLostFocus()
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{
if (!IsPaneOpen)
{
return;
}
//If we click within the Pane, don't do anything
//Otherwise, ClosePane if open & using an overlay display mode
bool closePane = ShouldClosePane();
if (!closePane)
{
return;
}
var src = e.Source as IVisual;
while (src != null)
{
if (src == _pane)
{
closePane = false;
break;
}
src = src.VisualParent;
}
if (closePane)
{
IsPaneOpen = false;
e.Handled = true;
}
}
private void OnNonClientClick(RawInputEventArgs obj)
{
if (!IsPaneOpen)
{
return;
}
var mouse = obj as RawPointerEventArgs;
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
if (ShouldClosePane())
IsPaneOpen = false;
}
}
private void Window_Deactivated(object sender, EventArgs e)
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
private bool ShouldClosePane()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
}
protected virtual void OnPaneOpening(SplitView sender, EventArgs args)
{
PaneOpening?.Invoke(sender, args);
}
protected virtual void OnPaneOpened(SplitView sender, EventArgs args)
{
PaneOpened?.Invoke(sender, args);
}
protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args)
{
PaneClosing?.Invoke(sender, args);
}
protected virtual void OnPaneClosed(SplitView sender, EventArgs args)
{
PaneClosed?.Invoke(sender, args);
}
private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e)
{
var newLen = (double)e.NewValue;
var displayMode = DisplayMode;
if (displayMode == SplitViewDisplayMode.CompactInline)
{
TemplateSettings.ClosedPaneWidth = newLen;
}
else if (displayMode == SplitViewDisplayMode.CompactOverlay)
{
TemplateSettings.ClosedPaneWidth = newLen;
TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
}
}
private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue.ToString().ToLower();
var newState = e.NewValue.ToString().ToLower();
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue.ToString().ToLower();
var newState = e.NewValue.ToString().ToLower();
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
_ => throw new NotImplementedException(),
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
}
private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e)
{
var mode = (bool)e.NewValue;
PseudoClasses.Set(":lightdismiss", mode);
}
}
}

14
src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs

@ -0,0 +1,14 @@
using System;
namespace Avalonia.Controls
{
public class SplitViewPaneClosingEventArgs : EventArgs
{
public bool Cancel { get; set; }
public SplitViewPaneClosingEventArgs(bool cancel)
{
Cancel = cancel;
}
}
}

24
src/Avalonia.Controls/TopLevel.cs

@ -11,6 +11,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls
@ -170,6 +171,8 @@ namespace Avalonia.Controls
nameof(IResourceHost.ResourcesChanged),
this);
}
impl.LostFocus += PlatformImpl_LostFocus;
}
/// <summary>
@ -315,7 +318,7 @@ namespace Avalonia.Controls
/// <summary>
/// Creates the layout manager for this <see cref="TopLevel" />.
/// </summary>
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager();
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
/// <summary>
/// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
@ -337,6 +340,9 @@ namespace Avalonia.Controls
_globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved;
}
Renderer?.Dispose();
Renderer = null;
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
@ -346,8 +352,8 @@ namespace Avalonia.Controls
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
OnClosed(EventArgs.Empty);
Renderer?.Dispose();
Renderer = null;
LayoutManager?.Dispose();
}
/// <summary>
@ -471,5 +477,17 @@ namespace Avalonia.Controls
{
(this as IInputRoot).MouseDevice.SceneInvalidated(this, e.DirtyRect);
}
void PlatformImpl_LostFocus()
{
var focused = (IVisual)FocusManager.Instance.Current;
if (focused == null)
return;
while (focused.VisualParent != null)
focused = focused.VisualParent;
if (focused == this)
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
}
}
}

1
src/Avalonia.Controls/TreeViewItem.cs

@ -49,6 +49,7 @@ namespace Avalonia.Controls
static TreeViewItem()
{
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
PressedMixin.Attach<TreeViewItem>();
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));

9
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Utils
@ -15,12 +16,14 @@ namespace Avalonia.Controls.Utils
{
if (items != null)
{
var collection = items as ICollection;
if (collection != null)
if (items is ICollection collection)
{
return collection.Count;
}
else if (items is IReadOnlyCollection<object> readOnly)
{
return readOnly.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());

4
src/Avalonia.Controls/Window.cs

@ -519,7 +519,7 @@ namespace Avalonia.Controls
}
}
LayoutManager.ExecuteInitialLayoutPass(this);
LayoutManager.ExecuteInitialLayoutPass();
using (BeginAutoSizing())
{
@ -592,7 +592,7 @@ namespace Avalonia.Controls
}
}
LayoutManager.ExecuteInitialLayoutPass(this);
LayoutManager.ExecuteInitialLayoutPass();
var result = new TaskCompletionSource<TResult>();

2
src/Avalonia.Controls/WindowBase.cs

@ -162,7 +162,7 @@ namespace Avalonia.Controls
if (!_hasExecutedInitialLayoutPass)
{
LayoutManager.ExecuteInitialLayoutPass(this);
LayoutManager.ExecuteInitialLayoutPass();
_hasExecutedInitialLayoutPass = true;
}
PlatformImpl?.Show();

7
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -10,7 +10,7 @@ using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl, IEmbeddableWindowImpl
class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl
{
private readonly IAvaloniaRemoteTransportConnection _transport;
@ -45,11 +45,6 @@ namespace Avalonia.DesignerSupport.Remote
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public Size MaxAutoSizeHint { get; } = new Size(4096, 4096);
public event Action LostFocus
{
add {}
remove {}
}
protected override void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj)
{

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Remote
public IWindowImpl CreateWindow() => new WindowStub();
public IEmbeddableWindowImpl CreateEmbeddableWindow()
public IWindowImpl CreateEmbeddableWindow()
{
if (s_lastWindow != null)
{

1
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -29,6 +29,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action<double> ScalingChanged { get; set; }
public Func<bool> Closing { get; set; }
public Action Closed { get; set; }
public Action LostFocus { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public IPopupImpl CreatePopup() => new WindowStub(this);

10
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -45,7 +45,15 @@ namespace Avalonia.Diagnostics
window.Closed += DevToolsClosed;
s_open.Add(root, window);
window.Show();
if (root is Window inspectedWindow)
{
window.Show(inspectedWindow);
}
else
{
window.Show();
}
}
return Disposable.Create(() => window?.Close());

29
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
@ -9,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels
public LogicalTreeNode(ILogical logical, TreeNode parent)
: base((Control)logical, parent)
{
Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this));
Children = new LogicalTreeNodeCollection(this, logical);
}
public static LogicalTreeNode[] Create(object control)
@ -17,5 +18,31 @@ namespace Avalonia.Diagnostics.ViewModels
var logical = control as ILogical;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
}
internal class LogicalTreeNodeCollection : TreeNodeCollection
{
private readonly ILogical _control;
private IDisposable _subscription;
public LogicalTreeNodeCollection(TreeNode owner, ILogical control)
: base(owner)
{
_control = control;
}
public override void Dispose()
{
base.Dispose();
_subscription?.Dispose();
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
_subscription = _control.LogicalChildren.ForEachItem(
(i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear());
}
}
}
}

23
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
@ -12,6 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events;
private readonly IDisposable _pointerOverSubscription;
private ViewModelBase _content;
private int _selectedTab;
private string _focusedControl;
@ -25,16 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
_events = new EventsPageViewModel(root);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
};
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
_pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
Console = new ConsoleViewModel(UpdateConsoleContext);
}
@ -129,6 +124,8 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose()
{
KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;
_pointerOverSubscription.Dispose();
_logicalTree.Dispose();
_visualTree.Dispose();
}
@ -137,5 +134,13 @@ namespace Avalonia.Diagnostics.ViewModels
{
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
}
private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
}
}
}

14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -3,15 +3,15 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreeNode : ViewModelBase
internal class TreeNode : ViewModelBase, IDisposable
{
private IDisposable _classesSubscription;
private string _classes;
private bool _isExpanded;
@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.ViewModels
x => control.Classes.CollectionChanged -= x)
.TakeUntil(removed);
classesChanged.Select(_ => Unit.Default)
_classesSubscription = classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
.Subscribe(_ =>
{
@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public IAvaloniaReadOnlyList<TreeNode> Children
public TreeNodeCollection Children
{
get;
protected set;
@ -104,6 +104,12 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public void Dispose()
{
_classesSubscription.Dispose();
Children.Dispose();
}
private static int IndexOf(IReadOnlyList<TreeNode> collection, TreeNode item)
{
var count = collection.Count;

78
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@ -0,0 +1,78 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IDisposable
{
private AvaloniaList<TreeNode> _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner;
public TreeNode this[int index]
{
get
{
EnsureInitialized();
return _inner[index];
}
}
public int Count
{
get
{
EnsureInitialized();
return _inner.Count;
}
}
protected TreeNode Owner { get; }
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add => _inner.CollectionChanged += value;
remove => _inner.CollectionChanged -= value;
}
public event PropertyChangedEventHandler PropertyChanged
{
add => _inner.PropertyChanged += value;
remove => _inner.PropertyChanged -= value;
}
public virtual void Dispose()
{
if (_inner is object)
{
foreach (var node in _inner)
{
node.Dispose();
}
}
}
public IEnumerator<TreeNode> GetEnumerator()
{
EnsureInitialized();
return _inner.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
protected abstract void Initialize(AvaloniaList<TreeNode> nodes);
private void EnsureInitialized()
{
if (_inner is null)
{
_inner = new AvaloniaList<TreeNode>();
Initialize(_inner);
}
}
}
}

10
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -62,7 +62,15 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public void Dispose() => _details?.Dispose();
public void Dispose()
{
foreach (var node in Nodes)
{
node.Dispose();
}
_details?.Dispose();
}
public TreeNode FindNode(IControl control)
{

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

@ -1,3 +1,4 @@
using System;
using Avalonia.Collections;
using Avalonia.Styling;
using Avalonia.VisualTree;
@ -9,16 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels
public VisualTreeNode(IVisual visual, TreeNode parent)
: base(visual, parent)
{
var host = visual as IVisualTreeHost;
if (host?.Root == null)
{
Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this));
}
else
{
Children = new AvaloniaList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
}
Children = new VisualTreeNodeCollection(this, visual);
if ((Visual is IStyleable styleable))
{
@ -33,5 +25,30 @@ namespace Avalonia.Diagnostics.ViewModels
var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
}
internal class VisualTreeNodeCollection : TreeNodeCollection
{
private readonly IVisual _control;
private IDisposable _subscription;
public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner)
{
_control = control;
}
public override void Dispose()
{
_subscription?.Dispose();
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
_subscription = _control.VisualChildren.ForEachItem(
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear());
}
}
}
}

22
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -14,8 +14,8 @@ namespace Avalonia.Diagnostics.Views
{
internal class MainWindow : Window, IStyleHost
{
private readonly IDisposable _keySubscription;
private TopLevel _root;
private IDisposable _keySubscription;
public MainWindow()
{
@ -33,8 +33,22 @@ namespace Avalonia.Diagnostics.Views
{
if (_root != value)
{
if (_root != null)
{
_root.Closed -= RootClosed;
}
_root = value;
DataContext = new MainViewModel(value);
if (_root != null)
{
_root.Closed += RootClosed;
DataContext = new MainViewModel(value);
}
else
{
DataContext = null;
}
}
}
}
@ -45,6 +59,8 @@ namespace Avalonia.Diagnostics.Views
{
base.OnClosed(e);
_keySubscription.Dispose();
_root.Closed -= RootClosed;
_root = null;
((MainViewModel)DataContext)?.Dispose();
}
@ -70,5 +86,7 @@ namespace Avalonia.Diagnostics.Views
}
}
}
private void RootClosed(object sender, EventArgs e) => Close();
}
}

33
src/Avalonia.Input/KeyboardNavigation.cs

@ -30,6 +30,19 @@ namespace Avalonia.Input
"TabOnceActiveElement",
typeof(KeyboardNavigation));
/// <summary>
/// Defines the IsTabStop attached property.
/// </summary>
/// <remarks>
/// The IsTabStop attached property determines whether the control is focusable by tab navigation.
/// </remarks>
public static readonly AttachedProperty<bool> IsTabStopProperty =
AvaloniaProperty.RegisterAttached<InputElement, bool>(
"IsTabStop",
typeof(KeyboardNavigation),
true);
/// <summary>
/// Gets the <see cref="TabNavigationProperty"/> for a container.
/// </summary>
@ -69,5 +82,25 @@ namespace Avalonia.Input
{
element.SetValue(TabOnceActiveElementProperty, value);
}
/// <summary>
/// Sets the <see cref="IsTabStopProperty"/> for a container.
/// </summary>
/// <param name="element">The container.</param>
/// <param name="value">Value indicating whether the container is a tab stop.</param>
public static void SetIsTabStop(InputElement element, bool value)
{
element.SetValue(IsTabStopProperty, value);
}
/// <summary>
/// Gets the <see cref="IsTabStopProperty"/> for a container.
/// </summary>
/// <param name="element">The container.</param>
/// <returns>Whether the container is a tab stop.</returns>
public static bool GetIsTabStop(InputElement element)
{
return element.GetValue(IsTabStopProperty);
}
}
}

39
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -77,7 +77,8 @@ namespace Avalonia.Input.Navigation
/// <param name="element">The element.</param>
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
/// <returns>The element's focusable descendants.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element, NavigationDirection direction)
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element,
NavigationDirection direction)
{
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
@ -113,7 +114,7 @@ namespace Avalonia.Input.Navigation
}
else
{
if (child.CanFocus())
if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child))
{
yield return child;
}
@ -122,7 +123,10 @@ namespace Avalonia.Input.Navigation
{
foreach (var descendant in GetFocusableDescendants(child, direction))
{
yield return descendant;
if (KeyboardNavigation.GetIsTabStop((InputElement)descendant))
{
yield return descendant;
}
}
}
}
@ -167,7 +171,9 @@ namespace Avalonia.Input.Navigation
{
element = navigable.GetControl(direction, element, false);
if (element != null && element.CanFocus())
if (element != null &&
element.CanFocus() &&
KeyboardNavigation.GetIsTabStop((InputElement) element))
{
break;
}
@ -233,26 +239,22 @@ namespace Avalonia.Input.Navigation
return customNext.next;
}
if (sibling.CanFocus())
if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling))
{
return sibling;
}
else
next = direction == NavigationDirection.Next ?
GetFocusableDescendants(sibling, direction).FirstOrDefault() :
GetFocusableDescendants(sibling, direction).LastOrDefault();
if (next != null)
{
next = direction == NavigationDirection.Next ?
GetFocusableDescendants(sibling, direction).FirstOrDefault() :
GetFocusableDescendants(sibling, direction).LastOrDefault();
if(next != null)
{
return next;
}
return next;
}
}
if (next == null)
{
next = GetFirstInNextContainer(element, parent, direction);
}
next = GetFirstInNextContainer(element, parent, direction);
}
else
{
@ -264,7 +266,8 @@ namespace Avalonia.Input.Navigation
return next;
}
private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction)
private static (bool handled, IInputElement next) GetCustomNext(IInputElement element,
NavigationDirection direction)
{
if (element is ICustomKeyboardNavigation custom)
{

2
src/Avalonia.Layout/AttachedLayout.cs

@ -12,7 +12,7 @@ namespace Avalonia.Layout
/// </summary>
public abstract class AttachedLayout : AvaloniaObject
{
internal string LayoutId { get; set; }
public string LayoutId { get; set; }
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.

24
src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Layout
{
/// <summary>
/// Provides data for the <see cref="Layoutable.EffectiveViewportChanged"/> event.
/// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs
{
public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}
/// <summary>
/// Gets the <see cref="Rect"/> representing the effective viewport.
/// </summary>
/// <remarks>
/// The viewport is expressed in coordinates relative to the control that the event is
/// raised on.
/// </remarks>
public Rect EffectiveViewport { get; }
}
}

4
src/Avalonia.Layout/ElementManager.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Layout.Utils;
using Avalonia.Logging;
namespace Avalonia.Layout
{
@ -78,6 +79,7 @@ namespace Avalonia.Layout
{
// Sentinel. Create the element now since we need it.
int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex);
element = _context.GetOrCreateElementAt(
dataIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
@ -232,6 +234,8 @@ namespace Avalonia.Layout
{
Insert(0, dataIndex, element);
}
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save