Browse Source

Merge branch 'master' into update-readme

pull/3563/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
c7269bcba9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/BindingDemo.v3.ncrunchproject
  2. 5
      .ncrunch/RenderDemo.v3.ncrunchproject
  3. 5
      .ncrunch/VirtualizationDemo.v3.ncrunchproject
  4. 14
      azure-pipelines.yml
  5. 1
      build/CoreLibraries.props
  6. 3
      global.json
  7. 8
      native/Avalonia.Native/inc/avalonia-native.h
  8. 7
      native/Avalonia.Native/src/OSX/platformthreading.mm
  9. 81
      native/Avalonia.Native/src/OSX/window.mm
  10. 166
      nukebuild/Build.cs
  11. 21
      nukebuild/BuildParameters.cs
  12. 8
      nukebuild/Shims.cs
  13. 8
      nukebuild/_build.csproj
  14. 6
      packages/Avalonia/Avalonia.csproj
  15. 2
      packages/Avalonia/Avalonia.props
  16. 3
      samples/BindingDemo/BindingDemo.csproj
  17. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  18. 1
      samples/ControlCatalog/ControlCatalog.csproj
  19. 12
      samples/ControlCatalog/MainView.xaml
  20. 14
      samples/ControlCatalog/MainView.xaml.cs
  21. 6
      samples/ControlCatalog/MainWindow.xaml
  22. 14
      samples/ControlCatalog/MainWindow.xaml.cs
  23. 19
      samples/ControlCatalog/Pages/ImagePage.xaml
  24. 33
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  25. 4
      samples/ControlCatalog/Pages/MenuPage.xaml
  26. 134
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  27. 18
      samples/ControlCatalog/Pages/TextBlockPage.xaml.cs
  28. 2
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  29. 2
      samples/Previewer/Previewer.csproj
  30. 2
      samples/RemoteDemo/RemoteDemo.csproj
  31. 3
      samples/RenderDemo/RenderDemo.csproj
  32. 3
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  33. 23
      src/Avalonia.Base/AvaloniaObject.cs
  34. 17
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  35. 14
      src/Avalonia.Base/AvaloniaProperty.cs
  36. 88
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  37. 11
      src/Avalonia.Base/DirectPropertyBase.cs
  38. 2
      src/Avalonia.Base/IAvaloniaObject.cs
  39. 4
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  40. 11
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  41. 5
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  42. 14
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  43. 2
      src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs
  44. 13
      src/Avalonia.Base/StyledPropertyBase.cs
  45. 34
      src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs
  46. 9
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  47. 34
      src/Avalonia.Base/ValueStore.cs
  48. 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  49. 88
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  50. 8
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  51. 37
      src/Avalonia.Controls/Application.cs
  52. 2
      src/Avalonia.Controls/Calendar/DatePicker.cs
  53. 3
      src/Avalonia.Controls/ComboBox.cs
  54. 3
      src/Avalonia.Controls/ContextMenu.cs
  55. 195
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  56. 47
      src/Avalonia.Controls/MenuItem.cs
  57. 2
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  58. 219
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  59. 89
      src/Avalonia.Controls/Primitives/AccessText.cs
  60. 2
      src/Avalonia.Controls/Primitives/Popup.cs
  61. 180
      src/Avalonia.Controls/TextBlock.cs
  62. 83
      src/Avalonia.Controls/TextBox.cs
  63. 98
      src/Avalonia.Controls/Window.cs
  64. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  65. 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  66. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  67. BIN
      src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf
  68. 12
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  69. 24
      src/Avalonia.Diagnostics/DevTools.xaml
  70. 159
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  71. 31
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  72. 22
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  73. 61
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  74. 36
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
  75. 19
      src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs
  76. 0
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  77. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  78. 95
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  79. 57
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  80. 112
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
  81. 179
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  82. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  83. 22
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  84. 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  85. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  86. 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  87. 6
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  88. 126
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  89. 60
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  90. 26
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  91. 47
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  92. 26
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  93. 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  94. 56
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  95. 64
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
  96. 25
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  97. 18
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  98. 5
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  99. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
  100. 55
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

5
.ncrunch/BindingDemo.v3.ncrunchproject

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

5
.ncrunch/RenderDemo.v3.ncrunchproject

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

5
.ncrunch/VirtualizationDemo.v3.ncrunchproject

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

14
azure-pipelines.yml

@ -14,7 +14,7 @@ jobs:
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:
script: | script: |
dotnet tool install --global Nuke.GlobalTool --version 0.12.3 dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Run Nuke' displayName: 'Run Nuke'
inputs: inputs:
@ -35,16 +35,16 @@ jobs:
vmImage: 'macOS-10.14' vmImage: 'macOS-10.14'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.0.x' displayName: 'Use .NET Core SDK 3.1.101'
inputs: inputs:
packageType: sdk packageType: sdk
version: 3.0.x version: 3.1.101
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core Runtime 2.1.x' displayName: 'Use .NET Core Runtime 3.1.1'
inputs: inputs:
packageType: runtime packageType: runtime
version: 2.1.x version: 3.1.1
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Mono 5.18' displayName: 'Install Mono 5.18'
@ -74,7 +74,7 @@ jobs:
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:
script: | script: |
dotnet tool install --global Nuke.GlobalTool --version 0.12.3 dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Run Nuke' displayName: 'Run Nuke'
@ -116,7 +116,7 @@ jobs:
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:
script: | script: |
dotnet tool install --global Nuke.GlobalTool --version 0.12.3 dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Run Nuke' displayName: 'Run Nuke'

1
build/CoreLibraries.props

@ -4,7 +4,6 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Animation/Avalonia.Animation.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Animation/Avalonia.Animation.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Controls/Avalonia.Controls.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Controls/Avalonia.Controls.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Input/Avalonia.Input.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Input/Avalonia.Input.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Interactivity/Avalonia.Interactivity.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Interactivity/Avalonia.Interactivity.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Layout/Avalonia.Layout.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Layout/Avalonia.Layout.csproj" />

3
global.json

@ -1,4 +1,7 @@
{ {
"sdk": {
"version": "3.1.101"
},
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43", "Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "2.0.46", "MSBuild.Sdk.Extras": "2.0.46",

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

@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession;
struct IAvnAppMenu; struct IAvnAppMenu;
struct IAvnAppMenuItem; struct IAvnAppMenuItem;
enum SystemDecorations {
SystemDecorationsNone = 0,
SystemDecorationsBorderOnly = 1,
SystemDecorationsFull = 2,
};
struct AvnSize struct AvnSize
{ {
double Width, Height; double Width, Height;
@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{ {
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0; virtual HRESULT SetCanResize(bool value) = 0;
virtual HRESULT SetHasDecorations(bool value) = 0; virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0;
virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0;

7
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -157,11 +157,14 @@ NSArray<NSString*>* _modes;
-(void) perform -(void) perform
{ {
ComPtr<IAvnSignaledCallback> cb;
@synchronized (self) { @synchronized (self) {
_signaled = false; _signaled = false;
if(_parent != NULL && _parent->SignaledCallback != NULL) if(_parent != NULL)
_parent->SignaledCallback->Signaled(0, false); cb = _parent->SignaledCallback;
} }
if(cb != nullptr)
cb->Signaled(0, false);
} }
-(void) setParent:(PlatformThreadingInterface *)parent -(void) setParent:(PlatformThreadingInterface *)parent

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

@ -115,7 +115,6 @@ public:
[NSApp activateIgnoringOtherApps:YES]; [NSApp activateIgnoringOtherApps:YES];
[Window setTitle:_lastTitle]; [Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
return S_OK; return S_OK;
} }
@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub
{ {
private: private:
bool _canResize = true; bool _canResize = true;
bool _hasDecorations = true; SystemDecorations _hasDecorations = SystemDecorationsFull;
CGRect _lastUndecoratedFrame; CGRect _lastUndecoratedFrame;
AvnWindowState _lastWindowState; AvnWindowState _lastWindowState;
@ -476,23 +475,26 @@ private:
bool IsZoomed () bool IsZoomed ()
{ {
return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
} }
void DoZoom() void DoZoom()
{ {
if (_hasDecorations) switch (_hasDecorations)
{ {
[Window performZoom:Window]; case SystemDecorationsNone:
} if (!UndecoratedIsMaximized())
else {
{ _lastUndecoratedFrame = [Window frame];
if (!UndecoratedIsMaximized()) }
{
_lastUndecoratedFrame = [Window frame]; [Window zoom:Window];
} break;
[Window zoom:Window]; case SystemDecorationsBorderOnly:
case SystemDecorationsFull:
[Window performZoom:Window];
break;
} }
} }
@ -506,13 +508,35 @@ private:
} }
} }
virtual HRESULT SetHasDecorations(bool value) override virtual HRESULT SetHasDecorations(SystemDecorations value) override
{ {
@autoreleasepool @autoreleasepool
{ {
_hasDecorations = value; _hasDecorations = value;
UpdateStyle(); UpdateStyle();
switch (_hasDecorations)
{
case SystemDecorationsNone:
[Window setHasShadow:NO];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
break;
case SystemDecorationsBorderOnly:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
break;
case SystemDecorationsFull:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
break;
}
return S_OK; return S_OK;
} }
} }
@ -523,7 +547,6 @@ private:
{ {
_lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title];
[Window setTitle:_lastTitle]; [Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
return S_OK; return S_OK;
} }
@ -645,10 +668,26 @@ protected:
virtual NSWindowStyleMask GetStyle() override virtual NSWindowStyleMask GetStyle() override
{ {
unsigned long s = NSWindowStyleMaskBorderless; unsigned long s = NSWindowStyleMaskBorderless;
if(_hasDecorations)
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; switch (_hasDecorations)
if(_canResize) {
s = s | NSWindowStyleMaskResizable; case SystemDecorationsNone:
break;
case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
if(_canResize)
{
s = s | NSWindowStyleMaskResizable;
}
break;
}
return s; return s;
} }
}; };

166
nukebuild/Build.cs

@ -13,6 +13,7 @@ using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild; using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Utilities; using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.IO.PathConstruction;
@ -26,11 +27,13 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks;
running and debugging a particular target (optionally without deps) would be way easier running and debugging a particular target (optionally without deps) would be way easier
ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support
VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support
*/ */
partial class Build : NukeBuild partial class Build : NukeBuild
{ {
[Solution("Avalonia.sln")] readonly Solution Solution;
static Lazy<string> MsBuildExe = new Lazy<string>(() => static Lazy<string> MsBuildExe = new Lazy<string>(() =>
{ {
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -54,7 +57,7 @@ partial class Build : NukeBuild
protected override void OnBuildInitialized() protected override void OnBuildInitialized()
{ {
Parameters = new BuildParameters(this); Parameters = new BuildParameters(this);
Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.",
Parameters.Version, Parameters.Version,
Parameters.Configuration, Parameters.Configuration,
typeof(NukeBuild).Assembly.GetName().Version.ToString()); typeof(NukeBuild).Assembly.GetName().Version.ToString());
@ -93,29 +96,24 @@ partial class Build : NukeBuild
string projectFile, string projectFile,
Configure<MSBuildSettings> configurator = null) Configure<MSBuildSettings> configurator = null)
{ {
return MSBuild(projectFile, c => return MSBuild(c => c
{ .SetProjectFile(projectFile)
// This is required for VS2019 image on Azure Pipelines // This is required for VS2019 image on Azure Pipelines
if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) .When(Parameters.IsRunningOnWindows &&
{ Parameters.IsRunningOnAzure, c => c
var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); .AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_8_X64")))
if (javaSdk != null) .AddProperty("PackageVersion", Parameters.Version)
c = c.AddProperty("JavaSdkDirectory", javaSdk); .AddProperty("iOSRoslynPathHackRequired", true)
} .SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
c = c.AddProperty("PackageVersion", Parameters.Version) .SetVerbosity(MSBuildVerbosity.Minimal)
.AddProperty("iOSRoslynPathHackRequired", "true") .Apply(configurator));
.SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal);
c = configurator?.Invoke(c) ?? c;
return c;
});
} }
Target Clean => _ => _.Executes(() => Target Clean => _ => _.Executes(() =>
{ {
DeleteDirectories(Parameters.BuildDirs); Parameters.BuildDirs.ForEach(DeleteDirectory);
EnsureCleanDirectories(Parameters.BuildDirs); Parameters.BuildDirs.ForEach(EnsureCleanDirectory);
EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.ArtifactsDir);
EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetIntermediateRoot);
EnsureCleanDirectory(Parameters.NugetRoot); EnsureCleanDirectory(Parameters.NugetRoot);
@ -134,97 +132,84 @@ partial class Build : NukeBuild
); );
else else
DotNetBuild(Parameters.MSBuildSolution, c => c DotNetBuild(c => c
.SetProjectFile(Parameters.MSBuildSolution)
.AddProperty("PackageVersion", Parameters.Version) .AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration) .SetConfiguration(Parameters.Configuration)
); );
}); });
void RunCoreTest(string project) void RunCoreTest(string projectName)
{ {
if(!project.EndsWith(".csproj")) Information($"Running tests from {projectName}");
project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); var project = Solution.GetProject(projectName).NotNull("project != null");
Information("Running tests from " + project);
XDocument xdoc; foreach (var fw in project.GetTargetFrameworks())
using (var s = File.OpenRead(project))
xdoc = XDocument.Load(s);
List<string> frameworks = null;
var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault();
if (targets != null)
frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList();
else
frameworks = new List<string> {xdoc.Root.Descendants("TargetFramework").First().Value};
foreach(var fw in frameworks)
{ {
if (fw.StartsWith("net4") if (fw.StartsWith("net4")
&& RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
&& Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1")
{ {
Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969");
continue; continue;
} }
Information("Running for " + fw); Information($"Running for {projectName} ({fw}) ...");
DotNetTest(c =>
{ DotNetTest(c => c
c = c .SetProjectFile(project)
.SetProjectFile(project) .SetConfiguration(Parameters.Configuration)
.SetConfiguration(Parameters.Configuration) .SetFramework(fw)
.SetFramework(fw) .EnableNoBuild()
.EnableNoBuild() .EnableNoRestore()
.EnableNoRestore(); .When(Parameters.PublishTestResults, c => c
// NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient .SetLogger("trx")
if (Parameters.PublishTestResults) .SetResultsDirectory(Parameters.TestResultsRoot)));
c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot);
return c;
});
} }
} }
Target RunCoreLibsTests => _ => _ Target RunCoreLibsTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests) .OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
RunCoreTest("./tests/Avalonia.Animation.UnitTests"); RunCoreTest("Avalonia.Animation.UnitTests");
RunCoreTest("./tests/Avalonia.Base.UnitTests"); RunCoreTest("Avalonia.Base.UnitTests");
RunCoreTest("./tests/Avalonia.Controls.UnitTests"); RunCoreTest("Avalonia.Controls.UnitTests");
RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); RunCoreTest("Avalonia.Controls.DataGrid.UnitTests");
RunCoreTest("./tests/Avalonia.Input.UnitTests"); RunCoreTest("Avalonia.Input.UnitTests");
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); RunCoreTest("Avalonia.Interactivity.UnitTests");
RunCoreTest("./tests/Avalonia.Layout.UnitTests"); RunCoreTest("Avalonia.Layout.UnitTests");
RunCoreTest("./tests/Avalonia.Markup.UnitTests"); RunCoreTest("Avalonia.Markup.UnitTests");
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("./tests/Avalonia.Styling.UnitTests"); RunCoreTest("Avalonia.Styling.UnitTests");
RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Visuals.UnitTests");
RunCoreTest("./tests/Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests");
}); });
Target RunRenderTests => _ => _ Target RunRenderTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests) .OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); RunCoreTest("Avalonia.Skia.RenderTests");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); RunCoreTest("Avalonia.Direct2D1.RenderTests");
}); });
Target RunDesignerTests => _ => _ Target RunDesignerTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); RunCoreTest("Avalonia.DesignerSupport.Tests");
}); });
[PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit;
Target RunLeakTests => _ => _ Target RunLeakTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
@ -235,7 +220,7 @@ partial class Build : NukeBuild
}); });
Target ZipFiles => _ => _ Target ZipFiles => _ => _
.After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)
.Executes(() => .Executes(() =>
{ {
var data = Parameters; var data = Parameters;
@ -259,9 +244,10 @@ partial class Build : NukeBuild
MsBuildCommon(Parameters.MSBuildSolution, c => c MsBuildCommon(Parameters.MSBuildSolution, c => c
.AddTargets("Pack")); .AddTargets("Pack"));
else else
DotNetPack(Parameters.MSBuildSolution, c => DotNetPack(c => c
c.SetConfiguration(Parameters.Configuration) .SetProject(Parameters.MSBuildSolution)
.AddProperty("PackageVersion", Parameters.Version)); .SetConfiguration(Parameters.Configuration)
.AddProperty("PackageVersion", Parameters.Version));
}); });
Target CreateNugetPackages => _ => _ Target CreateNugetPackages => _ => _
@ -274,32 +260,40 @@ partial class Build : NukeBuild
new NumergeNukeLogger())) new NumergeNukeLogger()))
throw new Exception("Package merge failed"); throw new Exception("Package merge failed");
}); });
Target RunTests => _ => _ Target RunTests => _ => _
.DependsOn(RunCoreLibsTests) .DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests) .DependsOn(RunRenderTests)
.DependsOn(RunDesignerTests) .DependsOn(RunDesignerTests)
.DependsOn(RunLeakTests); .DependsOn(RunLeakTests);
Target Package => _ => _ Target Package => _ => _
.DependsOn(RunTests) .DependsOn(RunTests)
.DependsOn(CreateNugetPackages); .DependsOn(CreateNugetPackages);
Target CiAzureLinux => _ => _ Target CiAzureLinux => _ => _
.DependsOn(RunTests); .DependsOn(RunTests);
Target CiAzureOSX => _ => _ Target CiAzureOSX => _ => _
.DependsOn(Package) .DependsOn(Package)
.DependsOn(ZipFiles); .DependsOn(ZipFiles);
Target CiAzureWindows => _ => _ Target CiAzureWindows => _ => _
.DependsOn(Package) .DependsOn(Package)
.DependsOn(ZipFiles); .DependsOn(ZipFiles);
public static int Main() => public static int Main() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Execute<Build>(x => x.Package) ? Execute<Build>(x => x.Package)
: Execute<Build>(x => x.RunTests); : Execute<Build>(x => x.RunTests);
} }
public static class ToolSettingsExtensions
{
public static T Apply<T>(this T settings, Configure<T> configurator)
{
return configurator != null ? configurator(settings) : settings;
}
}

