Browse Source

Merge branch 'master' into fixes/Warnings/XML_Comment

pull/8646/head
Max Katz 4 years ago
committed by GitHub
parent
commit
3991526ce2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      azure-pipelines.yml
  2. 26
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 33
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  4. 8
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  5. 16
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  6. 2
      src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
  7. 4
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  8. 11
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  9. 2
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  10. 23
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  11. 346
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  12. 94
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  13. 76
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  14. 20
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  15. 4
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  16. 56
      src/Avalonia.Controls/Button.cs
  17. 57
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  18. 2
      src/Avalonia.Controls/Primitives/Popup.cs
  19. 33
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  20. 45
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts
  21. 25
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

6
azure-pipelines.yml

@ -59,7 +59,7 @@ jobs:
variables: variables:
SolutionDir: '$(Build.SourcesDirectory)' SolutionDir: '$(Build.SourcesDirectory)'
pool: pool:
vmImage: 'macOS-10.15' vmImage: 'macos-12'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.418' displayName: 'Use .NET Core SDK 3.1.418'
@ -91,10 +91,10 @@ jobs:
inputs: inputs:
actions: 'build' actions: 'build'
scheme: '' scheme: ''
sdk: 'macosx11.1' sdk: 'macosx12.3'
configuration: 'Release' configuration: 'Release'
xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
xcodeVersion: '12' # Options: 8, 9, default, specifyPath xcodeVersion: '13' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./' args: '-derivedDataPath ./'
- task: CmdLine@2 - task: CmdLine@2

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

