Browse Source

Merge remote-tracking branch 'AvaloniaUI/master' into ListBoxScrollToElementAfterHide

pull/20798/head
timunie 5 months ago
parent
commit
f4653324bc
  1. 1
      .gitignore
  2. 7
      Directory.Packages.props
  3. 28
      samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml
  4. 39
      samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs
  5. 58
      samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs
  6. 3
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  7. 50
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  8. 12
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  9. 1
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  10. 19
      src/Avalonia.Controls/CommandBar/CommandBarButton.cs
  11. 19
      src/Avalonia.Controls/CommandBar/CommandBarToggleButton.cs
  12. 67
      src/Avalonia.Controls/TabControl.cs
  13. 2
      src/Avalonia.Native/.gitignore
  14. 4
      src/Avalonia.Native/Avalonia.Native.csproj
  15. 2
      src/Avalonia.Themes.Fluent/Controls/CarouselPage.xaml
  16. 8
      src/Avalonia.Themes.Fluent/Controls/NavigationPage.xaml
  17. 1
      src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml
  18. 1
      src/Avalonia.Themes.Simple/Controls/PathIcon.xaml
  19. 9
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  20. 1
      src/Windows/Avalonia.Win32/.gitignore
  21. 7
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  22. 2
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  23. 137
      tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
  24. 230
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs

1
.gitignore

@ -170,6 +170,7 @@ $RECYCLE.BIN/
## VS Code
#################
.vscode/
*.lscache
#################
## Cake

7
Directory.Packages.props

@ -15,9 +15,10 @@
<PackageVersion Include="HarfBuzzSharp" Version="8.3.1.3" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.3" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.3" />
<PackageVersion Include="MicroCom.CodeGenerator" Version="0.11.4" />
<PackageVersion Include="MicroCom.CodeGenerator.MSBuild" Version="0.11.4" />
<PackageVersion Include="MicroCom.Runtime" Version="0.11.4" />
<PackageVersion Include="MicroCom.CodeGenerator" Version="0.11.6" />
<PackageVersion Include="MicroCom.CodeGenerator.MSBuild" Version="0.11.6" />
<PackageVersion Include="MicroCom.CodeGenerator.Roslyn" Version="0.11.6" />
<PackageVersion Include="MicroCom.Runtime" Version="0.11.6" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
<PackageVersion Include="Microsoft.Build.Framework" Version="18.0.2" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="15.1.548" />

28
samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml

@ -29,10 +29,19 @@
HorizontalAlignment="Stretch"
SelectionChanged="OnBgPresetChanged">
<ComboBoxItem Content="Default" />
<ComboBoxItem Content="Blue (#0078D4)" />
<ComboBoxItem Content="Dark (#1C1C1E)" />
<ComboBoxItem Content="Gradient" />
<ComboBoxItem Content="Transparent" />
<ComboBoxItem Content="Blue (#0078D4)" Tag="#0078D4" />
<ComboBoxItem Content="Indigo (#3F51B5)" Tag="#3F51B5" />
<ComboBoxItem Content="Purple (#7B1FA2)" Tag="#7B1FA2" />
<ComboBoxItem Content="Pink (#C2185B)" Tag="#C2185B" />
<ComboBoxItem Content="Red (#D13438)" Tag="#D13438" />
<ComboBoxItem Content="Orange (#FF8C00)" Tag="#FF8C00" />
<ComboBoxItem Content="Green (#107C10)" Tag="#107C10" />
<ComboBoxItem Content="Teal (#008575)" Tag="#008575" />
<ComboBoxItem Content="Slate (#2F3A4A)" Tag="#2F3A4A" />
<ComboBoxItem Content="Light Gray (#F3F2F1)" Tag="#F3F2F1" />
<ComboBoxItem Content="Dark (#1C1C1E)" Tag="#1C1C1E" />
<ComboBoxItem Content="Gradient" Tag="Gradient" />
<ComboBoxItem Content="Transparent" Tag="Transparent" />
</ComboBox>
<TextBlock Text="Foreground" />
@ -41,8 +50,15 @@
HorizontalAlignment="Stretch"
SelectionChanged="OnFgChanged">
<ComboBoxItem Content="Default" />
<ComboBoxItem Content="White" />
<ComboBoxItem Content="Black" />
<ComboBoxItem Content="White (#FFFFFF)" Tag="#FFFFFF" />
<ComboBoxItem Content="Black (#000000)" Tag="#000000" />
<ComboBoxItem Content="Blue (#0078D4)" Tag="#0078D4" />
<ComboBoxItem Content="Purple (#7B1FA2)" Tag="#7B1FA2" />
<ComboBoxItem Content="Red (#D13438)" Tag="#D13438" />
<ComboBoxItem Content="Orange (#FF8C00)" Tag="#FF8C00" />
<ComboBoxItem Content="Green (#107C10)" Tag="#107C10" />
<ComboBoxItem Content="Teal (#008575)" Tag="#008575" />
<ComboBoxItem Content="Light Gray (#F3F2F1)" Tag="#F3F2F1" />
</ComboBox>
<TextBlock Text="Corner Radius" />

