Browse Source

Merge branch 'master' into colorpicker-updates-5

pull/10439/head
robloo 4 years ago
committed by GitHub
parent
commit
ec8fd30e97
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 5
      .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject
  3. 5
      .ncrunch/Generators.Sandbox.v3.ncrunchproject
  4. 5
      Avalonia.Desktop.slnf
  5. 10
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  6. 4
      packages/Avalonia/Avalonia.csproj
  7. 1
      packages/Avalonia/Avalonia.props
  8. 4
      readme.md
  9. 2
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  10. 2
      samples/ControlCatalog/MainView.xaml
  11. 60
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  12. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  13. 2
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  14. 2
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
  15. 4
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  16. 2
      samples/ControlCatalog/Pages/CursorPage.xaml
  17. 8
      samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs
  18. 16
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  19. 8
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  20. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  21. 6
      samples/ControlCatalog/Pages/MenuPage.xaml
  22. 8
      samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
  23. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  24. 2
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  25. 4
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  26. 4
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  27. 2
      samples/ControlCatalog/Pages/TabStripPage.xaml
  28. 2
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  29. 2
      samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
  30. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  31. 11
      samples/IntegrationTestApp/MainWindow.axaml
  32. 6
      samples/IntegrationTestApp/MainWindow.axaml.cs
  33. 24
      samples/IntegrationTestApp/ShowWindowTest.axaml
  34. 2
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  35. 2
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  36. 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  37. 247
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  38. 65
      src/Avalonia.Base/AvaloniaProperty.cs
  39. 10
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  40. 9
      src/Avalonia.Base/CornerRadius.cs
  41. 2
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  42. 2
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  43. 11
      src/Avalonia.Base/Media/BoxShadow.cs
  44. 4
      src/Avalonia.Base/Media/BoxShadows.cs
  45. 2
      src/Avalonia.Base/Media/DrawingGroup.cs
  46. 5
      src/Avalonia.Base/Media/FontFamily.cs
  47. 8
      src/Avalonia.Base/Media/FontManager.cs
  48. 15
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  49. 2
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  50. 5
      src/Avalonia.Base/Media/FormattedText.cs
  51. 2
      src/Avalonia.Base/Media/ImageDrawing.cs
  52. 2
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  53. 2
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  54. 19
      src/Avalonia.Base/PixelRect.cs
  55. 18
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  56. 47
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  57. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  58. 16
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  59. 13
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  60. 16
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  61. 8
      src/Avalonia.Base/Point.cs
  62. 2
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  63. 34
      src/Avalonia.Base/Rect.cs
  64. 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  65. 55
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  66. 2
      src/Avalonia.Base/Rendering/DirtyRects.cs
  67. 4
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  68. 11
      src/Avalonia.Base/Size.cs
  69. 21
      src/Avalonia.Base/StyledProperty.cs
  70. 2
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  71. 10
      src/Avalonia.Base/Thickness.cs
  72. 8
      src/Avalonia.Base/Vector.cs
  73. 63
      src/Avalonia.Base/VisualTree/VisualExtensions.cs
  74. 2
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  75. 2
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  76. 2
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  77. 2
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  78. 5
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  79. 8
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  80. 5
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  81. 4
      src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs
  82. 12
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  83. 3
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  84. 29
      src/Avalonia.Controls/Automation/Peers/ScrollBarAutomationPeer.cs
  85. 2
      src/Avalonia.Controls/BorderVisual.cs
  86. 20
      src/Avalonia.Controls/ContainerClearingEventArgs.cs
  87. 32
      src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs
  88. 26
      src/Avalonia.Controls/ContainerPreparedEventArgs.cs
  89. 103
      src/Avalonia.Controls/ContextMenu.cs
  90. 2
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  91. 2
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  92. 8
      src/Avalonia.Controls/Documents/InlineCollection.cs
  93. 4
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  94. 16
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  95. 165
      src/Avalonia.Controls/ItemCollection.cs
  96. 243
      src/Avalonia.Controls/ItemsControl.cs
  97. 166
      src/Avalonia.Controls/ItemsSourceView.cs
  98. 4
      src/Avalonia.Controls/LayoutTransformControl.cs
  99. 135
      src/Avalonia.Controls/MaskedTextBox.cs
  100. 2
      src/Avalonia.Controls/NativeControlHost.cs

1
.gitignore

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

5
.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject

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

5
.ncrunch/Generators.Sandbox.v3.ncrunchproject

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

5
Avalonia.Desktop.slnf

@ -8,9 +8,9 @@
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
@ -41,6 +41,7 @@
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
@ -63,4 +64,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
]
}
}
}

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

@ -281,10 +281,13 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
case SystemDecorationsFull:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
if (!_isClientAreaExtended) {
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
}
if (currentWindowState == Maximized) {
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
@ -611,7 +614,8 @@ void WindowImpl::UpdateStyle() {
}
bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
bool hasTrafficLights = (_decorations == SystemDecorationsFull) &&
(_isClientAreaExtended ? wantsChrome : true);
NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];

4
packages/Avalonia/Avalonia.csproj

@ -43,6 +43,10 @@
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.props">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>

1
packages/Avalonia/Avalonia.props

@ -6,6 +6,7 @@
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)\AvaloniaBuildTasks.props"/>
<Import Project="$(MSBuildThisFileDirectory)\Avalonia.Generators.props"/>
<!-- Allow loading the AvaloniaVS extension when referencing the Avalonia nuget package -->
<ItemGroup>

4
readme.md

@ -5,6 +5,10 @@
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
# ⚠️ **v11 Update - Pausing community contributions**
for more information see [this](https://github.com/AvaloniaUI/Avalonia/discussions/10599) discussion.
## 📖 About
Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM.

2
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -2,4 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE " />
</manifest>

2
samples/ControlCatalog/MainView.xaml

@ -241,7 +241,7 @@
</ComboBox.Items>
</ComboBox>
<ComboBox HorizontalAlignment="Stretch"
Items="{Binding WindowStates}"
ItemsSource="{Binding WindowStates}"
SelectedItem="{Binding WindowState}" />
</StackPanel>
</Flyout>

60
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@ -1,16 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace ControlCatalog.Pages
{
public partial class ClipboardPage : UserControl
{
private INotificationManager? _notificationManager;
private INotificationManager NotificationManager => _notificationManager
??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
public ClipboardPage()
{
InitializeComponent();
@ -31,7 +38,7 @@ namespace ControlCatalog.Pages
private async void PasteText(object? sender, RoutedEventArgs args)
{
if(Application.Current!.Clipboard is { } clipboard)
if (Application.Current!.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetTextAsync();
}
@ -59,15 +66,45 @@ namespace ControlCatalog.Pages
{
if (Application.Current!.Clipboard is { } clipboard)
{
var files = (ClipboardContent.Text ?? String.Empty)
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
if (files.Length == 0)
var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
var filesPath = (ClipboardContent.Text ?? string.Empty)
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
if (filesPath.Length == 0)
{
return;
}
var dataObject = new DataObject();
dataObject.Set(DataFormats.FileNames, files);
await clipboard.SetDataObjectAsync(dataObject);
List<string> invalidFile = new(filesPath.Length);
List<IStorageFile> files = new(filesPath.Length);
for (int i = 0; i < filesPath.Length; i++)
{
var file = await storageProvider.TryGetFileFromPathAsync(filesPath[i]);
if (file is null)
{
invalidFile.Add(filesPath[i]);
}
else
{
files.Add(file);
}
}
if (invalidFile.Count > 0)
{
NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
}
if (files.Count > 0)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Files, files);
await clipboard.SetDataObjectAsync(dataObject);
NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
}
else
{
NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
}
}
}
@ -75,8 +112,9 @@ namespace ControlCatalog.Pages
{
if (Application.Current!.Clipboard is { } clipboard)
{
var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable<string>;
ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty;
var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
}
}
@ -95,7 +133,7 @@ namespace ControlCatalog.Pages
{
await clipboard.ClearAsync();
}
}
}
}

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.ItemsSource = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}

2
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@ -32,7 +32,7 @@ public partial class CompositionPage : UserControl
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
this.Get<ItemsControl>("Items").Items = CreateColorItems();
this.Get<ItemsControl>("Items").ItemsSource = CreateColorItems();
}

2
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml

@ -61,7 +61,7 @@
<Border.Styles>
<Style Selector="MenuFlyoutPresenter MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>

4
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -51,13 +51,13 @@
<Border.Styles>
<Style Selector="ContextMenu MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</Border.Styles>
<Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}" />
<ContextMenu ItemsSource="{Binding MenuItems}" />
</Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/>
</Border>

2
samples/ControlCatalog/Pages/CursorPage.xaml

@ -8,7 +8,7 @@
<TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
</StackPanel>
<ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8">
<ListBox Grid.Row="1" ItemsSource="{Binding StandardCursors}" Margin="0 8 8 8">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Cursor" Value="{Binding Cursor}" x:DataType="viewModels:StandardCursorModel"/>

8
samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs

@ -133,17 +133,17 @@ namespace ControlCatalog.Pages
// 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world
var translateModifier = context.PushPreTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
var translateModifier = context.PushTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
// now 0,0 refers to the ViewportCenter(X,Y).
var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation);
var rotationModifier = context.PushPreTransform(rotationMatrix);
var rotationModifier = context.PushTransform(rotationMatrix);
// everything is rotated but not scaled
var scaleModifier = context.PushPreTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
var scaleModifier = context.PushTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
var mapPositionModifier = context.PushPreTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
var mapPositionModifier = context.PushTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
// now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates

16
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -106,7 +106,7 @@ namespace ControlCatalog.Pages
Directory = initialDirectory,
InitialFileName = initialFileName
}.ShowAsync(GetWindow());
results.Items = result;
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.Get<Button>("OpenMultipleFiles").Click += async delegate
@ -118,7 +118,7 @@ namespace ControlCatalog.Pages
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.Get<Button>("SaveFile").Click += async delegate
@ -132,7 +132,7 @@ namespace ControlCatalog.Pages
DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
results.Items = new[] { result };
results.ItemsSource = new[] { result };
resultsVisible.IsVisible = result != null;
};
this.Get<Button>("SelectFolder").Click += async delegate
@ -149,7 +149,7 @@ namespace ControlCatalog.Pages
else
{
SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
results.Items = new[] { result };
results.ItemsSource = new[] { result };
resultsVisible.IsVisible = true;
}
};
@ -164,7 +164,7 @@ namespace ControlCatalog.Pages
{
AllowDirectorySelection = true
});
results.Items = result;
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.Get<Button>("DecoratedWindow").Click += delegate
@ -324,15 +324,15 @@ namespace ControlCatalog.Pages
mappedResults.Add("+> " + FullPathOrName(selectedItem));
if (selectedItem is IStorageFolder folder)
{
foreach (var innerItems in await folder.GetItemsAsync())
await foreach (var innerItem in folder.GetItemsAsync())
{
mappedResults.Add("++> " + FullPathOrName(innerItems));
mappedResults.Add("++> " + FullPathOrName(innerItem));
}
}
}
}
results.Items = mappedResults;
results.ItemsSource = mappedResults;
resultsVisible.IsVisible = mappedResults.Any();
}
}