@ -195,10 +195,10 @@ namespace ControlCatalog.Pages
{ {
// Sync disposal of StreamWriter is not supported on WASM // Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await using var stream = await file.OpenWrite(); await using var stream = await file.OpenWriteAsync();
await using var reader = new System.IO.StreamWriter(stream); await using var reader = new System.IO.StreamWriter(stream);
#else #else
using var stream = await file.OpenWrite(); using var stream = await file.OpenWriteAsync();
using var reader = new System.IO.StreamWriter(stream); using var reader = new System.IO.StreamWriter(stream);
#endif #endif
await reader.WriteLineAsync(openedFileContent.Text); await reader.WriteLineAsync(openedFileContent.Text);
@ -243,8 +243,8 @@ namespace ControlCatalog.Pages
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items) async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{ {
items ??= Array.Empty<IStorageItem>(); items ??= Array.Empty<IStorageItem>();
var mappedResults = items.Select(FullPathOrName).ToList(); bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark";
bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark"; var mappedResults = new List<string>();
if (items.FirstOrDefault() is IStorageItem item) if (items.FirstOrDefault() is IStorageItem item)
{ {
@ -267,9 +267,9 @@ Content:
if (file.CanOpenRead) if (file.CanOpenRead)
{ {
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await using var stream = await file.OpenRead(); await using var stream = await file.OpenReadAsync();
#else #else
using var stream = await file.OpenRead(); using var stream = await file.OpenReadAsync();
#endif #endif
using var reader = new System.IO.StreamReader(stream); using var reader = new System.IO.StreamReader(stream);
@ -293,7 +293,19 @@ Content:
lastSelectedDirectory = await item.GetParentAsync(); lastSelectedDirectory = await item.GetParentAsync();
if (lastSelectedDirectory is not null) if (lastSelectedDirectory is not null)
{ {
mappedResults.Insert(0, "Parent: " + FullPathOrName(lastSelectedDirectory)); mappedResults.Add(FullPathOrName(lastSelectedDirectory));
}
foreach (var selectedItem in items)
{
mappedResults.Add("+> " + FullPathOrName(selectedItem));
if (selectedItem is IStorageFolder folder)
{
foreach (var innerItems in await folder.GetItemsAsync())
{
mappedResults.Add("++> " + FullPathOrName(innerItems));
}
}
} }
} }

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

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -35,13 +36,13 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
public bool CanBookmark => true; public bool CanBookmark => true;
public Task<string?> SaveBookmark() public Task<string?> SaveBookmarkAsync()
{ {
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.FromResult(Uri.ToString()); return Task.FromResult(Uri.ToString());
} }
public Task ReleaseBookmark() public Task ReleaseBookmarkAsync()
{ {
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.CompletedTask; return Task.CompletedTask;
@ -106,6 +107,30 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
{ {
return Task.FromResult(new StorageItemProperties()); return Task.FromResult(new StorageItemProperties());
} }
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
var files = await javaFile.ListFilesAsync().ConfigureAwait(false);
if (files is null)
{
return Array.Empty<IStorageItem>();
}
return files
.Select(f => (file: f, uri: AndroidUri.FromFile(f)))
.Where(t => t.uri is not null)
.Select(t => t.file switch
{
{ IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!),
{ IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!),
_ => null
})
.Where(i => i is not null)
.ToArray()!;
}
} }
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
@ -118,10 +143,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public Task<Stream> OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));
public Task<Stream> OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)

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

@ -47,22 +47,22 @@ public class BclStorageFile : IStorageBookmarkFile
return Task.FromResult<IStorageFolder?>(null); return Task.FromResult<IStorageFolder?>(null);
} }
public Task<Stream> OpenRead() public Task<Stream> OpenReadAsync()
{ {
return Task.FromResult<Stream>(_fileInfo.OpenRead()); return Task.FromResult<Stream>(_fileInfo.OpenRead());
} }
public Task<Stream> OpenWrite() public Task<Stream> OpenWriteAsync()
{ {
return Task.FromResult<Stream>(_fileInfo.OpenWrite()); return Task.FromResult<Stream>(_fileInfo.OpenWrite());
} }
public virtual Task<string?> SaveBookmark() public virtual Task<string?> SaveBookmarkAsync()
{ {
return Task.FromResult<string?>(_fileInfo.FullName); return Task.FromResult<string?>(_fileInfo.FullName);
} }
public Task ReleaseBookmark() public Task ReleaseBookmarkAsync()
{ {
// No-op // No-op
return Task.CompletedTask; return Task.CompletedTask;

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

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -43,12 +45,22 @@ public class BclStorageFolder : IStorageBookmarkFolder
return Task.FromResult<IStorageFolder?>(null); return Task.FromResult<IStorageFolder?>(null);
} }
public virtual Task<string?> SaveBookmark() public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
var items = _directoryInfo.GetDirectories()
.Select(d => (IStorageItem)new BclStorageFolder(d))
.Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f)))
.ToArray();
return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
}
public virtual Task<string?> SaveBookmarkAsync()
{ {
return Task.FromResult<string?>(_directoryInfo.FullName); return Task.FromResult<string?>(_directoryInfo.FullName);
} }
public Task ReleaseBookmark() public Task ReleaseBookmarkAsync()
{ {
// No-op // No-op
return Task.CompletedTask; return Task.CompletedTask;

2
src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs

@ -6,7 +6,7 @@ namespace Avalonia.Platform.Storage;
[NotClientImplementable] [NotClientImplementable]
public interface IStorageBookmarkItem : IStorageItem public interface IStorageBookmarkItem : IStorageItem
{ {
Task ReleaseBookmark(); Task ReleaseBookmarkAsync();
} }
[NotClientImplementable] [NotClientImplementable]

4
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem
/// <summary> /// <summary>
/// Opens a stream for read access. /// Opens a stream for read access.
/// </summary> /// </summary>
Task<Stream> OpenRead(); Task<Stream> OpenReadAsync();
/// <summary> /// <summary>
/// Returns true, if file is writeable. /// Returns true, if file is writeable.
@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem
/// <summary> /// <summary>
/// Opens stream for writing to the file. /// Opens stream for writing to the file.
/// </summary> /// </summary>
Task<Stream> OpenWrite(); Task<Stream> OpenWriteAsync();
} }

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

@ -1,4 +1,6 @@
using Avalonia.Metadata; using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage; namespace Avalonia.Platform.Storage;
@ -8,4 +10,11 @@ namespace Avalonia.Platform.Storage;
[NotClientImplementable] [NotClientImplementable]
public interface IStorageFolder : IStorageItem public interface IStorageFolder : IStorageItem
{ {
/// <summary>
/// Gets the files and subfolders in the current folder.
/// </summary>
/// <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();
} }

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

@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable
/// <returns> /// <returns>
/// Returns identifier of a bookmark. Can be null if OS denied request. /// Returns identifier of a bookmark. Can be null if OS denied request.
/// </returns> /// </returns>
Task<string?> SaveBookmark(); Task<string?> SaveBookmarkAsync();
/// <summary> /// <summary>
/// Gets the parent folder of the current storage item. /// Gets the parent folder of the current storage item.

23
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml

@ -1,15 +1,14 @@
<Styles xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pc="using:Avalonia.Controls.Primitives.Converters" xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
x:CompileBindings="True"> x:CompileBindings="True">
<Styles.Resources> <pc:AccentColorConverter x:Key="AccentColorConverter" />
<pc:AccentColorConverter x:Key="AccentColorConverter" /> <x:Double x:Key="ColorPreviewerAccentSectionWidth">80</x:Double>
<x:Double x:Key="ColorPreviewerAccentSectionWidth">80</x:Double> <x:Double x:Key="ColorPreviewerAccentSectionHeight">40</x:Double>
<x:Double x:Key="ColorPreviewerAccentSectionHeight">40</x:Double>
</Styles.Resources>
<Style Selector="ColorPreviewer"> <ControlTheme x:Key="{x:Type ColorPreviewer}"
TargetType="ColorPreviewer">
<Setter Property="Height" Value="70" /> <Setter Property="Height" Value="70" />
<Setter Property="CornerRadius" Value="0" /> <Setter Property="CornerRadius" Value="0" />
<Setter Property="Template"> <Setter Property="Template">
@ -97,6 +96,6 @@
</Panel> </Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </ControlTheme>
</Styles> </ResourceDictionary>

346
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml

@ -1,188 +1,190 @@
<Styles xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True"> x:CompileBindings="True">
<Style Selector="Thumb.ColorSliderThumbStyle"> <ControlTheme x:Key="ColorSliderThumbTheme"
<Setter Property="BorderThickness" Value="0" /> TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10" /> CornerRadius="{TemplateBinding CornerRadius}" />
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </ControlTheme>
<Style Selector="ColorSlider:horizontal"> <ControlTheme x:Key="{x:Type ColorSlider}"
<Setter Property="BorderThickness" Value="0" /> TargetType="ColorSlider">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Height" Value="20" />
<Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Horizontal">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseButton>
<Thumb Classes="ColorSliderThumbStyle"
Name="ColorSliderThumb"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Height}" />
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ColorSlider:vertical"> <Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="10" />
<Setter Property="Width" Value="20" /> <Setter Property="Height" Value="20" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch" <Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}" Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Track Name="PART_Track" <Track Name="PART_Track"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}" Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}" IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Vertical"> Orientation="Horizontal">
<Track.DecreaseButton> <Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton" <RepeatButton Name="PART_DecreaseButton"
Background="Transparent" Background="Transparent"
Focusable="False" Focusable="False"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<RepeatButton.Template> <RepeatButton.Template>
<ControlTemplate> <ControlTemplate>
<Border Name="FocusTarget" <Border Name="FocusTarget"
Background="Transparent" Background="Transparent"
Margin="0,-10" /> Margin="0,-10" />
</ControlTemplate> </ControlTemplate>
</RepeatButton.Template> </RepeatButton.Template>
</RepeatButton> </RepeatButton>
</Track.DecreaseButton> </Track.DecreaseButton>
<Track.IncreaseButton> <Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton" <RepeatButton Name="PART_IncreaseButton"
Background="Transparent" Background="Transparent"
Focusable="False" Focusable="False"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<RepeatButton.Template> <RepeatButton.Template>
<ControlTemplate> <ControlTemplate>
<Border Name="FocusTarget" <Border Name="FocusTarget"
Background="Transparent" Background="Transparent"
Margin="0,-10" /> Margin="0,-10" />
</ControlTemplate> </ControlTemplate>
</RepeatButton.Template> </RepeatButton.Template>
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Classes="ColorSliderThumbStyle" <Thumb Name="ColorSliderThumb"
Name="ColorSliderThumb" Theme="{StaticResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Width}" Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}" /> Width="{TemplateBinding Height}" />
</Track> </Track>
</Grid> </Grid>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
<!-- Normal State --> <Style Selector="^:vertical">
<Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle"> <Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" /> <Setter Property="CornerRadius" Value="10" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" /> <Setter Property="Width" Value="20" />
<Setter Property="BorderThickness" Value="3" /> <Setter Property="Template">
</Style> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Vertical">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseButton>
<Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Width}"
Width="{TemplateBinding Width}" />
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<!-- Selector/Thumb Color -->
<Style Selector="^:pointerover /template/ Thumb#ColorSliderThumb">
<Setter Property="Opacity" Value="0.75" />
</Style>
<Style Selector="^:pointerover:dark-selector /template/ Thumb#ColorSliderThumb">
<Setter Property="Opacity" Value="0.7" />
</Style>
<Style Selector="^:pointerover:light-selector /template/ Thumb#ColorSliderThumb">
<Setter Property="Opacity" Value="0.8" />
</Style>
<!-- Selector/Thumb Color --> <Style Selector="^:dark-selector /template/ Thumb#ColorSliderThumb">
<Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle"> <Setter Property="BorderBrush" Value="Black" />
<Setter Property="Opacity" Value="0.75" /> </Style>
</Style> <Style Selector="^:light-selector /template/ Thumb#ColorSliderThumb">
<Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle"> <Setter Property="BorderBrush" Value="White" />
<Setter Property="Opacity" Value="0.7" /> </Style>
</Style>
<Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.8" />
</Style>
<Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle"> </ControlTheme>
<Setter Property="BorderBrush" Value="Black" />
</Style>
<Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="BorderBrush" Value="White" />
</Style>
</Styles> </ResourceDictionary>