21
nukebuild/BuildParameters.cs

@ -4,24 +4,21 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Xml.Linq; using System.Xml.Linq;
using Nuke.Common; using Nuke.Common;
using Nuke.Common.BuildServers; using Nuke.Common.CI.AzurePipelines;
using Nuke.Common.Execution;
using Nuke.Common.IO; using Nuke.Common.IO;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.IO.PathConstruction;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;
public partial class Build public partial class Build
{ {
[Parameter("configuration")] [Parameter("configuration")]
public string Configuration { get; set; } public string Configuration { get; set; }
[Parameter("skip-tests")] [Parameter("skip-tests")]
public bool SkipTests { get; set; } public bool SkipTests { get; set; }
[Parameter("force-nuget-version")] [Parameter("force-nuget-version")]
public string ForceNugetVersion { get; set; } public string ForceNugetVersion { get; set; }
public class BuildParameters public class BuildParameters
{ {
public string Configuration { get; } public string Configuration { get; }
@ -79,15 +76,15 @@ public partial class Build
IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix ||
Environment.OSVersion.Platform == PlatformID.MacOSX; Environment.OSVersion.Platform == PlatformID.MacOSX;
IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsRunningOnAzure = Host == HostType.TeamServices || IsRunningOnAzure = Host == HostType.AzurePipelines ||
Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; Environment.GetEnvironmentVariable("LOGNAME") == "vsts";
if (IsRunningOnAzure) if (IsRunningOnAzure)
{ {
RepositoryName = TeamServices.Instance.RepositoryUri; RepositoryName = AzurePipelines.Instance.RepositoryUri;
RepositoryBranch = TeamServices.Instance.SourceBranch; RepositoryBranch = AzurePipelines.Instance.SourceBranch;
IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue;
IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri);
} }
IsMainRepo = IsMainRepo =
StringComparer.OrdinalIgnoreCase.Equals(MainRepo, StringComparer.OrdinalIgnoreCase.Equals(MainRepo,

8
nukebuild/Shims.cs

@ -19,9 +19,9 @@ public partial class Build
Logger.Info(info, args); Logger.Info(info, args);
} }
private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable());
private void Zip(PathConstruction.AbsolutePath target, IEnumerable<string> paths) private void Zip(AbsolutePath target, IEnumerable<string> paths)
{ {
var targetPath = target.ToString(); var targetPath = target.ToString();
bool finished = false, atLeastOneFileAdded = false; bool finished = false, atLeastOneFileAdded = false;
@ -38,7 +38,7 @@ public partial class Build
fileStream.CopyTo(entryStream); fileStream.CopyTo(entryStream);
atLeastOneFileAdded = true; atLeastOneFileAdded = true;
} }
foreach (var path in paths) foreach (var path in paths)
{ {
if (Directory.Exists(path)) if (Directory.Exists(path))
@ -64,7 +64,7 @@ public partial class Build
finished = true; finished = true;
} }
finally finally
{ {
try try
{ {

8
nukebuild/_build.csproj

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace> <RootNamespace></RootNamespace>
<IsPackable>False</IsPackable> <IsPackable>False</IsPackable>
@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Nuke.Common" Version="0.12.3" /> <PackageReference Include="Nuke.Common" Version="0.24.0" />
<PackageReference Include="xunit.runner.console" Version="2.3.1" /> <PackageReference Include="xunit.runner.console" Version="2.3.1" />
<PackageReference Include="JetBrains.dotMemoryUnit" Version="3.0.20171219.105559" /> <PackageReference Include="JetBrains.dotMemoryUnit" Version="3.0.20171219.105559" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " /> <PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
@ -20,11 +20,11 @@
<NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" /> <NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" />
<NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" /> <NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
<None Remove="*.csproj.DotSettings;*.ref.*.txt" /> <None Remove="*.csproj.DotSettings;*.ref.*.txt" />
<!-- Common build related files --> <!-- Common build related files -->
<None Include="..\build.ps1" /> <None Include="..\build.ps1" />
<None Include="..\build.sh" /> <None Include="..\build.sh" />
<None Include="..\.nuke" /> <None Include="..\.nuke" />
<None Include="..\global.json" Condition="Exists('..\global.json')" /> <None Include="..\global.json" Condition="Exists('..\global.json')" />
<None Include="..\nuget.config" Condition="Exists('..\nuget.config')" /> <None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
<None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" /> <None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />

6
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net461;netcoreapp3.1</TargetFrameworks>
<PackageId>Avalonia</PackageId> <PackageId>Avalonia</PackageId>
</PropertyGroup> </PropertyGroup>
@ -20,8 +20,8 @@
Platform=$(Platform)" /> Platform=$(Platform)" />
<ItemGroup> <ItemGroup>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll"> <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp2.0/designer</PackagePath> <PackagePath>tools/netcoreapp3.1/designer</PackagePath>
<Visible>false</Visible> <Visible>false</Visible>
<BuildAction>None</BuildAction> <BuildAction>None</BuildAction>
</_PackageFiles> </_PackageFiles>

2
packages/Avalonia/Avalonia.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath> <AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath> <AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
<AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation> <AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild> <AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>

3
samples/BindingDemo/BindingDemo.csproj

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
</ItemGroup> </ItemGroup>

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch> <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup> </PropertyGroup>

1
samples/ControlCatalog/ControlCatalog.csproj

@ -21,6 +21,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
</ItemGroup> </ItemGroup>

12
samples/ControlCatalog/MainView.xaml

@ -54,14 +54,22 @@
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem> <TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="TabStrip"><pages:TabStripPage/></TabItem> <TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem> <TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="TextBlock"><pages:TextBlockPage/></TabItem>
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem> <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem> <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem> <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabControl.Tag> <TabControl.Tag>
<ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom"> <StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8">
<ComboBoxItem>No Decorations</ComboBoxItem>
<ComboBoxItem>Border Only</ComboBoxItem>
<ComboBoxItem>Full Decorations</ComboBoxItem>
</ComboBox>
<ComboBox x:Name="Themes" SelectedIndex="0">
<ComboBoxItem>Light</ComboBoxItem> <ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem> <ComboBoxItem>Dark</ComboBoxItem>
</ComboBox> </ComboBox>
</StackPanel>
</TabControl.Tag> </TabControl.Tag>
</TabControl> </TabControl>
</Grid> </Grid>

14
samples/ControlCatalog/MainView.xaml.cs

@ -56,6 +56,20 @@ namespace ControlCatalog
} }
}; };
Styles.Add(light); Styles.Add(light);
var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
Window window = (Window)VisualRoot;
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
};
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations;
} }
} }
} }

6
samples/ControlCatalog/MainWindow.xaml

@ -14,7 +14,7 @@
<NativeMenuItem Header="File"> <NativeMenuItem Header="File">
<NativeMenuItem.Menu> <NativeMenuItem.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked"/> <NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/> <NativeMenuItemSeperator/>
<NativeMenuItem Header="Recent"> <NativeMenuItem Header="Recent">
<NativeMenuItem.Menu> <NativeMenuItem.Menu>
@ -22,7 +22,9 @@
</NativeMenuItem.Menu> </NativeMenuItem.Menu>
</NativeMenuItem> </NativeMenuItem>
<NativeMenuItemSeperator/> <NativeMenuItemSeperator/>
<NativeMenuItem Header="Quit Avalonia" Clicked="OnCloseClicked" Gesture="CMD+Q"/> <NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />
</NativeMenu> </NativeMenu>
</NativeMenuItem.Menu> </NativeMenuItem.Menu>
</NativeMenuItem> </NativeMenuItem>

14
samples/ControlCatalog/MainWindow.xaml.cs

@ -1,13 +1,11 @@
using System;
using System.Runtime.InteropServices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives; using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ControlCatalog.ViewModels; using ControlCatalog.ViewModels;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ControlCatalog namespace ControlCatalog
{ {
@ -35,6 +33,12 @@ namespace ControlCatalog
mainMenu.AttachedToVisualTree += MenuAttached; mainMenu.AttachedToVisualTree += MenuAttached;
} }
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
new KeyGesture(Key.Q, KeyModifiers.Meta) :
new KeyGesture(Key.F4, KeyModifiers.Alt);
public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e) public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
{ {
if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu) if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)

19
samples/ControlCatalog/Pages/ImagePage.xaml

@ -7,7 +7,7 @@
<TextBlock Classes="h2">Displays an image</TextBlock> <TextBlock Classes="h2">Displays an image</TextBlock>
</StackPanel> </StackPanel>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Margin="64"> <Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,*" Margin="64">
<DockPanel Grid.Column="0" Grid.Row="1" Margin="16"> <DockPanel Grid.Column="0" Grid.Row="1" Margin="16">
<TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock> <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock>
@ -22,6 +22,23 @@
</DockPanel> </DockPanel>
<DockPanel Grid.Column="1" Grid.Row="1" Margin="16"> <DockPanel Grid.Column="1" Grid.Row="1" Margin="16">
<TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Crop</TextBlock>
<ComboBox Name="bitmapCrop" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="BitmapCropChanged">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Center</ComboBoxItem>
<ComboBoxItem>TopLeft</ComboBoxItem>
<ComboBoxItem>TopRight</ComboBoxItem>
<ComboBoxItem>BottomLeft</ComboBoxItem>
<ComboBoxItem>BottomRight</ComboBoxItem>
</ComboBox>
<Image Name="croppedImage">
<Image.Source>
<CroppedBitmap Source="/Assets/delicate-arch-896885_640.jpg" SourceRect="0 0 320 240"/>
</Image.Source>
</Image>
</DockPanel>
<DockPanel Grid.Column="2" Grid.Row="1" Margin="16">
<TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock> <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock>
<ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged"> <ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged">
<ComboBoxItem>None</ComboBoxItem> <ComboBoxItem>None</ComboBoxItem>

33
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@ -1,6 +1,10 @@
using System;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -8,12 +12,14 @@ namespace ControlCatalog.Pages
{ {
private readonly Image _bitmapImage; private readonly Image _bitmapImage;
private readonly Image _drawingImage; private readonly Image _drawingImage;
private readonly Image _croppedImage;
public ImagePage() public ImagePage()
{ {
InitializeComponent(); InitializeComponent();
_bitmapImage = this.FindControl<Image>("bitmapImage"); _bitmapImage = this.FindControl<Image>("bitmapImage");
_drawingImage = this.FindControl<Image>("drawingImage"); _drawingImage = this.FindControl<Image>("drawingImage");
_croppedImage = this.FindControl<Image>("croppedImage");
} }
private void InitializeComponent() private void InitializeComponent()
@ -38,5 +44,32 @@ namespace ControlCatalog.Pages
_drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex;
} }
} }
public void BitmapCropChanged(object sender, SelectionChangedEventArgs e)
{
if (_croppedImage != null)
{
var comboxBox = (ComboBox)sender;
var croppedBitmap = _croppedImage.Source as CroppedBitmap;
croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex);
}
}
private PixelRect GetCropRect(int index)
{
var bitmapWidth = 640;
var bitmapHeight = 426;
var cropSize = new PixelSize(320, 240);
return index switch
{
1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize),
2 => new PixelRect(new PixelPoint(0, 0), cropSize),
3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize),
4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize),
5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize),
_ => PixelRect.Empty
};
}
} }
} }