8
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -104,8 +104,12 @@ namespace ControlCatalog.Pages
}
else if (item is IStorageFolder folder)
{
var items = await folder.GetItemsAsync();
contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
var childrenCount = 0;
await foreach (var _ in folder.GetItemsAsync())
{
childrenCount++;
}
contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}";
}
}

2
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -30,7 +30,7 @@
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
</StackPanel>
<ListBox Items="{Binding Items}"
<ListBox ItemsSource="{Binding Items}"
Selection="{Binding Selection}"
DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"

6
samples/ControlCatalog/Pages/MenuPage.xaml

@ -45,11 +45,11 @@
<StackPanel>
<TextBlock Classes="h3" Margin="4 8">Dyanamically generated</TextBlock>
<Menu Items="{Binding MenuItems}">
<Menu ItemsSource="{Binding MenuItems}">
<Menu.Styles>
<Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
@ -68,7 +68,7 @@
<Separator/>
<MenuItem Header="Execu_te Script..." />
<Separator/>
<MenuItem Header="_Recent" Items="{Binding RecentItems}">
<MenuItem Header="_Recent" ItemsSource="{Binding RecentItems}">
<MenuItem.Styles>
<Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>

8
samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs

@ -33,10 +33,10 @@ namespace ControlCatalog.Pages
{
new ContextMenu()
{
Items = new List<MenuItem>
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
Items =
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
}.Open((Control)sender);
}

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -27,7 +27,7 @@
</Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="8" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<ComboBox Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2">
<ComboBox.ItemTemplate>
<DataTemplate>
@ -41,11 +41,11 @@
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
<ComboBox Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
<ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
<ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}"
<ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>

2
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -21,7 +21,7 @@
Margin="5">
<ListBox HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Items="{Binding Items}"/>
ItemsSource="{Binding Items}"/>
</RefreshContainer>
</DockPanel>
</UserControl>

4
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@ -16,14 +16,14 @@
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Type" />
<ComboBox Items="{Binding AvailableSnapPointsType}"
<ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Alignment" />
<ComboBox Items="{Binding AvailableSnapPointsAlignment}"
<ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel>

4
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@ -13,12 +13,12 @@
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
<ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Vertical Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
<ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel>
</StackPanel>

2
samples/ControlCatalog/Pages/TabStripPage.xaml

@ -18,7 +18,7 @@
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Dynamically generated</TextBlock>
<TabStrip Items="{Binding Tabs}">
<TabStrip ItemsSource="{Binding Tabs}">
<TabStrip.Styles>
<Style Selector="TabStripItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>

2
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@ -12,7 +12,7 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
Selector.Items = new[]
Selector.ItemsSource = new[]
{
ThemeVariant.Default,
ThemeVariant.Dark,

2
samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml

@ -53,7 +53,7 @@
<StackPanel Margin="5" Spacing="5" Grid.IsSharedSizeScope="True">
<HeaderedContentControl Header="Select a transition">
<ComboBox Items="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
<ComboBox ItemsSource="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
</HeaderedContentControl>
<HeaderedContentControl Header="Duration">
<NumericUpDown Value="{Binding Duration}" Increment="250" Minimum="100" />

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -11,7 +11,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>

11
samples/IntegrationTestApp/MainWindow.axaml

@ -109,7 +109,7 @@
<StackPanel DockPanel.Dock="Bottom">
<Button Name="ListBoxSelectionClear">Clear Selection</Button>
</StackPanel>
<ListBox Name="BasicListBox" Items="{Binding ListBoxItems}" SelectionMode="Multiple"/>
<ListBox Name="BasicListBox" ItemsSource="{Binding ListBoxItems}" SelectionMode="Multiple"/>
</DockPanel>
</TabItem>
@ -151,6 +151,12 @@
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowSystemDecorations" SelectedIndex="2">
<ComboBoxItem Name="ShowWindowSystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
@ -167,6 +173,9 @@
<TabItem Header="SliderTab">
<Slider VerticalAlignment="Top" Name="Slider" Value="30"/>
</TabItem>
<TabItem Header="ScrollBarTab">
<ScrollBar Name="MyScrollBar" Orientation="Horizontal" AllowAutoHide="False" Width="200" Height="30" Value="20"/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

6
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -68,6 +68,8 @@ namespace IntegrationTestApp
var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
var systemDecorations = this.GetControl<ComboBox>("ShowWindowSystemDecorations");
var extendClientArea = this.GetControl<CheckBox>("ShowWindowExtendClientAreaToDecorationsHint");
var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
var owner = (Window)this.GetVisualRoot()!;
@ -95,6 +97,8 @@ namespace IntegrationTestApp
}
sizeTextBox.Text = string.Empty;
window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false;
window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex;
window.WindowState = (WindowState)stateComboBox.SelectedIndex;
switch (modeComboBox.SelectedIndex)
@ -158,7 +162,7 @@ namespace IntegrationTestApp
var popup = new Popup
{
WindowManagerAddShadowHint = false,
PlacementMode = PlacementMode.AnchorAndGravity,
Placement = PlacementMode.AnchorAndGravity,
PlacementAnchor = PopupAnchor.Top,
PlacementGravity = PopupGravity.Bottom,
Width= 200,

24
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -6,7 +6,7 @@
x:DataType="Window"
Title="Show Window Test">
<integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
@ -35,13 +35,25 @@
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="8">SystemDecorations</Label>
<ComboBox Name="CurrentSystemDecorations" Grid.Column="1" Grid.Row="8" SelectedIndex="{Binding SystemDecorations}">
<ComboBoxItem Name="SystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<CheckBox Name="CurrentExtendClientAreaToDecorationsHint" Grid.ColumnSpan="2" Grid.Row="9"
IsChecked="{Binding ExtendClientAreaToDecorationsHint}">
ExtendClientAreaToDecorationsHint
</CheckBox>
<Label Grid.Column="0" Grid.Row="10">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="10" IsReadOnly="True" />
<Label Grid.Row="9" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
<Button Name="HideButton" Grid.Row="12" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</integrationTestApp:MeasureBorder>

2
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -29,7 +29,7 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context)
{
using (var ctx = _bitmap.CreateDrawingContext())
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
using (ctx.PushTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100)))
{

2
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@ -57,7 +57,7 @@ namespace ControlSamples
{
if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
{
_splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation);
_splitView.SetCurrentValue(SplitView.IsPaneOpenProperty, false);
}
}
}

1
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.DocumentFile" Version="1.0.1.16" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
</ItemGroup>
<ItemGroup>

247
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -8,7 +8,10 @@ using System.Threading.Tasks;
using Android;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Provider;
using Android.Webkit;
using AndroidX.DocumentFile.Provider;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using Java.Lang;
@ -22,20 +25,25 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
private Activity? _activity;
private readonly bool _needsExternalFilesPermission;
private readonly AndroidStorageFolder? _parent;
private readonly AndroidUri? _permissionRoot;
protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission)
protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null)
{
_activity = activity;
_needsExternalFilesPermission = needsExternalFilesPermission;
_parent = parent;
_permissionRoot = permissionRoot ?? parent?.Uri ?? Uri;
Uri = uri;
}
internal AndroidUri Uri { get; }
internal AndroidUri Uri { get; set; }
protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
?? Document?.Name
?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty;
public Uri Path => new(Uri.ToString()!);
@ -92,6 +100,23 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return null;
}
if(_parent != null)
{
return _parent;
}
var document = Document;
if (document == null)
{
return null;
}
if(document.ParentFile != null)
{
return new AndroidStorageFolder(Activity, document.ParentFile.Uri, true);
}
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
@ -118,12 +143,88 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
_activity = null;
}
internal DocumentFile? Document
{
get
{
if (this is AndroidStorageFile)
{
return DocumentFile.FromSingleUri(Activity, Uri);
}
else
{
return DocumentFile.FromTreeUri(Activity, Uri);
}
}
}
internal AndroidUri? PermissionRoot => _permissionRoot;
public abstract Task DeleteAsync();
public abstract Task<IStorageItem?> MoveAsync(IStorageFolder destination);
}
internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission)
public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) : base(activity, uri, needsExternalFilesPermission, parent, permissionRoot)
{
}
public async Task<IStorageFile?> CreateFileAsync(string name)
{
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
var newFile = Document.CreateFile(mimeType, name);
if(newFile == null)
{
return null;
}
return new AndroidStorageFile(Activity, newFile.Uri, this);
}
public async Task<IStorageFolder?> CreateFolderAsync(string name)
{
var newFolder = Document?.CreateDirectory(name);
if (newFolder == null)
{
return null;
}
return new AndroidStorageFolder(Activity, newFolder.Uri, false, this, PermissionRoot);
}
public override async Task DeleteAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return;
}
if (Activity != null)
{
await DeleteContents(this);
}
async Task DeleteContents(AndroidStorageFolder storageFolder)
{
await foreach (var file in storageFolder.GetItemsAsync())
{
if(file is AndroidStorageFolder folder)
{
await DeleteContents(folder);
}
else if(file is AndroidStorageFile storageFile)
{
await storageFile.DeleteAsync();
}
}
DocumentFile.FromTreeUri(Activity, storageFolder.Uri)?.Delete();
}
}
public override Task<StorageItemProperties> GetBasicPropertiesAsync()
@ -131,22 +232,22 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
return Task.FromResult(new StorageItemProperties());
}
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return Array.Empty<IStorageItem>();
yield break;
}
List<IStorageItem> files = new List<IStorageItem>();
var contentResolver = Activity.ContentResolver;
if (contentResolver == null)
{
return files;
yield break;
}
var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri));
var root = PermissionRoot ?? Uri;
var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri);
var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId);
var projection = new[]
{
@ -162,19 +263,55 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
var mime = cursor.GetString(1);
var id = cursor.GetString(0);
var uri = DocumentsContract.BuildDocumentUriUsingTree(Uri!, id);
bool isDirectory = mime == DocumentsContract.Document.MimeTypeDir;
var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id);
if (uri == null)
{
continue;
}
files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) :
new AndroidStorageFile(Activity, uri));
yield return isDirectory ? new AndroidStorageFolder(Activity, uri, false, this, root) :
new AndroidStorageFile(Activity, uri, this, root);
}
}
}
public override async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (Activity != null)
{
return await MoveRecursively(this, (AndroidStorageFolder)destination);
}
return files;
}
return null;
async Task<AndroidStorageFolder?> MoveRecursively(AndroidStorageFolder storageFolder, AndroidStorageFolder destination)
{
destination = await destination.CreateFolderAsync(storageFolder.Name) as AndroidStorageFolder;
if (destination == null)
{
return null;
}
await foreach (var file in storageFolder.GetItemsAsync())
{
if (file is AndroidStorageFolder folder)
{
await MoveRecursively(folder, destination);
}
else if (file is AndroidStorageFile)
{
await file.MoveAsync(destination);
}
}
await storageFolder.DeleteAsync();
return destination;
}
}
}
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
@ -186,14 +323,14 @@ internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
}
public override string Name { get; }
}
}
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{
public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
public AndroidStorageFile(Activity activity, AndroidUri uri, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) : base(activity, uri, false, parent, permissionRoot)
{
}
public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
@ -317,4 +454,76 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified));
}
public override async Task DeleteAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return;
}
if (Activity != null)
{
DocumentsContract.DeleteDocument(Activity.ContentResolver!, Uri);
}
}
public override async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
if (Activity != null && destination is AndroidStorageFolder storageFolder)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
{
try
{
var uri = DocumentsContract.MoveDocument(Activity.ContentResolver!, Uri, ((await GetParentAsync()) as AndroidStorageFolder)!.Uri, storageFolder.Document!.Uri);
return new AndroidStorageFile(Activity, uri, storageFolder);
}
catch (Exception ex)
{
// There are many reason why DocumentContract will fail to move a file. We fallback to copying.
return await MoveFileByCopy();
}
}
else
{
return await MoveFileByCopy();
}
}
async Task<AndroidStorageFile?> MoveFileByCopy()
{
var newFile = await storageFolder.CreateFileAsync(Name) as AndroidStorageFile;
try
{
if (newFile != null)
{
using var input = await OpenReadAsync();
using var output = await newFile.OpenWriteAsync();
await input.CopyToAsync(output);
await DeleteAsync();
return new AndroidStorageFile(Activity, newFile.Uri, storageFolder);
}
}
catch
{
newFile?.DeleteAsync();
}
return null;
}
return null;
}
}