94
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

@ -1,9 +1,10 @@
<Styles xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Controls" xmlns:controls="using:Avalonia.Controls"
x:CompileBindings="True"> x:CompileBindings="True">
<Style Selector="ColorSpectrum"> <ControlTheme x:Key="{x:Type ColorSpectrum}"
TargetType="ColorSpectrum">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type ColorSpectrum}"> <ControlTemplate TargetType="{x:Type ColorSpectrum}">
@ -79,50 +80,51 @@
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style>
<!-- Normal --> <!-- Normal -->
<!-- Separating this allows easier customization in applications --> <!-- Separating this allows easier customization in applications -->
<Style Selector="ColorSpectrum /template/ Ellipse#BorderEllipse, <Style Selector="^ /template/ Ellipse#BorderEllipse,
ColorSpectrum /template/ Rectangle#BorderRectangle"> ^ /template/ Rectangle#BorderRectangle">
<Setter Property="Stroke" Value="{DynamicResource ThemeBorderLowBrush}" /> <Setter Property="Stroke" Value="{DynamicResource ThemeBorderLowBrush}" />
<Setter Property="StrokeThickness" Value="1" /> <Setter Property="StrokeThickness" Value="1" />
</Style> </Style>
<!-- Focus --> <!-- Focus -->
<Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse"> <Style Selector="^ /template/ Ellipse#FocusEllipse">
<Setter Property="IsVisible" Value="False" /> <Setter Property="IsVisible" Value="False" />
</Style> </Style>
<Style Selector="ColorSpectrum:focus-visible /template/ Ellipse#FocusEllipse"> <Style Selector="^:focus-visible /template/ Ellipse#FocusEllipse">
<Setter Property="IsVisible" Value="True" /> <Setter Property="IsVisible" Value="True" />
</Style> </Style>
<!-- Selector Color --> <!-- Selector Color -->
<Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse"> <Style Selector="^ /template/ Ellipse#FocusEllipse">
<Setter Property="Stroke" Value="White" /> <Setter Property="Stroke" Value="White" />
</Style> </Style>
<Style Selector="ColorSpectrum /template/ Ellipse#SelectionEllipse"> <Style Selector="^ /template/ Ellipse#SelectionEllipse">
<Setter Property="Stroke" Value="Black" /> <Setter Property="Stroke" Value="Black" />
</Style> </Style>
<Style Selector="ColorSpectrum:light-selector /template/ Ellipse#FocusEllipse"> <Style Selector="^:light-selector /template/ Ellipse#FocusEllipse">
<Setter Property="Stroke" Value="Black" /> <Setter Property="Stroke" Value="Black" />
</Style> </Style>
<Style Selector="ColorSpectrum:light-selector /template/ Ellipse#SelectionEllipse"> <Style Selector="^:light-selector /template/ Ellipse#SelectionEllipse">
<Setter Property="Stroke" Value="White" /> <Setter Property="Stroke" Value="White" />
</Style> </Style>
<Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse"> <Style Selector="^:pointerover /template/ Ellipse#SelectionEllipse">
<Setter Property="Opacity" Value="0.8" /> <Setter Property="Opacity" Value="0.8" />
</Style> </Style>
<!-- Selector Size --> <!-- Selector Size -->
<Style Selector="ColorSpectrum /template/ Panel#PART_SelectionEllipsePanel"> <Style Selector="^ /template/ Panel#PART_SelectionEllipsePanel">
<Setter Property="Width" Value="16" /> <Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" /> <Setter Property="Height" Value="16" />
</Style> </Style>
<Style Selector="ColorSpectrum:large-selector /template/ Panel#PART_SelectionEllipsePanel"> <Style Selector="^:large-selector /template/ Panel#PART_SelectionEllipsePanel">
<Setter Property="Width" Value="48" /> <Setter Property="Width" Value="48" />
<Setter Property="Height" Value="48" /> <Setter Property="Height" Value="48" />
</Style> </Style>
</Styles> </ControlTheme>
</ResourceDictionary>

76
src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml

@ -3,42 +3,48 @@
xmlns:converters="using:Avalonia.Controls.Converters"> xmlns:converters="using:Avalonia.Controls.Converters">
<Styles.Resources> <Styles.Resources>
<!-- Shared Resources --> <ResourceDictionary>
<VisualBrush x:Key="ColorControlCheckeredBackgroundBrush"
TileMode="Tile" <!-- Shared Resources -->
Stretch="Uniform" <VisualBrush x:Key="ColorControlCheckeredBackgroundBrush"
DestinationRect="0,0,8,8"> TileMode="Tile"
<VisualBrush.Visual> Stretch="Uniform"
<DrawingPresenter Width="8" DestinationRect="0,0,8,8">
Height="8"> <VisualBrush.Visual>
<DrawingGroup> <DrawingPresenter Width="8"
<GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z" Height="8">
Brush="Transparent" /> <DrawingGroup>
<GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z" <GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
Brush="#19808080" /> Brush="Transparent" />
</DrawingGroup> <GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
</DrawingPresenter> Brush="#19808080" />
</VisualBrush.Visual> </DrawingGroup>
</VisualBrush> </DrawingPresenter>
</VisualBrush.Visual>
<!-- Shared Converters --> </VisualBrush>
<converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
<converters:ToBrushConverter x:Key="ToBrushConverter" /> <!-- Shared Converters -->
<converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/> <converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
<converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/> <converters:ToBrushConverter x:Key="ToBrushConverter" />
<converters:CornerRadiusFilterConverter x:Key="TopCornerRadiusFilterConverter" Filter="TopLeft, TopRight"/> <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
<converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight"/> <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadiusConverter" Corner="TopLeft" /> <converters:CornerRadiusFilterConverter x:Key="TopCornerRadiusFilterConverter" Filter="TopLeft, TopRight"/>
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadiusConverter" Corner="BottomRight" /> <converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight"/>
</Styles.Resources> <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadiusConverter" Corner="TopLeft" />
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadiusConverter" Corner="BottomRight" />
<!-- Primitives --> <ResourceDictionary.MergedDictionaries>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" />
<!-- Controls --> <!-- Primitives -->
<!-- Note the ColorPicker and ColorView are unsupported in the default theme --> <ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" />
<!-- These controls depend on fluent styles for TabControl, Button, TextBox, etc. --> <ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" />
<ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" />
<!-- Controls -->
<!-- Note the ColorPicker and ColorView are unsupported in the default theme -->
<!-- These controls depend on fluent styles for TabControl, Button, TextBox, etc. -->
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>
</Styles> </Styles>

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

@ -3,9 +3,6 @@
xmlns:controls="using:Avalonia.Controls" xmlns:controls="using:Avalonia.Controls"
x:CompileBindings="True"> x:CompileBindings="True">
<!-- This must follow OverlayCornerRadius -->
<CornerRadius x:Key="TopOverlayCornerRadius">5,5,0,0</CornerRadius>
<ControlTheme x:Key="{x:Type ColorPicker}" <ControlTheme x:Key="{x:Type ColorPicker}"
TargetType="ColorPicker"> TargetType="ColorPicker">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" /> <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
@ -25,7 +22,7 @@
Padding="0,0,10,0" Padding="0,0,10,0"
UseLayoutRounding="False"> UseLayoutRounding="False">
<DropDownButton.Styles> <DropDownButton.Styles>
<Style Selector="FlyoutPresenter.NoPadding"> <Style Selector="FlyoutPresenter.nopadding">
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
</Style> </Style>
</DropDownButton.Styles> </DropDownButton.Styles>
@ -45,7 +42,7 @@
</Panel> </Panel>
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="NoPadding"> <Flyout FlyoutPresenterClasses="nopadding">
<ColorView x:Name="FlyoutColorView" <ColorView x:Name="FlyoutColorView"
Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
@ -73,7 +70,12 @@
PaletteColors="{TemplateBinding PaletteColors}" PaletteColors="{TemplateBinding PaletteColors}"
PaletteColumnCount="{TemplateBinding PaletteColumnCount}" PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
Palette="{TemplateBinding Palette}" Palette="{TemplateBinding Palette}"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" /> SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<ColorView.Resources>
<!-- This radius must follow OverlayCornerRadius -->
<CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">5,5,0,0</CornerRadius>
</ColorView.Resources>
</ColorView>
</Flyout> </Flyout>
</DropDownButton.Flyout> </DropDownButton.Flyout>
</DropDownButton> </DropDownButton>
@ -81,10 +83,4 @@
</Setter> </Setter>
</ControlTheme> </ControlTheme>
<!-- Adjust Background within Flyout -->
<!-- Note: This is implemented but there seems to be an issue and the selector can't match across the Flyout -->
<!--<Style Selector="ColorPicker /template/ ColorView#FlyoutColorView /template/ Border#TabBackgroundBorder">
<Setter Property="CornerRadius" Value="{DynamicResource TopOverlayCornerRadius}" />
</Style>-->
</ResourceDictionary> </ResourceDictionary>

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

