diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 477aaec6a8..d4cde99240 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -8,9 +8,9 @@ "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", - "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", @@ -41,6 +41,7 @@ "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", + "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", @@ -63,4 +64,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} +} \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e5f29abb68..02e3027aa6 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -324,9 +324,9 @@ namespace ControlCatalog.Pages mappedResults.Add("+> " + FullPathOrName(selectedItem)); if (selectedItem is IStorageFolder folder) { - foreach (var innerItems in await folder.GetItemsAsync()) + await foreach (var innerItem in folder.GetItemsAsync()) { - mappedResults.Add("++> " + FullPathOrName(innerItems)); + mappedResults.Add("++> " + FullPathOrName(innerItem)); } } } diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 26430b4b61..7fb5bec589 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -104,8 +104,12 @@ namespace ControlCatalog.Pages } else if (item is IStorageFolder folder) { - var items = await folder.GetItemsAsync(); - contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + var childrenCount = 0; + await foreach (var _ in folder.GetItemsAsync()) + { + childrenCount++; + } + contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}"; } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 0a34e6077c..8052b3911a 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -131,19 +131,17 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return Task.FromResult(new StorageItemProperties()); } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { if (!await EnsureExternalFilesPermission(false)) { - return Array.Empty(); + yield break; } - - List files = new List(); - + var contentResolver = Activity.ContentResolver; if (contentResolver == null) { - return files; + yield break; } var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri)); @@ -168,12 +166,10 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder continue; } - files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : - new AndroidStorageFile(Activity, uri)); + yield return mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : + new AndroidStorageFile(Activity, uri); } } - - return files; } } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index bda30c08fb..e98d9f0517 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.PropertyStore; -using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -20,12 +19,20 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; + + /// + /// Provides a metadata object for types which have no metadata of their own. + /// private readonly AvaloniaPropertyMetadata _defaultMetadata; + + /// + /// Provides a fast path when the property has no metadata overrides. + /// + private KeyValuePair? _singleMetadata; + private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); - private bool _hasMetadataOverrides; - /// /// Initializes a new instance of the class. /// @@ -57,7 +64,8 @@ namespace Avalonia Id = s_nextId++; _metadata.Add(ownerType, metadata ?? throw new ArgumentNullException(nameof(metadata))); - _defaultMetadata = metadata; + _defaultMetadata = metadata.GenerateTypeSafeMetadata(); + _singleMetadata = new(ownerType, metadata); } /// @@ -80,9 +88,6 @@ namespace Avalonia Id = source.Id; _defaultMetadata = source._defaultMetadata; - // Properties that have different owner can't use fast path for metadata. - _hasMetadataOverrides = true; - if (metadata != null) { _metadata.Add(ownerType, metadata); @@ -453,33 +458,14 @@ namespace Avalonia } /// - /// Gets the property metadata for the specified type. + /// Gets the which applies to this property when it is used with the specified type. /// - /// The type. - /// - /// The property metadata. - /// - public AvaloniaPropertyMetadata GetMetadata() where T : AvaloniaObject - { - return GetMetadata(typeof(T)); - } + /// The type for which to retrieve metadata. + public AvaloniaPropertyMetadata GetMetadata() where T : AvaloniaObject => GetMetadata(typeof(T)); - /// - /// Gets the property metadata for the specified type. - /// - /// The type. - /// - /// The property metadata. - /// - public AvaloniaPropertyMetadata GetMetadata(Type type) - { - if (!_hasMetadataOverrides) - { - return _defaultMetadata; - } - - return GetMetadataWithOverrides(type); - } + /// + /// The type for which to retrieve metadata. + public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type); /// /// Checks whether the is valid for the property. @@ -578,7 +564,7 @@ namespace Avalonia _metadata.Add(type, metadata); _metadataCache.Clear(); - _hasMetadataOverrides = true; + _singleMetadata = null; } protected abstract IObservable GetChanged(); @@ -595,7 +581,12 @@ namespace Avalonia return result; } - Type? currentType = type; + if (_singleMetadata is { } singleMetadata) + { + return _metadataCache[type] = singleMetadata.Key.IsAssignableFrom(type) ? singleMetadata.Value : _defaultMetadata; + } + + var currentType = type; while (currentType != null) { @@ -609,13 +600,11 @@ namespace Avalonia currentType = currentType.BaseType; } - _metadataCache[type] = _defaultMetadata; - - return _defaultMetadata; + return _metadataCache[type] = _defaultMetadata; } bool IPropertyInfo.CanGet => true; - bool IPropertyInfo.CanSet => true; + bool IPropertyInfo.CanSet => !IsReadOnly; object? IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); void IPropertyInfo.Set(object target, object? value) => ((AvaloniaObject)target).SetValue(this, value); } diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs index 62bb65351f..ec29d14693 100644 --- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs +++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs @@ -5,7 +5,7 @@ namespace Avalonia /// /// Base class for avalonia property metadata. /// - public class AvaloniaPropertyMetadata + public abstract class AvaloniaPropertyMetadata { private BindingMode _defaultBindingMode; @@ -61,5 +61,13 @@ namespace Avalonia EnableDataValidation ??= baseMetadata.EnableDataValidation; } + + /// + /// Gets a copy of this object configured for use with any owner type. + /// + /// + /// For example, delegates which receive the owner object should be removed. + /// + public abstract AvaloniaPropertyMetadata GenerateTypeSafeMetadata(); } } diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 451ff6ce00..5471826f9f 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -45,5 +45,7 @@ namespace Avalonia UnsetValue ??= src.UnsetValue; } } + + public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new DirectPropertyMetadata(UnsetValue, DefaultBindingMode, EnableDataValidation); } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index d8e3d91f75..e6551390d6 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -57,14 +57,16 @@ internal class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } - public Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { - var items = DirectoryInfo.GetDirectories() + var items = DirectoryInfo.EnumerateDirectories() .Select(d => (IStorageItem)new BclStorageFolder(d)) - .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f))) - .ToArray(); + .Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f))); - return Task.FromResult>(items); + foreach (var item in items) + { + yield return item; + } } public virtual Task SaveBookmarkAsync() diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 0ffb9f41c6..52b3256387 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -16,5 +16,5 @@ public interface IStorageFolder : IStorageItem /// /// 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 implementation object. /// - Task> GetItemsAsync(); + IAsyncEnumerable GetItemsAsync(); } diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 5052840013..dfe5a44f1b 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -18,7 +18,10 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. - /// A value validation callback. + /// + /// A method which returns "false" for values that are never valid for this property. + /// This method is not part of the property's metadata and so cannot be changed after registration. + /// /// A callback. public StyledProperty( string name, @@ -41,7 +44,7 @@ namespace Avalonia } /// - /// Gets the value validation callback for the property. + /// A method which returns "false" for values that are never valid for this property. /// public Func? ValidateValue { get; } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 6f10de3651..9db460dba3 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -58,5 +58,7 @@ namespace Avalonia } } } + + public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new StyledPropertyMetadata(DefaultValue, DefaultBindingMode, enableDataValidation: EnableDataValidation ?? false); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 91b65a1f72..72ebf10984 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -6066,8 +6066,9 @@ namespace Avalonia.Controls var numberOfItem = clipboardRowContent.Count; for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) { - var cellContent = clipboardRowContent[cellIndex]; - text.Append(cellContent.Content); + var cellContent = clipboardRowContent[cellIndex].Content?.ToString(); + cellContent = cellContent?.Replace("\"", "\"\""); + text.Append($"\"{cellContent}\""); if (cellIndex < numberOfItem - 1) { text.Append('\t'); diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index cd86045dee..320130bc91 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Platform [Unstable] public interface IPopupImpl : IWindowBaseImpl { - IPopupPositioner PopupPositioner { get; } + IPopupPositioner? PopupPositioner { get; } void SetWindowManagerAddShadowHint(bool enabled); } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index b3436d4176..952ba92e9b 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives private void UpdatePosition() { - PlatformImpl?.PopupPositioner.Update(_positionerParameters); + PlatformImpl?.PopupPositioner?.Update(_positionerParameters); } public void ConfigurePosition(Visual target, PlacementMode placement, Point offset, diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 9ec9013679..d983309a72 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -42,11 +42,12 @@ namespace Avalonia.Controls { Close(control); } - else + else { - var tip = control.GetValue(ToolTip.ToolTipProperty); - - tip!.Content = e.NewValue; + if (control.GetValue(ToolTip.ToolTipProperty) is { } tip) + { + tip.Content = e.NewValue; + } } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6634ab4d7b..22748f6aa3 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -25,6 +25,9 @@ using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo + +#nullable enable + namespace Avalonia.X11 { internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client @@ -35,11 +38,11 @@ namespace Avalonia.X11 private XConfigureEvent? _configure; private PixelPoint? _configurePoint; private bool _triggeredExpose; - private IInputRoot _inputRoot; + private IInputRoot? _inputRoot; private readonly MouseDevice _mouse; private readonly TouchDevice _touch; private readonly IKeyboardDevice _keyboard; - private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; + private readonly ITopLevelNativeMenuExporter? _nativeMenuExporter; private readonly IStorageProvider _storageProvider; private readonly X11NativeControlHost _nativeControlHost; private PixelPoint? _position; @@ -54,8 +57,8 @@ namespace Avalonia.X11 private bool _wasMappedAtLeastOnce = false; private double? _scalingOverride; private bool _disabled; - private TransparencyHelper _transparencyHelper; - private RawEventGrouper _rawEventGrouper; + private TransparencyHelper? _transparencyHelper; + private RawEventGrouper? _rawEventGrouper; private bool _useRenderWindow = false; private bool _usePositioningFlags = false; @@ -66,7 +69,7 @@ namespace Avalonia.X11 WaitPaint } - public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent) { _platform = platform; _popup = popupParent != null; @@ -196,7 +199,7 @@ namespace Avalonia.X11 XFlush(_x11.Display); if(_popup) - PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent!, MoveResize)); if (platform.Options.UseDBusMenu) _nativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle); _nativeControlHost = new X11NativeControlHost(_platform, this); @@ -214,7 +217,7 @@ namespace Avalonia.X11 _storageProvider = new CompositeStorageProvider(new[] { - () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), () => GtkSystemDialog.TryCreate(this) }); } @@ -351,17 +354,17 @@ namespace Avalonia.X11 public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } - public Action Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } //TODO - public Action ScalingChanged { get; set; } - public Action Deactivated { get; set; } - public Action Activated { get; set; } - public Func Closing { get; set; } - public Action WindowStateChanged { get; set; } + public Action? ScalingChanged { get; set; } + public Action? Deactivated { get; set; } + public Action? Activated { get; set; } + public Func? Closing { get; set; } + public Action? WindowStateChanged { get; set; } - public Action TransparencyLevelChanged + public Action? TransparencyLevelChanged { get => _transparencyHelper?.TransparencyLevelChanged; set @@ -371,7 +374,7 @@ namespace Avalonia.X11 } } - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } public Thickness ExtendedMargins { get; } = new Thickness(); @@ -379,15 +382,18 @@ namespace Avalonia.X11 public bool IsClientAreaExtendedToDecorations { get; } - public Action Closed { get; set; } - public Action PositionChanged { get; set; } - public Action LostFocus { get; set; } + public Action? Closed { get; set; } + public Action? PositionChanged { get; set; } + public Action? LostFocus { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, _platform.Compositor, () => Surfaces); private void OnEvent(ref XEvent ev) { + if (_inputRoot is null) + return; + if (ev.type == XEventName.MapNotify) { _mapped = true; @@ -434,7 +440,8 @@ namespace Avalonia.X11 2 => RawPointerEventType.MiddleButtonDown, 3 => RawPointerEventType.RightButtonDown, 8 => RawPointerEventType.XButton1Down, - 9 => RawPointerEventType.XButton2Down + 9 => RawPointerEventType.XButton2Down, + _ => throw new NotSupportedException("Unexepected RawPointerEventType.") }, ref ev, ev.ButtonEvent.state); else @@ -462,7 +469,8 @@ namespace Avalonia.X11 2 => RawPointerEventType.MiddleButtonUp, 3 => RawPointerEventType.RightButtonUp, 8 => RawPointerEventType.XButton1Up, - 9 => RawPointerEventType.XButton2Up + 9 => RawPointerEventType.XButton2Up, + _ => throw new NotSupportedException("Unexepected RawPointerEventType.") }, ref ev, ev.ButtonEvent.state); } @@ -618,7 +626,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -712,6 +720,8 @@ namespace Avalonia.X11 private void DispatchInput(RawInputEventArgs args) { + if (_inputRoot is null) + return; Input?.Invoke(args); if (!args.Handled && args is RawKeyEventArgsWithText text && !string.IsNullOrEmpty(text.Text)) Input?.Invoke(new RawTextInputEventArgs(_keyboard, args.Timestamp, _inputRoot, text.Text)); @@ -744,11 +754,13 @@ namespace Avalonia.X11 if (args is RawDragEvent drag) drag.Location = drag.Location / RenderScaling; - _rawEventGrouper.HandleEvent(args); + _rawEventGrouper?.HandleEvent(args); } private void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods) { + if (_inputRoot is null) + return; var mev = new RawPointerEventArgs( _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot, type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); @@ -783,7 +795,7 @@ namespace Avalonia.X11 } - public IInputRoot InputRoot => _inputRoot; + public IInputRoot? InputRoot => _inputRoot; public void SetInputRoot(IInputRoot inputRoot) { @@ -795,7 +807,7 @@ namespace Avalonia.X11 Cleanup(); } - public virtual object TryGetFeature(Type featureType) + public virtual object? TryGetFeature(Type featureType) { if (featureType == typeof(ITopLevelNativeMenuExporter)) { @@ -953,7 +965,7 @@ namespace Avalonia.X11 UpdateSizeHints(null); } - public void SetCursor(ICursorImpl cursor) + public void SetCursor(ICursorImpl? cursor) { if (cursor == null) XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor); @@ -996,7 +1008,7 @@ namespace Avalonia.X11 public IMouseDevice MouseDevice => _mouse; public TouchDevice TouchDevice => _touch; - public IPopupImpl CreatePopup() + public IPopupImpl? CreatePopup() => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); public void Activate() @@ -1082,7 +1094,7 @@ namespace Avalonia.X11 BeginMoveResize(side, e); } - public void SetTitle(string title) + public void SetTitle(string? title) { if (string.IsNullOrEmpty(title)) { @@ -1161,9 +1173,9 @@ namespace Avalonia.X11 { } - public Action GotInputWhenDisabled { get; set; } + public Action? GotInputWhenDisabled { get; set; } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { if (icon != null) { @@ -1218,7 +1230,7 @@ namespace Avalonia.X11 ); } - public IPopupPositioner PopupPositioner { get; } + public IPopupPositioner? PopupPositioner { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper?.SetTransparencyRequest(transparencyLevel); diff --git a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs index 6e3b41c05b..67d1cfb776 100644 --- a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs +++ b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; namespace Avalonia.Browser.Interop; @@ -8,15 +9,22 @@ internal static partial class GeneralHelpers public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key); public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key); + [JSImport("GeneralHelpers.itemAt", AvaloniaModule.MainModuleName)] + public static partial JSObject ItemAtInt(JSObject jsObject, int key); + public static JSObject GetArrayItem(this JSObject jsObject, int key) => ItemAtInt(jsObject, key); + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key); public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key); [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] - public static partial string IntCallMethodString(JSObject jsObject, string name); + public static partial string IntCallMethodStr(JSObject jsObject, string name); + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodStrStr(JSObject jsObject, string name, string arg1); [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] - public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1); + public static partial Task IntCallMethodPromiseObj(JSObject jsObject, string name); - public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name); - public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1); + public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodStr(jsObject, name); + public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStrStr(jsObject, name, arg1); + public static Task CallMethodObjectAsync(this JSObject jsObject, string name) => IntCallMethodPromiseObj(jsObject, name); } diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 2d96ee8d1f..dc3372d2d0 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -40,9 +40,9 @@ internal static partial class StorageHelper [JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)] public static partial Task OpenRead(JSObject item); - [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)] - [return: JSMarshalAs>] - public static partial Task GetItems(JSObject item); + [JSImport("StorageItem.getItemsIterator", AvaloniaModule.StorageModuleName)] + [return: JSMarshalAs] + public static partial JSObject? GetItemsIterator(JSObject item); [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index fc32b3b4f7..fcb956f294 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -258,24 +258,45 @@ internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder { } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { - using var items = await StorageHelper.GetItems(FileHandle); - if (items is null) + using var itemsIterator = StorageHelper.GetItemsIterator(FileHandle); + if (itemsIterator is null) { - return Array.Empty(); + yield break; } - var itemsArray = StorageHelper.ItemsArray(items); + while (true) + { + var nextResult = await itemsIterator.CallMethodObjectAsync("next"); + if (nextResult is null) + { + yield break; + } + + var isDone = nextResult.GetPropertyAsBoolean("done"); + if (isDone) + { + yield break; + } - return itemsArray - .Select(reference => reference.GetPropertyAsString("kind") switch + var valArray = nextResult.GetPropertyAsJSObject("value"); + var storageItem = valArray?.GetArrayItem(1); // 0 - item name, 1 - item instance + if (storageItem is null) { - "directory" => (IStorageItem)new JSStorageFolder(reference), - "file" => new JSStorageFile(reference), - _ => null - }) - .Where(i => i is not null) - .ToArray()!; + yield break; + } + + var kind = storageItem.GetPropertyAsString("kind"); + switch (kind) + { + case "directory": + yield return new JSStorageFolder(storageItem); + break; + case "file": + yield return new JSStorageFile(storageItem); + break; + } + } } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts index fa001006ab..31d167e38d 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts @@ -1,5 +1,5 @@ export class GeneralHelpers { - public static itemsArrayAt(instance: any, key: string): any[] { + public static itemsArrayAt(instance: any, key: any): any[] { const items = instance[key]; if (!items) { return []; @@ -12,6 +12,11 @@ export class GeneralHelpers { return retItems; } + public static itemAt(instance: any, key: any): any { + const item = instance[key]; + return item; + } + public static callMethod(instance: any, name: string /*, args */): any { const args = Array.prototype.slice.call(arguments, 2); return instance[name].apply(instance, args); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index f444717094..399e268915 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -89,16 +89,12 @@ export class StorageItem { } } - public static async getItems(item: StorageItem): Promise { + public static getItemsIterator(item: StorageItem): any | null { if (item.kind !== "directory" || !item.handle) { - return new StorageItems([]); + return null; } - const items: StorageItem[] = []; - for await (const [, value] of (item.handle as any).entries()) { - items.push(new StorageItem(value)); - } - return new StorageItems(items); + return (item.handle as any).entries(); } private async verityPermissions(mode: "read" | "readwrite"): Promise { diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 27bd8faf64..baf31ebc73 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -114,8 +114,9 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder { } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { + // TODO: find out if it can be lazily enumerated. var tcs = new TaskCompletionSource>(); new NSFileCoordinator().CoordinateRead(Url, @@ -142,6 +143,10 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder throw new NSErrorException(error); } - return await tcs.Task; + var items = await tcs.Task; + foreach (var item in items) + { + yield return item; + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 181596a681..bde750efdc 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using Avalonia.Data; using Avalonia.PropertyStore; -using Avalonia.Styling; -using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests @@ -29,7 +27,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value() { - var metadata = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); Assert.Same(metadata, target.GetMetadata()); @@ -38,26 +36,30 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetMetadata_Returns_Supplied_Value_For_Derived_Class() { - var metadata = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); Assert.Same(metadata, target.GetMetadata()); } [Fact] - public void GetMetadata_Returns_Supplied_Value_For_Unrelated_Class() + public void GetMetadata_Returns_TypeSafe_Metadata_For_Unrelated_Class() { - var metadata = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(BindingMode.OneWayToSource, true, x => { _ = (StyledElement)x; }); var target = new TestProperty("test", typeof(Class3), metadata); - Assert.Same(metadata, target.GetMetadata()); + var targetMetadata = (TestMetadata)target.GetMetadata(); + + Assert.Equal(metadata.DefaultBindingMode, targetMetadata.DefaultBindingMode); + Assert.Equal(metadata.EnableDataValidation, targetMetadata.EnableDataValidation); + Assert.Equal(null, targetMetadata.OwnerSpecificAction); } [Fact] public void GetMetadata_Returns_Overridden_Value() { - var metadata = new AvaloniaPropertyMetadata(); - var overridden = new AvaloniaPropertyMetadata(); + var metadata = new TestMetadata(); + var overridden = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); target.OverrideMetadata(overridden); @@ -68,9 +70,9 @@ namespace Avalonia.Base.UnitTests [Fact] public void OverrideMetadata_Should_Merge_Values() { - var metadata = new AvaloniaPropertyMetadata(BindingMode.TwoWay); + var metadata = new TestMetadata(BindingMode.TwoWay); var notify = (Action)((a, b) => { }); - var overridden = new AvaloniaPropertyMetadata(); + var overridden = new TestMetadata(); var target = new TestProperty("test", typeof(Class1), metadata); target.OverrideMetadata(overridden); @@ -131,15 +133,31 @@ namespace Avalonia.Base.UnitTests [Fact] public void PropertyMetadata_BindingMode_Default_Returns_OneWay() { - var data = new AvaloniaPropertyMetadata(defaultBindingMode: BindingMode.Default); + var data = new TestMetadata(defaultBindingMode: BindingMode.Default); Assert.Equal(BindingMode.OneWay, data.DefaultBindingMode); } + private class TestMetadata : AvaloniaPropertyMetadata + { + public Action OwnerSpecificAction { get; } + + public TestMetadata(BindingMode defaultBindingMode = BindingMode.Default, + bool? enableDataValidation = null, + Action ownerSpecificAction = null) + : base(defaultBindingMode, enableDataValidation) + { + OwnerSpecificAction = ownerSpecificAction; + } + + public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => + new TestMetadata(DefaultBindingMode, EnableDataValidation, null); + } + private class TestProperty : AvaloniaProperty { - public TestProperty(string name, Type ownerType, AvaloniaPropertyMetadata metadata = null) - : base(name, ownerType, metadata ?? new AvaloniaPropertyMetadata()) + public TestProperty(string name, Type ownerType, TestMetadata metadata = null) + : base(name, ownerType, metadata ?? new TestMetadata()) { }