65
src/Avalonia.Base/AvaloniaProperty.cs

@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia
@ -20,12 +19,20 @@ namespace Avalonia
public static readonly object UnsetValue = new UnsetValueType();
private static int s_nextId;
/// <summary>
/// Provides a metadata object for types which have no metadata of their own.
/// </summary>
private readonly AvaloniaPropertyMetadata _defaultMetadata;
/// <summary>
/// Provides a fast path when the property has no metadata overrides.
/// </summary>
private KeyValuePair<Type, AvaloniaPropertyMetadata>? _singleMetadata;
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata;
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new Dictionary<Type, AvaloniaPropertyMetadata>();
private bool _hasMetadataOverrides;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
/// </summary>
@ -57,7 +64,8 @@ namespace Avalonia
Id = s_nextId++;
_metadata.Add(ownerType, metadata ?? throw new ArgumentNullException(nameof(metadata)));
_defaultMetadata = metadata;
_defaultMetadata = metadata.GenerateTypeSafeMetadata();
_singleMetadata = new(ownerType, metadata);
}
/// <summary>
@ -80,9 +88,6 @@ namespace Avalonia
Id = source.Id;
_defaultMetadata = source._defaultMetadata;
// Properties that have different owner can't use fast path for metadata.
_hasMetadataOverrides = true;
if (metadata != null)
{
_metadata.Add(ownerType, metadata);
@ -453,33 +458,14 @@ namespace Avalonia
}
/// <summary>
/// Gets the property metadata for the specified type.
/// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>
/// The property metadata.
/// </returns>
public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject
{
return GetMetadata(typeof(T));
}
/// <typeparam name="T">The type for which to retrieve metadata.</typeparam>
public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject => GetMetadata(typeof(T));
/// <summary>
/// Gets the property metadata for the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The property metadata.
/// </returns>
public AvaloniaPropertyMetadata GetMetadata(Type type)
{
if (!_hasMetadataOverrides)
{
return _defaultMetadata;
}
return GetMetadataWithOverrides(type);
}
/// <inheritdoc cref="GetMetadata{T}"/>
/// <param name="type">The type for which to retrieve metadata.</param>
public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type);
/// <summary>
/// Checks whether the <paramref name="value"/> is valid for the property.
@ -578,7 +564,7 @@ namespace Avalonia
_metadata.Add(type, metadata);
_metadataCache.Clear();
_hasMetadataOverrides = true;
_singleMetadata = null;
}
protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged();
@ -595,7 +581,12 @@ namespace Avalonia
return result;
}
Type? currentType = type;
if (_singleMetadata is { } singleMetadata)
{
return _metadataCache[type] = singleMetadata.Key.IsAssignableFrom(type) ? singleMetadata.Value : _defaultMetadata;
}
var currentType = type;
while (currentType != null)
{
@ -609,13 +600,11 @@ namespace Avalonia
currentType = currentType.BaseType;
}
_metadataCache[type] = _defaultMetadata;
return _defaultMetadata;
return _metadataCache[type] = _defaultMetadata;
}
bool IPropertyInfo.CanGet => true;
bool IPropertyInfo.CanSet => true;
bool IPropertyInfo.CanSet => !IsReadOnly;
object? IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this);
void IPropertyInfo.Set(object target, object? value) => ((AvaloniaObject)target).SetValue(this, value);
}

10
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@ -5,7 +5,7 @@ namespace Avalonia
/// <summary>
/// Base class for avalonia property metadata.
/// </summary>
public class AvaloniaPropertyMetadata
public abstract class AvaloniaPropertyMetadata
{
private BindingMode _defaultBindingMode;
@ -61,5 +61,13 @@ namespace Avalonia
EnableDataValidation ??= baseMetadata.EnableDataValidation;
}
/// <summary>
/// Gets a copy of this object configured for use with any owner type.
/// </summary>
/// <remarks>
/// For example, delegates which receive the owner object should be removed.
/// </remarks>
public abstract AvaloniaPropertyMetadata GenerateTypeSafeMetadata();
}
}

9
src/Avalonia.Base/CornerRadius.cs

@ -60,15 +60,6 @@ namespace Avalonia
/// </summary>
public double BottomLeft { get; }
/// <summary>
/// Gets a value indicating whether the instance has default values (all corner radii are set to 0).
/// </summary>
public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary>
/// Gets a value indicating whether all corner radii are equal.
/// </summary>

2
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@ -45,5 +45,7 @@ namespace Avalonia
UnsetValue ??= src.UnsetValue;
}
}
public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation);
}
}

2
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@ -180,7 +180,7 @@ namespace Avalonia.Input.GestureRecognizers
internal Velocity GetVelocity()
{
var estimate = GetVelocityEstimate();
if (estimate == null || estimate.PixelsPerSecond.IsDefault)
if (estimate == null || estimate.PixelsPerSecond == default(Vector))
{
return new Velocity(Vector.Zero);
}

11
src/Avalonia.Base/Media/BoxShadow.cs

@ -45,15 +45,6 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Gets a value indicating whether the instance has default values.
/// </summary>
public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
private readonly static char[] s_Separator = new char[] { ' ', '\t' };
struct ArrayReader
@ -89,7 +80,7 @@ namespace Avalonia.Media
{
var sb = StringBuilderCache.Acquire();
if (IsDefault)
if (this == default)
{
return "none";
}

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

@ -21,7 +21,7 @@ namespace Avalonia.Media
{
_first = shadow;
_list = null;
Count = _first.IsDefault ? 0 : 1;
Count = _first == default ? 0 : 1;
}
public BoxShadows(BoxShadow first, BoxShadow[] rest)
@ -120,7 +120,7 @@ namespace Avalonia.Media
get
{
foreach(var boxShadow in this)
if (!boxShadow.IsDefault && boxShadow.IsInset)
if (boxShadow != default && boxShadow.IsInset)
return true;
return false;
}

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

@ -73,7 +73,7 @@ namespace Avalonia.Media
{
var bounds = GetBounds();
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)

5
src/Avalonia.Base/Media/FontFamily.cs

@ -79,11 +79,6 @@ namespace Avalonia.Media
/// <remarks>Key is only used for custom fonts.</remarks>
public FontFamilyKey? Key { get; }
/// <summary>
/// Returns <c>True</c> if this instance is the system's default.
/// </summary>
public bool IsDefault => Name.Equals(DefaultFontFamilyName);
/// <summary>
/// Implicit conversion of string to FontFamily
/// </summary>

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

@ -119,7 +119,9 @@ namespace Avalonia.Media
}
}
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
var familyName = fontFamily.FamilyNames.PrimaryFamilyName.ToUpperInvariant();
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(familyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
@ -133,13 +135,13 @@ namespace Avalonia.Media
foreach (var familyName in fontFamily.FamilyNames)
{
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
if (SystemFonts.TryGetGlyphTypeface(familyName.ToUpperInvariant(), typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
}
}
return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
return TryGetGlyphTypeface(new Typeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
/// <summary>

15
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
@ -43,11 +42,13 @@ namespace Avalonia.Media.Fonts
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
{
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
var familyName = glyphTypeface.FamilyName.ToUpperInvariant();
if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
if (_glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
{
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
}
@ -86,6 +87,8 @@ namespace Avalonia.Media.Fonts
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
familyName = familyName.ToUpperInvariant();
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
@ -101,9 +104,11 @@ namespace Avalonia.Media.Fonts
{
var fontFamily = _fontFamilies[i];
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
if (fontFamily.Name.ToUpperInvariant().StartsWith(familyName.ToUpperInvariant()))
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
familyName = fontFamily.Name.ToUpperInvariant();
if (_glyphTypefaceCache.TryGetValue(familyName, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;

2
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -42,6 +42,8 @@ namespace Avalonia.Media.Fonts
familyName = _fontManager.DefaultFontFamilyName;
}
familyName = familyName.ToUpperInvariant();
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))

5
src/Avalonia.Base/Media/FormattedText.cs

@ -1393,10 +1393,11 @@ namespace Avalonia.Media
}
}
if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault)
if (accumulatedBounds?.PlatformImpl == null ||
(accumulatedBounds.PlatformImpl.Bounds.Width == 0 && accumulatedBounds.PlatformImpl.Bounds.Height == 0))
{
return null;
}
}
return accumulatedBounds;
}

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

@ -42,7 +42,7 @@ namespace Avalonia.Media
var imageSource = ImageSource;
var rect = Rect;
if (imageSource is object && !rect.IsDefault)
if (imageSource is object && (rect.Width != 0 || rect.Height != 0))
{
context.DrawImage(imageSource, rect);
}

2
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging
{
if (Source is not IBitmap bmp)
return default;
if (SourceRect.IsDefault)
if (SourceRect.Width == 0 && SourceRect.Height == 0)
return Source.Size;
return SourceRect.Size.ToSizeWithDpi(bmp.Dpi);
}

