diff --git a/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 60b0f8b193..1c62de9bed 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -378,48 +378,48 @@ namespace Avalonia.Controls if (container is HeaderedContentControl hcc) { - hcc.Content = item; + SetIfUnset(hcc, HeaderedContentControl.ContentProperty, item); if (item is IHeadered headered) - hcc.Header = headered.Header; + SetIfUnset(hcc, HeaderedContentControl.HeaderProperty, headered.Header); else if (item is not Visual) - hcc.Header = item; + SetIfUnset(hcc, HeaderedContentControl.HeaderProperty, item); if (itemTemplate is not null) - hcc.HeaderTemplate = itemTemplate; + SetIfUnset(hcc, HeaderedContentControl.HeaderTemplateProperty, itemTemplate); } else if (container is ContentControl cc) { - cc.Content = item; + SetIfUnset(cc, ContentControl.ContentProperty, item); if (itemTemplate is not null) - cc.ContentTemplate = itemTemplate; + SetIfUnset(cc, ContentControl.ContentTemplateProperty, itemTemplate); } else if (container is ContentPresenter p) { - p.Content = item; + SetIfUnset(p, ContentPresenter.ContentProperty, item); if (itemTemplate is not null) - p.ContentTemplate = itemTemplate; + SetIfUnset(p, ContentPresenter.ContentTemplateProperty, itemTemplate); } else if (container is ItemsControl ic) { if (itemTemplate is not null) - ic.ItemTemplate = itemTemplate; - if (ItemContainerTheme is { } ict && !ict.IsSet(ItemContainerThemeProperty)) - ic.ItemContainerTheme = ict; + SetIfUnset(ic, ItemTemplateProperty, itemTemplate); + if (ItemContainerTheme is { } ict) + SetIfUnset(ic, ItemContainerThemeProperty, ict); } // These conditions are separate because HeaderedItemsControl and // HeaderedSelectingItemsControl also need to run the ItemsControl preparation. if (container is HeaderedItemsControl hic) { - hic.Header = item; - hic.HeaderTemplate = itemTemplate; + SetIfUnset(hic, HeaderedItemsControl.HeaderProperty, item); + SetIfUnset(hic, HeaderedItemsControl.HeaderTemplateProperty, itemTemplate); hic.PrepareItemContainer(this); } else if (container is HeaderedSelectingItemsControl hsic) { - hsic.Header = item; - hsic.HeaderTemplate = itemTemplate; + SetIfUnset(hsic, HeaderedSelectingItemsControl.HeaderProperty, item); + SetIfUnset(hsic, HeaderedSelectingItemsControl.HeaderTemplateProperty, itemTemplate); hsic.PrepareItemContainer(this); } } @@ -458,30 +458,35 @@ namespace Avalonia.Controls { if (container is HeaderedContentControl hcc) { - if (hcc.Content is Control) - hcc.Content = null; - if (hcc.Header is Control) - hcc.Header = null; + hcc.ClearValue(HeaderedContentControl.ContentProperty); + hcc.ClearValue(HeaderedContentControl.HeaderProperty); + hcc.ClearValue(HeaderedContentControl.HeaderTemplateProperty); } else if (container is ContentControl cc) { - if (cc.Content is Control) - cc.Content = null; + cc.ClearValue(ContentControl.ContentProperty); + cc.ClearValue(ContentControl.ContentTemplateProperty); } else if (container is ContentPresenter p) { - if (p.Content is Control) - p.Content = null; + p.ClearValue(ContentPresenter.ContentProperty); + p.ClearValue(ContentPresenter.ContentTemplateProperty); } - else if (container is HeaderedItemsControl hic) + else if (container is ItemsControl ic) + { + ic.ClearValue(ItemTemplateProperty); + ic.ClearValue(ItemContainerThemeProperty); + } + + if (container is HeaderedItemsControl hic) { - if (hic.Header is Control) - hic.Header = null; + hic.ClearValue(HeaderedItemsControl.HeaderProperty); + hic.ClearValue(HeaderedItemsControl.HeaderTemplateProperty); } else if (container is HeaderedSelectingItemsControl hsic) { - if (hsic.Header is Control) - hsic.Header = null; + hsic.ClearValue(HeaderedSelectingItemsControl.HeaderProperty); + hsic.ClearValue(HeaderedSelectingItemsControl.HeaderTemplateProperty); } // Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at @@ -707,6 +712,12 @@ namespace Avalonia.Controls LogicalChildren.AddRange(toAdd); } + private void SetIfUnset(AvaloniaObject target, StyledProperty property, T value) + { + if (!target.IsSet(property)) + target.SetCurrentValue(property, value); + } + private void RemoveControlItemsFromLogicalChildren(IEnumerable? items) { if (items is null) diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs index 64a883e88c..895ce53907 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs @@ -18,10 +18,10 @@ namespace Avalonia.Controls.Templates { } - public new ControlTemplateResult Build(TemplatedControl param) + public new TemplateResult Build(TemplatedControl param) { var (control, scope) = BuildWithNameScope(param); - return new ControlTemplateResult(control, scope); + return new(control, scope); } } } diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 38ad6561ab..c3f9c9e8aa 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -5,23 +5,7 @@ namespace Avalonia.Controls.Templates /// /// Interface representing a template used to build a . /// - public interface IControlTemplate : ITemplate + public interface IControlTemplate : ITemplate?> { } - - public class ControlTemplateResult : TemplateResult - { - public Control Control { get; } - - public ControlTemplateResult(Control control, INameScope nameScope) : base(control, nameScope) - { - Control = control; - } - - public new void Deconstruct(out Control control, out INameScope scope) - { - control = Control; - scope = NameScope; - } - } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs index 4bbdda31d8..b94eccf7c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -13,6 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates public Type? TargetType { get; set; } - public ControlTemplateResult? Build(TemplatedControl control) => TemplateContent.Load(Content); + public TemplateResult? Build(TemplatedControl control) => TemplateContent.Load(Content); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 89b0468c6e..b45898d8bd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates public Control? Build(object? data, Control? existing) { - return existing ?? TemplateContent.Load(Content)?.Control; + return existing ?? TemplateContent.Load(Content)?.Result; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index c228a58990..f31a693e72 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object? Content { get; set; } - public Panel? Build() => (Panel?)TemplateContent.Load(Content)?.Control; + public Panel? Build() => (Panel?)TemplateContent.Load(Content)?.Result; object? ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 62febebc8c..5999a8021e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object? Content { get; set; } - public Control? Build() => TemplateContent.Load(Content)?.Control; + public Control? Build() => TemplateContent.Load(Content)?.Result; object? ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 08e897c514..504478f9b3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,15 +1,16 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates { public static class TemplateContent { - public static ControlTemplateResult? Load(object? templateContent) + public static TemplateResult? Load(object? templateContent) { if (templateContent is Func direct) { - return (ControlTemplateResult?)direct(null); + return (TemplateResult?)direct(null); } if (templateContent is null) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index a5b308523f..98c3b61c9f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.Templates public Control? Build(object? data) { - var visualTreeForItem = TemplateContent.Load(Content)?.Control; + var visualTreeForItem = TemplateContent.Load(Content)?.Result; if (visualTreeForItem != null) { visualTreeForItem.DataContext = data; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index ba96ac15b3..0cc7cc5468 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime scope.Complete(); if(typeof(T) == typeof(Control)) - return new ControlTemplateResult((Control)obj, scope); + return new TemplateResult((Control)obj, scope); return new TemplateResult((T)obj, scope); }; diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 7a227a48ab..84eed5ec82 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -554,6 +554,36 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { "Bar" }, target.Selection.SelectedItems); } + [Fact] + public void Content_Can_Be_Bound_In_ItemContainerTheme() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] { new ItemViewModel("Foo"), new ItemViewModel("Bar") }; + var theme = new ControlTheme(typeof(ListBoxItem)) + { + Setters = + { + new Setter(ListBoxItem.ContentProperty, new Binding("Caption")), + } + }; + + var target = new ListBox + { + Template = ListBoxTemplate(), + ItemsSource = items, + ItemContainerTheme = theme, + }; + + Prepare(target); + + var containers = target.GetRealizedContainers().Cast().ToList(); + Assert.Equal(2, containers.Count); + Assert.Equal("Foo", containers[0].Content); + Assert.Equal("Bar", containers[1].Content); + } + } + private static FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => @@ -918,6 +948,8 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + private record ItemViewModel(string Caption); + private class ResettingCollection : List, INotifyCollectionChanged { public ResettingCollection(int itemCount) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index bd59183e92..08aedceac3 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -1,16 +1,16 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Windows.Input; -using Avalonia.Collections; using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.VisualTree; using Moq; using Xunit; @@ -36,7 +36,6 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.Focusable); } - [Fact] public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() { @@ -393,6 +392,87 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Header_And_ItemsSource_Can_Be_Bound_In_Style() + { + using var app = Application(); + var items = new[] + { + new MenuViewModel("Foo") + { + Children = new[] + { + new MenuViewModel("FooChild"), + }, + }, + new MenuViewModel("Bar"), + }; + + var target = new Menu + { + ItemsSource = items, + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(MenuItem.HeaderProperty, new Binding("Header")), + new Setter(MenuItem.ItemsSourceProperty, new Binding("Children")), + } + } + } + }; + + var root = new TestRoot(true, target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + var children = target.GetRealizedContainers().Cast().ToList(); + Assert.Equal(2, children.Count); + Assert.Equal("Foo", children[0].Header); + Assert.Equal("Bar", children[1].Header); + Assert.Same(items[0].Children, children[0].ItemsSource); + } + + [Fact] + public void Header_And_ItemsSource_Can_Be_Bound_In_ItemContainerTheme() + { + using var app = Application(); + var items = new[] + { + new MenuViewModel("Foo") + { + Children = new[] + { + new MenuViewModel("FooChild"), + }, + }, + new MenuViewModel("Bar"), + }; + + var target = new Menu + { + ItemsSource = items, + ItemContainerTheme = new ControlTheme(typeof(MenuItem)) + { + Setters = + { + new Setter(MenuItem.HeaderProperty, new Binding("Header")), + new Setter(MenuItem.ItemsSourceProperty, new Binding("Children")), + } + } + }; + + var root = new TestRoot(true, target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + var children = target.GetRealizedContainers().Cast().ToList(); + Assert.Equal(2, children.Count); + Assert.Equal("Foo", children[0].Header); + Assert.Equal("Bar", children[1].Header); + Assert.Same(items[0].Children, children[0].ItemsSource); + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); @@ -447,6 +527,9 @@ namespace Avalonia.Controls.UnitTests public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } - private record MenuViewModel(string Header); + private record MenuViewModel(string Header) + { + public IList Children { get; set;} + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs index 20594a9774..eb7740aa43 100644 --- a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.IntegrationTests.Appium } [PlatformFact(TestPlatforms.MacOS)] - public void View_Menu_Select_Button_Tab() + public void MacOS_View_Menu_Select_Button_Tab() { var tabs = _session.FindElementByAccessibilityId("MainTabs"); var buttonTab = tabs.FindElementByName("Button"); @@ -33,5 +33,21 @@ namespace Avalonia.IntegrationTests.Appium Assert.True(buttonTab.Selected); } + + [PlatformFact(TestPlatforms.Windows)] + public void Win32_View_Menu_Select_Button_Tab() + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var buttonTab = tabs.FindElementByName("Button"); + var viewMenu = _session.FindElementByXPath("//MenuItem[@Name='View']"); + + Assert.False(buttonTab.Selected); + + viewMenu.Click(); + var buttonMenu = viewMenu.FindElementByName("Button"); + buttonMenu.Click(); + + Assert.True(buttonTab.Selected); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 57d6a8902a..9f0b84733d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1978,7 +1978,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true; - public Control Build(object data) => TemplateContent.Load(Content)?.Control; + public Control Build(object data) => TemplateContent.Load(Content)?.Result; } public class CustomDataTemplateInherit : CustomDataTemplate { } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 5e30198d00..421ed2c979 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -605,7 +605,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var control = new ContentControl(); - var result = (ContentPresenter)template.Build(control).Control; + var result = (ContentPresenter)template.Build(control).Result; Assert.NotNull(result); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index 0a45814efe..4404564733 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -258,7 +258,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - var parent = (ContentControl)template.Build(new ContentControl()).Control; + var parent = (ContentControl)template.Build(new ContentControl()).Result; Assert.Equal("parent", parent.Name); @@ -283,7 +283,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(typeof(ContentControl), template.TargetType); - Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); + Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result); } [Fact] @@ -299,7 +299,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - var panel = (Panel)template.Build(new ContentControl()).Control; + var panel = (Panel)template.Build(new ContentControl()).Result; Assert.Equal(2, panel.Children.Count);