39
samples/ControlCatalog/Pages/CommandBar/CommandBarCustomizationPage.xaml.cs

@ -14,16 +14,16 @@ namespace ControlCatalog.Pages
{
if (LiveBar == null)
return;
switch (BgPresetCombo.SelectedIndex)
if (BgPresetCombo.SelectedItem is not ComboBoxItem { Tag: string preset })
{
case 1:
LiveBar.Background = new SolidColorBrush(Color.Parse("#0078D4"));
break;
case 2:
LiveBar.Background = new SolidColorBrush(Color.Parse("#1C1C1E"));
break;
case 3:
LiveBar.ClearValue(BackgroundProperty);
return;
}
switch (preset)
{
case "Gradient":
LiveBar.Background = new LinearGradientBrush
{
StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative),
@ -35,11 +35,11 @@ namespace ControlCatalog.Pages
}
};
break;
case 4:
case "Transparent":
LiveBar.Background = Brushes.Transparent;
break;
default:
LiveBar.ClearValue(BackgroundProperty);
LiveBar.Background = new SolidColorBrush(Color.Parse(preset));
break;
}
}
@ -48,19 +48,14 @@ namespace ControlCatalog.Pages
{
if (LiveBar == null)
return;
switch (FgCombo.SelectedIndex)
if (FgCombo.SelectedItem is ComboBoxItem { Tag: string color })
{
case 1:
LiveBar.Foreground = Brushes.White;
break;
case 2:
LiveBar.Foreground = Brushes.Black;
break;
default:
LiveBar.ClearValue(ForegroundProperty);
break;
LiveBar.Foreground = new SolidColorBrush(Color.Parse(color));
return;
}
LiveBar.ClearValue(ForegroundProperty);
}
private void OnRadiusChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e)

58
samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs

@ -7,6 +7,8 @@ using Avalonia;
using MiniMvvm;
using Avalonia.Collections;
using ControlCatalog.Pages;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
@ -105,10 +107,29 @@ namespace ControlCatalog.ViewModels
_ignoreListChange = true;
Pages.Clear();
if (!string.IsNullOrWhiteSpace(query))
Pages.AddRange(_items.Where(x => x.Header.Contains(query)));
else
if (string.IsNullOrWhiteSpace(query))
{
Pages.AddRange(_items);
}
else
{
var querySearchKey = PageItem.CreateSearchKey(query);
if (querySearchKey.Length == 0)
{
Pages.AddRange(_items);
}
else
{
foreach (var item in _items)
{
if (item.MatchesSearch(querySearchKey))
{
Pages.Add(item);
}
}
}
}
}
finally
{
@ -190,7 +211,38 @@ namespace ControlCatalog.ViewModels
public string Header { get; } = header;
public Func<object> Factory { get; } = factory;
public string? IconData { get; } = iconData;
private string SearchKey { get; } = CreateSearchKey(header);
public bool IsVisible { get; set; } = true;
public bool MatchesSearch(string searchKey)
{
return SearchKey.Contains(searchKey, StringComparison.Ordinal);
}
public static string CreateSearchKey(string value)
{
var normalizedValue = value.Normalize(NormalizationForm.FormKD);
var builder = new StringBuilder(normalizedValue.Length);
foreach (var c in normalizedValue)
{
var category = CharUnicodeInfo.GetUnicodeCategory(c);
if (category is UnicodeCategory.NonSpacingMark or
UnicodeCategory.SpacingCombiningMark or
UnicodeCategory.EnclosingMark)
{
continue;
}
if (char.IsLetterOrDigit(c))
{
builder.Append(char.ToUpperInvariant(c));
}
}
return builder.ToString();
}
}
}