4
samples/ControlCatalog/Pages/MenuPage.xaml

@ -16,13 +16,13 @@
<TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock> <TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock>
<Menu> <Menu>
<MenuItem Header="_First"> <MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/> <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A"/>
<Separator/> <Separator/>
<MenuItem Header="Menu with _Submenu"> <MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/> <MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/> <MenuItem Header="Submenu _2"/>
</MenuItem> </MenuItem>
<MenuItem Header="Menu Item with _Icon"> <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon> <MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/> <Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon> </MenuItem.Icon>

134
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -0,0 +1,134 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.TextBlockPage">
<StackPanel>
<TextBlock Classes="h1">TextBlock</TextBlock>
<TextBlock Classes="h2">A control that can display text</TextBlock>
<StackPanel
Orientation="Horizontal"
Spacing="16"
HorizontalAlignment="Center"
Margin="0,16,0,0">
<StackPanel.Styles>
<Style Selector="Border">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="Padding" Value="2"/>
</Style>
</StackPanel.Styles>
<Border>
<StackPanel Width="200" Spacing="8">
<TextBlock TextTrimming="CharacterEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
<TextBlock TextTrimming="WordEllipsis" Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
<TextBlock Text="Left aligned text" TextAlignment="Left" />
<TextBlock Text="Center aligned text" TextAlignment="Center" />
<TextBlock Text="Right aligned text" TextAlignment="Right" />
</StackPanel>
</Border>
<Border>
<StackPanel Width="200" Spacing="8">
<TextBlock
TextWrapping="Wrap"
Text="Multiline TextBlock with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
</StackPanel>
</Border>
<Border>
<StackPanel Width="200" Spacing="8">
<TextBlock Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBlock Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBlock Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-Italic.ttf#Source Sans Pro"/>
<TextBlock Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
</StackPanel>
</Border>
</StackPanel>
<StackPanel
Orientation="Horizontal"
Spacing="16"
HorizontalAlignment="Center"
Margin="0,16,0,0">
<StackPanel.Styles>
<Style Selector="Border">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="Padding" Value="2"/>
</Style>
</StackPanel.Styles>
<Border>
<StackPanel Width="200" Spacing="8">
<TextBlock TextDecorations="Underline" Text="Underline"/>
<TextBlock TextDecorations="Strikethrough" Text="Strikethrough"/>
<TextBlock TextDecorations="Overline" Text="Overline" />
<TextBlock TextDecorations="Baseline" Text="Baseline"/>
<TextBlock Text="Custom TextDecorations">
<TextBlock.TextDecorations>
<TextDecorationCollection>
<TextDecoration
Location="Overline"
PenThicknessUnit="Pixel">
<TextDecoration.Pen>
<Pen Thickness="2">
<Pen.Brush>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Green"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Pen.Brush>
</Pen>
</TextDecoration.Pen>
</TextDecoration>
<TextDecoration
Location="Strikethrough"
PenThicknessUnit="Pixel">
<TextDecoration.Pen>
<Pen Thickness="1">
<Pen.Brush>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Green"/>
<GradientStop Offset="1" Color="Blue"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Pen.Brush>
</Pen>
</TextDecoration.Pen>
</TextDecoration>
<TextDecoration
Location="Underline"
PenThicknessUnit="Pixel">
<TextDecoration.Pen>
<Pen Thickness="2">
<Pen.Brush>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Blue"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Pen.Brush>
</Pen>
</TextDecoration.Pen>
</TextDecoration>
</TextDecorationCollection>
</TextBlock.TextDecorations>
</TextBlock>
</StackPanel>
</Border>
<Border>
<StackPanel Width="200" Spacing="8">
<TextBlock Text="🏻 👌🏻"/>
<TextBlock Text="🏼 👌🏼" />
<TextBlock Text="🏽 👌🏽"/>
<TextBlock Text="🏾 👌🏾"/>
<TextBlock Text="🏿 👌🏿"/>
</StackPanel>
</Border>
<Border>
<StackPanel Width="200" Spacing="8">
<TextBlock Text="👪 👨‍👩‍👧 👨‍👩‍👧‍👦"/>
</StackPanel>
</Border>
</StackPanel>
</StackPanel>
</UserControl>

18
samples/ControlCatalog/Pages/TextBlockPage.xaml.cs

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class TextBlockPage : UserControl
{
public TextBlockPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

2
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

2
samples/Previewer/Previewer.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Update="**\*.xaml.cs"> <Compile Update="**\*.xaml.cs">

2
samples/RemoteDemo/RemoteDemo.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />

3
samples/RenderDemo/RenderDemo.csproj

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
</ItemGroup> </ItemGroup>

3
samples/VirtualizationDemo/VirtualizationDemo.csproj

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
</ItemGroup> </ItemGroup>

23
src/Avalonia.Base/AvaloniaObject.cs

@ -80,8 +80,12 @@ namespace Avalonia
_inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent?.RemoveInheritanceChild(this);
_inheritanceParent = value; _inheritanceParent = value;
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType());
var propertiesCount = properties.Count;
for (var i = 0; i < propertiesCount; i++)
{ {
var property = properties[i];
if (valuestore?.IsSet(property) == true) if (valuestore?.IsSet(property) == true)
{ {
// If local value set there can be no change. // If local value set there can be no change.
@ -311,7 +315,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param> /// <param name="priority">The priority of the value.</param>
public void SetValue<T>( /// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public IDisposable SetValue<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
@ -335,8 +342,10 @@ namespace Avalonia
} }
else if (!(value is DoNothingType)) else if (!(value is DoNothingType))
{ {
Values.SetValue(property, value, priority); return Values.SetValue(property, value, priority);
} }
return null;
} }
/// <summary> /// <summary>
@ -478,7 +487,13 @@ namespace Avalonia
} }
} }
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { } void IValueSink.Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{
((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default);
}
/// <summary> /// <summary>
/// Called for each inherited property when the <see cref="InheritanceParent"/> changes. /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.

17
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -458,7 +458,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param> /// <param name="priority">The priority of the value.</param>
public static void SetValue( /// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public static IDisposable SetValue(
this IAvaloniaObject target, this IAvaloniaObject target,
AvaloniaProperty property, AvaloniaProperty property,
object value, object value,
@ -467,7 +470,7 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
property.RouteSetValue(target, value, priority); return property.RouteSetValue(target, value, priority);
} }
/// <summary> /// <summary>
@ -478,7 +481,10 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param> /// <param name="priority">The priority of the value.</param>
public static void SetValue<T>( /// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public static IDisposable SetValue<T>(
this IAvaloniaObject target, this IAvaloniaObject target,
AvaloniaProperty<T> property, AvaloniaProperty<T> property,
T value, T value,
@ -490,11 +496,10 @@ namespace Avalonia
switch (property) switch (property)
{ {
case StyledPropertyBase<T> styled: case StyledPropertyBase<T> styled:
target.SetValue(styled, value, priority); return target.SetValue(styled, value, priority);
break;
case DirectPropertyBase<T> direct: case DirectPropertyBase<T> direct:
target.SetValue(direct, value); target.SetValue(direct, value);
break; return null;
default: default:
throw new NotSupportedException("Unsupported AvaloniaProperty type."); throw new NotSupportedException("Unsupported AvaloniaProperty type.");
} }

14
src/Avalonia.Base/AvaloniaProperty.cs

@ -469,6 +469,15 @@ namespace Avalonia
return Name; return Name;
} }
/// <summary>
/// Uses the visitor pattern to resolve an untyped property to a typed property.
/// </summary>
/// <typeparam name="TData">The type of user data passed.</typeparam>
/// <param name="vistor">The visitor which will accept the typed property.</param>
/// <param name="data">The user data to pass.</param>
public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
where TData : struct;
/// <summary> /// <summary>
/// Notifies the <see cref="Changed"/> observable. /// Notifies the <see cref="Changed"/> observable.
/// </summary> /// </summary>
@ -496,7 +505,10 @@ namespace Avalonia
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority.</param> /// <param name="priority">The priority.</param>
internal abstract void RouteSetValue( /// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
internal abstract IDisposable? RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,
object value, object value,
BindingPriority priority); BindingPriority priority);

88
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -3,9 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia namespace Avalonia
{ {
@ -28,8 +26,6 @@ namespace Avalonia
new Dictionary<Type, List<AvaloniaProperty>>(); new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache = private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
new Dictionary<Type, List<AvaloniaProperty>>(); new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
new Dictionary<Type, List<PropertyInitializationData>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache = private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
new Dictionary<Type, List<AvaloniaProperty>>(); new Dictionary<Type, List<AvaloniaProperty>>();
@ -49,7 +45,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegistered(Type type) public IReadOnlyList<AvaloniaProperty> GetRegistered(Type type)
{ {
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
@ -83,7 +79,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredAttached(Type type) public IReadOnlyList<AvaloniaProperty> GetRegisteredAttached(Type type)
{ {
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
@ -114,7 +110,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type) public IReadOnlyList<AvaloniaProperty> GetRegisteredDirect(Type type)
{ {
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
@ -145,7 +141,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type) public IReadOnlyList<AvaloniaProperty> GetRegisteredInherited(Type type)
{ {
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
@ -157,16 +153,27 @@ namespace Avalonia
result = new List<AvaloniaProperty>(); result = new List<AvaloniaProperty>();
var visited = new HashSet<AvaloniaProperty>(); var visited = new HashSet<AvaloniaProperty>();
foreach (var property in GetRegistered(type)) var registered = GetRegistered(type);
var registeredCount = registered.Count;
for (var i = 0; i < registeredCount; i++)
{ {
var property = registered[i];
if (property.Inherits) if (property.Inherits)
{ {
result.Add(property); result.Add(property);
visited.Add(property); visited.Add(property);
} }
} }
foreach (var property in GetRegisteredAttached(type))
var registeredAttached = GetRegisteredAttached(type);
var registeredAttachedCount = registeredAttached.Count;
for (var i = 0; i < registeredAttachedCount; i++)
{ {
var property = registeredAttached[i];
if (property.Inherits) if (property.Inherits)
{ {
if (!visited.Contains(property)) if (!visited.Contains(property))
@ -185,7 +192,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o) public IReadOnlyList<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
{ {
Contract.Requires<ArgumentNullException>(o != null); Contract.Requires<ArgumentNullException>(o != null);
@ -229,8 +236,13 @@ namespace Avalonia
throw new InvalidOperationException("Attached properties not supported."); throw new InvalidOperationException("Attached properties not supported.");
} }
foreach (AvaloniaProperty x in GetRegistered(type)) var registered = GetRegistered(type);
var registeredCount = registered.Count;
for (var i = 0; i < registeredCount; i++)
{ {
AvaloniaProperty x = registered[i];
if (x.Name == name) if (x.Name == name)
{ {
return x; return x;
@ -276,8 +288,13 @@ namespace Avalonia
return property; return property;
} }
foreach (var p in GetRegisteredDirect(o.GetType())) var registeredDirect = GetRegisteredDirect(o.GetType());
var registeredDirectCount = registeredDirect.Count;
for (var i = 0; i < registeredDirectCount; i++)
{ {
var p = registeredDirect[i];
if (p == property) if (p == property)
{ {
return (DirectPropertyBase<T>)p; return (DirectPropertyBase<T>)p;
@ -308,8 +325,23 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
return Instance.GetRegistered(type).Any(x => x == property) || static bool ContainsProperty(IReadOnlyList<AvaloniaProperty> properties, AvaloniaProperty property)
Instance.GetRegisteredAttached(type).Any(x => x == property); {
var propertiesCount = properties.Count;
for (var i = 0; i < propertiesCount; i++)
{
if (properties[i] == property)
{
return true;
}
}
return false;
}
return ContainsProperty(Instance.GetRegistered(type), property) ||
ContainsProperty(Instance.GetRegisteredAttached(type), property);
} }
/// <summary> /// <summary>
@ -374,7 +406,6 @@ namespace Avalonia
} }
_registeredCache.Clear(); _registeredCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear(); _inheritedCache.Clear();
} }
@ -411,32 +442,7 @@ namespace Avalonia
} }
_attachedCache.Clear(); _attachedCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear(); _inheritedCache.Clear();
} }
private readonly struct PropertyInitializationData
{
public AvaloniaProperty Property { get; }
public object Value { get; }
public bool IsDirect { get; }
public IDirectPropertyAccessor DirectAccessor { get; }
public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor)
{
Property = property;
Value = null;
IsDirect = true;
DirectAccessor = directAccessor;
}
public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type)
{
Property = property;
Value = styledAccessor.GetDefaultValue(type);
IsDirect = false;
DirectAccessor = null;
}
}
} }
} }

11
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Utilities;
#nullable enable #nullable enable
@ -101,6 +102,12 @@ namespace Avalonia
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type); return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
} }
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
{
vistor.Visit(this, ref data);
}
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o) internal override void RouteClearValue(IAvaloniaObject o)
{ {
@ -114,7 +121,7 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteSetValue( internal override IDisposable? RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,
object value, object value,
BindingPriority priority) BindingPriority priority)
@ -133,6 +140,8 @@ namespace Avalonia
{ {
throw v.Error!; throw v.Error!;
} }
return null;
} }
/// <inheritdoc/> /// <inheritdoc/>

2
src/Avalonia.Base/IAvaloniaObject.cs

@ -65,7 +65,7 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param> /// <param name="priority">The priority of the value.</param>
void SetValue<T>( IDisposable SetValue<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority = BindingPriority.LocalValue); BindingPriority priority = BindingPriority.LocalValue);

4
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -48,10 +48,10 @@ namespace Avalonia.PropertyStore
{ {
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _subscription = null;
_sink.Completed(Property, this); _sink.Completed(Property, this, Value);
} }
public void OnCompleted() => _sink.Completed(Property, this); public void OnCompleted() => _sink.Completed(Property, this, Value);
public void OnError(Exception error) public void OnError(Exception error)
{ {

11
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -10,16 +10,20 @@ namespace Avalonia.PropertyStore
/// <see cref="PriorityValue{T}"/>. /// <see cref="PriorityValue{T}"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T> internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
{ {
private IValueSink _sink;
public ConstantValueEntry( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority) BindingPriority priority,
IValueSink sink)
{ {
Property = property; Property = property;
Value = value; Value = value;
Priority = priority; Priority = priority;
_sink = sink;
} }
public StyledPropertyBase<T> Property { get; } public StyledPropertyBase<T> Property { get; }
@ -28,6 +32,7 @@ namespace Avalonia.PropertyStore
Optional<object> IValue.Value => Value.ToObject(); Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority; BindingPriority IValue.ValuePriority => Priority;
public void Reparent(IValueSink sink) { } public void Dispose() => _sink.Completed(Property, this, Value);
public void Reparent(IValueSink sink) => _sink = sink;
} }
} }