2
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
/// allowing the template to inherit the data type from the Items collection binding.
/// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class InheritDataTypeFromItemsAttribute : Attribute
{
/// <summary>

19
src/Avalonia.Base/PixelRect.cs

@ -9,12 +9,6 @@ namespace Avalonia
/// </summary>
public readonly struct PixelRect : IEquatable<PixelRect>
{
/// <summary>
/// An empty rectangle.
/// </summary>
[Obsolete("Use the default keyword instead.")]
public static readonly PixelRect Empty = default;
/// <summary>
/// Initializes a new instance of the <see cref="PixelRect"/> structure.
/// </summary>
@ -133,15 +127,6 @@ namespace Avalonia
/// </summary>
public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2));
/// <summary>
/// Gets a value indicating whether the instance has default values (the rectangle is empty).
/// </summary>
public bool IsDefault => Width == 0 && Height == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary>
/// Checks for equality between two <see cref="PixelRect"/>s.
/// </summary>
@ -295,11 +280,11 @@ namespace Avalonia
/// <returns>The union.</returns>
public PixelRect Union(PixelRect rect)
{
if (IsDefault)
if (Width == 0 && Height == 0)
{
return rect;
}
else if (rect.IsDefault)
else if (rect.Width == 0 && rect.Height == 0)
{
return this;
}

18
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -92,4 +92,22 @@ internal class BclStorageFile : IStorageBookmarkFile
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async Task DeleteAsync()
{
FileInfo.Delete();
}
public async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (destination is BclStorageFolder storageFolder)
{
var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, FileInfo.Name);
FileInfo.MoveTo(newPath);
return new BclStorageFile(new FileInfo(newPath));
}
return null;
}
}

47
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -57,14 +57,16 @@ internal class BclStorageFolder : IStorageBookmarkFolder
return Task.FromResult<IStorageFolder?>(null);
}
public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
{
var items = DirectoryInfo.GetDirectories()
var items = DirectoryInfo.EnumerateDirectories()
.Select(d => (IStorageItem)new BclStorageFolder(d))
.Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f)))
.ToArray();
.Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f)));
return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
foreach (var item in items)
{
yield return item;
}
}
public virtual Task<string?> SaveBookmarkAsync()
@ -92,4 +94,39 @@ internal class BclStorageFolder : IStorageBookmarkFolder
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async Task DeleteAsync()
{
DirectoryInfo.Delete(true);
}
public async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (destination is BclStorageFolder storageFolder)
{
var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, DirectoryInfo.Name);
DirectoryInfo.MoveTo(newPath);
return new BclStorageFolder(new DirectoryInfo(newPath));
}
return null;
}
public async Task<IStorageFile?> CreateFileAsync(string name)
{
var fileName = System.IO.Path.Combine(DirectoryInfo.FullName, name);
var newFile = new FileInfo(fileName);
using var stream = newFile.Create();
return new BclStorageFile(newFile);
}
public async Task<IStorageFolder?> CreateFolderAsync(string name)
{
var newFolder = DirectoryInfo.CreateSubdirectory(name);
return new BclStorageFolder(newFolder);
}
}

17
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
@ -23,7 +24,7 @@ internal static class StorageProviderHelpers
return null;
}
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)
@ -35,6 +36,20 @@ internal static class StorageProviderHelpers
return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
}
public static bool TryFilePathToUri(string path, [NotNullWhen(true)] out Uri? uri)
{
try
{
uri = FilePathToUri(path);
return true;
}
catch
{
uri = null;
return false;
}
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);

16
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@ -16,5 +16,19 @@ public interface IStorageFolder : IStorageItem
/// <returns>
/// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an <see cref="IStorageItem"/> implementation object.
/// </returns>
Task<IReadOnlyList<IStorageItem>> GetItemsAsync();
IAsyncEnumerable<IStorageItem> GetItemsAsync();
/// <summary>
/// Creates a file with specified name as a child of the current storage folder
/// </summary>
/// <param name="name">The display name</param>
/// <returns>A new <see cref="IStorageFile"/> pointing to the moved file. If not null, the current storage item becomes invalid</returns>
Task<IStorageFile?> CreateFileAsync(string name);
/// <summary>
/// Creates a folder with specified name as a child of the current storage folder
/// </summary>
/// <param name="name">The display name</param>
/// <returns>A new <see cref="IStorageFolder"/> pointing to the moved file. If not null, the current storage item becomes invalid</returns>
Task<IStorageFolder?> CreateFolderAsync(string name);
}

13
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@ -50,4 +50,17 @@ public interface IStorageItem : IDisposable
/// Gets the parent folder of the current storage item.
/// </summary>
Task<IStorageFolder?> GetParentAsync();
/// <summary>
/// Deletes the current storage item and it's contents
/// </summary>
/// <returns></returns>
Task DeleteAsync();
/// <summary>
/// Moves the current storage item and it's contents to a <see cref="IStorageFolder"/>
/// </summary>
/// <param name="destination">The <see cref="IStorageFolder"/> to move the item into</param>
/// <returns></returns>
Task<IStorageItem?> MoveAsync(IStorageFolder destination);
}

16
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -16,8 +16,13 @@ public static class StorageProviderExtensions
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
}
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
if (StorageProviderHelpers.TryFilePathToUri(filePath, out var uri))
{
return provider.TryGetFileFromPathAsync(uri);
}
return Task.FromResult<IStorageFile?>(null);
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
@ -29,7 +34,12 @@ public static class StorageProviderExtensions
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
}
return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
if (StorageProviderHelpers.TryFilePathToUri(folderPath, out var uri))
{
return provider.TryGetFolderFromPathAsync(uri);
}
return Task.FromResult<IStorageFolder?>(null);
}
/// <summary>

8
src/Avalonia.Base/Point.cs

@ -288,13 +288,5 @@ namespace Avalonia
x = this._x;
y = this._y;
}
/// <summary>
/// Gets a value indicating whether the X and Y coordinates are zero.
/// </summary>
public bool IsDefault
{
get { return (_x == 0) && (_y == 0); }
}
}
}

2
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -31,7 +31,7 @@ namespace Avalonia.PropertyStore
var value = inherited is null ? _metadata.DefaultValue : inherited.Value;
if (property.HasCoercion && _metadata.CoerceValue is { } coerce)
if (_metadata.CoerceValue is { } coerce)
{
_uncommon = new()
{

34
src/Avalonia.Base/Rect.cs

@ -16,12 +16,6 @@ namespace Avalonia
Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
}
/// <summary>
/// An empty rectangle.
/// </summary>
[Obsolete("Use the default keyword instead.")]
public static readonly Rect Empty = default;
/// <summary>
/// The X position.
/// </summary>
@ -170,17 +164,6 @@ namespace Avalonia
/// </summary>
public Point Center => new Point(_x + (_width / 2), _y + (_height / 2));
/// <summary>
/// Gets a value indicating whether the instance has default values (the rectangle is empty).
/// </summary>
// ReSharper disable CompareOfFloatsByEqualityOperator
public bool IsDefault => _width == 0 && _height == 0;
// ReSharper restore CompareOfFloatsByEqualityOperator
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary>
/// Checks for equality between two <see cref="Rect"/>s.
/// </summary>
@ -517,19 +500,18 @@ namespace Avalonia
return rect;
}
/// <summary>
/// Gets the union of two rectangles.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>The union.</returns>
public Rect Union(Rect rect)
/// <summary>
/// Gets the union of two rectangles.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>The union.</returns>
public Rect Union(Rect rect)
{
if (IsDefault)
if (Width == 0 && Height == 0)
{
return rect;
}
else if (rect.IsDefault)
else if (rect.Width == 0 && rect.Height == 0)
{
return this;
}

12
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -130,8 +130,8 @@ namespace Avalonia.Rendering.Composition.Server
}
_renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
if(_dirtyRect.IsDefault && !_redrawRequested)
if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested)
return;
Revision++;
@ -163,7 +163,7 @@ namespace Avalonia.Rendering.Composition.Server
_dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
}
if (!_dirtyRect.IsDefault)
if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0)
{
using (var context = _layer.CreateDrawingContext())
{
@ -260,7 +260,7 @@ namespace Avalonia.Rendering.Composition.Server
public void AddDirtyRect(Rect rect)
{
if(rect.IsDefault)
if (rect.Width == 0 && rect.Height == 0)
return;
var snapped = SnapToDevicePixels(rect, Scaling);
DebugEvents?.RectInvalidated(rect);
@ -275,7 +275,7 @@ namespace Avalonia.Rendering.Composition.Server
public void Dispose()
{
if(_disposed)
if (_disposed)
return;
_disposed = true;
using (_compositor.RenderInterface.EnsureCurrent())
@ -302,7 +302,7 @@ namespace Avalonia.Rendering.Composition.Server
{
if (_attachedVisuals.Remove(visual) && IsEnabled)
visual.Deactivate();
if(visual.IsVisibleInFrame)
if (visual.IsVisibleInFrame)
AddDirtyRect(visual.TransformedOwnContentBounds);
}

55
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -23,21 +23,20 @@ namespace Avalonia.Rendering.Composition.Server
private bool _isBackface;
private Rect? _transformedClipBounds;
private Rect _combinedTransformedClipBounds;
protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
}
public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
if(Visible == false || IsVisibleInFrame == false)
if (Visible == false || IsVisibleInFrame == false)
return;
if(Opacity == 0)
if (Opacity == 0)
return;
currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
if(currentTransformedClip.IsDefault)
if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
return;
Root!.RenderedVisuals++;
@ -61,7 +60,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null)
canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null)
if (OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
RenderCore(canvas, currentTransformedClip);
@ -78,12 +77,12 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopClip();
if (AdornedVisual != null && AdornerIsClipped)
canvas.PopClip();
if(Opacity != 1)
if (Opacity != 1)
canvas.PopOpacity();
}
protected virtual bool HandlesClipToBounds => false;
private ReadbackData _readback0, _readback1, _readback2;
/// <summary>
@ -98,17 +97,17 @@ namespace Avalonia.Rendering.Composition.Server
return ref _readback1;
return ref _readback2;
}
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; }
public virtual void Update(ServerCompositionTarget root)
{
if(Parent == null && Root == null)
if (Parent == null && Root == null)
return;
var wasVisible = IsVisibleInFrame;
// Calculate new parent-relative transform
if (_combinedTransformDirty)
{
@ -122,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity;
var newTransform = CombinedTransformMatrix * parentTransform;
// Check if visual was moved and recalculate face orientation
var positionChanged = false;
if (GlobalTransformMatrix != newTransform)
@ -134,23 +133,23 @@ namespace Avalonia.Rendering.Composition.Server
var oldTransformedContentBounds = TransformedOwnContentBounds;
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
if (_parent?.IsDirtyComposition == true)
{
IsDirtyComposition = true;
_isDirtyForUpdate = true;
}
var invalidateOldBounds = _isDirtyForUpdate;
var invalidateNewBounds = _isDirtyForUpdate;
GlobalTransformMatrix = newTransform;
var ownBounds = OwnContentBounds;
if (ownBounds != _oldOwnContentBounds || positionChanged)
{
_oldOwnContentBounds = ownBounds;
if (ownBounds.IsDefault)
if (ownBounds.Width == 0 && ownBounds.Height == 0)
TransformedOwnContentBounds = default;
else
TransformedOwnContentBounds =
@ -171,16 +170,16 @@ namespace Avalonia.Rendering.Composition.Server
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
&& Visible
&& !_isBackface
&& !_combinedTransformedClipBounds.IsDefault;
&& (_combinedTransformedClipBounds.Width != 0 || _combinedTransformedClipBounds.Height != 0);
IsVisibleInFrame = IsHitTestVisibleInFrame
&& _parent?.IsVisibleInFrame != false
@ -213,11 +212,11 @@ namespace Avalonia.Rendering.Composition.Server
void AddDirtyRect(Rect rc)
{
if(rc == default)
if (rc == default)
return;
Root?.AddDirtyRect(rc);
}
/// <summary>
/// Data that can be read from the UI thread
/// </summary>
@ -228,7 +227,7 @@ namespace Avalonia.Rendering.Composition.Server
public long TargetId;
public bool Visible;
}
partial void DeserializeChangesExtra(BatchStreamReader c)
{
ValuesInvalidated();
@ -245,9 +244,8 @@ namespace Avalonia.Rendering.Composition.Server
protected virtual void OnDetachedFromRoot(ServerCompositionTarget target)
{
}
partial void OnRootChanged()
{
if (Root != null)
@ -256,12 +254,11 @@ namespace Avalonia.Rendering.Composition.Server
OnAttachedToRoot(Root);
}
}
protected virtual void OnAttachedToRoot(ServerCompositionTarget target)
{
}
protected override void ValuesInvalidated()
{
_isDirtyForUpdate = true;
@ -274,6 +271,4 @@ namespace Avalonia.Rendering.Composition.Server
public Rect TransformedOwnContentBounds { get; set; }
public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
}
}

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

@ -30,7 +30,7 @@ namespace Avalonia.Rendering
/// </remarks>
public void Add(Rect rect)
{
if (!rect.IsDefault)
if (rect.Width != 0 || rect.Height != 0)
{
for (var i = 0; i < _rects.Count; ++i)
{

4
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -83,7 +83,7 @@ namespace Avalonia.Rendering
}
}
using (context.PushPostTransform(m))
using (context.PushTransform(m))
using (context.PushOpacity(opacity, bounds))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
@ -95,7 +95,7 @@ namespace Avalonia.Rendering
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
using (context.PushTransformContainer())
using (context.PushTransform(Matrix.Identity))
{
visual.Render(context);

11
src/Avalonia.Base/Size.cs

@ -27,12 +27,6 @@ namespace Avalonia
/// </summary>
public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);
/// <summary>
/// A size representing zero.
/// </summary>
[Obsolete("Use the default keyword instead.")]
public static readonly Size Empty = new Size(0, 0);
/// <summary>
/// The width.
/// </summary>
@ -306,10 +300,5 @@ namespace Avalonia
width = this._width;
height = this._height;
}
/// <summary>
/// Gets a value indicating whether the Width and Height values are zero.
/// </summary>
public bool IsDefault => (_width == 0) && (_height == 0);
}
}