3
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -95,7 +95,10 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity
public override void OnBackPressed()
{
if (OperatingSystem.IsAndroidVersionAtLeast(33))
{
OnBackPressedDispatcher.OnBackPressed();
return;
}
var eventArgs = new AndroidBackRequestedEventArgs();

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

@ -157,29 +157,61 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
}
public Task<IStorageFile?> CreateFileAsync(string name)
public async Task<IStorageFile?> CreateFileAsync(string name)
{
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
// Try to return an existing file to avoid creating file (1).
var existingItem = await GetItemAsync(name, false);
if (existingItem != null)
{
if (existingItem is IStorageFile existingFile)
{
// The file should be truncated when it is created.
using (var _ = await existingFile.OpenWriteAsync()) { }
return existingFile;
}
else if (existingItem is IStorageFolder)
{
// There is an item with the same name but it's not a file. We can't create a file in this case.
throw new IOException($"Can not create '{name}' because a directory with the same name already exists.");
}
}
// Create new one and return it.
var treeUri = GetTreeUri().treeUri;
var newFile = CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name);
if (newFile == null)
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
var newFile = DocumentsContract.CreateDocument(Activity.ContentResolver!, treeUri!, mimeType, name);
if(newFile == null)
{
return Task.FromResult<IStorageFile?>(null);
return null;
}
return Task.FromResult<IStorageFile?>(new AndroidStorageFile(Activity, newFile, this));
return new AndroidStorageFile(Activity, newFile, this);
}
public Task<IStorageFolder?> CreateFolderAsync(string name)
public async Task<IStorageFolder?> CreateFolderAsync(string name)
{
// Try to return an existing folder to avoid creating folder (1).
var existingItem = await GetItemAsync(name, true);
if (existingItem != null)
{
if (existingItem is IStorageFolder existingFolder)
{
return existingFolder;
}
else if (existingItem is IStorageFile)
{
// There is an item with the same name but it's not a folder. We can't create a folder in this case.
throw new IOException($"Can not create '{name}' because a file with the same name already exists.");
}
}
// Create new one and return it.
var treeUri = GetTreeUri().treeUri;
var newFolder = CreateDocument(Activity.ContentResolver!, treeUri!, Document.MimeTypeDir, name);
if (newFolder == null)
{
return Task.FromResult<IStorageFolder?>(null);
return null;
}
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Activity, newFolder, false, this, PermissionRoot));
return new AndroidStorageFolder(Activity, newFolder, false, this, PermissionRoot);
}
public override async Task DeleteAsync()

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

@ -37,16 +37,20 @@ public interface IStorageFolder : IStorageItem
Task<IStorageFile?> GetFileAsync(string name);
/// <summary>
/// Creates a file with specified name as a child of the current storage folder
/// Creates, or truncates and overwrites, 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>
/// <returns>
/// A <see cref="IStorageFile"/> that provides read/write access to the file specified in <c>name</c>.
/// </returns>
Task<IStorageFile?> CreateFileAsync(string name);
/// <summary>
/// Creates a folder with specified name as a child of the current storage folder
/// Creates a folder with specified name as a child of the current storage folder unless they already exist.
/// </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>
/// <returns>
/// A <see cref="IStorageFolder"/> that represents the directory at the specified <c>name</c>. This object is returned regardless of whether a directory at the specified <c>name</c> already exists.
/// </returns>
Task<IStorageFolder?> CreateFolderAsync(string name);
}

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

@ -1,5 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia.Metadata;

19
src/Avalonia.Controls/CommandBar/CommandBarButton.cs

@ -5,12 +5,6 @@ namespace Avalonia.Controls
/// </summary>
public class CommandBarButton : Button, ICommandBarElement
{
static CommandBarButton()
{
ForegroundProperty.Changed.AddClassHandler<CommandBarButton>((x, _) => x.UpdateIconForeground());
IconProperty.Changed.AddClassHandler<CommandBarButton>((x, _) => x.UpdateIconForeground());
}
/// <summary>
/// Defines the <see cref="Label"/> property.
/// </summary>
@ -102,18 +96,5 @@ namespace Avalonia.Controls
get => GetValue(IsInOverflowProperty);
set => SetValue(IsInOverflowProperty, value);
}
private void UpdateIconForeground()
{
if (Icon is IconElement icon)
{
var fg = Foreground;
if (fg != null)
icon.SetValue(ForegroundProperty, fg);
else
icon.ClearValue(ForegroundProperty);
}
}
}
}