5
src/Avalonia.Base/PropertyStore/IValueSink.cs

@ -15,6 +15,9 @@ namespace Avalonia.PropertyStore
Optional<T> oldValue, Optional<T> oldValue,
BindingValue<T> newValue); BindingValue<T> newValue);
void Completed(AvaloniaProperty property, IPriorityValueEntry entry); void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue);
} }
} }

14
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -78,8 +78,10 @@ namespace Avalonia.PropertyStore
public void ClearLocalValue() => UpdateEffectiveValue(); public void ClearLocalValue() => UpdateEffectiveValue();
public void SetValue(T value, BindingPriority priority) public IDisposable? SetValue(T value, BindingPriority priority)
{ {
IDisposable? result = null;
if (priority == BindingPriority.LocalValue) if (priority == BindingPriority.LocalValue)
{ {
_localValue = value; _localValue = value;
@ -87,10 +89,13 @@ namespace Avalonia.PropertyStore
else else
{ {
var insert = FindInsertPoint(priority); var insert = FindInsertPoint(priority);
_entries.Insert(insert, new ConstantValueEntry<T>(Property, value, priority)); var entry = new ConstantValueEntry<T>(Property, value, priority, this);
_entries.Insert(insert, entry);
result = entry;
} }
UpdateEffectiveValue(); UpdateEffectiveValue();
return result;
} }
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority) public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
@ -117,7 +122,10 @@ namespace Avalonia.PropertyStore
UpdateEffectiveValue(); UpdateEffectiveValue();
} }
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) void IValueSink.Completed<TValue>(
StyledPropertyBase<TValue> property,
IPriorityValueEntry entry,
Optional<TValue> oldValue)
{ {
_entries.Remove((IPriorityValueEntry<T>)entry); _entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue(); UpdateEffectiveValue();

2
src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs

@ -36,7 +36,7 @@ namespace Avalonia.Reactive
return this; return this;
} }
void IDisposable.Dispose() public virtual void Dispose()
{ {
Unsubscribed(); Unsubscribed();
_observer = null; _observer = null;

13
src/Avalonia.Base/StyledPropertyBase.cs

@ -5,6 +5,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -169,6 +170,12 @@ namespace Avalonia
base.OverrideMetadata(type, metadata); base.OverrideMetadata(type, metadata);
} }
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
{
vistor.Visit(this, ref data);
}
/// <summary> /// <summary>
/// Gets the string representation of the property. /// Gets the string representation of the property.
/// </summary> /// </summary>
@ -194,7 +201,7 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteSetValue( internal override IDisposable RouteSetValue(
IAvaloniaObject o, IAvaloniaObject o,
object value, object value,
BindingPriority priority) BindingPriority priority)
@ -203,7 +210,7 @@ namespace Avalonia
if (v.HasValue) if (v.HasValue)
{ {
o.SetValue<TValue>(this, (TValue)v.Value, priority); return o.SetValue<TValue>(this, (TValue)v.Value, priority);
} }
else if (v.Type == BindingValueType.UnsetValue) else if (v.Type == BindingValueType.UnsetValue)
{ {
@ -213,6 +220,8 @@ namespace Avalonia
{ {
throw v.Error; throw v.Error;
} }
return null;
} }
/// <inheritdoc/> /// <inheritdoc/>

34
src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs

@ -0,0 +1,34 @@
#nullable enable
namespace Avalonia.Utilities
{
/// <summary>
/// A visitor to resolve an untyped <see cref="AvaloniaProperty"/> to a typed property.
/// </summary>
/// <typeparam name="TData">The type of user data passed.</typeparam>
/// <remarks>
/// Pass an instance that implements this interface to
/// <see cref="AvaloniaProperty.Accept{TData}(IAvaloniaPropertyVisitor{TData}, ref TData)"/>
/// in order to resolve un untyped <see cref="AvaloniaProperty"/> to a typed
/// <see cref="StyledPropertyBase{TValue}"/> or <see cref="DirectPropertyBase{TValue}"/>.
/// </remarks>
public interface IAvaloniaPropertyVisitor<TData>
where TData : struct
{
/// <summary>
/// Called when the property is a styled property.
/// </summary>
/// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="data">The user data.</param>
void Visit<T>(StyledPropertyBase<T> property, ref TData data);
/// <summary>
/// Called when the property is a direct property.
/// </summary>
/// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="data">The user data.</param>
void Visit<T>(DirectPropertyBase<T> property, ref TData data);
}
}

9
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -185,6 +186,14 @@ namespace Avalonia.Utilities
} }
} }
var typeConverter = TypeDescriptor.GetConverter(to);
if (typeConverter.CanConvertFrom(from) == true)
{
result = typeConverter.ConvertFrom(null, culture, value);
return true;
}
var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
if (cast != null) if (cast != null)

34
src/Avalonia.Base/ValueStore.cs

@ -70,23 +70,25 @@ namespace Avalonia
return false; return false;
} }
public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority) public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{ {
if (property.ValidateValue?.Invoke(value) == false) if (property.ValidateValue?.Invoke(value) == false)
{ {
throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
} }
IDisposable? result = null;
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
SetExisting(slot, property, value, priority); result = SetExisting(slot, property, value, priority);
} }
else if (property.HasCoercion) else if (property.HasCoercion)
{ {
// If the property has any coercion callbacks then always create a PriorityValue. // If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this); var entry = new PriorityValue<T>(_owner, property, this);
_values.AddValue(property, entry); _values.AddValue(property, entry);
entry.SetValue(value, priority); result = entry.SetValue(value, priority);
} }
else if (priority == BindingPriority.LocalValue) else if (priority == BindingPriority.LocalValue)
{ {
@ -95,10 +97,13 @@ namespace Avalonia
} }
else else
{ {
var entry = new ConstantValueEntry<T>(property, value, priority); var entry = new ConstantValueEntry<T>(property, value, priority, this);
_values.AddValue(property, entry); _values.AddValue(property, entry);
_sink.ValueChanged(property, priority, default, value); _sink.ValueChanged(property, priority, default, value);
result = entry;
} }
return result;
} }
public IDisposable AddBinding<T>( public IDisposable AddBinding<T>(
@ -148,7 +153,7 @@ namespace Avalonia
_values.Remove(property); _values.Remove(property);
_sink.ValueChanged( _sink.ValueChanged(
property, property,
BindingPriority.LocalValue, BindingPriority.Unset,
old, old,
BindingValue<T>.Unset); BindingValue<T>.Unset);
} }
@ -173,7 +178,7 @@ namespace Avalonia
{ {
return new Diagnostics.AvaloniaPropertyValue( return new Diagnostics.AvaloniaPropertyValue(
property, property,
slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue, slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue,
slot.ValuePriority, slot.ValuePriority,
null); null);
} }
@ -190,32 +195,38 @@ namespace Avalonia
_sink.ValueChanged(property, priority, oldValue, newValue); _sink.ValueChanged(property, priority, oldValue, newValue);
} }
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) void IValueSink.Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
{ {
if (_values.TryGetValue(property, out var slot)) if (_values.TryGetValue(property, out var slot))
{ {
if (slot == entry) if (slot == entry)
{ {
_values.Remove(property); _values.Remove(property);
_sink.Completed(property, entry, oldValue);
} }
} }
} }
private void SetExisting<T>( private IDisposable? SetExisting<T>(
object slot, object slot,
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority) BindingPriority priority)
{ {
IDisposable? result = null;
if (slot is IPriorityValueEntry<T> e) if (slot is IPriorityValueEntry<T> e)
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, e); var priorityValue = new PriorityValue<T>(_owner, property, this, e);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
priorityValue.SetValue(value, priority); result = priorityValue.SetValue(value, priority);
} }
else if (slot is PriorityValue<T> p) else if (slot is PriorityValue<T> p)
{ {
p.SetValue(value, priority); result = p.SetValue(value, priority);
} }
else if (slot is LocalValueEntry<T> l) else if (slot is LocalValueEntry<T> l)
{ {
@ -228,6 +239,7 @@ namespace Avalonia
else else
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, l); var priorityValue = new PriorityValue<T>(_owner, property, this, l);
result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
} }
} }
@ -235,6 +247,8 @@ namespace Avalonia
{ {
throw new NotSupportedException("Unrecognised value store slot type."); throw new NotSupportedException("Unrecognised value store slot type.");
} }
return result;
} }
private IDisposable BindExisting<T>( private IDisposable BindExisting<T>(

2
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -65,7 +65,7 @@ namespace Avalonia.Controls
private const double DATAGRID_minimumColumnHeaderHeight = 4; private const double DATAGRID_minimumColumnHeaderHeight = 4;
internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_maximumStarColumnWidth = 10000;
internal const double DATAGRID_minimumStarColumnWidth = 0.001; internal const double DATAGRID_minimumStarColumnWidth = 0.001;
private const double DATAGRID_mouseWheelDelta = 48.0; private const double DATAGRID_mouseWheelDelta = 72.0;
private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_maxHeadersThickness = 32768;
private const double DATAGRID_defaultRowHeight = 22; private const double DATAGRID_defaultRowHeight = 22;

88
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -67,7 +67,7 @@ namespace Avalonia.Controls
static DataGridColumnHeader() static DataGridColumnHeader()
{ {
AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x,e) => x.OnAreSeparatorsVisibleChanged(e)); AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
} }
/// <summary> /// <summary>
@ -103,7 +103,7 @@ namespace Avalonia.Controls
{ {
get; get;
set; set;
} }
internal DataGrid OwningGrid => OwningColumn?.OwningGrid; internal DataGrid OwningGrid => OwningColumn?.OwningGrid;
internal int ColumnIndex internal int ColumnIndex
@ -116,19 +116,19 @@ namespace Avalonia.Controls
} }
return OwningColumn.Index; return OwningColumn.Index;
} }
} }
internal ListSortDirection? CurrentSortingState internal ListSortDirection? CurrentSortingState
{ {
get; get;
private set; private set;
} }
private bool IsMouseOver private bool IsMouseOver
{ {
get; get;
set; set;
} }
private bool IsPressed private bool IsPressed
{ {
@ -158,14 +158,14 @@ namespace Avalonia.Controls
&& OwningGrid.DataConnection.AllowSort) && OwningGrid.DataConnection.AllowSort)
{ {
var sort = OwningColumn.GetSortDescription(); var sort = OwningColumn.GetSortDescription();
if(sort != null) if (sort != null)
{ {
CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending;
} }
} }
PseudoClasses.Set(":sortascending", PseudoClasses.Set(":sortascending",
CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending);
PseudoClasses.Set(":sortdescending", PseudoClasses.Set(":sortdescending",
CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending);
} }
@ -195,7 +195,7 @@ namespace Avalonia.Controls
// completed a click without dragging, so we're sorting // completed a click without dragging, so we're sorting
InvokeProcessSort(keyModifiers); InvokeProcessSort(keyModifiers);
handled = true; handled = true;
} }
internal void InvokeProcessSort(KeyModifiers keyModifiers) internal void InvokeProcessSort(KeyModifiers keyModifiers)
{ {
@ -208,7 +208,7 @@ namespace Avalonia.Controls
{ {
Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers));
} }
} }
//TODO GroupSorting //TODO GroupSorting
internal void ProcessSort(KeyModifiers keyModifiers) internal void ProcessSort(KeyModifiers keyModifiers)
@ -246,45 +246,49 @@ namespace Avalonia.Controls
owningGrid.DataConnection.SortDescriptions.Clear(); owningGrid.DataConnection.SortDescriptions.Clear();
} }
if (sort != null) // if ctrl is held down, we only clear the sort directions
if (!ctrl)
{ {
newSort = sort.SwitchSortDirection(); if (sort != null)
// changing direction should not affect sort order, so we replace this column's
// sort description instead of just adding it to the end of the collection
int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
if (oldIndex >= 0)
{ {
owningGrid.DataConnection.SortDescriptions.Remove(sort); newSort = sort.SwitchSortDirection();
owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
// changing direction should not affect sort order, so we replace this column's
// sort description instead of just adding it to the end of the collection
int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
if (oldIndex >= 0)
{
owningGrid.DataConnection.SortDescriptions.Remove(sort);
owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
}
else
{
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
} }
else else
{ {
string propertyName = OwningColumn.GetSortPropertyName();
// no-opt if we couldn't find a property to sort on
if (string.IsNullOrEmpty(propertyName))
{
return;
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
owningGrid.DataConnection.SortDescriptions.Add(newSort); owningGrid.DataConnection.SortDescriptions.Add(newSort);
} }
} }
else
{
string propertyName = OwningColumn.GetSortPropertyName();
// no-opt if we couldn't find a property to sort on
if (string.IsNullOrEmpty(propertyName))
{
return;
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
} }
} }
} }
private bool CanReorderColumn(DataGridColumn column) private bool CanReorderColumn(DataGridColumn column)
{ {
return OwningGrid.CanUserReorderColumns return OwningGrid.CanUserReorderColumns
&& !(column is DataGridFillerColumn) && !(column is DataGridFillerColumn)
&& (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue); && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue);
} }
/// <summary> /// <summary>
/// Determines whether a column can be resized by dragging the border of its header. If star sizing /// Determines whether a column can be resized by dragging the border of its header. If star sizing
@ -302,7 +306,7 @@ namespace Avalonia.Controls
return false; return false;
} }
return column.ActualCanUserResize; return column.ActualCanUserResize;
} }
private static bool TrySetResizeColumn(DataGridColumn column) private static bool TrySetResizeColumn(DataGridColumn column)
{ {
@ -316,7 +320,7 @@ namespace Avalonia.Controls
return true; return true;
} }
return false; return false;
} }
//TODO DragDrop //TODO DragDrop
@ -371,7 +375,7 @@ namespace Avalonia.Controls
{ {
if (_dragMode == DragMode.MouseDown) if (_dragMode == DragMode.MouseDown)
{ {
OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled); OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled);
} }
else if (_dragMode == DragMode.Reorder) else if (_dragMode == DragMode.Reorder)
{ {
@ -449,7 +453,7 @@ namespace Avalonia.Controls
OnMouseLeave(); OnMouseLeave();
ApplyState(); ApplyState();
} }
private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e) private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{ {
@ -577,7 +581,7 @@ namespace Avalonia.Controls
{ {
return OwningGrid.Columns.Count - 1; return OwningGrid.Columns.Count - 1;
} }
} }
/// <summary> /// <summary>
/// Returns true if the mouse is /// Returns true if the mouse is
@ -723,7 +727,7 @@ namespace Avalonia.Controls
Point targetPosition = new Point(0, 0); Point targetPosition = new Point(0, 0);
if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen) if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen)
{ {
targetColumn = targetColumn =
OwningGrid.ColumnsInternal.GetLastColumn( OwningGrid.ColumnsInternal.GetLastColumn(
isVisible: true, isVisible: true,
isFrozen: OwningColumn.IsFrozen, isFrozen: OwningColumn.IsFrozen,
@ -741,7 +745,7 @@ namespace Avalonia.Controls
handled = true; handled = true;
} }
} }
private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders) private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders)
{ {
@ -764,7 +768,7 @@ namespace Avalonia.Controls
handled = true; handled = true;
} }
} }
private void SetDragCursor(Point mousePosition) private void SetDragCursor(Point mousePosition)
{ {

8
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -202,7 +202,7 @@
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" /> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate"> <Setter Property="DropLocationIndicatorTemplate">
<Template> <Template>
<Rectangle Fill="#FF3F4346" Width="2"/> <Rectangle Fill="{DynamicResource ThemeBorderHighColor}" Width="2"/>
</Template> </Template>
</Setter> </Setter>
<Setter Property="Template"> <Setter Property="Template">
@ -215,11 +215,11 @@
<DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" /> <DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/> <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>
<DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/> <DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="#FFC9CACA"/> <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" /> <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
<Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" /> <Rectangle Name="BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" /> <Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/> <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>
<Grid Grid.Column="1" Grid.Row="2" <Grid Grid.Column="1" Grid.Row="2"

37
src/Avalonia.Controls/Application.cs

@ -44,6 +44,7 @@ namespace Avalonia
private readonly Styler _styler = new Styler(); private readonly Styler _styler = new Styler();
private Styles _styles; private Styles _styles;
private IResourceDictionary _resources; private IResourceDictionary _resources;
private bool _notifyingResourcesChanged;
/// <summary> /// <summary>
/// Defines the <see cref="DataContext"/> property. /// Defines the <see cref="DataContext"/> property.
@ -160,7 +161,19 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// Global styles apply to all windows in the application. /// Global styles apply to all windows in the application.
/// </remarks> /// </remarks>
public Styles Styles => _styles ?? (_styles = new Styles()); public Styles Styles
{
get
{
if (_styles == null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@ -233,9 +246,29 @@ namespace Avalonia
} }
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
if (_notifyingResourcesChanged)
{
return;
}
try
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
finally
{
_notifyingResourcesChanged = false;
}
}
private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
{ {
ResourcesChanged?.Invoke(this, e); NotifyResourcesChanged(e);
} }
private string _name; private string _name;

2
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -476,7 +476,7 @@ namespace Avalonia.Controls
{ {
_dropDownButton.Click += DropDownButton_Click; _dropDownButton.Click += DropDownButton_Click;
_buttonPointerPressedSubscription = _buttonPointerPressedSubscription =
_dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true); _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
} }
if (_textBox != null) if (_textBox != null)

3
src/Avalonia.Controls/ComboBox.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -265,7 +266,7 @@ namespace Avalonia.Controls
var toplevel = this.GetVisualRoot() as TopLevel; var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null) if (toplevel != null)
{ {
_subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
{ {
//eat wheel scroll event outside dropdown popup while it's open //eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)

3
src/Avalonia.Controls/ContextMenu.cs

@ -104,8 +104,7 @@ namespace Avalonia.Controls
{ {
PlacementMode = PlacementMode.Pointer, PlacementMode = PlacementMode.Pointer,
PlacementTarget = control, PlacementTarget = control,
StaysOpen = false, StaysOpen = false
ObeyScreenEdges = true
}; };
_popup.Opened += PopupOpened; _popup.Opened += PopupOpened;

195
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@ -0,0 +1,195 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Data.Converters;
using Avalonia.Input;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
/// platform's style guidelines.
/// </summary>
public class PlatformKeyGestureConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is null)
{
return null;
}
else if (value is KeyGesture gesture && targetType == typeof(string))
{
return ToPlatformString(gesture);
}
else
{
throw new NotSupportedException();
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
/// <summary>
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
/// platform's style guidelines.
/// </summary>
/// <param name="gesture">The gesture.</param>
/// <returns>The gesture formatted according to the current platform.</returns>
public static string ToPlatformString(KeyGesture gesture)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ToString(gesture, "Win");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return ToString(gesture, "Super");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return ToOSXString(gesture);
}
else
{
return gesture.ToString();
}
}
private static string ToString(KeyGesture gesture, string meta)
{
var s = new StringBuilder();
static void Plus(StringBuilder s)
{
if (s.Length > 0)
{
s.Append("+");
}
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append("Ctrl");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
Plus(s);
s.Append("Shift");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
Plus(s);
s.Append("Alt");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
Plus(s);
s.Append(meta);
}
Plus(s);
s.Append(ToString(gesture.Key));
return s.ToString();
}
private static string ToOSXString(KeyGesture gesture)
{
var s = new StringBuilder();
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append('⌃');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
s.Append('⌥');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
s.Append('⇧');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
s.Append('⌘');
}
s.Append(ToOSXString(gesture.Key));
return s.ToString();
}
private static string ToString(Key key)
{
return key switch
{
Key.Add => "+",
Key.Back => "Backspace",
Key.D0 => "0",
Key.D1 => "1",
Key.D2 => "2",
Key.D3 => "3",
Key.D4 => "4",
Key.D5 => "5",
Key.D6 => "6",
Key.D7 => "7",
Key.D8 => "8",
Key.D9 => "9",
Key.Decimal => ".",
Key.Divide => "/",
Key.Down => "Down Arrow",
Key.Left => "Left Arrow",
Key.Multiply => "*",
Key.OemBackslash => "\\",
Key.OemCloseBrackets => "]",
Key.OemComma => ",",
Key.OemMinus => "-",
Key.OemOpenBrackets => "[",
Key.OemPeriod=> ".",
Key.OemPipe => "|",
Key.OemPlus => "+",
Key.OemQuestion => "/",
Key.OemQuotes => "\"",
Key.OemSemicolon => ";",
Key.OemTilde => "`",
Key.Right => "Right Arrow",
Key.Separator => "/",
Key.Subtract => "-",
Key.Up => "Up Arrow",
_ => key.ToString(),
};
}
private static string ToOSXString(Key key)
{
return key switch
{
Key.Back => "⌫",
Key.Down => "↓",
Key.End => "↘",
Key.Escape => "⎋",
Key.Home => "↖",
Key.Left => "←",
Key.Return => "↩",
Key.PageDown => "⇞",
Key.PageUp => "⇟",
Key.Right => "→",
Key.Space => "␣",
Key.Tab => "⇥",
Key.Up => "↑",
_ => ToString(key),
};
}
}
}