21
src/Avalonia.Base/StyledProperty.cs

@ -18,7 +18,10 @@ namespace Avalonia
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="validate">
/// <para>A method which returns "false" for values that are never valid for this property.</para>
/// <para>This method is not part of the property's metadata and so cannot be changed after registration.</para>
/// </param>
/// <param name="notifying">A <see cref="AvaloniaProperty.Notifying"/> callback.</param>
public StyledProperty(
string name,
@ -31,7 +34,6 @@ namespace Avalonia
{
Inherits = inherits;
ValidateValue = validate;
HasCoercion |= metadata.CoerceValue != null;
if (validate?.Invoke(metadata.DefaultValue) == false)
{
@ -41,16 +43,10 @@ namespace Avalonia
}
/// <summary>
/// Gets the value validation callback for the property.
/// A method which returns "false" for values that are never valid for this property.
/// </summary>
public Func<TValue, bool>? ValidateValue { get; }
/// <summary>
/// Gets a value indicating whether this property has any value coercion callbacks defined
/// in its metadata.
/// </summary>
internal bool HasCoercion { get; private set; }
/// <summary>
/// Registers the property on another type.
/// </summary>
@ -127,10 +123,7 @@ namespace Avalonia
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject
{
base.OverrideMetadata(typeof(T), metadata);
}
public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject => OverrideMetadata(typeof(T), metadata);
/// <summary>
/// Overrides the metadata for the property on the specified type.
@ -148,8 +141,6 @@ namespace Avalonia
}
}
HasCoercion |= metadata.CoerceValue != null;
base.OverrideMetadata(type, metadata);
}

2
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -58,5 +58,7 @@ namespace Avalonia
}
}
}
public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new StyledPropertyMetadata<TValue>(DefaultValue, DefaultBindingMode, enableDataValidation: EnableDataValidation ?? false);
}
}

10
src/Avalonia.Base/Thickness.cs

@ -97,10 +97,6 @@ namespace Avalonia
/// </summary>
public double Bottom => _bottom;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary>
/// Gets a value indicating whether all sides are equal.
/// </summary>
@ -293,11 +289,5 @@ namespace Avalonia
right = this._right;
bottom = this._bottom;
}
/// <summary>
/// Gets a value indicating whether the instance has default values
/// (the left, top, right and bottom values are zero).
/// </summary>
public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0);
}
}

8
src/Avalonia.Base/Vector.cs

@ -360,13 +360,5 @@ namespace Avalonia
x = this._x;
y = this._y;
}
/// <summary>
/// Gets a value indicating whether the X and Y components are zero.
/// </summary>
public bool IsDefault
{
get { return (_x == 0) && (_y == 0); }
}
}
}

63
src/Avalonia.Base/VisualTree/VisualExtensions.cs

@ -204,6 +204,69 @@ namespace Avalonia.VisualTree
}
}
public static TransformedBounds? GetTransformedBounds(this Visual visual)
{
Rect clip = default;
var transform = Matrix.Identity;
bool Visit(Visual visual)
{
if (!visual.IsVisible)
return false;
// The visual's bounds in local coordinates.
var bounds = new Rect(visual.Bounds.Size);
// If the visual has no parent, we've reached the root. We start the clip
// rectangle with these bounds.
if (visual.GetVisualParent() is not { } parent)
{
clip = bounds;
return true;
}
// Otherwise recurse until the root visual is found, exiting early if one of the
// ancestors is invisible.
if (!Visit(parent))
return false;
// Calculate the transform for this control from its offset and render transform.
var renderTransform = Matrix.Identity;
if (visual.HasMirrorTransform)
{
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
renderTransform *= mirrorMatrix;
}
if (visual.RenderTransform != null)
{
var origin = visual.RenderTransformOrigin.ToPixels(bounds.Size);
var offset = Matrix.CreateTranslation(origin);
var finalTransform = (-offset) * visual.RenderTransform.Value * offset;
renderTransform *= finalTransform;
}
transform = renderTransform *
Matrix.CreateTranslation(visual.Bounds.Position) *
transform;
// If the visual is clipped, update the clip bounds.
if (visual.ClipToBounds)
{
var globalBounds = bounds.TransformToAABB(transform);
var clipBounds = visual.ClipToBounds ?
globalBounds.Intersect(clip) :
clip;
clip = clip.Intersect(clipBounds);
}
return true;
}
return Visit(visual) ? new(new(visual.Bounds.Size), clip, transform) : null;
}
/// <summary>
/// Gets the first visual in the visual tree whose bounds contain a point.
/// </summary>

2
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -166,7 +166,7 @@
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">

2
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -414,7 +414,7 @@
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">

2
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -165,7 +165,7 @@
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">

2
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -376,7 +376,7 @@
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
ItemsSource="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">