@ -77,6 +77,8 @@
17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5
13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z
</PathGeometry> </PathGeometry>
<!-- This radius should follow ControlCornerRadius -->
<CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">3</CornerRadius>
<ControlTheme x:Key="{x:Type ColorView}" <ControlTheme x:Key="{x:Type ColorView}"
TargetType="ColorView"> TargetType="ColorView">
@ -97,7 +99,7 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundBaseLowBrush}" Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
CornerRadius="{TemplateBinding CornerRadius}" /> CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" />
<Border x:Name="ContentBackgroundBorder" <Border x:Name="ContentBackgroundBorder"
Grid.Row="0" Grid.Row="0"
Grid.RowSpan="2" Grid.RowSpan="2"

56
src/Avalonia.Controls/Button.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Windows.Input; using System.Windows.Input;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
@ -281,24 +282,29 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
if (e.Key == Key.Enter) switch (e.Key)
{ {
OnClick(); case Key.Enter:
e.Handled = true;
}
else if (e.Key == Key.Space)
{
if (ClickMode == ClickMode.Press)
{
OnClick(); OnClick();
e.Handled = true;
break;
case Key.Space:
{
if (ClickMode == ClickMode.Press)
{
OnClick();
}
IsPressed = true;
e.Handled = true;
break;
} }
IsPressed = true;
e.Handled = true; case Key.Escape when Flyout != null:
} // If Flyout doesn't have focusable content, close the flyout here
else if (e.Key == Key.Escape && Flyout != null) CloseFlyout();
{ break;
// If Flyout doesn't have focusable content, close the flyout here
Flyout.Hide();
} }
base.OnKeyDown(e); base.OnKeyDown(e);
@ -327,7 +333,14 @@ namespace Avalonia.Controls
{ {
if (IsEffectivelyEnabled) if (IsEffectivelyEnabled)
{ {
OpenFlyout(); if (_isFlyoutOpen)
{
CloseFlyout();
}
else
{
OpenFlyout();
}
var e = new RoutedEventArgs(ClickEvent); var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e); RaiseEvent(e);
@ -348,6 +361,14 @@ namespace Avalonia.Controls
Flyout?.ShowAt(this); Flyout?.ShowAt(this);
} }
/// <summary>
/// Closes the button's flyout.
/// </summary>
protected virtual void CloseFlyout()
{
Flyout?.Hide();
}
/// <summary> /// <summary>
/// Invoked when the button's flyout is opened. /// Invoked when the button's flyout is opened.
/// </summary> /// </summary>
@ -494,8 +515,7 @@ namespace Avalonia.Controls
// If flyout is changed while one is already open, make sure we // If flyout is changed while one is already open, make sure we
// close the old one first // close the old one first
if (oldFlyout != null && if (oldFlyout != null && oldFlyout.IsOpen)
oldFlyout.IsOpen)
{ {
oldFlyout.Hide(); oldFlyout.Hide();
} }

57
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -12,17 +12,12 @@ namespace Avalonia.Controls.Primitives
{ {
public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
{ {
static FlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
/// <summary> /// <summary>
/// Defines the <see cref="IsOpen"/> property /// Defines the <see cref="IsOpen"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<FlyoutBase, bool> IsOpenProperty = public static readonly DirectProperty<FlyoutBase, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen), AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen),
x => x.IsOpen); x => x.IsOpen);
/// <summary> /// <summary>
/// Defines the <see cref="Target"/> property /// Defines the <see cref="Target"/> property
@ -43,6 +38,14 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode), AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode),
x => x.ShowMode, (x, v) => x.ShowMode = v); x => x.ShowMode, (x, v) => x.ShowMode = v);
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly DirectProperty<FlyoutBase, IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>(
o => o._overlayInputPassThroughElement,
(o, v) => o._overlayInputPassThroughElement = v);
/// <summary> /// <summary>
/// Defines the AttachedFlyout property /// Defines the AttachedFlyout property
/// </summary> /// </summary>
@ -57,6 +60,12 @@ namespace Avalonia.Controls.Primitives
private PixelRect? _enlargePopupRectScreenPixelRect; private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable; private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler; private Action<IPopupHost?>? _popupHostChangedHandler;
private IInputElement? _overlayInputPassThroughElement;
static FlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
public FlyoutBase() public FlyoutBase()
{ {
@ -101,11 +110,21 @@ namespace Avalonia.Controls.Primitives
private set => SetAndRaise(TargetProperty, ref _target, value); private set => SetAndRaise(TargetProperty, ref _target, value);
} }
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the flyout's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => _overlayInputPassThroughElement;
set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{ {
add => _popupHostChangedHandler += value; add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value; remove => _popupHostChangedHandler -= value;
} }
@ -175,8 +194,9 @@ namespace Avalonia.Controls.Primitives
IsOpen = false; IsOpen = false;
Popup.IsOpen = false; Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null); ((ISetLogicalParent)Popup).SetParent(null);
// Ensure this isn't active // Ensure this isn't active
_transientDisposable?.Dispose(); _transientDisposable?.Dispose();
_transientDisposable = null; _transientDisposable = null;
@ -231,6 +251,8 @@ namespace Avalonia.Controls.Primitives
Popup.Child = CreatePresenter(); Popup.Child = CreatePresenter();
} }
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
if (CancelOpening()) if (CancelOpening())
{ {
return false; return false;
@ -356,10 +378,13 @@ namespace Avalonia.Controls.Primitives
private Popup CreatePopup() private Popup CreatePopup()
{ {
var popup = new Popup(); var popup = new Popup
popup.WindowManagerAddShadowHint = false; {
popup.IsLightDismissEnabled = true; WindowManagerAddShadowHint = false,
popup.OverlayDismissEventPassThrough = true; IsLightDismissEnabled = true,
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
OverlayDismissEventPassThrough = false
};
popup.Opened += OnPopupOpened; popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed; popup.Closed += OnPopupClosed;
@ -372,7 +397,7 @@ namespace Avalonia.Controls.Primitives
{ {
IsOpen = true; IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup!.Host); _popupHostChangedHandler?.Invoke(Popup.Host);
} }
private void OnPopupClosing(object? sender, CancelEventArgs e) private void OnPopupClosing(object? sender, CancelEventArgs e)

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

@ -501,7 +501,7 @@ namespace Avalonia.Controls.Primitives
if (dismissLayer != null) if (dismissLayer != null)
{ {
dismissLayer.IsVisible = true; dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement;
Disposable.Create(() => Disposable.Create(() =>
{ {

33
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@ -145,7 +145,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
public bool CanBookmark => true; public bool CanBookmark => true;
public Task<string?> SaveBookmark() public Task<string?> SaveBookmarkAsync()
{ {
return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask(); return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask();
} }
@ -155,7 +155,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
return Task.FromResult<IStorageFolder?>(null); return Task.FromResult<IStorageFolder?>(null);
} }
public Task ReleaseBookmark() public Task ReleaseBookmarkAsync()
{ {
return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask(); return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask();
} }
@ -174,7 +174,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
} }
public bool CanOpenRead => true; public bool CanOpenRead => true;
public async Task<Stream> OpenRead() public async Task<Stream> OpenReadAsync()
{ {
var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead"); var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead");
// Remove maxAllowedSize limit, as developer can decide if they read only small part or everything. // Remove maxAllowedSize limit, as developer can decide if they read only small part or everything.
@ -182,7 +182,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
} }
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public async Task<Stream> OpenWrite() public async Task<Stream> OpenWriteAsync()
{ {
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties");
var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite"); var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite");
@ -196,5 +196,30 @@ namespace Avalonia.Web.Blazor.Interop.Storage
public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle)
{ {
} }
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
var items = await FileHandle.InvokeAsync<IJSInProcessObjectReference?>("getItems");
if (items is null)
{
return Array.Empty<IStorageItem>();
}
var count = items.Invoke<int>("count");
return Enumerable.Range(0, count)
.Select(index =>
{
var reference = items.Invoke<IJSInProcessObjectReference>("at", index);
return reference.Invoke<string>("getKind") switch
{
"directory" => (IStorageItem)new JSStorageFolder(reference),
"file" => new JSStorageFile(reference),
_ => null
};
})
.Where(i => i is not null)
.ToArray()!;
}
} }
} }