47
src/Avalonia.Controls/MenuItem.cs

@ -13,6 +13,7 @@ using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -48,6 +49,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object> IconProperty = public static readonly StyledProperty<object> IconProperty =
AvaloniaProperty.Register<MenuItem, object>(nameof(Icon)); AvaloniaProperty.Register<MenuItem, object>(nameof(Icon));
/// <summary>
/// Defines the <see cref="InputGesture"/> property.
/// </summary>
public static readonly StyledProperty<KeyGesture> InputGestureProperty =
AvaloniaProperty.Register<MenuItem, KeyGesture>(nameof(InputGesture));
/// <summary> /// <summary>
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
/// </summary> /// </summary>
@ -93,6 +100,7 @@ namespace Avalonia.Controls
private ICommand _command; private ICommand _command;
private bool _commandCanExecute = true; private bool _commandCanExecute = true;
private Popup _popup; private Popup _popup;
private IDisposable _gridHack;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class. /// Initializes static members of the <see cref="MenuItem"/> class.
@ -194,6 +202,19 @@ namespace Avalonia.Controls
set { SetValue(IconProperty, value); } set { SetValue(IconProperty, value); }
} }
/// <summary>
/// Gets or sets the input gesture that will be displayed in the menu item.
/// </summary>
/// <remarks>
/// Setting this property does not cause the input gesture to be handled by the menu item,
/// it simply displays the gesture text in the menu.
/// </remarks>
public KeyGesture InputGesture
{
get { return GetValue(InputGestureProperty); }
set { SetValue(InputGestureProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected. /// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
/// </summary> /// </summary>
@ -304,6 +325,32 @@ namespace Avalonia.Controls
{ {
Command.CanExecuteChanged -= CanExecuteChanged; Command.CanExecuteChanged -= CanExecuteChanged;
} }
_gridHack?.Dispose();
_gridHack = null;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (this.GetVisualParent() is IControl parent)
{
// HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
// property to store SharedSizeGroup state, except property inheritance is done
// down the logical tree. In this case, the control which is setting
// Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
// the way Grid stores shared size state, the developers of WPF just created a
// binding of the internal state of the visual parent to the menu item. We don't
// have much choice but to do the same for now unless we want to refactor Grid,
// which I honestly am not brave enough to do right now. Here's the same hack in
// the WPF codebase:
//
// https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
_gridHack = Bind(
DefinitionBase.PrivateSharedSizeScopeProperty,
parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty));
}
} }
/// <summary> /// <summary>

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

@ -36,7 +36,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc) /// Enables or disables system window decorations (title bar, buttons, etc)
/// </summary> /// </summary>
void SetSystemDecorations(bool enabled); void SetSystemDecorations(SystemDecorations enabled);
/// <summary> /// <summary>
/// Sets the icon of this window. /// Sets the icon of this window.

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