5
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -6066,8 +6066,9 @@ namespace Avalonia.Controls
var numberOfItem = clipboardRowContent.Count;
for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
{
var cellContent = clipboardRowContent[cellIndex];
text.Append(cellContent.Content);
var cellContent = clipboardRowContent[cellIndex].Content?.ToString();
cellContent = cellContent?.Replace("\"", "\"\"");
text.Append($"\"{cellContent}\"");
if (cellIndex < numberOfItem - 1)
{
text.Append('\t');

8
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@ -177,14 +177,14 @@ namespace Avalonia.Controls
}
bool? uneditedValue = editingCheckBox.IsChecked;
if(editingEventArgs is PointerPressedEventArgs args)
if (editingEventArgs is PointerPressedEventArgs args)
{
void ProcessPointerArgs()
{
// Editing was triggered by a mouse click
Point position = args.GetPosition(editingCheckBox);
Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
if(rect.Contains(position))
if (rect.Contains(position))
{
EditValue();
}
@ -192,14 +192,14 @@ namespace Avalonia.Controls
void OnLayoutUpdated(object sender, EventArgs e)
{
if(!editingCheckBox.Bounds.IsDefault)
if (editingCheckBox.Bounds.Width != 0 || editingCheckBox.Bounds.Height != 0)
{
editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
ProcessPointerArgs();
}
}
if(editingCheckBox.Bounds.IsDefault)
if (editingCheckBox.Bounds.Width == 0 && editingCheckBox.Bounds.Height == 0)
{
editingCheckBox.LayoutUpdated += OnLayoutUpdated;
}

5
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@ -39,7 +39,10 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
AvaloniaProperty.RegisterDirect<ItemsRepeater, IEnumerable?>(
nameof(Items),
o => o.Items,
(o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="Layout"/> property.

4
src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs

@ -441,7 +441,7 @@ namespace Avalonia.Controls
_pendingViewportShift = default;
_unshiftableShift = default;
if (_visibleWindow.IsDefault)
if (_visibleWindow.Width == 0 && _visibleWindow.Height == 0)
{
// We got cleared.
_layoutExtent = default;
@ -527,7 +527,7 @@ namespace Avalonia.Controls
private void TryInvalidateMeasure()
{
// Don't invalidate measure if we have an invalid window.
if (!_visibleWindow.IsDefault)
if (_visibleWindow.Width != 0 || _visibleWindow.Height != 0)
{
// We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to

12
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@ -87,12 +87,10 @@ namespace Avalonia.Controls
/// Identifies the <see cref="Text" /> property.
/// </summary>
/// <value>The identifier for the <see cref="Text" /> property.</value>
public static readonly DirectProperty<AutoCompleteBox, string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<AutoCompleteBox>(
o => o.Text,
(o, v) => o.Text = v,
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<AutoCompleteBox>(new(string.Empty,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
enableDataValidation: true));
/// <summary>
/// Identifies the <see cref="SearchText" /> property.
@ -317,8 +315,8 @@ namespace Avalonia.Controls
/// <see cref="AutoCompleteBox" /> control.</value>
public string? Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>

3
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -198,7 +198,6 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private bool _isFocused = false;
private string? _text = string.Empty;
private string? _searchText = string.Empty;
private AutoCompleteFilterPredicate<object?>? _itemFilter;
@ -1275,7 +1274,7 @@ namespace Avalonia.Controls
if ((userInitiated ?? true) && Text != value)
{
_ignoreTextPropertyChange++;
Text = value;
SetCurrentValue(TextProperty, value);
callTextChanged = true;
}

29
src/Avalonia.Controls/Automation/Peers/ScrollBarAutomationPeer.cs

@ -0,0 +1,29 @@
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
{
public class ScrollBarAutomationPeer : RangeBaseAutomationPeer
{
public ScrollBarAutomationPeer(ScrollBar owner) : base(owner)
{
}
override protected string GetClassNameCore()
{
return "ScrollBar";
}
override protected AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.ScrollBar;
}
// AutomationControlType.ScrollBar must return IsContentElement false.
// See http://msdn.microsoft.com/en-us/library/ms743712.aspx
override protected bool IsContentElementCore()
{
return false;
}
}
}

2
src/Avalonia.Controls/BorderVisual.cs

@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
if (ClipToBounds)
{
var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
if (_cornerRadius.IsDefault)
if (_cornerRadius == default)
canvas.PushClip(clipRect);
else
canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));

20
src/Avalonia.Controls/ContainerClearingEventArgs.cs

@ -0,0 +1,20 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsControl.ContainerClearing"/> event.
/// </summary>
public class ContainerClearingEventArgs : EventArgs
{
public ContainerClearingEventArgs(Control container)
{
Container = container;
}
/// <summary>
/// Gets the prepared container.
/// </summary>
public Control Container { get; }
}
}

32
src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs

@ -0,0 +1,32 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsControl.ContainerIndexChanged"/> event.
/// </summary>
public class ContainerIndexChangedEventArgs : EventArgs
{
public ContainerIndexChangedEventArgs(Control container, int oldIndex, int newIndex)
{
Container = container;
OldIndex = oldIndex;
NewIndex = newIndex;
}
/// <summary>
/// Get the container for which the index changed.
/// </summary>
public Control Container { get; }
/// <summary>
/// Gets the index of the container after the change.
/// </summary>
public int NewIndex { get; }
/// <summary>
/// Gets the index of the container before the change.
/// </summary>
public int OldIndex { get; }
}
}

26
src/Avalonia.Controls/ContainerPreparedEventArgs.cs

@ -0,0 +1,26 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsControl.ContainerPrepared"/> event.
/// </summary>
public class ContainerPreparedEventArgs : EventArgs
{
public ContainerPreparedEventArgs(Control container, int index)
{
Container = container;
Index = index;
}
/// <summary>
/// Gets the prepared container.
/// </summary>
public Control Container { get; }
/// <summary>
/// Gets the index of the item the container was prepared for.
/// </summary>
public int Index { get; }
}
}

103
src/Avalonia.Controls/ContextMenu.cs

@ -54,11 +54,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="Placement"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
Popup.PlacementModeProperty.AddOwner<ContextMenu>();
[Obsolete("Use the Placement property instead.")]
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
@ -108,99 +114,80 @@ namespace Avalonia.Controls
static ContextMenu()
{
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu);
}
/// <summary>
/// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
/// <inheritdoc cref="Popup.HorizontalOffset"/>
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
get => GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
/// <inheritdoc cref="Popup.VerticalOffset"/>
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
get => GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <inheritdoc cref="Popup.PlacementAnchor"/>
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
get => GetValue(PlacementAnchorProperty);
set => SetValue(PlacementAnchorProperty, value);
}
/// <summary>
/// Gets or sets a value describing how the context menu position will be adjusted if the
/// unadjusted position would result in the context menu being partly constrained.
/// </summary>
/// <inheritdoc cref="Popup.PlacementConstraintAdjustment"/>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
get => GetValue(PlacementConstraintAdjustmentProperty);
set => SetValue(PlacementConstraintAdjustmentProperty, value);
}
/// <summary>
/// Gets or sets a value which defines in what direction the context menu should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <summary>
/// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
/// </summary>
/// <inheritdoc cref="Placement"/>
[Obsolete("Use the Placement property instead.")]
public PlacementMode PlacementMode
{
get { return GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.Placement"/>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
public bool WindowManagerAddShadowHint
{
get { return GetValue(WindowManagerAddShadowHintProperty); }
set { SetValue(WindowManagerAddShadowHintProperty, value); }
get => GetValue(WindowManagerAddShadowHintProperty);
set => SetValue(WindowManagerAddShadowHintProperty, value);
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
/// <inheritdoc cref="Popup.PlacementRect"/>
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
get => GetValue(PlacementRectProperty);
set => SetValue(PlacementRectProperty, value);
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
/// <inheritdoc cref="Popup.PlacementTarget"/>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
get => GetValue(PlacementTargetProperty);
set => SetValue(PlacementTargetProperty, value);
}
/// <summary>
@ -343,9 +330,9 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.PlacementMode = !requestedByPointer && PlacementMode == PlacementMode.Pointer
_popup.Placement = !requestedByPointer && Placement == PlacementMode.Pointer
? PlacementMode.Bottom
: PlacementMode;
: Placement;
//Position of the line below is really important.
//All styles are being applied only when control has logical parent.

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

@ -389,7 +389,7 @@ namespace Avalonia.Controls
_presenter.Date = SelectedDate ?? DateTimeOffset.Now;
_popup.PlacementMode = PlacementMode.AnchorAndGravity;
_popup.Placement = PlacementMode.AnchorAndGravity;
_popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
_popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
_popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

2
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -255,7 +255,7 @@ namespace Avalonia.Controls
_presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
_popup.PlacementMode = PlacementMode.AnchorAndGravity;
_popup.Placement = PlacementMode.AnchorAndGravity;
_popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
_popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
_popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

8
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -91,11 +91,11 @@ namespace Avalonia.Controls.Documents
public override void Add(Inline inline)
{
if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text))
if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock.Text))
{
base.Add(new Run(textBlock._text));
base.Add(new Run(textBlock.Text));
textBlock._text = null;
textBlock.ClearTextInternal();
}
base.Add(inline);
@ -113,7 +113,7 @@ namespace Avalonia.Controls.Documents
{
if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
textBlock.Text += text;
}
else
{

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

@ -19,7 +19,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="Items"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
AvaloniaProperty.RegisterDirect<MenuFlyout, IEnumerable?>(
nameof(Items),
x => x.Items,
(x, v) => x.Items = v);
/// <summary>

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

@ -14,9 +14,9 @@ namespace Avalonia.Controls.Primitives
{
public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
{
/// <inheritdoc cref="Popup.PlacementModeProperty"/>
/// <inheritdoc cref="Popup.PlacementProperty"/>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>();
Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
@ -64,15 +64,13 @@ namespace Avalonia.Controls.Primitives
protected Popup Popup => _popupLazy.Value;
/// <summary>
/// Gets or sets the desired placement.
/// </summary>
/// <inheritdoc cref="Popup.Placement"/>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity
{
@ -407,7 +405,7 @@ namespace Avalonia.Controls.Primitives
{
Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize.IsDefault)
if (Popup.Child!.DesiredSize == default)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@ -423,11 +421,11 @@ namespace Avalonia.Controls.Primitives
Popup.PlacementGravity = PlacementGravity;
if (showAtPointer)
{
Popup.PlacementMode = PlacementMode.Pointer;
Popup.Placement = PlacementMode.Pointer;
}
else
{
Popup.PlacementMode = Placement;
Popup.Placement = Placement;
Popup.PlacementConstraintAdjustment =
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

165
src/Avalonia.Controls/ItemCollection.cs

@ -0,0 +1,165 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Collections;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the list of items that constitute the content of an <see cref="ItemsControl"/>.
/// </summary>
public class ItemCollection : ItemsSourceView, IList
{
// Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique.
#pragma warning disable CA1825
private static readonly object?[] s_uninitialized = new object?[0];
#pragma warning restore CA1825
private Mode _mode;
internal ItemCollection()
: base(s_uninitialized)
{
}
public new object? this[int index]
{
get => base[index];
set => WritableSource[index] = value;
}
public bool IsReadOnly => _mode == Mode.ItemsSource;
internal event EventHandler? SourceChanged;
/// <summary>
/// Adds an item to the <see cref="ItemsControl"/>.
/// </summary>
/// <param name="value">The item to add to the collection.</param>
/// <returns>
/// The position into which the new element was inserted, or -1 to indicate that
/// the item was not inserted into the collection.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public int Add(object? value) => WritableSource.Add(value);
/// <summary>
/// Clears the collection and releases the references on all items currently in the
/// collection.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void Clear() => WritableSource.Clear();
/// <summary>
/// Inserts an element into the collection at the specified index.
/// </summary>
/// <param name="index">The zero-based index at which to insert the item.</param>
/// <param name="value">The item to insert.</param>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void Insert(int index, object? value) => WritableSource.Insert(index, value);
/// <summary>
/// Removes the item at the specified index of the collection or view.
/// </summary>
/// <param name="index">The zero-based index of the item to remove.</param>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public void RemoveAt(int index) => WritableSource.RemoveAt(index);
/// <summary>
/// Removes the specified item reference from the collection or view.
/// </summary>
/// <param name="value">The object to remove.</param>
/// <returns>True if the item was removed; otherwise false.</returns>
/// <exception cref="InvalidOperationException">
/// The collection is in ItemsSource mode.
/// </exception>
public bool Remove(object? value)
{
var c = Count;
WritableSource.Remove(value);
return Count < c;
}
int IList.Add(object? value) => Add(value);
void IList.Clear() => Clear();
void IList.Insert(int index, object? value) => Insert(index, value);
void IList.RemoveAt(int index) => RemoveAt(index);
private IList WritableSource
{
get
{
if (IsReadOnly)
ThrowIsItemsSource();
if (Source == s_uninitialized)
SetSource(CreateDefaultCollection());
return Source;
}
}
internal IList? GetItemsPropertyValue()
{
if (_mode == Mode.ObsoleteItemsSetter)
return Source == s_uninitialized ? null : Source;
return this;
}
internal void SetItems(IList? items)
{
_mode = Mode.ObsoleteItemsSetter;
SetSource(items ?? s_uninitialized);
}
internal void SetItemsSource(IEnumerable? value)
{
if (_mode != Mode.ItemsSource && Count > 0)
throw new InvalidOperationException(
"Items collection must be empty before using ItemsSource.");
_mode = value is not null ? Mode.ItemsSource : Mode.Items;
SetSource(value ?? CreateDefaultCollection());
}
private new void SetSource(IEnumerable source)
{
var oldSource = Source;
base.SetSource(source);
if (oldSource.Count > 0)
RaiseCollectionChanged(new(NotifyCollectionChangedAction.Remove, oldSource, 0));
if (Source.Count > 0)
RaiseCollectionChanged(new(NotifyCollectionChangedAction.Add, Source, 0));
SourceChanged?.Invoke(this, EventArgs.Empty);
}
private static AvaloniaList<object?> CreateDefaultCollection()
{
return new() { ResetBehavior = ResetBehavior.Remove };
}
[DoesNotReturn]
private static void ThrowIsItemsSource()
{
throw new InvalidOperationException(
"Operation is not valid while ItemsSource is in use." +
"Access and modify elements with ItemsControl.ItemsSource instead.");
}
private enum Mode
{
Items,
ItemsSource,
ObsoleteItemsSetter,
}
}
}

243
src/Avalonia.Controls/ItemsControl.cs

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Automation.Peers;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
@ -34,8 +35,13 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IList?>(
nameof(Items),
o => o.Items,
#pragma warning disable CS0618 // Type or member is obsolete
(o, v) => o.Items = v);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
@ -56,23 +62,23 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// Defines the <see cref="ItemsSource"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.Register<ItemsControl, IEnumerable?>(nameof(ItemsSource));
/// <summary>
/// Defines the <see cref="ItemsView"/> property.
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView);
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary>
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
/// </summary>
@ -89,15 +95,15 @@ namespace Avalonia.Controls
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
[AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IBinding? DisplayMemberBinding
{
get => GetValue(DisplayMemberBindingProperty);
set => SetValue(DisplayMemberBindingProperty, value);
}
private IEnumerable? _items = new AvaloniaList<object>();
private ItemsSourceView _itemsView;
private readonly ItemCollection _items = new();
private int _itemCount;
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@ -110,9 +116,8 @@ namespace Avalonia.Controls
/// </summary>
public ItemsControl()
{
_itemsView = ItemsSourceView.GetOrCreate(_items);
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
UpdatePseudoClasses(0);
UpdatePseudoClasses();
_items.CollectionChanged += OnItemsViewCollectionChanged;
}
/// <summary>
@ -128,11 +133,45 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the items to display.
/// </summary>
/// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
/// and an <see cref="ItemsSource"/> property. The properties have the following differences:
///
/// <list type="bullet">
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
/// meaning that it cannot be styled </item>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
/// is marked as the content property and will be used for items added via inline XAML.</item>
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
///
/// <list type="bullet">
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
/// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks>
[Content]
public IEnumerable? Items
public IList? Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
get => _items.GetItemsPropertyValue();
[Obsolete("Use ItemsSource to set or bind items.")]
set
{
var oldItems = _items.GetItemsPropertyValue();
if (value != oldItems)
{
_items.SetItems(value);
RaisePropertyChanged(ItemsProperty, oldItems, value);
}
}
}
/// <summary>
@ -140,17 +179,24 @@ namespace Avalonia.Controls
/// </summary>
public ControlTheme? ItemContainerTheme
{
get => GetValue(ItemContainerThemeProperty);
get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value);
}
/// <summary>
/// Gets the number of items in <see cref="Items"/>.
/// Gets the number of items being displayed by the <see cref="ItemsControl"/>.
/// </summary>
public int ItemCount
{
get => _itemCount;
private set => SetAndRaise(ItemCountProperty, ref _itemCount, value);
private set
{
if (SetAndRaise(ItemCountProperty, ref _itemCount, value))
{
UpdatePseudoClasses();
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
}
}
/// <summary>
@ -162,13 +208,46 @@ namespace Avalonia.Controls
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
/// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>.
/// </summary>
/// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
/// and an <see cref="ItemsSource"/> property. The properties have the following differences:
///
/// <list type="bullet">
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
/// meaning that it cannot be styled </item>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
/// is marked as the content property and will be used for items added via inline XAML.</item>
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
///
/// <list type="bullet">
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
/// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks>
public IEnumerable? ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
[InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate
{
get => GetValue(ItemTemplateProperty);
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
@ -183,31 +262,9 @@ namespace Avalonia.Controls
public Panel? ItemsPanelRoot => Presenter?.Panel;
/// <summary>
/// Gets a standardized view over <see cref="Items"/>.
/// Gets a read-only view of the items in the <see cref="ItemsControl"/>.
/// </summary>
/// <remarks>
/// The <see cref="Items"/> property may be an enumerable which does not implement
/// <see cref="IList"/> or may be null. This view can be used to provide a standardized
/// view of the current items regardless of the type of the concrete collection, and
/// without having to deal with null values.
/// </remarks>
public ItemsSourceView ItemsView
{
get => _itemsView;
private set
{
if (ReferenceEquals(_itemsView, value))
return;
var oldValue = _itemsView;
RemoveControlItemsFromLogicalChildren(_itemsView);
_itemsView.PostCollectionChanged -= ItemsCollectionChanged;
_itemsView = value;
_itemsView.PostCollectionChanged += ItemsCollectionChanged;
AddControlItemsToLogicalChildren(_itemsView);
RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView);
}
}
public ItemsSourceView ItemsView => _items;
private protected bool WrapFocus { get; set; }
@ -217,6 +274,34 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
/// <summary>
/// Occurs each time a container is prepared for use.
/// </summary>
/// <remarks>
/// The prepared element might be newly created or an existing container that is being re-
/// used.
/// </remarks>
public event EventHandler<ContainerPreparedEventArgs>? ContainerPrepared;
/// <summary>
/// Occurs for each realized container when the index for the item it represents has changed.
/// </summary>
/// <remarks>
/// This event is raised for each realized container where the index for the item it
/// represents has changed. For example, when another item is added or removed in the data
/// source, the index for items that come after in the ordering will be impacted.
/// </remarks>
public event EventHandler<ContainerIndexChangedEventArgs>? ContainerIndexChanged;
/// <summary>
/// Occurs each time a container is cleared.
/// </summary>
/// <remarks>
/// This event is raised immediately each time an container is cleared, such as when it
/// falls outside the range of realized items or the corresponding item is removed.
/// </remarks>
public event EventHandler<ContainerClearingEventArgs>? ContainerClearing;
/// <inheritdoc />
public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
{
@ -262,7 +347,7 @@ namespace Avalonia.Controls
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get => GetValue(AreHorizontalSnapPointsRegularProperty);
get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
@ -271,7 +356,7 @@ namespace Avalonia.Controls
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get => GetValue(AreVerticalSnapPointsRegularProperty);
get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
@ -295,7 +380,7 @@ namespace Avalonia.Controls
/// </returns>
public Control? ContainerFromItem(object item)
{
var index = ItemsView.IndexOf(item);
var index = _items.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null;
}
@ -319,7 +404,7 @@ namespace Avalonia.Controls
public object? ItemFromContainer(Control container)
{
var index = IndexFromContainer(container);
return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null;
return index >= 0 && index < _items.Count ? _items[index] : null;
}
/// <summary>
@ -478,19 +563,13 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
if (change.Property == ItemsProperty)
{
ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
ItemCount = ItemsView.Count;
}
else if (change.Property == ItemCountProperty)
if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
UpdatePseudoClasses(change.GetNewValue<int>());
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
RefreshContainers();
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
else if (change.Property == ItemsSourceProperty)
{
RefreshContainers();
_items.SetItemsSource(change.GetNewValue<IEnumerable?>());
}
else if (change.Property == ItemTemplateProperty)
{
@ -517,24 +596,27 @@ namespace Avalonia.Controls
/// <summary>
/// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
/// raised on <see cref="Items"/>.
/// raised on <see cref="ItemsView"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ItemCount = _itemsView.Count;
switch (e.Action)
if (!_items.IsReadOnly)
{
case NotifyCollectionChangedAction.Add:
AddControlItemsToLogicalChildren(e.NewItems);
break;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddControlItemsToLogicalChildren(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
}
ItemCount = ItemsView.Count;
}
/// <summary>
@ -578,7 +660,7 @@ namespace Avalonia.Controls
{
var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null &&
if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{
@ -595,24 +677,23 @@ namespace Avalonia.Controls
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
_scrollViewer?.RegisterAnchorCandidate(container);
ContainerPrepared?.Invoke(this, new(container, index));
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
ContainerIndexChanged?.Invoke(this, new(container, oldIndex, newIndex));
}
internal void ClearItemContainer(Control container)
{
_scrollViewer?.UnregisterAnchorCandidate(container);
ClearContainerForItemOverride(container);
ContainerClearing?.Invoke(this, new(container));
}
/// <summary>
/// Given a collection of items, adds those that are controls to the logical children.
/// </summary>
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable? items)
{
if (items is null)
@ -633,10 +714,6 @@ namespace Avalonia.Controls
LogicalChildren.AddRange(toAdd);
}
/// <summary>
/// Given a collection of items, removes those that are controls to from logical children.
/// </summary>
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{
if (items is null)
@ -674,10 +751,10 @@ namespace Avalonia.Controls
return _displayMemberItemTemplate;
}
private void UpdatePseudoClasses(int itemCount)
private void UpdatePseudoClasses()
{
PseudoClasses.Set(":empty", itemCount == 0);
PseudoClasses.Set(":singleitem", itemCount == 1);
PseudoClasses.Set(":empty", ItemCount == 0);
PseudoClasses.Set(":singleitem", ItemCount == 1);
}
protected static IInputElement? GetNextControl(

166
src/Avalonia.Controls/ItemsSourceView.cs

@ -7,6 +7,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Utils;
@ -17,16 +18,18 @@ namespace Avalonia.Controls
/// and an items control.
/// </summary>
public class ItemsSourceView : IReadOnlyList<object?>,
IList,
INotifyCollectionChanged,
ICollectionChangedListener
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object?>());
private readonly IList _inner;
private IList _source;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening;
@ -34,30 +37,17 @@ namespace Avalonia.Controls
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
private protected ItemsSourceView(IEnumerable source)
{
_inner = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
}
private protected ItemsSourceView(IEnumerable source) => SetSource(source);
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count => Inner.Count;
public int Count => Source.Count;
/// <summary>
/// Gets the inner collection.
/// Gets the source collection.
/// </summary>
public IList Inner => _inner;
public IList Source => _source;
/// <summary>
/// Retrieves the item at the specified index.
@ -66,12 +56,20 @@ namespace Avalonia.Controls
/// <returns>The item.</returns>
public object? this[int index] => GetAt(index);
bool IList.IsFixedSize => false;
bool IList.IsReadOnly => true;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get => GetAt(index);
set => ThrowReadOnly();
}
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal bool HasKeyIndexMapping => false;
/// <summary>
@ -96,38 +94,37 @@ namespace Avalonia.Controls
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PostCollectionChanged
internal event NotifyCollectionChangedEventHandler? PreCollectionChanged
{
add
{
AddListenerIfNecessary();
_postCollectionChanged += value;
_preCollectionChanged += value;
}
remove
{
_postCollectionChanged -= value;
_preCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
private void AddListenerIfNecessary()
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PostCollectionChanged
{
if (!_listening)
add
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
AddListenerIfNecessary();
_postCollectionChanged += value;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
remove
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
_postCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
@ -136,14 +133,9 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public object? GetAt(int index) => Inner[index];
/// <summary>
/// Determines the index of a specific item in the collection.
/// </summary>
/// <param name="item">The object to locate in the collection.</param>
/// <returns>The index of value if found in the list; otherwise, -1.</returns>
public int IndexOf(object? item) => Inner.IndexOf(item);
public object? GetAt(int index) => Source[index];
public bool Contains(object? item) => Source.Contains(item);
public int IndexOf(object? item) => Source.IndexOf(item);
/// <summary>
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
@ -181,7 +173,8 @@ namespace Avalonia.Controls
{
return items switch
{
ItemsSourceView<T> isv => isv,
ItemsSourceView<T> isvt => isvt,
ItemsSourceView isv => new ItemsSourceView<T>(isv.Source),
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
@ -216,7 +209,7 @@ namespace Avalonia.Controls
yield return o;
}
var inner = Inner;
var inner = Source;
return inner switch
{
@ -225,10 +218,11 @@ namespace Avalonia.Controls
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
@ -241,15 +235,69 @@ namespace Avalonia.Controls
_postCollectionChanged?.Invoke(this, e);
}
int IList.Add(object? value) => ThrowReadOnly();
void IList.Clear() => ThrowReadOnly();
void IList.Insert(int index, object? value) => ThrowReadOnly();
void IList.Remove(object? value) => ThrowReadOnly();
void IList.RemoveAt(int index) => ThrowReadOnly();
void ICollection.CopyTo(Array array, int index) => Source.CopyTo(array, index);
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
internal string KeyFromIndex(int index) => throw new NotImplementedException();
private protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
_collectionChanged?.Invoke(this, e);
_postCollectionChanged?.Invoke(this, e);
}
[MemberNotNull(nameof(_source))]
private protected void SetSource(IEnumerable source)
{
if (_listening && _source is INotifyCollectionChanged inccOld)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
_source = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
INotifyCollectionChanged => throw new ArgumentException(
"Collection implements INotifyCollectionChanged but not IList.",
nameof(source)),
IEnumerable<object> iObj => new List<object>(iObj),
null => throw new ArgumentNullException(nameof(source)),
_ => new List<object>(source.Cast<object>())
};
if (_listening && _source is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this);
}
private void AddListenerIfNecessary()
{
if (!_listening)
{
if (_source is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_source is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
[DoesNotReturn]
private static int ThrowReadOnly() => throw new NotSupportedException("Collection is read-only.");
}
public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@ -285,7 +333,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public new T GetAt(int index) => (T)Inner[index]!;
public new T GetAt(int index) => (T)Source[index]!;
public new IEnumerator<T> GetEnumerator()
{
@ -295,7 +343,7 @@ namespace Avalonia.Controls
yield return (T)o;
}
var inner = Inner;
var inner = Source;
return inner switch
{
@ -304,6 +352,6 @@ namespace Avalonia.Controls
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
}
}

4
src/Avalonia.Controls/LayoutTransformControl.cs

@ -91,7 +91,7 @@ namespace Avalonia.Controls
arrangedsize = TransformRoot.Bounds.Size;
// This is the first opportunity under Silverlight to find out the Child's true DesiredSize
if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault)
if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize == default)
{
//// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used
//// Make a note of the actual DesiredSize
@ -122,7 +122,7 @@ namespace Avalonia.Controls
}
Size measureSize;
if (_childActualSize.IsDefault)
if (_childActualSize == default)
{
// Determine the largest size after the transformation
measureSize = ComputeLargestTransformedSize(availableSize);

135
src/Avalonia.Controls/MaskedTextBox.cs

@ -15,9 +15,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> AsciiOnlyProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture,
(o, v) => o.Culture = v, CultureInfo.CurrentCulture);
public static readonly StyledProperty<CultureInfo?> CultureProperty =
AvaloniaProperty.Register<MaskedTextBox, CultureInfo?>(nameof(Culture), CultureInfo.CurrentCulture);
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
@ -32,26 +31,49 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static readonly StyledProperty<char> PromptCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_');
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_', coerce: CoercePromptChar);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v);
public static readonly StyledProperty<bool> ResetOnPromptProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnPrompt), true);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v);
public static readonly StyledProperty<bool> ResetOnSpaceProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnSpace), true);
private CultureInfo? _culture;
private bool _ignoreTextChanges;
private bool _resetOnPrompt = true;
static MaskedTextBox()
{
PasswordCharProperty.OverrideMetadata<MaskedTextBox>(new('\0', coerce: CoercePasswordChar));
}
private bool _ignoreTextChanges;
private static char CoercePasswordChar(AvaloniaObject sender, char baseValue)
{
if (!MaskedTextProvider.IsValidPasswordChar(baseValue))
{
throw new ArgumentException($"'{baseValue}' is not a valid value for PasswordChar.");
}
var textbox = (MaskedTextBox)sender;
if (textbox.MaskProvider is { } maskProvider && baseValue == maskProvider.PromptChar)
{
// Prompt and password chars must be different.
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
private bool _resetOnSpace = true;
return baseValue;
}
static MaskedTextBox()
private static char CoercePromptChar(AvaloniaObject sender, char baseValue)
{
PasswordCharProperty
.OverrideDefaultValue<MaskedTextBox>('\0');
if (!MaskedTextProvider.IsValidInputChar(baseValue))
{
throw new ArgumentException($"'{baseValue}' is not a valid value for PromptChar.");
}
if (baseValue == sender.GetValue(PasswordCharProperty))
{
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
return baseValue;
}
public MaskedTextBox() { }
@ -59,6 +81,9 @@ namespace Avalonia.Controls
/// <summary>
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty",
"AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values",
Justification = "These values are being explicitly provided by a constructor parameter.")]
public MaskedTextBox(MaskedTextProvider maskedTextProvider)
{
if (maskedTextProvider == null)
@ -87,8 +112,8 @@ namespace Avalonia.Controls
/// </summary>
public CultureInfo? Culture
{
get => _culture;
set => SetAndRaise(CultureProperty, ref _culture, value);
get => GetValue(CultureProperty);
set => SetValue(CultureProperty, value);
}
/// <summary>
@ -131,15 +156,6 @@ namespace Avalonia.Controls
/// </summary>
public MaskedTextProvider? MaskProvider { get; private set; }
/// <summary>
/// Gets or sets the character to be displayed in substitute for user input.
/// </summary>
public new char PasswordChar
{
get => GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value);
}
/// <summary>
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
/// </summary>
@ -154,16 +170,8 @@ namespace Avalonia.Controls
/// </summary>
public bool ResetOnPrompt
{
get => _resetOnPrompt;
set
{
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnPrompt = value;
}
}
get => GetValue(ResetOnPromptProperty);
set => SetValue(ResetOnPromptProperty, value);
}
/// <summary>
@ -171,16 +179,8 @@ namespace Avalonia.Controls
/// </summary>
public bool ResetOnSpace
{
get => _resetOnSpace;
set
{
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnSpace = value;
}
}
get => GetValue(ResetOnSpaceProperty);
set => SetValue(ResetOnSpaceProperty, value);
}
Type IStyleable.StyleKey => typeof(TextBox);
@ -190,7 +190,7 @@ namespace Avalonia.Controls
{
if (HidePromptOnLeave == true && MaskProvider != null)
{
Text = MaskProvider.ToDisplayString();
SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
}
base.OnGotFocus(e);
}
@ -225,11 +225,11 @@ namespace Avalonia.Controls
var index = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(item, index))
{
CaretIndex = ++index;
SetCurrentValue(CaretIndexProperty, ++index);
}
}
Text = MaskProvider.ToDisplayString();
SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
e.Handled = true;
return;
}
@ -279,7 +279,7 @@ namespace Avalonia.Controls
{
if (HidePromptOnLeave && MaskProvider != null)
{
Text = MaskProvider.ToString(!HidePromptOnLeave, true);
SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true));
}
base.OnLostFocus(e);
}
@ -326,15 +326,6 @@ namespace Avalonia.Controls
}
else if (change.Property == PasswordCharProperty)
{
if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar))
{
throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar));
}
if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar)
{
// Prompt and password chars must be different.
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
{
UpdateMaskProvider();
@ -342,17 +333,23 @@ namespace Avalonia.Controls
}
else if (change.Property == PromptCharProperty)
{
if (!MaskedTextProvider.IsValidInputChar(PromptChar))
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
{
throw new ArgumentException("Specified character value is not allowed for this property.");
UpdateMaskProvider();
}
if (PromptChar == PasswordChar)
}
else if (change.Property == ResetOnPromptProperty)
{
if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
{
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
MaskProvider.ResetOnPrompt = newValue;
}
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
}
else if (change.Property == ResetOnSpaceProperty)
{
if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
{
UpdateMaskProvider();
MaskProvider.ResetOnSpace = newValue;
}
}
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
@ -390,7 +387,7 @@ namespace Avalonia.Controls
if (CaretIndex < Text?.Length)
{
CaretIndex = GetNextCharacterPosition(CaretIndex);
SetCurrentValue(CaretIndexProperty, GetNextCharacterPosition(CaretIndex));
if (MaskProvider.InsertAt(e.Text!, CaretIndex))
{
@ -399,7 +396,7 @@ namespace Avalonia.Controls
var nextPos = GetNextCharacterPosition(CaretIndex);
if (nextPos != 0 && CaretIndex != Text.Length)
{
CaretIndex = nextPos;
SetCurrentValue(CaretIndexProperty, nextPos);
}
}
@ -434,8 +431,8 @@ namespace Avalonia.Controls
{
if (provider != null)
{
Text = provider.ToDisplayString();
CaretIndex = position;
SetCurrentValue(TextProperty, provider.ToDisplayString());
SetCurrentValue(CaretIndexProperty, position);
}
}

2
src/Avalonia.Controls/NativeControlHost.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls
if (IsEffectivelyVisible && bounds.HasValue)
{
if (bounds.Value.IsDefault)
if (bounds.Value.Width == 0 && bounds.Value.Height == 0)
return false;
_attachment?.ShowInBounds(bounds.Value);
}

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

Loading…
Cancel
Save