19
src/Avalonia.Controls/CommandBar/CommandBarToggleButton.cs

@ -7,12 +7,6 @@ namespace Avalonia.Controls
/// </summary>
public class CommandBarToggleButton : ToggleButton, ICommandBarElement
{
static CommandBarToggleButton()
{
ForegroundProperty.Changed.AddClassHandler<CommandBarToggleButton>((x, _) => x.UpdateIconForeground());
IconProperty.Changed.AddClassHandler<CommandBarToggleButton>((x, _) => x.UpdateIconForeground());
}
/// <summary>
/// Defines the <see cref="Label"/> property.
/// </summary>
@ -104,18 +98,5 @@ namespace Avalonia.Controls
get => GetValue(IsInOverflowProperty);
set => SetValue(IsInOverflowProperty, value);
}
private void UpdateIconForeground()
{
if (Icon is IconElement icon)
{
var fg = Foreground;
if (fg != null)
icon.SetValue(ForegroundProperty, fg);
else
icon.ClearValue(ForegroundProperty);
}
}
}
}

67
src/Avalonia.Controls/TabControl.cs

@ -254,13 +254,7 @@ namespace Avalonia.Controls
_currentTransition = null;
_shouldAnimate = false;
if (_contentPresenter2 is { IsVisible: true })
{
_contentPresenter2.IsVisible = false;
_contentPresenter2.Content = null;
_contentPresenter2.ContentTemplate = null;
_contentPresenter2.DataContext = null;
}
ResetTransitionPresenter(_contentPresenter2);
int oldIndex = _previousSelectedIndex;
_previousSelectedIndex = SelectedIndex;
@ -306,8 +300,8 @@ namespace Avalonia.Controls
_contentPresenter2!.ContentTemplate = template;
_contentPresenter2!.IsVisible = true;
if (content is Control && template is null)
_contentPresenter2.SetContentWithDataContext(content, container.DataContext);
if (content is Control control && template is null)
SetControlContent(_contentPresenter2, control, container.DataContext);
else
_contentPresenter2.Content = content;
@ -320,8 +314,8 @@ namespace Avalonia.Controls
if (ContentPart != null)
{
var template = SelectContentTemplate(container.GetValue(ContentControl.ContentTemplateProperty));
if (content is Control && template is null)
ContentPart.SetContentWithDataContext(content, container.DataContext);
if (content is Control control && template is null)
SetControlContent(ContentPart, control, container.DataContext);
else
ContentPart.Content = content;
}
@ -408,6 +402,7 @@ namespace Avalonia.Controls
_currentTransition?.Dispose();
_currentTransition = null;
_shouldAnimate = false;
ResetTransitionPresenter(_contentPresenter2);
}
protected override Size ArrangeOverride(Size finalSize)
@ -496,6 +491,56 @@ namespace Avalonia.Controls
(_contentPart, _contentPresenter2) = (_contentPresenter2, _contentPart);
}
private void SetControlContent(ContentPresenter presenter, Control content, object? dataContext)
{
ClearOwningContentPresenter(presenter, content);
ClearPresenterContent(ContentPart, presenter, content);
ClearPresenterContent(_contentPresenter2, presenter, content);
presenter.SetContentWithDataContext(content, dataContext);
}
private void ClearOwningContentPresenter(ContentPresenter targetPresenter, Control content)
{
if (content.VisualParent is ContentPresenter parent &&
!ReferenceEquals(parent, targetPresenter) &&
ReferenceEquals(parent.Host, this) &&
ReferenceEquals(parent.Content, content))
{
ClearPresenterContent(parent, content);
}
}
private static void ClearPresenterContent(ContentPresenter? presenter, ContentPresenter targetPresenter, Control content)
{
if (!ReferenceEquals(presenter, targetPresenter))
ClearPresenterContent(presenter, content);
}
private static void ClearPresenterContent(ContentPresenter? presenter, Control content)
{
if (presenter != null && ReferenceEquals(presenter.Content, content))
{
presenter.Content = null;
presenter.ContentTemplate = null;
presenter.DataContext = null;
presenter.RenderTransform = null;
presenter.Opacity = 1;
}
}
private static void ResetTransitionPresenter(ContentPresenter? presenter)
{
if (presenter == null)
return;
presenter.IsVisible = false;
presenter.Content = null;
presenter.ContentTemplate = null;
presenter.DataContext = null;
presenter.RenderTransform = null;
presenter.Opacity = 1;
}
private void UpdateTabStripPlacement()
{
var controls = ItemsPresenterPart?.Panel?.Children;

2
src/Avalonia.Native/.gitignore

@ -1,2 +0,0 @@
Generated/*.cs
*.Generated.cs

4
src/Avalonia.Native/Avalonia.Native.csproj

@ -7,7 +7,7 @@
<IsPackable Condition="'$(ForcePackAvaloniaNative)' == 'True'">True</IsPackable>
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<UseMicroComGeneratorMSBuild>true</UseMicroComGeneratorMSBuild>
<UseMicroComGeneratorRoslyn>true</UseMicroComGeneratorRoslyn>
</PropertyGroup>
<ItemGroup Condition="'$(PackAvaloniaNative)' == 'true'">
@ -22,7 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<MicroComIdl Include="avn.idl" CSharpInteropPath="Interop.Generated.cs" />
<MicroComIdl Include="avn.idl" />
</ItemGroup>
<Import Project="..\..\build\MicroCOM.props" />

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

@ -14,7 +14,7 @@
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type CarouselPage}" TargetType="CarouselPage">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlPageBackgroundAltHighBrush}" />
<Setter Property="Template">
<ControlTemplate>
<Carousel Name="PART_Carousel"

8
src/Avalonia.Themes.Fluent/Controls/NavigationPage.xaml

@ -83,10 +83,10 @@
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="Button#PART_BackButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightListLowBrush}" />
</Style>
<Style Selector="Button#PART_BackButton:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightListMediumBrush}" />
</Style>
<Style Selector="Button#PART_BackButton:disabled">
<Setter Property="Opacity" Value="0.4" />
@ -104,10 +104,10 @@
<Setter Property="CornerRadius" Value="0" />
</Style>
<Style Selector="Button#PART_OverflowButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightListLowBrush}" />
</Style>
<Style Selector="Button#PART_OverflowButton:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightListMediumBrush}" />
</Style>
</Grid.Styles>

1
src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml

@ -10,7 +10,6 @@
</StackPanel>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type PathIcon}" TargetType="PathIcon">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />

1
src/Avalonia.Themes.Simple/Controls/PathIcon.xaml

@ -3,7 +3,6 @@
x:ClassModifier="internal">
<ControlTheme x:Key="{x:Type PathIcon}"
TargetType="PathIcon">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundColor}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />

9
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -65,7 +65,7 @@ export class StorageItem {
await item.verityPermissions("readwrite");
return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true });
return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: false });
}
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
@ -103,8 +103,11 @@ export class StorageItem {
}
await item.verityPermissions("readwrite");
return await ((item.handle as any).getFileHandle(name, { create: true }) as Promise<any>);
// The file should be truncated when it is created.
const fileHandle = await ((item.handle as any).getFileHandle(name, { create: true }) as Promise<any>);
const writable = await fileHandle.createWritable({ keepExistingData: false });
await writable.close();
return fileHandle;
}
public static async getFile(item: StorageItem, name: string): Promise<any | null> {

1
src/Windows/Avalonia.Win32/.gitignore

@ -1 +0,0 @@
*.Generated.cs

7
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -3,7 +3,7 @@
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableRuntimeMarshalling>true</EnableRuntimeMarshalling>
<UseMicroComGeneratorMSBuild>true</UseMicroComGeneratorMSBuild>
<UseMicroComGeneratorRoslyn>true</UseMicroComGeneratorRoslyn>
</PropertyGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<PackageReference Include="System.Numerics.Vectors" />
@ -18,10 +18,6 @@
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" />
<MicroComIdl Include="WinRT\winrt.idl" CSharpInteropPath="WinRT\WinRT.Generated.cs" />
<MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" />
<MicroComIdl Include="DirectX\directx.idl" CSharpInteropPath="DirectX\directx.Generated.cs" />
<MicroComIdl Include="DComposition\dcomp.idl" CSharpInteropPath="DComposition\DComp.Generated.cs" />
<ProjectReference Include="..\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\MicroCOM.props" />
@ -29,6 +25,7 @@
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\SourceGenerators.props" />
<ItemGroup>
<MicroComIdl Include="**/*.idl" />
<Compile Remove="..\..\Shared\SourceGeneratorAttributes.cs" />
</ItemGroup>
<Import Project="..\..\..\build\TrimmingEnable.props" />

2
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -299,7 +299,7 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
var path = System.IO.Path.Combine(FilePath, name);
NSFileAttributes? attributes = null;
if (NSFileManager.DefaultManager.CreateDirectory(path, false, attributes, out var error))
if (NSFileManager.DefaultManager.CreateDirectory(path, true, attributes, out var error))
{
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(new NSUrl(path, true), SecurityScopedAncestorUrl));
}