@ -4,12 +4,13 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
public class TextPresenter : TextBlock public class TextPresenter : Control
{ {
public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty = public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>( TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
@ -38,11 +39,41 @@ namespace Avalonia.Controls.Presenters
o => o.SelectionEnd, o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v); (o, v) => o.SelectionEnd = v);
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, string> TextProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, string>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="TextWrapping"/> property.
/// </summary>
public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
TextBlock.TextWrappingProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
public static readonly StyledProperty<IBrush> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TextPresenter>();
private readonly DispatcherTimer _caretTimer; private readonly DispatcherTimer _caretTimer;
private int _caretIndex; private int _caretIndex;
private int _selectionStart; private int _selectionStart;
private int _selectionEnd; private int _selectionEnd;
private bool _caretBlink; private bool _caretBlink;
private string _text;
private FormattedText _formattedText;
private Size _constraint;
static TextPresenter() static TextPresenter()
{ {
@ -51,6 +82,7 @@ namespace Avalonia.Controls.Presenters
SelectionStartProperty, SelectionEndProperty); SelectionStartProperty, SelectionEndProperty);
Observable.Merge( Observable.Merge(
TextProperty.Changed,
SelectionStartProperty.Changed, SelectionStartProperty.Changed,
SelectionEndProperty.Changed, SelectionEndProperty.Changed,
PasswordCharProperty.Changed PasswordCharProperty.Changed
@ -61,11 +93,104 @@ namespace Avalonia.Controls.Presenters
public TextPresenter() public TextPresenter()
{ {
_caretTimer = new DispatcherTimer(); _text = string.Empty;
_caretTimer.Interval = TimeSpan.FromMilliseconds(500); _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_caretTimer.Tick += CaretTimerTick; _caretTimer.Tick += CaretTimerTick;
} }
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
public IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets the text.
/// </summary>
[Content]
public string Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
public FontFamily FontFamily
{
get => TextBlock.GetFontFamily(this);
set => TextBlock.SetFontFamily(this, value);
}
/// <summary>
/// Gets or sets the font size.
/// </summary>
public double FontSize
{
get => TextBlock.GetFontSize(this);
set => TextBlock.SetFontSize(this, value);
}
/// <summary>
/// Gets or sets the font style.
/// </summary>
public FontStyle FontStyle
{
get => TextBlock.GetFontStyle(this);
set => TextBlock.SetFontStyle(this, value);
}
/// <summary>
/// Gets or sets the font weight.
/// </summary>
public FontWeight FontWeight
{
get => TextBlock.GetFontWeight(this);
set => TextBlock.SetFontWeight(this, value);
}
/// <summary>
/// Gets or sets a brush used to paint the text.
/// </summary>
public IBrush Foreground
{
get => TextBlock.GetForeground(this);
set => TextBlock.SetForeground(this, value);
}
/// <summary>
/// Gets or sets the control's text wrapping mode.
/// </summary>
public TextWrapping TextWrapping
{
get => GetValue(TextWrappingProperty);
set => SetValue(TextWrappingProperty, value);
}
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
public TextAlignment TextAlignment
{
get => GetValue(TextAlignmentProperty);
set => SetValue(TextAlignmentProperty, value);
}
/// <summary>
/// Gets the <see cref="FormattedText"/> used to render the text.
/// </summary>
public FormattedText FormattedText
{
get
{
return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text));
}
}
public int CaretIndex public int CaretIndex
{ {
get get
@ -138,6 +263,56 @@ namespace Avalonia.Controls.Presenters
return hit.TextPosition + (hit.IsTrailing ? 1 : 0); return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
} }
/// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns>
private FormattedText CreateFormattedTextInternal(Size constraint, string text)
{
return new FormattedText
{
Constraint = constraint,
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
TextWrapping = TextWrapping,
};
}
/// <summary>
/// Invalidates <see cref="FormattedText"/>.
/// </summary>
protected void InvalidateFormattedText()
{
if (_formattedText != null)
{
_constraint = _formattedText.Constraint;
_formattedText = null;
}
InvalidateVisual();
}
/// <summary>
/// Renders the <see cref="TextPresenter"/> to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
private void RenderInternal(DrawingContext context)
{
var background = Background;
if (background != null)
{
context.FillRectangle(background, new Rect(Bounds.Size));
}
FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText);
}
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
@ -150,7 +325,7 @@ namespace Avalonia.Controls.Presenters
// issue #600: set constraint before any FormattedText manipulation // issue #600: set constraint before any FormattedText manipulation
// see base.Render(...) implementation // see base.Render(...) implementation
FormattedText.Constraint = Bounds.Size; FormattedText.Constraint = _constraint;
var rects = FormattedText.HitTestTextRange(start, length); var rects = FormattedText.HitTestTextRange(start, length);
@ -160,7 +335,7 @@ namespace Avalonia.Controls.Presenters
} }
} }
base.Render(context); RenderInternal(context);
if (selectionStart == selectionEnd) if (selectionStart == selectionEnd)
{ {
@ -168,7 +343,7 @@ namespace Avalonia.Controls.Presenters
if (caretBrush is null) if (caretBrush is null)
{ {
var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; var backgroundColor = (Background as SolidColorBrush)?.Color;
if (backgroundColor.HasValue) if (backgroundColor.HasValue)
{ {
byte red = (byte)~(backgroundColor.Value.R); byte red = (byte)~(backgroundColor.Value.R);
@ -255,17 +430,17 @@ namespace Avalonia.Controls.Presenters
/// <param name="constraint">The constraint of the text.</param> /// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param> /// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="FormattedText"/> object.</returns>
protected override FormattedText CreateFormattedText(Size constraint, string text) protected virtual FormattedText CreateFormattedText(Size constraint, string text)
{ {
FormattedText result = null; FormattedText result = null;
if (PasswordChar != default(char)) if (PasswordChar != default(char))
{ {
result = base.CreateFormattedText(constraint, new string(PasswordChar, text?.Length ?? 0)); result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0));
} }
else else
{ {
result = base.CreateFormattedText(constraint, text); result = CreateFormattedTextInternal(constraint, text);
} }
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
@ -284,13 +459,37 @@ namespace Avalonia.Controls.Presenters
return result; return result;
} }
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
private Size MeasureInternal(Size availableSize)
{
if (!string.IsNullOrEmpty(Text))
{
if (TextWrapping == TextWrapping.Wrap)
{
FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
}
else
{
FormattedText.Constraint = Size.Infinity;
}
return FormattedText.Bounds.Size;
}
return new Size();
}
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var text = Text; var text = Text;
if (!string.IsNullOrEmpty(text)) if (!string.IsNullOrEmpty(text))
{ {
return base.MeasureOverride(availableSize); return MeasureInternal(availableSize);
} }
else else
{ {

89
src/Avalonia.Controls/Primitives/AccessText.cs

@ -4,6 +4,7 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -69,7 +70,7 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey) if (underscore != -1 && ShowAccessKey)
{ {
var rect = FormattedText.HitTestTextPosition(underscore); var rect = HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5); var offset = new Vector(0, -0.5);
context.DrawLine( context.DrawLine(
new Pen(Foreground, 1), new Pen(Foreground, 1),
@ -78,21 +79,85 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <inheritdoc/> /// <summary>
protected override FormattedText CreateFormattedText(Size constraint, string text) /// Get the pixel location relative to the top-left of the layout box given the text position.
/// </summary>
/// <param name="textPosition">The text position.</param>
/// <returns></returns>
private Rect HitTestTextPosition(int textPosition)
{ {
return base.CreateFormattedText(constraint, StripAccessKey(text)); if (TextLayout == null)
{
return new Rect();
}
if (TextLayout.TextLines.Count == 0)
{
return new Rect();
}
if (textPosition < 0 || textPosition >= Text.Length)
{
var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
var offsetX = lastLine.LineMetrics.BaselineOrigin.X;
var lineX = offsetX + lastLine.LineMetrics.Size.Width;
var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
return new Rect(lineX, lineY, 0, lastLine.LineMetrics.Size.Height);
}
var currentY = 0.0;
foreach (var textLine in TextLayout.TextLines)
{
if (textLine.Text.End < textPosition)
{
currentY += textLine.LineMetrics.Size.Height;
continue;
}
var currentX = textLine.LineMetrics.BaselineOrigin.X;
foreach (var textRun in textLine.TextRuns)
{
if (!(textRun is ShapedTextRun shapedRun))
{
continue;
}
if (shapedRun.GlyphRun.Characters.End < textPosition)
{
currentX += shapedRun.GlyphRun.Bounds.Width;
continue;
}
var characterHit = shapedRun.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
var distance = shapedRun.GlyphRun.GetDistanceFromCharacterHit(characterHit);
currentX += distance - width;
if (characterHit.TrailingLength == 0)
{
width = 0.0;
}
return new Rect(currentX, currentY, width, shapedRun.GlyphRun.Bounds.Height);
}
}
return new Rect();
} }
/// <summary> /// <inheritdoc/>
/// Measures the control. protected override TextLayout CreateTextLayout(Size constraint, string text)
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size availableSize)
{ {
var result = base.MeasureOverride(availableSize); return base.CreateTextLayout(constraint, StripAccessKey(text));
return result.WithHeight(result.Height + 1);
} }
/// <inheritdoc/> /// <inheritdoc/>

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

@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives
} }
} }
DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));

180
src/Avalonia.Controls/TextBlock.cs

@ -4,6 +4,7 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -19,6 +20,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> BackgroundProperty = public static readonly StyledProperty<IBrush> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TextBlock>(); Border.BackgroundProperty.AddOwner<TextBlock>();
/// <summary>
/// Defines the <see cref="Padding"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> PaddingProperty =
Decorator.PaddingProperty.AddOwner<TextBlock>();
// TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner
// them into TextBlock. // them into TextBlock.
@ -28,7 +35,7 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<FontFamily> FontFamilyProperty = public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, FontFamily>( AvaloniaProperty.RegisterAttached<TextBlock, Control, FontFamily>(
nameof(FontFamily), nameof(FontFamily),
defaultValue: FontFamily.Default, defaultValue: FontFamily.Default,
inherits: true); inherits: true);
/// <summary> /// <summary>
@ -87,8 +94,20 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextWrapping> TextWrappingProperty = public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
AvaloniaProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping)); AvaloniaProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping));
/// <summary>
/// Defines the <see cref="TextTrimming"/> property.
/// </summary>
public static readonly StyledProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.Register<TextBlock, TextTrimming>(nameof(TextTrimming));
/// <summary>
/// Defines the <see cref="TextDecorations"/> property.
/// </summary>
public static readonly StyledProperty<TextDecorationCollection> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection>(nameof(TextDecorations));
private string _text; private string _text;
private FormattedText _formattedText; private TextLayout _textLayout;
private Size _constraint; private Size _constraint;
/// <summary> /// <summary>
@ -97,20 +116,31 @@ namespace Avalonia.Controls
static TextBlock() static TextBlock()
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>( AffectsRender<TextBlock>(
BackgroundProperty, BackgroundProperty, ForegroundProperty, FontSizeProperty,
ForegroundProperty, FontWeightProperty, FontStyleProperty, TextWrappingProperty,
FontWeightProperty, TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty,
FontSizeProperty, TextDecorationsProperty, TextProperty, PaddingProperty);
FontStyleProperty);
AffectsMeasure<TextBlock>(
FontSizeProperty, FontWeightProperty, FontStyleProperty,
FontFamilyProperty, TextTrimmingProperty, TextProperty,
PaddingProperty);
Observable.Merge( Observable.Merge(
TextProperty.Changed, TextProperty.Changed,
ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextAlignmentProperty.Changed,
TextWrappingProperty.Changed,
TextTrimmingProperty.Changed,
FontSizeProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontStyleProperty.Changed,
FontWeightProperty.Changed FontWeightProperty.Changed,
).AddClassHandler<TextBlock>((x,_) => x.OnTextPropertiesChanged()); FontFamilyProperty.Changed,
TextDecorationsProperty.Changed,
PaddingProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
} }
/// <summary> /// <summary>
@ -121,6 +151,26 @@ namespace Avalonia.Controls
_text = string.Empty; _text = string.Empty;
} }
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
public TextLayout TextLayout
{
get
{
return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text));
}
}
/// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>.
/// </summary>
public Thickness Padding
{
get { return GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets a brush used to paint the control's background. /// Gets or sets a brush used to paint the control's background.
/// </summary> /// </summary>
@ -186,28 +236,21 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets the <see cref="FormattedText"/> used to render the text. /// Gets or sets the control's text wrapping mode.
/// </summary> /// </summary>
public FormattedText FormattedText public TextWrapping TextWrapping
{ {
get get { return GetValue(TextWrappingProperty); }
{ set { SetValue(TextWrappingProperty, value); }
if (_formattedText == null)
{
_formattedText = CreateFormattedText(_constraint, Text);
}
return _formattedText;
}
} }
/// <summary> /// <summary>
/// Gets or sets the control's text wrapping mode. /// Gets or sets the control's text trimming mode.
/// </summary> /// </summary>
public TextWrapping TextWrapping public TextTrimming TextTrimming
{ {
get { return GetValue(TextWrappingProperty); } get { return GetValue(TextTrimmingProperty); }
set { SetValue(TextWrappingProperty, value); } set { SetValue(TextTrimmingProperty, value); }
} }
/// <summary> /// <summary>
@ -219,6 +262,15 @@ namespace Avalonia.Controls
set { SetValue(TextAlignmentProperty, value); } set { SetValue(TextAlignmentProperty, value); }
} }
/// <summary>
/// Gets or sets the text decorations.
/// </summary>
public TextDecorationCollection TextDecorations
{
get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value);
}
/// <summary> /// <summary>
/// Gets the value of the attached <see cref="FontFamilyProperty"/> on a control. /// Gets the value of the attached <see cref="FontFamilyProperty"/> on a control.
/// </summary> /// </summary>
@ -337,39 +389,43 @@ namespace Avalonia.Controls
context.FillRectangle(background, new Rect(Bounds.Size)); context.FillRectangle(background, new Rect(Bounds.Size));
} }
FormattedText.Constraint = Bounds.Size; var padding = Padding;
context.DrawText(Foreground, new Point(), FormattedText);
TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top));
} }
/// <summary> /// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
/// <param name="constraint">The constraint of the text.</param> /// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param> /// <param name="text">The text to format.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual FormattedText CreateFormattedText(Size constraint, string text) protected virtual TextLayout CreateTextLayout(Size constraint, string text)
{ {
return new FormattedText if (constraint == Size.Empty)
{ {
Constraint = constraint, return null;
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), }
FontSize = FontSize,
Text = text ?? string.Empty, return new TextLayout(
TextAlignment = TextAlignment, text ?? string.Empty,
TextWrapping = TextWrapping, FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
}; FontSize,
Foreground,
TextAlignment,
TextWrapping,
TextTrimming,
TextDecorations,
constraint.Width,
constraint.Height);
} }
/// <summary> /// <summary>
/// Invalidates <see cref="FormattedText"/>. /// Invalidates <see cref="TextLayout"/>.
/// </summary> /// </summary>
protected void InvalidateFormattedText() protected void InvalidateTextLayout()
{ {
if (_formattedText != null) _textLayout = null;
{
_constraint = _formattedText.Constraint;
_formattedText = null;
}
} }
/// <summary> /// <summary>
@ -379,33 +435,33 @@ namespace Avalonia.Controls
/// <returns>The desired size.</returns> /// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (!string.IsNullOrEmpty(Text)) if (string.IsNullOrEmpty(Text))
{
return new Size();
}
var padding = Padding;
availableSize = availableSize.Deflate(padding);
if (_constraint != availableSize)
{ {
if (TextWrapping == TextWrapping.Wrap) InvalidateTextLayout();
{
FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
}
else
{
FormattedText.Constraint = Size.Infinity;
}
return FormattedText.Bounds.Size;
} }
return new Size(); _constraint = availableSize;
var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty;
return measuredSize.Inflate(padding);
} }
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
base.OnAttachedToLogicalTree(e); base.OnAttachedToLogicalTree(e);
InvalidateFormattedText();
InvalidateMeasure();
}
private void OnTextPropertiesChanged() InvalidateTextLayout();
{
InvalidateFormattedText();
InvalidateMeasure(); InvalidateMeasure();
} }
} }

83
src/Avalonia.Controls/TextBox.cs

@ -14,6 +14,7 @@ using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -72,6 +73,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty = public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextBox>(); TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
/// <summary>
/// Defines the <see cref="HorizontalAlignment"/> property.
/// </summary>
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner<TextBox>();
/// <summary>
/// Defines the <see cref="VerticalAlignment"/> property.
/// </summary>
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<TextBox>();
public static readonly StyledProperty<TextWrapping> TextWrappingProperty = public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
TextBlock.TextWrappingProperty.AddOwner<TextBox>(); TextBlock.TextWrappingProperty.AddOwner<TextBox>();
@ -262,6 +275,40 @@ namespace Avalonia.Controls
} }
} }
public string SelectedText
{
get { return GetSelection(); }
set
{
if (value == null)
{
return;
}
_undoRedoHelper.Snapshot();
HandleTextInput(value);
_undoRedoHelper.Snapshot();
}
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
public HorizontalAlignment HorizontalContentAlignment
{
get { return GetValue(HorizontalContentAlignmentProperty); }
set { SetValue(HorizontalContentAlignmentProperty, value); }
}
/// <summary>
/// Gets or sets the vertical alignment of the content within the control.
/// </summary>
public VerticalAlignment VerticalContentAlignment
{
get { return GetValue(VerticalContentAlignmentProperty); }
set { SetValue(VerticalContentAlignmentProperty, value); }
}
public TextAlignment TextAlignment public TextAlignment TextAlignment
{ {
get { return GetValue(TextAlignmentProperty); } get { return GetValue(TextAlignmentProperty); }
@ -316,8 +363,7 @@ namespace Avalonia.Controls
!AcceptsReturn && !AcceptsReturn &&
Text?.Length > 0) Text?.Length > 0)
{ {
SelectionStart = 0; SelectAll();
SelectionEnd = Text.Length;
} }
else else
{ {
@ -653,12 +699,12 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point);
var text = Text; var text = Text;
if (text != null && e.MouseButton == MouseButton.Left) if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point);
switch (e.ClickCount) switch (e.ClickCount)
{ {
case 1: case 1:
@ -673,8 +719,7 @@ namespace Avalonia.Controls
SelectionEnd = StringUtils.NextWord(text, index); SelectionEnd = StringUtils.NextWord(text, index);
break; break;
case 3: case 3:
SelectionStart = 0; SelectAll();
SelectionEnd = text.Length;
break; break;
} }
} }
@ -685,7 +730,8 @@ namespace Avalonia.Controls
protected override void OnPointerMoved(PointerEventArgs e) protected override void OnPointerMoved(PointerEventArgs e)
{ {
if (_presenter != null && e.Pointer.Captured == _presenter) // selection should not change during pointer move if the user right clicks
if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
@ -698,6 +744,22 @@ namespace Avalonia.Controls
{ {
if (_presenter != null && e.Pointer.Captured == _presenter) if (_presenter != null && e.Pointer.Captured == _presenter)
{ {
if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(_presenter);
var caretIndex = _presenter.GetCaretIndex(point);
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
CaretIndex = SelectionEnd = SelectionStart = caretIndex;
}
}
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }
} }
@ -896,7 +958,10 @@ namespace Avalonia.Controls
CaretIndex = caretIndex; CaretIndex = caretIndex;
} }
private void SelectAll() /// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{ {
SelectionStart = 0; SelectionStart = 0;
SelectionEnd = Text?.Length ?? 0; SelectionEnd = Text?.Length ?? 0;

98
src/Avalonia.Controls/Window.cs

