diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index f21d6fefb4..547af5d0dc 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,6 +5,7 @@ + all @@ -60,4 +61,20 @@ + + + + $(IntermediateOutputPath)/AvaloniaVersion.props + + + + + true + build + + + diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 2334aa91bc..a2da228887 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -4,9 +4,11 @@ $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll false + $(UsedAvaloniaProducts);AvaloniaUI + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 23d45edf6a..b8e9f0ff42 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -6,17 +6,11 @@ using Avalonia.Win32.WinRT.Composition; namespace Sandbox { - public class MainWindow : Window + public partial class MainWindow : Window { public MainWindow() { - this.InitializeComponent(); - this.AttachDevTools(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); + InitializeComponent(); } } } diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index f23e391a2a..d2e66988e0 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -4,6 +4,7 @@ WinExe net6.0 true + true @@ -17,4 +18,5 @@ + diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index c555087879..49660e508e 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -42,19 +42,21 @@ namespace Avalonia.Input public class Cursor : IDisposable { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); + private string _name; - internal Cursor(ICursorImpl platformImpl) + private Cursor(ICursorImpl platformImpl, string name) { PlatformImpl = platformImpl; + _name = name; } public Cursor(StandardCursorType cursorType) - : this(GetCursorFactory().GetCursor(cursorType)) + : this(GetCursorFactory().GetCursor(cursorType), cursorType.ToString()) { } public Cursor(IBitmap cursor, PixelPoint hotSpot) - : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot)) + : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") { } @@ -73,5 +75,10 @@ namespace Avalonia.Input { return AvaloniaLocator.Current.GetRequiredService(); } + + public override string ToString() + { + return _name; + } } } diff --git a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs new file mode 100644 index 0000000000..d523808d32 --- /dev/null +++ b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Defines how compiler should split avalonia list string value before parsing individual items. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class AvaloniaListAttribute : Attribute +{ + /// + /// Separator used to split input string. + /// Default value is ','. + /// + public string[]? Separators { get; init; } + + /// + /// Split options used to split input string. + /// Default value is RemoveEmptyEntries with TrimEntries. + /// + // StringSplitOptions.TrimEntries = 2, but only on net6 target. + public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2; +} diff --git a/src/Avalonia.Controls/DefinitionList.cs b/src/Avalonia.Controls/DefinitionList.cs index c850647bf4..63a54731e0 100644 --- a/src/Avalonia.Controls/DefinitionList.cs +++ b/src/Avalonia.Controls/DefinitionList.cs @@ -1,9 +1,11 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Collections; +using Avalonia.Metadata; namespace Avalonia.Controls { + [AvaloniaList(Separators = new [] { ",", " " })] public abstract class DefinitionList : AvaloniaList where T : DefinitionBase { public DefinitionList() diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 57ed67b508..f2b105c901 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -7,8 +7,8 @@ namespace Avalonia.Controls.Generators /// /// /// When creating a container for an item from a , the following - /// method order should be followed: - /// + /// process should be followed: + /// /// - should first be called if the item is /// derived from the class. If this method returns true then the /// item itself should be used as the container. @@ -19,9 +19,29 @@ namespace Avalonia.Controls.Generators /// - The container should then be added to the panel using /// /// - Finally, should be called. - /// - When the item is ready to be recycled, should - /// be called if returned false. /// + /// NOTE: If in the first step above returns true + /// then the above steps should be carried out a single time; the first time the item is + /// displayed. Otherwise the steps should be carried out each time a new container is realized + /// for an item. + /// + /// When unrealizing a container, the following process should be followed: + /// + /// - If for the item returned true then the item + /// cannot be unrealized or recycled. + /// - Otherwise, should be called for the container + /// - If recycling is supported then the container should be added to a recycle pool. + /// - It is assumed that recyclable containers will not be removed from the panel but instead + /// hidden from view using e.g. `container.IsVisible = false`. + /// + /// When recycling an unrealized container, the following process should be followed: + /// + /// - An element should be taken from the recycle pool. + /// - The container should be made visible. + /// - method should be called for the + /// container. + /// - should be called. + /// /// NOTE: Although this class is similar to that found in WPF/UWP, in Avalonia this class only /// concerns itself with generating and clearing item containers; it does not maintain a /// record of the currently realized containers, that responsibility is delegated to the diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index 796ee8433a..5a6f9fc4f9 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -67,9 +67,12 @@ namespace Avalonia.Controls.Presenters for (var i = 0; i < count; ++i) { var c = children[index + i]; + if (!c.IsSet(ItemIsOwnContainerProperty)) + { itemsControl.RemoveLogicalChild(children[i + index]); - generator.ClearItemContainer(c); + generator.ClearItemContainer(c); + } } children.RemoveRange(index, count); diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 8a1cdf3f80..881474c761 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -285,6 +285,11 @@ namespace Avalonia.Controls /// public event EventHandler? Closed; + /// + /// Gets or sets a method called when the TopLevel's scaling changes. + /// + public event EventHandler? ScalingChanged; + /// /// Gets or sets the client size of the window. /// @@ -428,7 +433,7 @@ namespace Avalonia.Controls double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; /// - double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1; + public double RenderScaling => PlatformImpl?.RenderScaling ?? 1; IStyleHost IStyleHost.StylingParent => _globalStyles!; @@ -590,6 +595,7 @@ namespace Avalonia.Controls protected virtual void HandleScalingChanged(double scaling) { LayoutHelper.InvalidateSelfAndChildrenMeasure(this); + ScalingChanged?.Invoke(this, EventArgs.Empty); } private static bool TransparencyLevelsMatch (WindowTransparencyLevel requested, WindowTransparencyLevel received) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 806d7e320b..70ffd218b1 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -45,6 +45,7 @@ namespace Avalonia.Controls private TreeView? _treeView; private Control? _header; + private Control? _headerPresenter; private int _level; private bool _templateApplied; private bool _deferredBringIntoViewFlag; @@ -255,15 +256,16 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if (_header is InputElement previousInputMethod) + if (_headerPresenter is InputElement previousInputMethod) { previousInputMethod.DoubleTapped -= HeaderDoubleTapped; } _header = e.NameScope.Find("PART_Header"); + _headerPresenter = e.NameScope.Find("PART_HeaderPresenter"); _templateApplied = true; - if (_header is InputElement im) + if (_headerPresenter is InputElement im) { im.DoubleTapped += HeaderDoubleTapped; } diff --git a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs index da0ff1eb69..28d6a83309 100644 --- a/src/Avalonia.Controls/VirtualizingCarouselPanel.cs +++ b/src/Avalonia.Controls/VirtualizingCarouselPanel.cs @@ -168,7 +168,13 @@ namespace Avalonia.Controls protected internal override Control? ContainerFromIndex(int index) { - return index == _realizedIndex ? _realized : null; + if (index < 0 || index >= Items.Count) + return null; + if (index == _realizedIndex) + return _realized; + if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty)) + return c; + return null; } protected internal override IEnumerable? GetRealizedContainers() @@ -264,7 +270,6 @@ namespace Avalonia.Controls if (controlItem.IsSet(ItemIsOwnContainerProperty)) { controlItem.IsVisible = true; - generator.ItemContainerPrepared(controlItem, item, index); return controlItem; } else if (generator.IsItemItsOwnContainer(controlItem)) diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index a95d4f1ffa..ed92f30454 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -76,6 +76,11 @@ namespace Avalonia.Controls /// The container for the item at the specified index within the item collection, if the /// item is realized; otherwise, null. /// + /// + /// Note for implementors: if the item at the the specified index is an ItemIsOwnContainer + /// item that has previously been realized, then the item should be returned even if it + /// currently falls outside the realized viewport. + /// protected internal abstract Control? ContainerFromIndex(int index); /// diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 071ac95f45..e0768edfa4 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Reflection; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -326,7 +325,17 @@ namespace Avalonia.Controls return _realizedElements?.Elements.Where(x => x is not null)!; } - protected internal override Control? ContainerFromIndex(int index) => _realizedElements?.GetElement(index); + protected internal override Control? ContainerFromIndex(int index) + { + if (index < 0 || index >= Items.Count) + return null; + if (_realizedElements?.GetElement(index) is { } realized) + return realized; + if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty)) + return c; + return null; + } + protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1; protected internal override Control? ScrollIntoView(int index) @@ -577,7 +586,6 @@ namespace Avalonia.Controls if (controlItem.IsSet(ItemIsOwnContainerProperty)) { controlItem.IsVisible = true; - generator.ItemContainerPrepared(controlItem, item, index); return controlItem; } else if (generator.IsItemItsOwnContainer(controlItem)) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c2523207e4..ac47e744e0 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -83,6 +83,19 @@ namespace Avalonia.Controls /// /// Occurs when the window is resized. /// + /// + /// Although this event is similar to the event, they are + /// conceptually different: + /// + /// - is a window-level event, fired when a resize notification arrives + /// from the platform windowing subsystem. The event args contain details of the source of + /// the resize event in the property. This + /// event is raised before layout has been run on the window's content. + /// - is a layout-level event, fired when a layout pass + /// completes on a control. is present on all controls + /// and is fired when the control's size changes for any reason, including a + /// event in the case of a Window. + /// public event EventHandler? Resized; public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl; @@ -116,6 +129,11 @@ namespace Avalonia.Controls set { SetValue(TopmostProperty, value); } } + /// + /// Gets the scaling factor for Window positioning and sizing. + /// + public double DesktopScaling => PlatformImpl?.DesktopScaling ?? 1; + /// /// Activates the window. /// @@ -232,14 +250,16 @@ namespace Avalonia.Controls { FrameSize = PlatformImpl?.FrameSize; - if (ClientSize != clientSize) + var clientSizeChanged = ClientSize != clientSize; + + ClientSize = clientSize; + OnResized(new WindowResizedEventArgs(clientSize, reason)); + + if (clientSizeChanged) { - ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); Renderer.Resized(clientSize); } - - OnResized(new WindowResizedEventArgs(clientSize, reason)); } /// diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index e916995bae..4e8b4c66a2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -1,5 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; +using Avalonia.Metadata; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels @@ -21,6 +27,8 @@ namespace Avalonia.Diagnostics.ViewModels SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters(); } + public event EventHandler? ClipboardCopyRequested; + public MainViewModel MainView { get; } public FilterViewModel PropertiesFilter { get; } @@ -106,6 +114,105 @@ namespace Avalonia.Diagnostics.ViewModels } } + public void CopySelector() + { + var currentVisual = SelectedNode?.Visual as Visual; + if (currentVisual is not null) + { + var selector = GetVisualSelector(currentVisual); + + ClipboardCopyRequested?.Invoke(this, selector); + } + } + + public void CopySelectorFromTemplateParent() + { + var parts = new List(); + + var currentVisual = SelectedNode?.Visual as Visual; + while (currentVisual is not null) + { + parts.Add(GetVisualSelector(currentVisual)); + + currentVisual = currentVisual.TemplatedParent as Visual; + } + + if (parts.Any()) + { + parts.Reverse(); + var selector = string.Join(" /template/ ", parts); + + ClipboardCopyRequested?.Invoke(this, selector); + } + } + + public void ExpandRecursively() + { + if (SelectedNode is { } selectedNode) + { + ExpandNode(selectedNode); + + var stack = new Stack(); + stack.Push(selectedNode); + + while (stack.Count > 0) + { + var item = stack.Pop(); + item.IsExpanded = true; + foreach (var child in item.Children) + { + stack.Push(child); + } + } + } + } + + public void CollapseChildren() + { + if (SelectedNode is { } selectedNode) + { + var stack = new Stack(); + stack.Push(selectedNode); + + while (stack.Count > 0) + { + var item = stack.Pop(); + item.IsExpanded = false; + foreach (var child in item.Children) + { + stack.Push(child); + } + } + } + } + + public void CaptureNodeScreenshot() + { + MainView.Shot(null); + } + + public void BringIntoView() + { + (SelectedNode?.Visual as Control)?.BringIntoView(); + } + + + public void Focus() + { + (SelectedNode?.Visual as Control)?.Focus(); + } + + private static string GetVisualSelector(Visual visual) + { + var name = string.IsNullOrEmpty(visual.Name) ? "" : $"#{visual.Name}"; + var classes = string.Concat(visual.Classes + .Where(c => !c.StartsWith(":")) + .Select(c => '.' + c)); + var typeName = ((IStyleable)visual).StyleKey.Name; + + return $"{typeName}{name}{classes}"; + } + private void ExpandNode(TreeNode? node) { if (node != null) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index 9ffc301dc1..ecdd46dd74 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -26,6 +26,20 @@ + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 1a4bb6170a..b0aea64994 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; using System.Linq; using Avalonia.Controls; @@ -97,9 +98,27 @@ namespace Avalonia.Diagnostics.Views _currentLayer = null; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DataContextProperty) + { + if (change.GetOldValue() is TreePageViewModel oldViewModel) + oldViewModel.ClipboardCopyRequested -= OnClipboardCopyRequested; + if (change.GetNewValue() is TreePageViewModel newViewModel) + newViewModel.ClipboardCopyRequested += OnClipboardCopyRequested; + } + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void OnClipboardCopyRequested(object? sender, string e) + { + TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(e); + } } } diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index d8162c0486..4805c3a034 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index b5f1220bc8..eff3920b12 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -75,7 +75,6 @@ MinHeight="{TemplateBinding MinHeight}" TemplatedControl.IsTemplateFocusTarget="True"> a.Type == types.AvaloniaListAttribute); + if (attribute is not null) + { + if (attribute.Properties.TryGetValue("Separators", out var separatorsArray)) + { + separators = ((Array)separatorsArray)?.OfType().ToArray(); + } + + if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj)) + { + splitOptions = (StringSplitOptions)splitOptionsObj; + } + } + items = text.Split(separators, splitOptions ^ trimOption); // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. if (splitOptions.HasFlag(trimOption)) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 8ab84f4615..b5c0c7734d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -33,6 +33,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType InheritDataTypeFromItemsAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } + public IXamlType AvaloniaListAttribute { get; } public IXamlType AvaloniaList { get; } public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } @@ -143,6 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); + AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute"); AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1"); OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 5d1025f30d..e5254eb1b2 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393 +Subproject commit e5254eb1b2017f78a92acd466c8fa1e47401056b diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 6624d13165..2a35787f3b 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -261,6 +261,71 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Can_Move_Forward_Back_Forward() + { + using var app = Start(); + var items = new[] { "foo", "bar" }; + var target = new Carousel + { + Template = CarouselTemplate(), + ItemsSource = items, + }; + + Prepare(target); + + target.SelectedIndex = 1; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + + target.SelectedIndex = 0; + Layout(target); + + Assert.Equal(0, target.SelectedIndex); + + target.SelectedIndex = 1; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + } + + [Fact] + public void Can_Move_Forward_Back_Forward_With_Control_Items() + { + // Issue #11119 + using var app = Start(); + var items = new[] { new Canvas(), new Canvas() }; + var target = new Carousel + { + Template = CarouselTemplate(), + ItemsSource = items, + }; + + Prepare(target); + + target.SelectedIndex = 1; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + + target.SelectedIndex = 0; + Layout(target); + + Assert.Equal(0, target.SelectedIndex); + + target.SelectedIndex = 1; + target.PropertyChanged += (s, e) => + { + if (e.Property == Carousel.SelectedIndexProperty) + { + } + }; + Layout(target); + + Assert.Equal(1, target.SelectedIndex); + } + private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); private static void Prepare(Carousel target) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 1a0ea5fdab..5e741cdc1d 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -828,6 +828,19 @@ namespace Avalonia.Controls.UnitTests Layout(target); } + [Fact] + public void ItemIsOwnContainer_Content_Should_Not_Be_Cleared_When_Removed() + { + // Issue #11128. + using var app = Start(); + var item = new ContentPresenter { Content = "foo" }; + var target = CreateTarget(items: new[] { item }); + + target.Items.RemoveAt(0); + + Assert.Equal("foo", item.Content); + } + private static ItemsControl CreateTarget( object? dataContext = null, IBinding? displayMemberBinding = null, diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index db6460e8aa..dfdcd09bf9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -676,12 +676,8 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Moving_Selected_Item_Should_Clear_Selection() { - var items = new AvaloniaList - { - new Item(), - new Item(), - }; - + using var app = Start(); + var items = new ObservableCollection { "foo", "bar" }; var target = new SelectingItemsControl { ItemsSource = items, @@ -706,7 +702,46 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.NotNull(receivedArgs); Assert.Empty(receivedArgs.AddedItems); Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); - Assert.All(items, x => Assert.False(x.IsSelected)); + } + + [Fact] + public void Moving_Selected_Container_Should_Not_Clear_Selection() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + ItemsSource = items, + Template = Template(), + }; + + Prepare(target); + target.SelectedIndex = 1; + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + var receivedArgs = new List(); + + target.SelectionChanged += (_, args) => receivedArgs.Add(args); + + var moved = items[1]; + items.Move(1, 0); + + // Because the moved container is still marked as selected on the insert part of the + // move, it will remain selected. + Assert.Same(moved, target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Equal(2, receivedArgs.Count); + Assert.Equal(new[] { moved }, receivedArgs[0].RemovedItems); + Assert.Equal(new[] { moved }, receivedArgs[1].AddedItems); + Assert.True(items[0].IsSelected); + Assert.False(items[1].IsSelected); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 0817979e33..928c0c94ef 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1024,6 +1024,56 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { 15 }, SelectedContainers(target)); } + [Fact] + public void Can_Change_Selection_For_Containers_Outside_Of_Viewport() + { + // Issue #11119 + using var app = Start(); + var items = Enumerable.Range(0, 100).Select(x => new TestContainer + { + Content = $"Item {x}", + Height = 100, + }).ToList(); + + // Create a SelectingItemsControl with a virtualizing stack panel. + var target = CreateTarget(itemsSource: items, virtualizing: true); + target.AutoScrollToSelectedItem = false; + + var panel = Assert.IsType(target.ItemsPanelRoot); + var scroll = panel.FindAncestorOfType()!; + + // Select item 1. + target.SelectedIndex = 1; + + // Scroll item 1 and 2 out of view. + scroll.Offset = new(0, 1000); + Layout(target); + + Assert.Equal(10, panel.FirstRealizedIndex); + Assert.Equal(19, panel.LastRealizedIndex); + + // Select item 2 now that items 1 and 2 are both unrealized. + target.SelectedIndex = 2; + + // The selection should be updated. + Assert.Empty(SelectedContainers(target)); + Assert.Equal(2, target.SelectedIndex); + Assert.Same(items[2], target.SelectedItem); + Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems); + + // Scroll selected item back into view. + scroll.Offset = new(0, 0); + Layout(target); + + // The selection should be preserved. + Assert.Equal(new[] { 2 }, SelectedContainers(target)); + Assert.Equal(2, target.SelectedIndex); + Assert.Same(items[2], target.SelectedItem); + Assert.Equal(new[] { 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[2] }, target.Selection.SelectedItems); + } + [Fact] public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding() { @@ -1197,7 +1247,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Setters = { - new Setter(TreeView.TemplateProperty, CreateTestContainerTemplate()), + new Setter(TestContainer.TemplateProperty, CreateTestContainerTemplate()), }, }; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 421ed2c979..70541b6196 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -261,6 +261,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var grid = AvaloniaRuntimeXamlLoader.Parse(xaml); + Assert.Equal(4, grid.ColumnDefinitions.Count); + Assert.Equal(4, grid.RowDefinitions.Count); + + var expected1 = new GridLength(100); + var expected2 = GridLength.Auto; + var expected3 = new GridLength(1, GridUnitType.Star); + var expected4 = new GridLength(100, GridUnitType.Star); + + Assert.Equal(expected1, grid.ColumnDefinitions[0].Width); + Assert.Equal(expected2, grid.ColumnDefinitions[1].Width); + Assert.Equal(expected3, grid.ColumnDefinitions[2].Width); + Assert.Equal(expected4, grid.ColumnDefinitions[3].Width); + + Assert.Equal(expected1, grid.RowDefinitions[0].Height); + Assert.Equal(expected2, grid.RowDefinitions[1].Height); + Assert.Equal(expected3, grid.RowDefinitions[2].Height); + Assert.Equal(expected4, grid.RowDefinitions[3].Height); + } + + [Fact] + public void Grid_Row_Col_Definitions_Are_Parsed_Space_Delimiter() + { + var xaml = @" + +"; + + var grid = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.Equal(4, grid.ColumnDefinitions.Count); Assert.Equal(4, grid.RowDefinitions.Count);