137
tests/Avalonia.Controls.UnitTests/CommandBarTests.cs

@ -3,9 +3,14 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Themes.Simple;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@ -118,6 +123,28 @@ public class CommandBarButtonTests : ScopedTestBase
var btn = new CommandBarButton { CommandParameter = "param" };
Assert.Equal("param", btn.CommandParameter);
}
[Fact]
public void Foreground_DoesNotSetOrOverwriteIconElementForeground()
{
var icon = new PathIcon();
var btn = new CommandBarButton
{
Icon = icon,
Foreground = Brushes.Red
};
Assert.False(icon.IsSet(TemplatedControl.ForegroundProperty));
btn.Foreground = Brushes.Blue;
Assert.False(icon.IsSet(TemplatedControl.ForegroundProperty));
icon.Foreground = Brushes.Green;
btn.Foreground = Brushes.Red;
Assert.Same(Brushes.Green, icon.Foreground);
}
}
public class CommandBarToggleButtonTests : ScopedTestBase
@ -205,6 +232,28 @@ public class CommandBarToggleButtonTests : ScopedTestBase
var btn = new CommandBarToggleButton { CommandParameter = 42 };
Assert.Equal(42, btn.CommandParameter);
}
[Fact]
public void Foreground_DoesNotSetOrOverwriteIconElementForeground()
{
var icon = new PathIcon();
var btn = new CommandBarToggleButton
{
Icon = icon,
Foreground = Brushes.Red
};
Assert.False(icon.IsSet(TemplatedControl.ForegroundProperty));
btn.Foreground = Brushes.Blue;
Assert.False(icon.IsSet(TemplatedControl.ForegroundProperty));
icon.Foreground = Brushes.Green;
btn.Foreground = Brushes.Red;
Assert.Same(Brushes.Green, icon.Foreground);
}
}
public class CommandBarSeparatorTests : ScopedTestBase
@ -309,6 +358,54 @@ public class CommandBarDefaultsTests : ScopedTestBase
public void Content_DefaultIsNull()
=> Assert.Null(new CommandBar().Content);
[Fact]
public void Foreground_IsInheritedByCommandBarButtonPathIconThroughThemeTemplate()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var icon = new PathIcon();
var btn = new CommandBarButton
{
Icon = icon
};
var commandBar = new CommandBar
{
Foreground = Brushes.Red
};
commandBar.PrimaryCommands.Add(btn);
Assert.Same(icon, ApplySimpleThemeAndGetPresentedPathIcon(commandBar, btn));
Assert.Same(Brushes.Red, icon.Foreground);
commandBar.Foreground = Brushes.Blue;
Assert.Same(Brushes.Blue, icon.Foreground);
}
[Fact]
public void Foreground_IsInheritedByCommandBarToggleButtonPathIconThroughThemeTemplate()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var icon = new PathIcon();
var btn = new CommandBarToggleButton
{
Icon = icon
};
var commandBar = new CommandBar
{
Foreground = Brushes.Red
};
commandBar.PrimaryCommands.Add(btn);
Assert.Same(icon, ApplySimpleThemeAndGetPresentedPathIcon(commandBar, btn));
Assert.Same(Brushes.Red, icon.Foreground);
commandBar.Foreground = Brushes.Blue;
Assert.Same(Brushes.Blue, icon.Foreground);
}
[Fact]
public void PrimaryCommands_NotNull()
=> Assert.NotNull(new CommandBar().PrimaryCommands);
@ -370,6 +467,46 @@ public class CommandBarDefaultsTests : ScopedTestBase
[Fact]
public void ItemWidthCollapsed_DefaultIs42()
=> Assert.Equal(42d, new CommandBar().ItemWidthCollapsed);
private static PathIcon ApplySimpleThemeAndGetPresentedPathIcon(CommandBar commandBar, TemplatedControl command)
{
var simpleTheme = new SimpleTheme();
Assert.True(simpleTheme.TryGetResource(typeof(CommandBar), ThemeVariant.Default, out var commandBarTheme));
Assert.True(simpleTheme.TryGetResource(command.GetType(), ThemeVariant.Default, out var commandTheme));
commandBar.Theme = Assert.IsType<ControlTheme>(commandBarTheme);
command.Theme = Assert.IsType<ControlTheme>(commandTheme);
var root = new TestRoot
{
Width = 500,
Height = 200,
Child = commandBar,
Styles =
{
simpleTheme
}
};
root.ApplyStyling();
commandBar.ApplyStyling();
commandBar.ApplyTemplate();
root.LayoutManager.ExecuteInitialLayoutPass();
command.ApplyStyling();
command.ApplyTemplate();
var presenter = command.GetTemplateChildren()
.OfType<ContentPresenter>()
.Single(x => x.Name == "PART_IconPresenter");
presenter.ApplyStyling();
presenter.UpdateChild();
var pathIcon = Assert.IsType<PathIcon>(presenter.Child);
pathIcon.ApplyStyling();
return pathIcon;
}
}
public class CommandBarPropertyRoundTripTests : ScopedTestBase