@ -2,19 +2,19 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using System.ComponentModel;
using Avalonia.Interactivity;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -45,6 +45,27 @@ namespace Avalonia.Controls
WidthAndHeight = 3, WidthAndHeight = 3,
} }
/// <summary>
/// Determines system decorations (title bar, border, etc) for a <see cref="Window"/>
/// </summary>
public enum SystemDecorations
{
/// <summary>
/// No decorations
/// </summary>
None = 0,
/// <summary>
/// Window border without titlebar
/// </summary>
BorderOnly = 1,
/// <summary>
/// Fully decorated (default)
/// </summary>
Full = 2
}
/// <summary> /// <summary>
/// A top-level window. /// A top-level window.
/// </summary> /// </summary>
@ -59,8 +80,18 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc) /// Enables or disables system window decorations (title bar, buttons, etc)
/// </summary> /// </summary>
public static readonly StyledProperty<bool> HasSystemDecorationsProperty = [Obsolete("Use SystemDecorationsProperty instead")]
AvaloniaProperty.Register<Window, bool>(nameof(HasSystemDecorations), true); public static readonly DirectProperty<Window, bool> HasSystemDecorationsProperty =
AvaloniaProperty.RegisterDirect<Window, bool>(
nameof(HasSystemDecorations),
o => o.HasSystemDecorations,
(o, v) => o.HasSystemDecorations = v);
/// <summary>
/// Defines the <see cref="SystemDecorations"/> property.
/// </summary>
public static readonly StyledProperty<SystemDecorations> SystemDecorationsProperty =
AvaloniaProperty.Register<Window, SystemDecorations>(nameof(SystemDecorations), SystemDecorations.Full);
/// <summary> /// <summary>
/// Enables or disables the taskbar icon /// Enables or disables the taskbar icon
@ -124,9 +155,6 @@ namespace Avalonia.Controls
{ {
BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
(s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl)); IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl));
@ -135,12 +163,11 @@ namespace Avalonia.Controls
WindowStateProperty.Changed.AddClassHandler<Window>( WindowStateProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
} }
/// <summary> /// <summary>
@ -191,11 +218,30 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc) /// Enables or disables system window decorations (title bar, buttons, etc)
/// </summary> /// </summary>
/// [Obsolete("Use SystemDecorations instead")]
public bool HasSystemDecorations public bool HasSystemDecorations
{ {
get { return GetValue(HasSystemDecorationsProperty); } get => SystemDecorations == SystemDecorations.Full;
set { SetValue(HasSystemDecorationsProperty, value); } set
{
var oldValue = HasSystemDecorations;
if (oldValue != value)
{
SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None;
RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value);
}
}
}
/// <summary>
/// Sets the system decorations (title bar, border, etc)
/// </summary>
///
public SystemDecorations SystemDecorations
{
get { return GetValue(SystemDecorationsProperty); }
set { SetValue(SystemDecorationsProperty, value); }
} }
/// <summary> /// <summary>
@ -529,7 +575,7 @@ namespace Avalonia.Controls
{ {
var sizeToContent = SizeToContent; var sizeToContent = SizeToContent;
var clientSize = ClientSize; var clientSize = ClientSize;
Size constraint = clientSize; var constraint = availableSize;
if ((sizeToContent & SizeToContent.Width) != 0) if ((sizeToContent & SizeToContent.Width) != 0)
{ {
@ -584,5 +630,27 @@ namespace Avalonia.Controls
/// <see cref="Closing"/> event needs to be raised. /// <see cref="Closing"/> event needs to be raised.
/// </remarks> /// </remarks>
protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e);
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
if (property == SystemDecorationsProperty)
{
var typedNewValue = newValue.GetValueOrDefault<SystemDecorations>();
PlatformImpl?.SetSystemDecorations(typedNewValue);
var o = oldValue.GetValueOrDefault<SystemDecorations>() == SystemDecorations.Full;
var n = typedNewValue == SystemDecorations.Full;
if (o != n)
{
RaisePropertyChanged(HasSystemDecorationsProperty, o, n);
}
}
}
} }
} }

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

@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void SetSystemDecorations(bool enabled) public void SetSystemDecorations(SystemDecorations enabled)
{ {
} }

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

@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void SetSystemDecorations(bool enabled) public void SetSystemDecorations(SystemDecorations enabled)
{ {
} }

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks> <TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

BIN
src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf

Binary file not shown.

12
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -1,8 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" /> <ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -14,7 +21,10 @@
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" /> <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" /> <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" /> <ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.4.0" />
</ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />

24
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,24 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="*,Auto" Margin="4">
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

159
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -1,159 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia
{
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12));
}
public static void AttachDevTools(this TopLevel control, KeyGesture gesture)
{
Diagnostics.DevTools.Attach(control, gesture);
}
public static void OpenDevTools(this TopLevel control)
{
Diagnostics.DevTools.OpenDevTools(control);
}
}
}
namespace Avalonia.Diagnostics
{
public class DevTools : UserControl
{
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
InitializeComponent();
Root = root;
DataContext = new DevToolsViewModel(root);
_keySubscription = InputManager.Instance.Process
.OfType<RawKeyEventArgs>()
.Subscribe(RawKeyDown);
}
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
}
public IControl Root { get; }
public static IDisposable Attach(TopLevel control, KeyGesture gesture)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
{
if (gesture.Matches(e))
{
OpenDevTools(control);
}
}
return control.AddHandler(
KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);
}
internal static void OpenDevTools(TopLevel control)
{
if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
else
{
var devTools = new DevTools(control);
devToolsWindow = new Window
{
Width = 1024,
Height = 512,
Content = devTools,
DataTemplates = { new ViewLocator<ViewModelBase>() },
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;
s_open.Add(control, devToolsWindow);
MarkAsDevTool(devToolsWindow);
devToolsWindow.Show();
}
}
private void DevToolsClosed(object sender, EventArgs e)
{
var devToolsWindow = (Window)sender;
var devTools = (DevTools)devToolsWindow.Content;
s_open.Remove((TopLevel)devTools.Root);
RemoveDevTool(devToolsWindow);
_keySubscription.Dispose();
devToolsWindow.Closed -= DevToolsClosed;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void RawKeyDown(RawKeyEventArgs e)
{
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers)
{
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();
if (control != null)
{
var vm = (DevToolsViewModel)DataContext;
vm.SelectControl((IControl)control);
}
}
}
/// <summary>
/// Marks a visual as part of the DevTools, so it can be excluded from event tracking.
/// </summary>
/// <param name="visual">The visual whose root is to be marked.</param>
public static void MarkAsDevTool(IVisual visual)
{
s_visualTreeRoots.Add(visual.GetVisualRoot());
}
public static void RemoveDevTool(IVisual visual)
{
s_visualTreeRoots.Remove(visual.GetVisualRoot());
}
public static bool BelongsToDevTool(IVisual visual)
{
return s_visualTreeRoots.Contains(visual.GetVisualRoot());
}
}
}

31
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -0,0 +1,31 @@
using Avalonia.Controls;
using Avalonia.Diagnostics;
using Avalonia.Input;
namespace Avalonia
{
/// <summary>
/// Extension methods for attaching DevTools..
/// </summary>
public static class DevToolsExtensions
{
/// <summary>
/// Attaches DevTools to a window, to be opened with the F12 key.
/// </summary>
/// <param name="root">The window to attach DevTools to.</param>
public static void AttachDevTools(this TopLevel root)
{
DevTools.Attach(root, new KeyGesture(Key.F12));
}
/// <summary>
/// Attaches DevTools to a window, to be opened with the specified key gesture.
/// </summary>
/// <param name="root">The window to attach DevTools to.</param>
/// <param name="gesture">The key gesture to open DevTools.</param>
public static void AttachDevTools(this TopLevel root, KeyGesture gesture)
{
DevTools.Attach(root, gesture);
}
}
}

22
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs

@ -0,0 +1,22 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters
{
internal class BoolToBrushConverter : IValueConverter
{
public IBrush Brush { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brush : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

61
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics
{
public static class DevTools
{
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
void PreviewKeyDown(object sender, KeyEventArgs e)
{
if (gesture.Matches(e))
{
Open(root);
}
}
return root.AddDisposableHandler(
InputElement.KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);
}
public static IDisposable Open(TopLevel root)
{
if (s_open.TryGetValue(root, out var window))
{
window.Activate();
}
else
{
window = new MainWindow
{
Width = 1024,
Height = 512,
Root = root,
};
window.Closed += DevToolsClosed;
s_open.Add(root, window);
window.Show();
}
return Disposable.Create(() => window?.Close());
}
private static void DevToolsClosed(object sender, EventArgs e)
{
var window = (MainWindow)sender;
s_open.Remove(window.Root);
window.Closed -= DevToolsClosed;
}
}
}

36
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs

@ -0,0 +1,36 @@
#pragma warning disable IDE1006 // Naming Styles
using Avalonia.Diagnostics.ViewModels;
namespace Avalonia.Diagnostics.Models
{
public class ConsoleContext
{
private readonly ConsoleViewModel _owner;
internal ConsoleContext(ConsoleViewModel owner) => _owner = owner;
public readonly string help = @"Welcome to Avalonia DevTools. Here you can execute arbitrary C# code using Roslyn scripting.
The following variables are available:
e: The control currently selected in the logical or visual tree view
root: The root of the visual tree
The following commands are available:
clear(): Clear the output history
";
public dynamic e { get; internal set; }
public dynamic root { get; internal set; }
internal static object NoOutput { get; } = new object();
public object clear()
{
_owner.History.Clear();
return NoOutput;
}
}
}

19
src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs

@ -0,0 +1,19 @@
using System;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Models
{
internal class ConsoleHistoryItem
{
public ConsoleHistoryItem(string input, object output)
{
Input = input;
Output = output;
Foreground = output is Exception ? Brushes.Red : Brushes.Green;
}
public string Input { get; }
public object Output { get; }
public IBrush Foreground { get; }
}
}

0
src/Avalonia.Diagnostics/Models/EventChainLink.cs → src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

8
src/Avalonia.Diagnostics/ViewLocator.cs → src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@ -1,13 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Diagnostics.ViewModels;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
internal class ViewLocator<TViewModel> : IDataTemplate internal class ViewLocator : IDataTemplate
{ {
public bool SupportsRecycling => false; public bool SupportsRecycling => false;
@ -28,7 +26,7 @@ namespace Avalonia.Diagnostics
public bool Match(object data) public bool Match(object data)
{ {
return data is TViewModel; return data is ViewModelBase;
} }
} }
} }

95
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -0,0 +1,95 @@
using System.ComponentModel;
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal class AvaloniaPropertyViewModel : PropertyViewModel
{
private readonly AvaloniaObject _target;
private string _type;
private object _value;
private string _priority;
private string _group;
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
{
_target = o;
Property = property;
Name = property.IsAttached ?
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
if (property.IsDirect)
{
_group = "Properties";
Priority = "Direct";
}
Update();
}
public AvaloniaProperty Property { get; }
public override object Key => Property;
public override string Name { get; }
public bool IsAttached => Property.IsAttached;
public string Priority
{
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public override string Type => _type;
public override string Value
{
get => ConvertToString(_value);
set
{
try
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
_target.SetValue(Property, convertedValue);
}
catch { }
}
}
public override string Group
{
get => _group;
}
public override void Update()
{
if (Property.IsDirect)
{
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
}
else
{
var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
if (val != null)
{
SetGroup(IsAttached ? "Attached Properties" : "Properties");
Priority = val.Priority.ToString();
}
else
{
SetGroup(Priority = "Unset");
}
}
}
private void SetGroup(string group)
{
RaiseAndSetIfChanged(ref _group, group, nameof(Group));
}
}
}

57
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -0,0 +1,57 @@
using System.ComponentModel;
using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ClrPropertyViewModel : PropertyViewModel
{
private readonly object _target;
private string _type;
private object _value;
public ClrPropertyViewModel(object o, PropertyInfo property)
{
_target = o;
Property = property;
if (!property.DeclaringType.IsInterface)
{
Name = property.Name;
}
else
{
Name = property.DeclaringType.Name + '.' + property.Name;
}
Update();
}
public PropertyInfo Property { get; }
public override object Key => Name;
public override string Name { get; }
public override string Group => "CLR Properties";
public override string Type => _type;
public override string Value
{
get => ConvertToString(_value);
set
{
try
{
var convertedValue = ConvertFromString(value, Property.PropertyType);
Property.SetValue(_target, convertedValue);
}
catch { }
}
}
public override void Update()
{
var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value));
RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
}
}
}

112
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs

@ -0,0 +1,112 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Diagnostics.Models;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ConsoleViewModel : ViewModelBase
{
private readonly ConsoleContext _context;
private readonly Action<ConsoleContext> _updateContext;
private int _historyIndex = -1;
private string _input;
private bool _isVisible;
private ScriptState<object> _state;
public ConsoleViewModel(Action<ConsoleContext> updateContext)
{
_context = new ConsoleContext(this);
_updateContext = updateContext;
}
public string Input
{
get => _input;
set => RaiseAndSetIfChanged(ref _input, value);
}
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public AvaloniaList<ConsoleHistoryItem> History { get; } = new AvaloniaList<ConsoleHistoryItem>();
public async Task Execute()
{
if (string.IsNullOrWhiteSpace(Input))
{
return;
}
try
{
var options = ScriptOptions.Default
.AddReferences(Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo)));
_updateContext(_context);
if (_state == null)
{
_state = await CSharpScript.RunAsync(Input, options: options, globals: _context);
}
else
{
_state = await _state.ContinueWithAsync(Input);
}
if (_state.ReturnValue != ConsoleContext.NoOutput)
{
History.Add(new ConsoleHistoryItem(Input, _state.ReturnValue ?? "(null)"));
}
}
catch (Exception ex)
{
History.Add(new ConsoleHistoryItem(Input, ex));
}
Input = string.Empty;
_historyIndex = -1;
}
public void HistoryUp()
{
if (History.Count > 0)
{
if (_historyIndex == -1)
{
_historyIndex = History.Count - 1;
}
else if (_historyIndex > 0)
{
--_historyIndex;
}
Input = History[_historyIndex].Input;
}
}
public void HistoryDown()
{
if (History.Count > 0 && _historyIndex >= 0)
{
if (_historyIndex == History.Count - 1)
{
_historyIndex = -1;
Input = string.Empty;
}
else
{
Input = History[++_historyIndex].Input;
}
}
}
public void ToggleVisibility() => IsVisible = !IsVisible;
}
}

179
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private string _propertyFilter;
public ControlDetailsViewModel(IVisual control, string propertyFilter)
{
_control = control;
var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance)
.ThenBy(x => x.Name)
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
_propertyFilter = propertyFilter;
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
view.Filter = FilterProperty;
PropertiesView = view;
if (control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged += ControlPropertyChanged;
}
if (control is AvaloniaObject ao)
{
ao.PropertyChanged += ControlPropertyChanged;
}
}
public DataGridCollectionView PropertiesView { get; }
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
PropertiesView.Refresh();
}
}
}
public AvaloniaPropertyViewModel SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
public void Dispose()
{
if (_control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged -= ControlPropertyChanged;
}
if (_control is AvaloniaObject ao)
{
ao.PropertyChanged -= ControlPropertyChanged;
}
}
private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
{
if (o is AvaloniaObject ao)
{
return AvaloniaPropertyRegistry.Instance.GetRegistered(ao)
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType()))
.Select(x => new AvaloniaPropertyViewModel(ao, x));
}
else
{
return Enumerable.Empty<AvaloniaPropertyViewModel>();
}
}
private IEnumerable<PropertyViewModel> GetClrProperties(object o)
{
foreach (var p in GetClrProperties(o, o.GetType()))
{
yield return p;
}
foreach (var i in o.GetType().GetInterfaces())
{
foreach (var p in GetClrProperties(o, i))
{
yield return p;
}
}
}
private IEnumerable<PropertyViewModel> GetClrProperties(object o, Type t)
{
return t.GetProperties()
.Where(x => x.GetIndexParameters().Length == 0)
.Select(x => new ClrPropertyViewModel(o, x));
}
private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.Property, out var properties))
{
foreach (var property in properties)
{
property.Update();
}
}
}
private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
{
foreach (var property in properties)
{
property.Update();
}
}
}
private bool FilterProperty(object arg)
{
if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property)
{
return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
}
return true;
}
private class PropertyComparer : IComparer<PropertyViewModel>
{
public static PropertyComparer Instance { get; } = new PropertyComparer();
public int Compare(PropertyViewModel x, PropertyViewModel y)
{
var groupX = GroupIndex(x.Group);
var groupY = GroupIndex(y.Group);
if (groupX != groupY)
{
return groupX - groupY;
}
else
{
return string.CompareOrdinal(x.Name, y.Name);
}
}
private int GroupIndex(string group)
{
switch (group)
{
case "Properties": return 0;
case "Attached Properties": return 1;
case "CLR Properties": return 2;
default: return 3;
}
}
}
}
}