45
src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts

@ -14,6 +14,8 @@ declare global {
queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>;
} }
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; type StartInDirectory = WellKnownDirectory | FileSystemFileHandle;
@ -53,7 +55,7 @@ class IndexedDbWrapper {
} }
public connect(): Promise<InnerDbConnection> { public connect(): Promise<InnerDbConnection> {
var conn = window.indexedDB.open(this.databaseName, 1); const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => { conn.onupgradeneeded = event => {
const db = (<IDBRequest<IDBDatabase>>event.target).result; const db = (<IDBRequest<IDBDatabase>>event.target).result;
@ -85,7 +87,7 @@ class InnerDbConnection {
const os = this.openStore(store, "readwrite"); const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var response = os.put(obj, key); const response = os.put(obj, key);
response.onsuccess = () => { response.onsuccess = () => {
resolve(response.result); resolve(response.result);
}; };
@ -99,7 +101,7 @@ class InnerDbConnection {
const os = this.openStore(store, "readonly"); const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var response = os.get(key); const response = os.get(key);
response.onsuccess = () => { response.onsuccess = () => {
resolve(response.result); resolve(response.result);
}; };
@ -113,7 +115,7 @@ class InnerDbConnection {
const os = this.openStore(store, "readwrite"); const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var response = os.delete(key); const response = os.delete(key);
response.onsuccess = () => { response.onsuccess = () => {
resolve(); resolve();
}; };
@ -134,17 +136,20 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
]) ])
class StorageItem { class StorageItem {
constructor(private handle: FileSystemFileHandle, private bookmarkId?: string) { } constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { }
public getName(): string { public getName(): string {
return this.handle.name return this.handle.name
} }
public getKind(): string {
return this.handle.kind;
}
public async openRead(): Promise<Blob> { public async openRead(): Promise<Blob> {
await this.verityPermissions('read'); await this.verityPermissions('read');
var file = await this.handle.getFile(); return await this.handle.getFile();
return file;
} }
public async openWrite(): Promise<FileSystemWritableFileStream> { public async openWrite(): Promise<FileSystemWritableFileStream> {
@ -154,7 +159,7 @@ class StorageItem {
} }
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> { public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> {
var file = this.handle.getFile && await this.handle.getFile(); const file = this.handle.getFile && await this.handle.getFile();
return file && { return file && {
Size: file.size, Size: file.size,
@ -163,6 +168,18 @@ class StorageItem {
} }
} }
public async getItems(): Promise<StorageItems> {
if (this.handle.kind !== "directory"){
return new StorageItems([]);
}
const items: StorageItem[] = [];
for await (const [key, value] of this.handle.entries()) {
items.push(new StorageItem(value));
}
return new StorageItems(items);
}
private async verityPermissions(mode: PermissionsMode): Promise<void | never> { private async verityPermissions(mode: PermissionsMode): Promise<void | never> {
if (await this.handle.queryPermission({ mode }) === 'granted') { if (await this.handle.queryPermission({ mode }) === 'granted') {
return; return;
@ -235,12 +252,12 @@ export class StorageProvider {
} }
public static async selectFolderDialog( public static async selectFolderDialog(
startIn: StartInDirectory | null) startIn: StorageItem | null)
: Promise<StorageItem> { : Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options: DirectoryPickerOptions = { const options: DirectoryPickerOptions = {
startIn: (startIn || undefined) startIn: (startIn?.handle || undefined)
}; };
const handle = await window.showDirectoryPicker(options); const handle = await window.showDirectoryPicker(options);
@ -248,12 +265,12 @@ export class StorageProvider {
} }
public static async openFileDialog( public static async openFileDialog(
startIn: StartInDirectory | null, multiple: boolean, startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean)
: Promise<StorageItems> { : Promise<StorageItems> {
const options: OpenFilePickerOptions = { const options: OpenFilePickerOptions = {
startIn: (startIn || undefined), startIn: (startIn?.handle || undefined),
multiple, multiple,
excludeAcceptAllOption, excludeAcceptAllOption,
types: (types || undefined) types: (types || undefined)
@ -264,12 +281,12 @@ export class StorageProvider {
} }
public static async saveFileDialog( public static async saveFileDialog(
startIn: StartInDirectory | null, suggestedName: string | null, startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean)
: Promise<StorageItem> { : Promise<StorageItem> {
const options: SaveFilePickerOptions = { const options: SaveFilePickerOptions = {
startIn: (startIn || undefined), startIn: (startIn?.handle || undefined),
suggestedName: (suggestedName || undefined), suggestedName: (suggestedName || undefined),
excludeAcceptAllOption, excludeAcceptAllOption,
types: (types || undefined) types: (types || undefined)

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

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
@ -49,13 +51,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent())); return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent()));
} }
public Task ReleaseBookmark() public Task ReleaseBookmarkAsync()
{ {
// no-op // no-op
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<string?> SaveBookmark() public Task<string?> SaveBookmarkAsync()
{ {
try try
{ {
@ -102,12 +104,12 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public Task<Stream> OpenRead() public Task<Stream> OpenReadAsync()
{ {
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read)); return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read));
} }
public Task<Stream> OpenWrite() public Task<Stream> OpenWriteAsync()
{ {
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Write)); return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Write));
} }
@ -118,4 +120,19 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
public IOSStorageFolder(NSUrl url) : base(url) public IOSStorageFolder(NSUrl url) : base(url)
{ {
} }
public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
var content = NSFileManager.DefaultManager.GetDirectoryContent(Url, null, NSDirectoryEnumerationOptions.None, out var error);
if (error is not null)
{
return Task.FromException<IReadOnlyList<IStorageItem>>(new NSErrorException(error));
}
var items = content
.Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u))
.ToArray();
return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
}
} }

Loading…
Cancel
Save