230
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -21,6 +22,7 @@ using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
@ -913,6 +915,165 @@ namespace Avalonia.Controls.UnitTests
Times.Once);
}
[Fact]
public void Interrupted_PageTransition_Can_Select_Original_Control_Before_Previous_Transition_Completes()
{
using var app = Start();
var firstPage = new ContentPage { Content = "Alpha" };
var secondPage = new ContentPage { Content = "Beta" };
var starts = new List<(object? FromContent, object? ToContent, bool Forward)>();
var transitionGate = new TaskCompletionSource();
var transition = new Mock<IPageTransition>();
transition
.Setup(t => t.Start(
It.IsAny<Visual?>(), It.IsAny<Visual?>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Callback<Visual?, Visual?, bool, CancellationToken>((from, to, forward, _) =>
{
starts.Add((
(from as ContentPresenter)?.Content,
(to as ContentPresenter)?.Content,
forward));
})
.Returns(transitionGate.Task);
var target = new TabControl
{
PageTransition = transition.Object,
Items =
{
new TabItem { Name = "first", Content = firstPage },
new TabItem { Name = "second", Content = secondPage },
},
};
var root = CreateRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.SelectedIndex = 1;
root.LayoutManager.ExecuteLayoutPass();
Assert.Single(starts);
Assert.Same(firstPage, starts[0].FromContent);
Assert.Same(secondPage, starts[0].ToContent);
Assert.True(starts[0].Forward);
var exception = Record.Exception(() => target.SelectedIndex = 0);
Assert.Null(exception);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(2, starts.Count);
Assert.Null(starts[1].FromContent);
Assert.Same(firstPage, starts[1].ToContent);
Assert.False(starts[1].Forward);
Assert.Same(firstPage, target.SelectedContent);
}
[Fact]
public void Pending_PageTransition_Can_Select_Original_Control_Before_Transition_Starts()
{
using var app = Start();
var firstPage = new ContentPage { Content = "Alpha" };
var secondPage = new ContentPage { Content = "Beta" };
var starts = new List<(object? FromContent, object? ToContent, bool Forward)>();
var transition = new Mock<IPageTransition>();
transition
.Setup(t => t.Start(
It.IsAny<Visual?>(), It.IsAny<Visual?>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Callback<Visual?, Visual?, bool, CancellationToken>((from, to, forward, _) =>
{
starts.Add((
(from as ContentPresenter)?.Content,
(to as ContentPresenter)?.Content,
forward));
})
.Returns(Task.CompletedTask);
var target = new TabControl
{
PageTransition = transition.Object,
Items =
{
new TabItem { Name = "first", Content = firstPage },
new TabItem { Name = "second", Content = secondPage },
},
};
var root = CreateRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.SelectedIndex = 1;
var exception = Record.Exception(() => target.SelectedIndex = 0);
Assert.Null(exception);
root.LayoutManager.ExecuteLayoutPass();
Assert.Single(starts);
Assert.Null(starts[0].FromContent);
Assert.Same(firstPage, starts[0].ToContent);
Assert.False(starts[0].Forward);
Assert.Same(firstPage, target.SelectedContent);
}
[Fact]
public void Interrupted_PageTransition_Clears_Reused_Control_From_Owning_SelectedContentHost()
{
using var app = Start();
var firstPage = new ContentPage { Content = "Alpha" };
var secondPage = new ContentPage { Content = "Beta" };
var transition = new Mock<IPageTransition>();
transition
.Setup(t => t.Start(
It.IsAny<Visual?>(), It.IsAny<Visual?>(),
It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
var target = new TabControl
{
Items =
{
new TabItem { Name = "first", Content = firstPage },
new TabItem { Name = "second", Content = secondPage },
},
};
var root = CreateRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.SelectedIndex = 1;
root.LayoutManager.ExecuteLayoutPass();
var primary = target.GetVisualDescendants()
.OfType<ContentPresenter>()
.Single(x => x.Name == "PART_SelectedContentHost");
var secondary = target.GetVisualDescendants()
.OfType<ContentPresenter>()
.Single(x => x.Name == "PART_SelectedContentHost2");
// Simulate the stale presenter ownership that can happen when tab changes
// interrupt a transition: the page is still parented by the named content
// host, but the active field no longer points at that host.
primary.SetContentWithDataContext(firstPage, null);
secondary.IsVisible = false;
SetPrivateField(target, "_contentPart", secondary);
SetPrivateField(target, "_contentPresenter2", secondary);
target.PageTransition = transition.Object;
var exception = Record.Exception(() => target.SelectedIndex = 0);
Assert.Null(exception);
Assert.Same(firstPage, target.SelectedContent);
Assert.Null(primary.Content);
Assert.Same(firstPage, secondary.Content);
}
private static IControlTemplate TabControlTemplate()
{
return new FuncControlTemplate<TabControl>((parent, scope) =>
@ -943,72 +1104,11 @@ namespace Avalonia.Controls.UnitTests
});
}
/// <summary>
/// This test ensures that the selected tab is the first visible and enabled tab.
/// </summary>
[Fact]
public void Only_First_Visible_And_Enabled_Tab_Should_Be_Selected_By_Default()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
TabItem expectedSelectionItem;
var target = new TabControl
{
Template = TabControlTemplate(),
Items =
{
new TabItem { Name = "first", Content = "foo", IsVisible = false},
new TabItem { Name = "second", Content = "bar", IsEnabled = false},
(expectedSelectionItem = new TabItem { Name = "third", Content = "baz" }),
}
};
target.ApplyTemplate();
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
// the 3rd item should be selected
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(expectedSelectionItem, target.SelectedItem);
}
}
/// <summary>
/// This test ensures that also an invisible TabControl can receive a selection
/// </summary>
[Fact]
public void SelectedIndex_Should_Restore_After_Control_Gets_Visible()
private static void SetPrivateField<T>(TabControl target, string name, T value)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
TabItem expectedSelectionItem;
var target = new TabControl
{
IsVisible = false,
Template = TabControlTemplate(),
Items =
{
new TabItem { Name = "first", Content = "foo"},
(expectedSelectionItem = new TabItem { Name = "second", Content = "bar"}),
new TabItem { Name = "third", Content = "baz" },
},
SelectedIndex = 1
};
target.ApplyTemplate();
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.IsVisible = true;
// the 2nd item should be selected
Assert.Equal(1, target.SelectedIndex);
Assert.Equal(expectedSelectionItem, target.SelectedItem);
}
var field = typeof(TabControl).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(field);
field.SetValue(target, value);
}
private static IControlTemplate TabItemTemplate()

Loading…
Cancel
Save