2
src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels
InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
}; };
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm) public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm)
: base(null, type.Name) : base(null, type.Name)
{ {
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name) Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)

22
src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@ -3,6 +3,7 @@
using System; using System;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
using Avalonia.Diagnostics.Views;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -12,11 +13,11 @@ namespace Avalonia.Diagnostics.ViewModels
internal class EventTreeNode : EventTreeNodeBase internal class EventTreeNode : EventTreeNodeBase
{ {
private readonly RoutedEvent _event; private readonly RoutedEvent _event;
private readonly EventsViewModel _parentViewModel; private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered; private bool _isRegistered;
private FiredEvent _currentEvent; private FiredEvent _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm) public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
: base(parent, @event.Name) : base(parent, @event.Name)
{ {
Contract.Requires<ArgumentNullException>(@event != null); Contract.Requires<ArgumentNullException>(@event != null);
@ -65,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
if (!_isRegistered || IsEnabled == false) if (!_isRegistered || IsEnabled == false)
return; return;
if (sender is IVisual v && DevTools.BelongsToDevTool(v)) if (sender is IVisual v && BelongsToDevTool(v))
return; return;
var s = sender; var s = sender;
@ -94,5 +95,20 @@ namespace Avalonia.Diagnostics.ViewModels
else else
handler(); handler();
} }
private static bool BelongsToDevTool(IVisual v)
{
while (v != null)
{
if (v is MainView || v is MainWindow)
{
return true;
}
v = v.VisualParent;
}
return false;
}
} }
} }

0
src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs

17
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@ -12,12 +12,12 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class EventsViewModel : ViewModelBase, IDevToolViewModel internal class EventsPageViewModel : ViewModelBase
{ {
private readonly IControl _root; private readonly IControl _root;
private FiredEvent _selectedEvent; private FiredEvent _selectedEvent;
public EventsViewModel(IControl root) public EventsPageViewModel(IControl root)
{ {
_root = root; _root = root;
@ -45,17 +45,4 @@ namespace Avalonia.Diagnostics.ViewModels
RecordedEvents.Clear(); RecordedEvents.Clear();
} }
} }
internal class BoolToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brushes.Green : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
} }

0
src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

6
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -1,6 +1,3 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
@ -17,7 +14,8 @@ namespace Avalonia.Diagnostics.ViewModels
public static LogicalTreeNode[] Create(object control) public static LogicalTreeNode[] Create(object control)
{ {
return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null; var logical = control as ILogical;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
} }
} }
} }

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

@ -0,0 +1,126 @@
using System;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
namespace Avalonia.Diagnostics.ViewModels
{
internal class MainViewModel : ViewModelBase, IDisposable
{
private readonly IControl _root;
private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events;
private ViewModelBase _content;
private int _selectedTab;
private string _focusedControl;
private string _pointerOverElement;
public MainViewModel(IControl root)
{
_root = root;
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_events = new EventsPageViewModel(root);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
Console = new ConsoleViewModel(UpdateConsoleContext);
}
public ConsoleViewModel Console { get; }
public ViewModelBase Content
{
get { return _content; }
private set
{
if (_content is TreePageViewModel oldTree &&
value is TreePageViewModel newTree &&
oldTree?.SelectedNode?.Visual is IControl control)
{
newTree.SelectControl(control);
}
RaiseAndSetIfChanged(ref _content, value);
}
}
public int SelectedTab
{
get { return _selectedTab; }
set
{
_selectedTab = value;
switch (value)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _events;
break;
}
RaisePropertyChanged();
}
}
public string FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
}
public string PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
}
private void UpdateConsoleContext(ConsoleContext context)
{
context.root = _root;
if (Content is TreePageViewModel tree)
{
context.e = tree.SelectedNode?.Visual;
}
}
public void SelectControl(IControl control)
{
var tree = Content as TreePageViewModel;
if (tree != null)
{
tree.SelectControl(control);
}
}
public void Dispose()
{
_logicalTree.Dispose();
_visualTree.Dispose();
}
private void UpdateFocusedControl()
{
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
}
}
}

60
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -0,0 +1,60 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class PropertyViewModel : ViewModelBase
{
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = new[] { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) };
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract string Type { get; }
public abstract string Value { get; set; }
public abstract void Update();
protected static string ConvertToString(object value)
{
if (value is null)
{
return "(null)";
}
var converter = TypeDescriptor.GetConverter(value);
return converter?.ConvertToString(value) ?? value.ToString();
}
protected static object ConvertFromString(string s, Type targetType)
{
var converter = TypeDescriptor.GetConverter(targetType);
if (converter != null && converter.CanConvertFrom(typeof(string)))
{
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
}
else
{
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
if (method != null)
{
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
}
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
if (method != null)
{
return method.Invoke(null, new object[] { s });
}
}
throw new InvalidCastException("Unable to convert value.");
}
}
}

26
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -6,6 +6,8 @@ using System.Collections.Specialized;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -22,22 +24,25 @@ namespace Avalonia.Diagnostics.ViewModels
Type = visual.GetType().Name; Type = visual.GetType().Name;
Visual = visual; Visual = visual;
if (visual is IStyleable styleable) if (visual is IControl control)
{ {
var removed = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => control.DetachedFromLogicalTree += x,
x => control.DetachedFromLogicalTree -= x);
var classesChanged = Observable.FromEventPattern< var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>( NotifyCollectionChangedEventArgs>(
x => styleable.Classes.CollectionChanged += x, x => control.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x) x => control.Classes.CollectionChanged -= x)
.TakeUntil(styleable.StyleDetach); .TakeUntil(removed);
classesChanged.Select(_ => Unit.Default) classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default) .StartWith(Unit.Default)
.Subscribe(_ => .Subscribe(_ =>
{ {
if (styleable.Classes.Count > 0) if (control.Classes.Count > 0)
{ {
Classes = "(" + string.Join(" ", styleable.Classes) + ")"; Classes = "(" + string.Join(" ", control.Classes) + ")";
} }
else else
{ {
@ -55,8 +60,8 @@ namespace Avalonia.Diagnostics.ViewModels
public string Classes public string Classes
{ {
get => _classes; get { return _classes; }
private set => RaiseAndSetIfChanged(ref _classes, value); private set { RaiseAndSetIfChanged(ref _classes, value); }
} }
public IVisual Visual public IVisual Visual
@ -66,8 +71,8 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded public bool IsExpanded
{ {
get => _isExpanded; get { return _isExpanded; }
set => RaiseAndSetIfChanged(ref _isExpanded, value); set { RaiseAndSetIfChanged(ref _isExpanded, value); }
} }
public TreeNode Parent public TreeNode Parent
@ -78,6 +83,7 @@ namespace Avalonia.Diagnostics.ViewModels
public string Type public string Type
{ {
get; get;
private set;
} }
} }
} }

47
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -1,24 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved. using System;
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class TreePageViewModel : ViewModelBase, IDevToolViewModel internal class TreePageViewModel : ViewModelBase, IDisposable
{ {
private TreeNode _selected; private TreeNode _selected;
private ControlDetailsViewModel _details; private ControlDetailsViewModel _details;
private string _propertyFilter;
public TreePageViewModel(TreeNode[] nodes, string name) public TreePageViewModel(TreeNode[] nodes)
{ {
Nodes = nodes; Nodes = nodes;
Name = name;
} }
public string Name { get; }
public TreeNode[] Nodes { get; protected set; } public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode public TreeNode SelectedNode
@ -26,9 +22,16 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selected; get => _selected;
set set
{ {
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
if (RaiseAndSetIfChanged(ref _selected, value)) if (RaiseAndSetIfChanged(ref _selected, value))
{ {
Details = value != null ? new ControlDetailsViewModel(value.Visual) : null; Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) :
null;
} }
} }
} }
@ -36,9 +39,19 @@ namespace Avalonia.Diagnostics.ViewModels
public ControlDetailsViewModel Details public ControlDetailsViewModel Details
{ {
get => _details; get => _details;
private set => RaiseAndSetIfChanged(ref _details, value); private set
{
var oldValue = _details;
if (RaiseAndSetIfChanged(ref _details, value))
{
oldValue?.Dispose();
}
}
} }
public void Dispose() => _details?.Dispose();
public TreeNode FindNode(IControl control) public TreeNode FindNode(IControl control)
{ {
foreach (var node in Nodes) foreach (var node in Nodes)
@ -90,14 +103,16 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
return node; return node;
} }
else
foreach (var child in node.Children)
{ {
var result = FindNode(child, control); foreach (var child in node.Children)
if (result != null)
{ {
return result; var result = FindNode(child, control);
if (result != null)
{
return result;
}
} }
} }

26
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs

@ -1,34 +1,42 @@
// Copyright (c) The Avalonia Project. All rights reserved. using System;
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class ViewModelBase : INotifyPropertyChanged internal class ViewModelBase : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler PropertyChanged; private PropertyChangedEventHandler _propertyChanged;
private List<string> events = new List<string>();
public event PropertyChangedEventHandler PropertyChanged
{
add { _propertyChanged += value; events.Add("added"); }
remove { _propertyChanged -= value; events.Add("removed"); }
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
}
[NotifyPropertyChangedInvocator]
protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null) protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{ {
if (!EqualityComparer<T>.Default.Equals(field, value)) if (!EqualityComparer<T>.Default.Equals(field, value))
{ {
field = value; field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); RaisePropertyChanged(propertyName);
return true; return true;
} }
return false; return false;
} }
[NotifyPropertyChangedInvocator]
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
_propertyChanged?.Invoke(this, e);
} }
} }
} }

5
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs → src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -29,11 +29,12 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public bool IsInTemplate { get; } public bool IsInTemplate { get; private set; }
public static VisualTreeNode[] Create(object control) public static VisualTreeNode[] Create(object control)
{ {
return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null; var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
} }
} }
} }

56
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml

@ -0,0 +1,56 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.Views.ConsoleView">
<UserControl.Styles>
<Style Selector="TextBox.console">
<Setter Property="FontFamily" Value="/Assets/Fonts/SourceSansPro-Regular.ttf"/>
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel Margin="{TemplateBinding Padding}">
<TextBlock DockPanel.Dock="Left" Margin="0,0,4,0">></TextBlock>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}"
SelectionEnd="{TemplateBinding SelectionEnd}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
PasswordChar="{TemplateBinding PasswordChar}"/>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</Style>
</UserControl.Styles>
<DockPanel>
<TextBox Name="input"
Classes="console"
DockPanel.Dock="Bottom"
BorderThickness="0"
Text="{Binding Input}"/>
<ListBox Name="historyList"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,0,0,1"
FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf"
Items="{Binding History}"
VirtualizationMode="None">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<DockPanel>
<TextBlock DockPanel.Dock="Left" Margin="0,0,4,0">></TextBlock>
<TextBlock Text="{Binding Input}"/>
</DockPanel>
<TextBlock Foreground="{Binding Foreground}" Text="{Binding Output}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UserControl>

64
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Specialized;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
internal class ConsoleView : UserControl
{
private readonly ListBox _historyList;
private readonly TextBox _input;
public ConsoleView()
{
this.InitializeComponent();
_historyList = this.FindControl<ListBox>("historyList");
((ILogical)_historyList).LogicalChildren.CollectionChanged += HistoryChanged;
_input = this.FindControl<TextBox>("input");
_input.KeyDown += InputKeyDown;
}
public void FocusInput() => _input.Focus();
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control)
{
DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
}
}
private void InputKeyDown(object sender, KeyEventArgs e)
{
var vm = (ConsoleViewModel)DataContext;
switch (e.Key)
{
case Key.Enter:
vm.Execute();
e.Handled = true;
break;
case Key.Up:
vm.HistoryUp();
_input.CaretIndex = _input.Text.Length;
e.Handled = true;
break;
case Key.Down:
vm.HistoryDown();
_input.CaretIndex = _input.Text.Length;
e.Handled = true;
break;
}
}
}
}

25
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*">
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top"
BorderThickness="0"
Text="{Binding PropertyFilter}"
Watermark="Filter properties"/>
<DataGrid Items="{Binding PropertiesView}"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
</UserControl>

18
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
public ControlDetailsView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

5
src/Avalonia.Diagnostics/Views/EventsView.xaml → src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@ -1,9 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.EventsView"> xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.EventsPageView">
<UserControl.Resources> <UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" /> <conv:BoolToBrushConverter x:Key="boolToBrush" Brush="#d9ffdc"/>
</UserControl.Resources> </UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*"> <Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"

4
src/Avalonia.Diagnostics/Views/EventsView.xaml.cs → src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@ -8,11 +8,11 @@ using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views namespace Avalonia.Diagnostics.Views
{ {
public class EventsView : UserControl internal class EventsPageView : UserControl
{ {
private readonly ListBox _events; private readonly ListBox _events;
public EventsView() public EventsPageView()
{ {
InitializeComponent(); InitializeComponent();
_events = this.FindControl<ListBox>("events"); _events = this.FindControl<ListBox>("events");

55
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -0,0 +1,55 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
x:Class="Avalonia.Diagnostics.Views.MainView">
<Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto">
<Menu>
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}"/>
</MenuItem>
<MenuItem Header="_View">
<MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding Console.IsVisible}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip>
<ContentControl Grid.Row="2"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0"
Content="{Binding Content}"/>
<GridSplitter Name="consoleSplitter" Grid.Row="3" Height="1"
Background="{DynamicResource ThemeControlMidBrush}"
IsVisible="False"/>
<views:ConsoleView Name="console"
Grid.Row="4"
DataContext="{Binding Console}"
IsVisible="{Binding IsVisible}"/>
<Border Grid.Row="5"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0">
<StackPanel Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator Width="8"/>
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
</Border>
</Grid>
</UserControl>

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

Loading…
Cancel
Save