diff --git a/.editorconfig b/.editorconfig index 5f08d1e940..f6bce9cb76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +space_within_single_line_array_initializer_braces = true # Wrapping preferences csharp_wrap_before_ternary_opsigns = false # Xaml files [*.xaml] -indent_size = 4 +indent_size = 2 # Xml project files [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e3532ee23..92e4afdca8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -134,3 +134,4 @@ jobs: pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip' artifactName: 'Samples' condition: succeeded() + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index cf8e0fd13a..c03ad0fefd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - + diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index b491cf2a92..b62fe8a968 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -11,14 +11,26 @@ class AvnStringImpl : public virtual ComSingleObject { private: - NSString* _string; + int _length; + const char* _cstring; public: FORWARD_IUNKNOWN() AvnStringImpl(NSString* string) + { + auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding]; + _length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + _cstring = (const char*)malloc(_length + 5); + + memset((void*)_cstring, 0, _length + 5); + memcpy((void*)_cstring, (void*)cstring, _length); + } + + virtual ~AvnStringImpl() { - _string = string; + free((void*)_cstring); } virtual HRESULT Pointer(void**retOut) override @@ -30,7 +42,7 @@ public: return E_POINTER; } - *retOut = (void*)_string.UTF8String; + *retOut = (void*)_cstring; return S_OK; } @@ -43,7 +55,7 @@ public: return E_POINTER; } - *retOut = (int)_string.length; + *retOut = _length; return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 53c1fe3c2c..6e4d3ce668 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject { public: FORWARD_IUNKNOWN() + + Clipboard() + { + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard stringForType:NSPasteboardTypeString]; + } + virtual HRESULT GetText (IAvnString**ppv) override { @autoreleasepool @@ -39,7 +46,9 @@ public: { @autoreleasepool { - [[NSPasteboard generalPasteboard] clearContents]; + NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; + [pasteBoard clearContents]; + [pasteBoard setString:@"" forType:NSPasteboardTypeString]; } return S_OK; diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..09d2612ac3 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,9 +1,11 @@ using System; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; @@ -27,10 +29,24 @@ namespace ControlCatalog.NetCore } var builder = BuildAvaloniaApp(); + + double GetScaling() + { + var idx = Array.IndexOf(args, "--scaling"); + if (idx != 0 && args.Length > idx + 1 && + double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling)) + return scaling; + return 1; + } if (args.Contains("--fbdev")) { - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - return builder.StartLinuxFramebuffer(args); + SilenceConsole(); + return builder.StartLinuxFbDev(args, scaling: GetScaling()); + } + else if (args.Contains("--drm")) + { + SilenceConsole(); + return builder.StartLinuxDrm(args, scaling: GetScaling()); } else return builder.StartWithClassicDesktopLifetime(args); @@ -51,11 +67,14 @@ namespace ControlCatalog.NetCore .UseSkia() .UseReactiveUI(); - static void ConsoleSilencer() + static void SilenceConsole() { - Console.CursorVisible = false; - while (true) - Console.ReadKey(true); + new Thread(() => + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(true); + }) {IsBackground = true}.Start(); } } } diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index 749f83c1ab..2e7218b956 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -34,7 +34,7 @@ namespace ControlCatalog SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West); SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East); SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North); - SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South); + SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South); SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest); SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast); SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 8699508320..c35f8a3c0c 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -6,10 +6,13 @@ Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> - - Light - Dark - + + + @@ -21,11 +24,17 @@ - + + + + + @@ -41,6 +50,12 @@ + + + Light + Dark + + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6a9e865e26..9527ac3b4e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -1,4 +1,5 @@  + + + ItemsRepeater + A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization. + + + + Stack - Vertical + Stack - Horizontal + UniformGrid - Vertical + UniformGrid - Horizontal + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs new file mode 100644 index 0000000000..1a607342f3 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class ItemsRepeaterPage : UserControl + { + private ItemsRepeater _repeater; + private ScrollViewer _scroller; + + public ItemsRepeaterPage() + { + this.InitializeComponent(); + _repeater = this.FindControl("repeater"); + _scroller = this.FindControl("scroller"); + _repeater.PointerPressed += RepeaterClick; + DataContext = new ItemsRepeaterPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void LayoutChanged(object sender, SelectionChangedEventArgs e) + { + if (_repeater == null) + { + return; + } + + var comboBox = (ComboBox)sender; + + switch (comboBox.SelectedIndex) + { + case 0: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical }; + break; + case 1: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal }; + break; + case 2: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Vertical, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + case 3: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Horizontal, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + } + } + + private void RepeaterClick(object sender, PointerPressedEventArgs e) + { + var item = (e.Source as TextBlock)?.DataContext as string; + ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item; + } + } +} diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 4783c8cfb8..49e9aafc4a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -9,7 +9,20 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index dbe6c74800..8a67766c76 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -11,9 +11,8 @@ namespace ControlCatalog.Pages { public ListBoxPage() { - this.InitializeComponent(); - DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" ) - .ToArray(); + InitializeComponent(); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -21,5 +20,46 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + private class PageViewModel : ReactiveObject + { + private int _counter; + private SelectionMode _selectionMode; + + public PageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + SelectedItems = new ObservableCollection(); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (SelectedItems.Count > 0) + { + Items.Remove(SelectedItems[0]); + } + }); + } + + public ObservableCollection Items { get; } + + public ObservableCollection SelectedItems { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++}"; + } } } diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index a1359519e6..60e946dfbe 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -69,16 +69,25 @@ namespace ControlCatalog.Pages { UpdatePointer(e); e.Pointer.Capture(this); + e.Handled = true; base.OnPointerPressed(e); } protected override void OnPointerMoved(PointerEventArgs e) { UpdatePointer(e); + e.Handled = true; base.OnPointerMoved(e); } protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { _pointers.Remove(e.Pointer); InvalidateVisual(); diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b9b384e8fe..13c1667ed2 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -22,7 +22,10 @@ namespace ControlCatalog.Pages public override void Render(DrawingContext context) { base.Render(context); - Window w = (Window)VisualRoot; + if (!(VisualRoot is Window w)) + { + return; + } var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c03edb8b03..3a81e2ed02 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -6,16 +6,29 @@ Displays a hierachical tree of data. - - - - - - - + Margin="0,16,0,0" + HorizontalAlignment="Center" + Spacing="16"> + + + + + + + + + + + + + + + Single + Multiple + Toggle + AlwaysSelected + + diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index a83f9cf43f..1f35f05f1d 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,8 +1,9 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ReactiveUI; namespace ControlCatalog.Pages { @@ -10,8 +11,8 @@ namespace ControlCatalog.Pages { public TreeViewPage() { - this.InitializeComponent(); - DataContext = new Node().Children; + InitializeComponent(); + DataContext = new PageViewModel(); } private void InitializeComponent() @@ -19,22 +20,96 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - public class Node + private class PageViewModel : ReactiveObject { - private IList _children; + private SelectionMode _selectionMode; + + public PageViewModel() + { + Node root = new Node(); + Items = root.Children; + SelectedItems = new ObservableCollection(); + + AddItemCommand = ReactiveCommand.Create(() => + { + Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root; + parentItem.AddNewItem(); + }); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (SelectedItems.Count > 0) + { + Node lastItem = SelectedItems[0]; + RecursiveRemove(Items, lastItem); + SelectedItems.Remove(lastItem); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + }); + } + + public ObservableCollection Items { get; } + + public ObservableCollection SelectedItems { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + SelectedItems.Clear(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + } + + private class Node + { + private int _counter; + private ObservableCollection _children; + public string Header { get; private set; } - public IList Children + + public bool AreChildrenInitialized => _children != null; + + public ObservableCollection Children { get { if (_children == null) { - _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"}) - .ToArray(); + _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); } return _children; } } + + public void AddNewItem() => Children.Add(CreateNewNode()); + + public override string ToString() => Header; + + private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"}; } } } diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3513e94107..26d25a6266 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -24,7 +24,8 @@ Name="PART_ScrollViewer" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" - Background="{TemplateBinding Background}"> + Background="{TemplateBinding Background}" + DockPanel.Dock="Left"> - - + + + + + @@ -58,6 +64,8 @@ + + - \ No newline at end of file + diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 8232697c18..395196d926 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -4,6 +4,7 @@ // All other rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Avalonia.Data; @@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives { if (MonthView != null) { + var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth; + var children = new List(childCount); + for (int i = 0; i < Calendar.RowsPerMonth; i++) { if (_dayTitleTemplate != null) @@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); - MonthView.Children.Add(cell); + children.Add(cell); } } @@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives cell.PointerEnter += Cell_MouseEnter; cell.PointerLeave += Cell_MouseLeave; cell.Click += Cell_Click; - MonthView.Children.Add(cell); + children.Add(cell); } } + + MonthView.Children.AddRange(children); } if (YearView != null) { + var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear; + var children = new List(childCount); + CalendarButton month; for (int i = 0; i < Calendar.RowsPerYear; i++) { @@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; month.PointerEnter += Month_MouseEnter; month.PointerLeave += Month_MouseLeave; - YearView.Children.Add(month); + children.Add(month); } } + + YearView.Children.AddRange(children); } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f32b8fabc6..a70d26624c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -202,7 +202,7 @@ namespace Avalonia.Controls { if (!e.Handled) { - if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot) + if (_popup?.IsInsidePopup((IVisual)e.Source) == true) { if (UpdateSelectionFromEventSource(e.Source)) { diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 58b4324a3e..a5025df82d 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,6 +91,8 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (control == null) + throw new ArgumentNullException(nameof(control)); if (IsOpen) { return; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 9c53dc0c10..29f0374301 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Closed { get; set; } public abstract IMouseDevice MouseDevice { get; } + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 304a760216..28b9b3a38f 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs new file mode 100644 index 0000000000..6b5cb2ee25 --- /dev/null +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls +{ + public interface IScrollAnchorProvider + { + IControl CurrentAnchor { get; } + void RegisterAnchorCandidate(IControl element); + void UnregisterAnchorCandidate(IControl element); + } +} diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index fa6f5787be..ff6cd482df 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls } } - return result.Constrain(availableSize); + return result; } /// diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 07372eb714..1430c39c76 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls } /// - /// Utilize the for layout transforms. + /// Utilize the for layout transforms. /// public bool UseRenderTransform { diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b60a97e1c8..6ec97aa04e 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index bd558af5ef..38cc3f6daf 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls public bool IsTopLevel => Parent is Menu; /// - bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false; + bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index 25b29e37e6..b826fb982e 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -150,6 +150,7 @@ namespace Avalonia.Controls.Mixins if (oldValue is IControl child) { logicalChildren.Remove(child); + ((ISetInheritanceParent)child).SetParent(child.Parent); } child = newValue as IControl; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 93873cbf7d..aa91224572 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications private void Install(Window host) { var adornerLayer = host.GetVisualDescendants() - .OfType() + .OfType() .FirstOrDefault() ?.AdornerLayer; diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 0f365fcb08..a4c674a03b 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Add: controls = e.NewItems.OfType().ToList(); LogicalChildren.InsertRange(e.NewStartingIndex, controls); - VisualChildren.AddRange(e.NewItems.OfType()); + VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Move: diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs index db77b6a365..99958c4c9e 100644 --- a/src/Avalonia.Controls/PlacementMode.cs +++ b/src/Avalonia.Controls/PlacementMode.cs @@ -23,6 +23,21 @@ namespace Avalonia.Controls /// /// The popup is placed at the top right of its target. /// - Right + Right, + + /// + /// The popup is placed at the top left of its target. + /// + Left, + + /// + /// The popup is placed at the top left of its target. + /// + Top, + + /// + /// The popup is placed according to anchor and gravity rules + /// + AnchorAndGravity } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 5f63a44717..b0dfa4185e 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -396,7 +396,7 @@ namespace Avalonia.Controls.Platform protected internal virtual void WindowDeactivated(object sender, EventArgs e) { - Menu.Close(); + Menu?.Close(); } protected void Click(IMenuItem item) diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 1b606f550b..2978016519 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Primitives.PopupPositioning; + namespace Avalonia.Platform { /// @@ -8,6 +10,6 @@ namespace Avalonia.Platform /// public interface IPopupImpl : IWindowBaseImpl { - + IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 8d8ce35c38..cfbc0b1c4b 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -107,5 +107,7 @@ namespace Avalonia.Platform /// [CanBeNull] IMouseDevice MouseDevice { get; } + + IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index b37521de30..8c99dffc28 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -15,21 +15,10 @@ namespace Avalonia.Platform /// void Hide(); - /// - /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. - /// - void BeginMoveDrag(); - - /// - /// Starts resizing a window. This function is used if an application has window resizing controls. - /// Should be called from left mouse button press event handler - /// - void BeginResizeDrag(WindowEdge edge); - /// /// Gets the position of the window in device pixels. /// - PixelPoint Position { get; set; } + PixelPoint Position { get; } /// /// Gets or sets a method called when the window's position changes. @@ -61,17 +50,6 @@ namespace Avalonia.Platform /// Size MaxClientSize { get; } - /// - /// Sets the client size of the top level. - /// - void Resize(Size clientSize); - - /// - /// Minimum width of the window. - /// - /// - void SetMinMaxSize(Size minSize, Size maxSize); - /// /// Sets whether this window appears on top of all other windows /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 2ddc5a5c85..bc5d38c845 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -57,5 +57,32 @@ namespace Avalonia.Platform /// Return true to prevent the underlying implementation from closing. /// Func Closing { get; set; } + + /// + /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. + /// + void BeginMoveDrag(); + + /// + /// Starts resizing a window. This function is used if an application has window resizing controls. + /// Should be called from left mouse button press event handler + /// + void BeginResizeDrag(WindowEdge edge); + + /// + /// Sets the client size of the top level. + /// + void Resize(Size clientSize); + + /// + /// Sets the client size of the top level. + /// + void Move(PixelPoint point); + + /// + /// Minimum width of the window. + /// + /// + void SetMinMaxSize(Size minSize, Size maxSize); } } diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5c2c1a8da3..a55bd63c6a 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -4,6 +4,5 @@ namespace Avalonia.Platform { IWindowImpl CreateWindow(); IEmbeddableWindowImpl CreateEmbeddableWindow(); - IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 76f17332bf..85916bcdd0 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -33,9 +33,10 @@ namespace Avalonia.Platform _dragDrop = AvaloniaLocator.Current.GetService(); } - public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + public async Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) { Dispatcher.UIThread.VerifyAccess(); + triggerEvent.Pointer.Capture(null); if (_draggedData == null) { _draggedData = data; diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index bb357453ff..cb1291410a 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -9,94 +9,69 @@ using Avalonia.Threading; namespace Avalonia.Controls.Platform { - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer + public class InternalPlatformThreadingInterface : IPlatformThreadingInterface { public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer( - DispatcherPriority.Render, - new TimeSpan(0, 0, 0, 0, 66), - () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount))); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); - private readonly AutoResetEvent _queued = new AutoResetEvent(false); - private readonly Queue _actions = new Queue(); public void RunLoop(CancellationToken cancellationToken) { - var handles = new[] {_signaled, _queued}; while (true) { - if (0 == WaitHandle.WaitAny(handles)) - Signaled?.Invoke(null); - else - { - while (true) - { - Action item; - lock (_actions) - if (_actions.Count == 0) - break; - else - item = _actions.Dequeue(); - item(); - } - } + Signaled?.Invoke(null); + _signaled.WaitOne(); } } - public void Send(Action cb) - { - lock (_actions) - { - _actions.Enqueue(cb); - _queued.Set(); - } - } - class WatTimer : IDisposable + class TimerImpl : IDisposable { - private readonly IDisposable _timer; + private readonly DispatcherPriority _priority; + private readonly TimeSpan _interval; + private readonly Action _tick; + private Timer _timer; private GCHandle _handle; - public WatTimer(IDisposable timer) + public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) { - _timer = timer; + _priority = priority; + _interval = interval; + _tick = tick; + _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1)); _handle = GCHandle.Alloc(_timer); } + private void OnTimer(object state) + { + if (_timer == null) + return; + Dispatcher.UIThread.Post(() => + { + + if (_timer == null) + return; + _tick(); + _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1)); + }); + } + + public void Dispose() { _handle.Free(); _timer.Dispose(); + _timer = null; } } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - return new WatTimer(new System.Threading.Timer(delegate - { - var tcs = new TaskCompletionSource(); - Send(() => - { - try - { - tick(); - } - finally - { - tcs.SetResult(0); - } - }); - - - tcs.Task.Wait(); - }, null, TimeSpan.Zero, interval)); - - + return new TimerImpl(priority, interval, tick); } public void Signal(DispatcherPriority prio) diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index fa01b9e839..ef453274b8 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered."); return platform.CreateEmbeddableWindow(); } - - public static IPopupImpl CreatePopup() - { - return AvaloniaLocator.Current.GetService().CreatePopup(); - } } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index c2690d503d..1072b21b1b 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -237,7 +237,7 @@ namespace Avalonia.Controls.Presenters // template. LogicalChildren.Remove(oldChild); } - else + else if (TemplatedParent != null) { // If we're in a ContentControl's template then invoke ChildChanging to let // ContentControlMixin handle removing the logical child. @@ -248,6 +248,10 @@ namespace Avalonia.Controls.Presenters newChild, BindingPriority.LocalValue)); } + else if (oldChild != null) + { + ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent); + } } // Set the DataContext if the data isn't a control. @@ -433,6 +437,7 @@ namespace Avalonia.Controls.Presenters { VisualChildren.Remove(Child); LogicalChildren.Remove(Child); + ((ISetInheritanceParent)Child).SetParent(Child.Parent); Child = null; _dataTemplate = null; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 46da8fe3f8..ae52e733b7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index b3345ec101..debbb81264 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters AffectsRender(PasswordCharProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, SelectionStartProperty, SelectionEndProperty); + + Observable.Merge( + SelectionStartProperty.Changed, + SelectionEndProperty.Changed, + PasswordCharProperty.Changed + ).AddClassHandler((x,_) => x.InvalidateFormattedText()); + + CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); } public TextPresenter() @@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters _caretTimer = new DispatcherTimer(); _caretTimer.Interval = TimeSpan.FromMilliseconds(500); _caretTimer.Tick += CaretTimerTick; - - Observable.Merge( - this.GetObservable(SelectionStartProperty), - this.GetObservable(SelectionEndProperty)) - .Subscribe(_ => InvalidateFormattedText()); - - this.GetObservable(CaretIndexProperty) - .Subscribe(CaretIndexChanged); - - this.GetObservable(PasswordCharProperty) - .Subscribe(_ => InvalidateFormattedText()); } public int CaretIndex diff --git a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs b/src/Avalonia.Controls/Primitives/AdornerDecorator.cs deleted file mode 100644 index 4608d64806..0000000000 --- a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.LogicalTree; - -namespace Avalonia.Controls.Primitives -{ - public class AdornerDecorator : Decorator - { - public AdornerDecorator() - { - AdornerLayer = new AdornerLayer(); - ((ISetLogicalParent)AdornerLayer).SetParent(this); - AdornerLayer.ZIndex = int.MaxValue; - VisualChildren.Add(AdornerLayer); - } - - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - - ((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e); - } - - public AdornerLayer AdornerLayer - { - get; - } - - protected override Size MeasureOverride(Size availableSize) - { - AdornerLayer.Measure(availableSize); - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - AdornerLayer.Arrange(new Rect(finalSize)); - return base.ArrangeOverride(finalSize); - } - } -} diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index d198570909..ebe5e0a93e 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives public static AdornerLayer GetAdornerLayer(IVisual visual) { return visual.GetVisualAncestors() - .OfType() + .OfType() .FirstOrDefault() ?.AdornerLayer; } diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs new file mode 100644 index 0000000000..74a3ca8818 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public interface IPopupHost : IDisposable + { + void SetChild(IControl control); + IContentPresenter Presenter { get; } + IVisual HostedVisualTreeRoot { get; } + + event EventHandler TemplateApplied; + + void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None); + void Show(); + void Hide(); + IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, + StyledProperty minWidthProperty, StyledProperty maxWidthProperty, + StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty); + } +} diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs new file mode 100644 index 0000000000..487a5e91e4 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -0,0 +1,38 @@ +using System.Linq; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class OverlayLayer : Canvas, ICustomSimpleHitTest + { + public Size AvailableSize { get; private set; } + public static OverlayLayer GetOverlayLayer(IVisual visual) + { + foreach(var v in visual.GetVisualAncestors()) + if(v is VisualLayerManager vlm) + if (vlm.OverlayLayer != null) + return vlm.OverlayLayer; + if (visual is TopLevel tl) + { + var layers = tl.GetVisualDescendants().OfType().FirstOrDefault(); + return layers?.OverlayLayer; + } + + return null; + } + + public bool HitTest(Point point) + { + return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); + } + + protected override Size ArrangeOverride(Size finalSize) + { + // We are saving it here since child controls might need to know the entire size of the overlay + // and Bounds won't be updated in time + AvailableSize = finalSize; + return base.ArrangeOverride(finalSize); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs new file mode 100644 index 0000000000..3dc9d302db --- /dev/null +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup + { + private readonly OverlayLayer _overlayLayer; + private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); + private ManagedPopupPositioner _positioner; + private Point _lastRequestedPosition; + private bool _shown; + + public OverlayPopupHost(OverlayLayer overlayLayer) + { + _overlayLayer = overlayLayer; + _positioner = new ManagedPopupPositioner(this); + } + + public void SetChild(IControl control) + { + Content = control; + } + + public IVisual HostedVisualTreeRoot => null; + + /// + IInteractive IInteractive.InteractiveParent => Parent; + + public void Dispose() => Hide(); + + + public void Show() + { + _overlayLayer.Children.Add(this); + _shown = true; + } + + public void Hide() + { + _overlayLayer.Children.Remove(this); + _shown = false; + } + + public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, + StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty) + { + // Topmost property is not supported + var bindings = new List(); + + void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); + Bind(WidthProperty, widthProperty); + Bind(MinWidthProperty, minWidthProperty); + Bind(MaxWidthProperty, maxWidthProperty); + Bind(HeightProperty, heightProperty); + Bind(MinHeightProperty, minHeightProperty); + Bind(MaxHeightProperty, maxHeightProperty); + + return Disposable.Create(() => + { + foreach (var x in bindings) + x.Dispose(); + }); + } + + public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None) + { + _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor, + gravity); + UpdatePosition(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_positionerParameters.Size != finalSize) + { + _positionerParameters.Size = finalSize; + UpdatePosition(); + } + return base.ArrangeOverride(finalSize); + } + + + private void UpdatePosition() + { + // Don't bother the positioner with layout system artifacts + if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0) + return; + if (_shown) + { + _positioner.Update(_positionerParameters); + } + } + + IReadOnlyList IManagedPopupPositionerPopup.Screens + { + get + { + var rc = new Rect(default, _overlayLayer.AvailableSize); + return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)}; + } + } + + Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry => + new Rect(default, _overlayLayer.Bounds.Size); + + void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize) + { + _lastRequestedPosition = devicePoint; + Dispatcher.UIThread.Post(() => + { + OverlayLayer.SetLeft(this, _lastRequestedPosition.X); + OverlayLayer.SetTop(this, _lastRequestedPosition.Y); + }, DispatcherPriority.Layout); + } + + Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt; + + Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size; + + public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver) + { + var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); + if (platform != null) + return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver); + + var overlayLayer = OverlayLayer.GetOverlayLayer(target); + if (overlayLayer == null) + throw new InvalidOperationException( + "Unable to create IPopupImpl and no overlay layer is found for the target control"); + + + return new OverlayPopupHost(overlayLayer); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size)); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 058658357f..5ddbed5944 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,7 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Reactive.Disposables; +using Avalonia.Controls.Presenters; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -42,7 +47,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty ObeyScreenEdgesProperty = - AvaloniaProperty.Register(nameof(ObeyScreenEdges)); + AvaloniaProperty.Register(nameof(ObeyScreenEdges), true); /// /// Defines the property. @@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(Topmost)); private bool _isOpen; - private PopupRoot _popupRoot; + private IPopupHost _popupHost; private TopLevel _topLevel; private IDisposable _nonClientListener; + private IDisposable _presenterSubscription; bool _ignoreIsOpenChanged = false; + private List _bindings = new List(); /// /// Initializes static members of the class. @@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); - TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue); + } + + public Popup() + { + } /// @@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler Opened; - /// - /// Raised when the popup root has been created, but before it has been shown. - /// - public event EventHandler PopupRootCreated; + public IPopupHost Host => _popupHost; /// /// Gets or sets the control to display in the popup. @@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } - /// - /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary - /// when its opened at a position where it would otherwise overlap the screen edge. - /// + [Obsolete("This property has no effect")] public bool ObeyScreenEdges { get => GetValue(ObeyScreenEdgesProperty); @@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementTargetProperty, value); } } - /// - /// Gets the root of the popup window. - /// - public PopupRoot PopupRoot => _popupRoot; - /// /// Gets or sets a value indicating whether the popup should stay open when the popup is /// pressed or loses focus. @@ -211,63 +211,58 @@ namespace Avalonia.Controls.Primitives /// /// Gets the root of the popup window. /// - IVisual IVisualTreeHost.Root => _popupRoot; + IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot; /// /// Opens the popup. /// public void Open() { - if (_popupRoot == null) + // Popup is currently open + if (_topLevel != null) + return; + CloseCurrent(); + var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType().FirstOrDefault(); + if (placementTarget == null) + throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null"); + + _topLevel = placementTarget.GetVisualRoot() as TopLevel; + + if (_topLevel == null) { - _popupRoot = new PopupRoot(DependencyResolver) - { - [~ContentControl.ContentProperty] = this[~ChildProperty], - [~WidthProperty] = this[~WidthProperty], - [~HeightProperty] = this[~HeightProperty], - [~MinWidthProperty] = this[~MinWidthProperty], - [~MaxWidthProperty] = this[~MaxWidthProperty], - [~MinHeightProperty] = this[~MinHeightProperty], - [~MaxHeightProperty] = this[~MaxHeightProperty], - }; - - ((ISetLogicalParent)_popupRoot).SetParent(this); + throw new InvalidOperationException( + "Attempted to open a popup not attached to a TopLevel"); } - _popupRoot.Position = GetPosition(); + _popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); + + _bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, + HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); - if (_topLevel == null && PlacementTarget != null) + _popupHost.SetChild(Child); + ((ISetLogicalParent)_popupHost).SetParent(this); + _popupHost.ConfigurePosition(placementTarget, + PlacementMode, new Point(HorizontalOffset, VerticalOffset)); + _popupHost.TemplateApplied += RootTemplateApplied; + + var window = _topLevel as Window; + if (window != null) { - _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel; + window.Deactivated += WindowDeactivated; } - - if (_topLevel != null) + else { - var window = _topLevel as Window; - if (window != null) + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot?.Parent is Popup popup) { - window.Deactivated += WindowDeactivated; + popup.Closed += ParentClosed; } - else - { - var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot?.Parent is Popup popup) - { - popup.Closed += ParentClosed; - } - } - _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); - _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); } + _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); + _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick); + - PopupRootCreated?.Invoke(this, EventArgs.Empty); - - _popupRoot.Show(); - - if (ObeyScreenEdges) - { - _popupRoot.SnapInsideScreenEdges(); - } + _popupHost.Show(); using (BeginIgnoringIsOpen()) { @@ -282,29 +277,14 @@ namespace Avalonia.Controls.Primitives /// public void Close() { - if (_popupRoot != null) + if (_popupHost != null) { - if (_topLevel != null) - { - _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); - var window = _topLevel as Window; - if (window != null) - window.Deactivated -= WindowDeactivated; - else - { - var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot?.Parent is Popup popup) - { - popup.Closed -= ParentClosed; - } - } - _nonClientListener?.Dispose(); - _nonClientListener = null; - } - - _popupRoot.Hide(); + _popupHost.TemplateApplied -= RootTemplateApplied; } + _presenterSubscription?.Dispose(); + + CloseCurrent(); using (BeginIgnoringIsOpen()) { IsOpen = false; @@ -313,6 +293,41 @@ namespace Avalonia.Controls.Primitives Closed?.Invoke(this, EventArgs.Empty); } + void CloseCurrent() + { + if (_topLevel != null) + { + _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); + var window = _topLevel as Window; + if (window != null) + window.Deactivated -= WindowDeactivated; + else + { + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot?.Parent is Popup popup) + { + popup.Closed -= ParentClosed; + } + } + _nonClientListener?.Dispose(); + _nonClientListener = null; + + _topLevel = null; + } + if (_popupHost != null) + { + foreach(var b in _bindings) + b.Dispose(); + _bindings.Clear(); + _popupHost.SetChild(null); + _popupHost.Hide(); + ((ISetLogicalParent)_popupHost).SetParent(null); + _popupHost.Dispose(); + _popupHost = null; + } + + } + /// /// Measures the control. /// @@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives return new Size(); } - /// - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - _topLevel = e.Root as TopLevel; - } - /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); - _topLevel = null; - - if (_popupRoot != null) - { - ((ISetLogicalParent)_popupRoot).SetParent(null); - _popupRoot.Dispose(); - _popupRoot = null; - } + Close(); } + /// /// Called when the property changes. /// @@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Gets the position for the popup based on the placement properties. - /// - /// The popup's position in screen coordinates. - protected virtual PixelPoint GetPosition() - { - var result = GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, - HorizontalOffset, VerticalOffset); - - return result; - } - - internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset) - { - var root = target?.GetVisualRoot(); - var mode = root != null ? placement : PlacementMode.Pointer; - var scaling = root?.RenderScaling ?? 1; - - switch (mode) - { - case PlacementMode.Pointer: - if (popupRoot != null) - { - var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling); - var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default; - return new PixelPoint( - screenOffset.X + mouseOffset.X, - screenOffset.Y + mouseOffset.Y); - } - - return default; - - case PlacementMode.Bottom: - return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default; - - case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default; - - default: - throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); - } - } - private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; @@ -445,17 +404,62 @@ namespace Avalonia.Controls.Primitives } } - private bool IsChildOrThis(IVisual child) + private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e) { - IVisual root = child.GetVisualRoot(); - while (root is PopupRoot) + _popupHost.TemplateApplied -= RootTemplateApplied; + + if (_presenterSubscription != null) { - if (root == PopupRoot) return true; - root = ((PopupRoot)root).Parent.GetVisualRoot(); + _presenterSubscription.Dispose(); + _presenterSubscription = null; + } + + // If the Popup appears in a control template, then the child controls + // that appear in the popup host need to have their TemplatedParent + // properties set. + if (TemplatedParent != null) + { + _popupHost.Presenter?.ApplyTemplate(); + _popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty) + .Subscribe(SetTemplatedParentAndApplyChildTemplates); + } + } + + private void SetTemplatedParentAndApplyChildTemplates(IControl control) + { + if (control != null) + { + var templatedParent = TemplatedParent; + + if (control.TemplatedParent == null) + { + control.SetValue(TemplatedParentProperty, templatedParent); + } + + control.ApplyTemplate(); + + if (!(control is IPresenter) && control.TemplatedParent == templatedParent) + { + foreach (IControl child in control.GetVisualChildren()) + { + SetTemplatedParentAndApplyChildTemplates(child); + } + } } - return false; } + private bool IsChildOrThis(IVisual child) + { + return _popupHost != null && ((IVisual)_popupHost).FindCommonVisualAncestor(child) == _popupHost; + } + + public bool IsInsidePopup(IVisual visual) + { + return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true; + } + + public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver; + private void WindowDeactivated(object sender, EventArgs e) { if (!StaysOpen) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs new file mode 100644 index 0000000000..3010a3d8a8 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -0,0 +1,358 @@ +// The documentation and flag names in this file are initially taken from +// xdg_shell wayland protocol this API is designed after +// therefore, I'm including the license from wayland-protocols repo + +/* +Copyright © 2008-2013 Kristian Høgsberg +Copyright © 2010-2013 Intel Corporation +Copyright © 2013 Rafael Antognolli +Copyright © 2013 Jasper St. Pierre +Copyright © 2014 Jonas Ådahl +Copyright © 2014 Jason Ekstrand +Copyright © 2014-2015 Collabora, Ltd. +Copyright © 2015 Red Hat Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +--- + +The above is the version of the MIT "Expat" License used by X.org: + + http://cgit.freedesktop.org/xorg/xserver/tree/COPYING + + +Adjustments for Avalonia needs: +Copyright © 2019 Nikita Tsukanov + + +*/ + +using System; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + /// + /// + /// The IPopupPositioner provides a collection of rules for the placement of a + /// a popup relative to its parent. Rules can be defined to ensure + /// the popup remains within the visible area's borders, and to + /// specify how the popup changes its position, such as sliding along + /// an axis, or flipping around a rectangle. These positioner-created rules are + /// constrained by the requirement that a popup must intersect with or + /// be at least partially adjacent to its parent surface. + /// + public struct PopupPositionerParameters + { + private PopupPositioningEdge _gravity; + private PopupPositioningEdge _anchor; + + /// + /// Set the size of the popup that is to be positioned with the positioner + /// object. The size is in scaled coordinates. + /// + public Size Size { get; set; } + + /// + /// Specify the anchor rectangle within the parent that the popup + /// will be placed relative to. The rectangle is relative to the + /// parent geometry + /// + /// The anchor rectangle may not extend outside the window geometry of the + /// popup's parent. The anchor rectangle is in scaled coordinates + /// + public Rect AnchorRectangle { get; set; } + + + /// + /// Defines the anchor point for the anchor rectangle. The specified anchor + /// is used derive an anchor point that the popup will be + /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or + /// 'BottomRight'), the anchor point will be at the specified corner; + /// otherwise, the derived anchor point will be centered on the specified + /// edge, or in the center of the anchor rectangle if no edge is specified. + /// + public PopupPositioningEdge Anchor + { + get => _anchor; + set + { + PopupPositioningEdgeHelper.ValidateEdge(value); + _anchor = value; + } + } + + /// + /// Defines in what direction a popup should be positioned, relative to + /// the anchor point of the parent. If a corner gravity is + /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup + /// will be placed towards the specified gravity; otherwise, the popup + /// will be centered over the anchor point on any axis that had no + /// gravity specified. + /// + public PopupPositioningEdge Gravity + { + get => _gravity; + set + { + PopupPositioningEdgeHelper.ValidateEdge(value); + _gravity = value; + } + } + + /// + /// Specify how the popup should be positioned if the originally intended + /// position caused the popup to be constrained, meaning at least + /// partially outside positioning boundaries set by the positioner. The + /// adjustment is set by constructing a bitmask describing the adjustment to + /// be made when the popup is constrained on that axis. + /// + /// If no bit for one axis is set, the positioner will assume that the child + /// surface should not change its position on that axis when constrained. + /// + /// If more than one bit for one axis is set, the order of how adjustments + /// are applied is specified in the corresponding adjustment descriptions. + /// + /// The default adjustment is none. + /// + public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; } + + /// + /// Specify the popup position offset relative to the position of the + /// anchor on the anchor rectangle and the anchor on the popup. For + /// example if the anchor of the anchor rectangle is at (x, y), the popup + /// has the gravity bottom|right, and the offset is (ox, oy), the calculated + /// surface position will be (x + ox, y + oy). The offset position of the + /// surface is the one used for constraint testing. See + /// set_constraint_adjustment. + /// + /// An example use case is placing a popup menu on top of a user interface + /// element, while aligning the user interface element of the parent surface + /// with some user interface element placed somewhere in the popup. + /// + public Point Offset { get; set; } + } + + /// + /// The constraint adjustment value define ways how popup position will + /// be adjusted if the unadjusted position would result in the popup + /// being partly constrained. + /// + /// Whether a popup is considered 'constrained' is left to the positioner + /// to determine. For example, the popup may be partly outside the + /// target platform defined 'work area', thus necessitating the popup's + /// position be adjusted until it is entirely inside the work area. + /// + [Flags] + public enum PopupPositionerConstraintAdjustment + { + /// + /// Don't alter the surface position even if it is constrained on some + /// axis, for example partially outside the edge of an output. + /// + None = 0, + + /// + /// Slide the surface along the x axis until it is no longer constrained. + /// First try to slide towards the direction of the gravity on the x axis + /// until either the edge in the opposite direction of the gravity is + /// unconstrained or the edge in the direction of the gravity is + /// constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the + /// x axis until either the edge in the direction of the gravity is + /// unconstrained or the edge in the opposite direction of the gravity is + /// constrained. + /// + SlideX = 1, + + + /// + /// Slide the surface along the y axis until it is no longer constrained. + /// + /// First try to slide towards the direction of the gravity on the y axis + /// until either the edge in the opposite direction of the gravity is + /// unconstrained or the edge in the direction of the gravity is + /// constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the + /// y axis until either the edge in the direction of the gravity is + /// unconstrained or the edge in the opposite direction of the gravity is + /// constrained. + /// */ + /// + SlideY = 2, + + /// + /// Invert the anchor and gravity on the x axis if the surface is + /// constrained on the x axis. For example, if the left edge of the + /// surface is constrained, the gravity is 'left' and the anchor is + /// 'left', change the gravity to 'right' and the anchor to 'right'. + /// + /// If the adjusted position also ends up being constrained, the resulting + /// position of the flip_x adjustment will be the one before the + /// adjustment. + /// + FlipX = 4, + + /// + /// Invert the anchor and gravity on the y axis if the surface is + /// constrained on the y axis. For example, if the bottom edge of the + /// surface is constrained, the gravity is 'bottom' and the anchor is + /// 'bottom', change the gravity to 'top' and the anchor to 'top'. + /// + /// The adjusted position is calculated given the original anchor + /// rectangle and offset, but with the new flipped anchor and gravity + /// values. + /// + /// If the adjusted position also ends up being constrained, the resulting + /// position of the flip_y adjustment will be the one before the + /// adjustment. + /// + FlipY = 8, + All = SlideX|SlideY|FlipX|FlipY + } + + static class PopupPositioningEdgeHelper + { + public static void ValidateEdge(this PopupPositioningEdge edge) + { + if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0) + || + ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0)) + throw new ArgumentException("Opposite edges specified"); + } + + public static PopupPositioningEdge Flip(this PopupPositioningEdge edge) + { + var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right; + var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom; + if ((edge & hmask) != 0) + edge ^= hmask; + if ((edge & vmask) != 0) + edge ^= vmask; + return edge; + } + + public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge) + { + if ((edge & PopupPositioningEdge.HorizontalMask) != 0) + edge ^= PopupPositioningEdge.HorizontalMask; + return edge; + } + + public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge) + { + if ((edge & PopupPositioningEdge.VerticalMask) != 0) + edge ^= PopupPositioningEdge.VerticalMask; + return edge; + } + + } + + [Flags] + public enum PopupPositioningEdge + { + None, + Top = 1, + Bottom = 2, + Left = 4, + Right = 8, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right, + + + VerticalMask = Top | Bottom, + HorizontalMask = Left | Right, + AllMask = VerticalMask|HorizontalMask + } + + public interface IPopupPositioner + { + void Update(PopupPositionerParameters parameters); + } + + static class PopupPositionerExtensions + { + public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters, + TopLevel topLevel, + IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor, PopupPositioningEdge gravity) + { + // We need a better way for tracking the last pointer position + var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position); + + positionerParameters.Offset = offset; + positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All; + if (placement == PlacementMode.Pointer) + { + positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); + positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else + { + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) + { + if (target.GetVisualRoot() == null) + throw new InvalidCastException("Target control is not attached to the visual tree"); + throw new InvalidCastException("Target control is not in the same tree as the popup parent"); + } + + positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) + .TransformToAABB(matrix.Value); + + if (placement == PlacementMode.Right) + { + positionerParameters.Anchor = PopupPositioningEdge.TopRight; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Bottom) + { + positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Left) + { + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; + } + else if (placement == PlacementMode.Top) + { + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + positionerParameters.Gravity = PopupPositioningEdge.TopRight; + } + else if (placement == PlacementMode.AnchorAndGravity) + { + positionerParameters.Anchor = anchor; + positionerParameters.Gravity = gravity; + } + else + throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); + } + } + } + +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs new file mode 100644 index 0000000000..d428952bb9 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + public interface IManagedPopupPositionerPopup + { + IReadOnlyList Screens { get; } + Rect ParentClientAreaScreenGeometry { get; } + void MoveAndResize(Point devicePoint, Size virtualSize); + Point TranslatePoint(Point pt); + Size TranslateSize(Size size); + } + + public class ManagedPopupPositionerScreenInfo + { + public Rect Bounds { get; } + public Rect WorkingArea { get; } + + public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea) + { + Bounds = bounds; + WorkingArea = workingArea; + } + } + + public class ManagedPopupPositioner : IPopupPositioner + { + private readonly IManagedPopupPositionerPopup _popup; + + public ManagedPopupPositioner(IManagedPopupPositionerPopup popup) + { + _popup = popup; + } + + + private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge) + { + double x, y; + if ((edge & PopupPositioningEdge.Left) != 0) + x = anchorRect.X; + else if ((edge & PopupPositioningEdge.Right) != 0) + x = anchorRect.Right; + else + x = anchorRect.X + anchorRect.Width / 2; + + if ((edge & PopupPositioningEdge.Top) != 0) + y = anchorRect.Y; + else if ((edge & PopupPositioningEdge.Bottom) != 0) + y = anchorRect.Bottom; + else + y = anchorRect.Y + anchorRect.Height / 2; + return new Point(x, y); + } + + private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity) + { + double x, y; + if ((gravity & PopupPositioningEdge.Left) != 0) + x = -size.Width; + else if ((gravity & PopupPositioningEdge.Right) != 0) + x = 0; + else + x = -size.Width / 2; + + if ((gravity & PopupPositioningEdge.Top) != 0) + y = -size.Height; + else if ((gravity & PopupPositioningEdge.Bottom) != 0) + y = 0; + else + y = -size.Height / 2; + return anchorPoint + new Point(x, y); + } + + public void Update(PopupPositionerParameters parameters) + { + + Update(_popup.TranslateSize(parameters.Size), parameters.Size, + new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft), + _popup.TranslateSize(parameters.AnchorRectangle.Size)), + parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment, + _popup.TranslatePoint(parameters.Offset)); + } + + + private void Update(Size translatedSize, Size originalSize, + Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity, + PopupPositionerConstraintAdjustment constraintAdjustment, Point offset) + { + var parentGeometry = _popup.ParentClientAreaScreenGeometry; + anchorRect = anchorRect.Translate(parentGeometry.TopLeft); + + Rect GetBounds() + { + var screens = _popup.Screens; + + var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect)) + ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) + ?? screens.FirstOrDefault(); + return targetScreen?.WorkingArea + ?? new Rect(0, 0, double.MaxValue, double.MaxValue); + } + + var bounds = GetBounds(); + + bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask) + { + if ((edge & PopupPositioningEdge.Left) != 0 + && rc.X < bounds.X) + return false; + + if ((edge & PopupPositioningEdge.Top) != 0 + && rc.Y < bounds.Y) + return false; + + if ((edge & PopupPositioningEdge.Right) != 0 + && rc.Right > bounds.Right) + return false; + + if ((edge & PopupPositioningEdge.Bottom) != 0 + && rc.Bottom > bounds.Bottom) + return false; + + return true; + } + + Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) => + new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize); + + + var geo = GetUnconstrained(anchor, gravity); + + // If flipping geometry and anchor is allowed and helps, use the flipped one, + // otherwise leave it as is + if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask) + && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) + { + var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); + if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask)) + geo = geo.WithX(flipped.X); + } + + // If sliding is allowed, try moving the rect into the bounds + if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0) + { + geo = geo.WithX(Math.Max(geo.X, bounds.X)); + if (geo.Right > bounds.Right) + geo = geo.WithX(bounds.Right - geo.Width); + } + + // If flipping geometry and anchor is allowed and helps, use the flipped one, + // otherwise leave it as is + if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask) + && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) + { + var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); + if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask)) + geo = geo.WithY(flipped.Y); + } + + // If sliding is allowed, try moving the rect into the bounds + if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0) + { + geo = geo.WithY(Math.Max(geo.Y, bounds.Y)); + if (geo.Bottom > bounds.Bottom) + geo = geo.WithY(bounds.Bottom - geo.Height); + } + + _popup.MoveAndResize(geo.TopLeft, originalSize); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs new file mode 100644 index 0000000000..bb701da651 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + /// + /// This class is used to simplify integration of IPopupImpl implementations with popup positioner + /// + public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + private readonly IWindowBaseImpl _parent; + + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + + public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize) + { + _parent = parent; + _moveResize = moveResize; + } + + public IReadOnlyList Screens => + + _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + // Popup positioner operates with abstract coordinates, but in our case they are pixel ones + var point = _parent.PointToScreen(default); + var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling); + return new Rect(point.X, point.Y, size.Width, size.Height); + + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling); + } + + public Point TranslatePoint(Point pt) => pt * _parent.Scaling; + + public Size TranslateSize(Size size) => size * _parent.Scaling; + } +} diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index d2e8f1ab92..b7f0c8f47d 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,8 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Controls.Platform; -using Avalonia.Controls.Presenters; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost + public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { - private IDisposable _presenterSubscription; + private readonly TopLevel _parent; + private PopupPositionerParameters _positionerParameters; /// /// Initializes static members of the class. @@ -31,8 +33,8 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public PopupRoot() - : this(null) + public PopupRoot(TopLevel parent, IPopupImpl impl) + : this(parent, impl,null) { } @@ -42,9 +44,10 @@ namespace Avalonia.Controls.Primitives /// /// The dependency resolver to use. If null the default dependency resolver will be used. /// - public PopupRoot(IAvaloniaDependencyResolver dependencyResolver) - : base(PlatformManager.CreatePopup(), dependencyResolver) + public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver) + : base(impl, dependencyResolver) { + _parent = parent; } /// @@ -74,73 +77,61 @@ namespace Avalonia.Controls.Primitives /// public void Dispose() => PlatformImpl?.Dispose(); - /// - /// Moves the Popups position so that it doesnt overlap screen edges. - /// This method can be called immediately after Show has been called. - /// - public void SnapInsideScreenEdges() + private void UpdatePosition() { - var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position); - - if (screen != null) - { - var scaling = VisualRoot.RenderScaling; - var bounds = PixelRect.FromRect(Bounds, scaling); - var screenX = Position.X + bounds.Width - screen.Bounds.X; - var screenY = Position.Y + bounds.Height - screen.Bounds.Y; - - if (screenX > screen.Bounds.Width) - { - Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); - } - - if (screenY > screen.Bounds.Height) - { - Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); - } - } + PlatformImpl?.PopupPositioner.Update(_positionerParameters); } - /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None) { - base.OnTemplateApplied(e); + _positionerParameters.ConfigurePosition(_parent, target, + placement, offset, anchor, gravity); + + if (_positionerParameters.Size != default) + UpdatePosition(); + } + + public void SetChild(IControl control) => Content = control; - if (Parent?.TemplatedParent != null) + IVisual IPopupHost.HostedVisualTreeRoot => this; + + public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, + StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty) + { + var bindings = new List(); + + void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); + Bind(WidthProperty, widthProperty); + Bind(MinWidthProperty, minWidthProperty); + Bind(MaxWidthProperty, maxWidthProperty); + Bind(HeightProperty, heightProperty); + Bind(MinHeightProperty, minHeightProperty); + Bind(MaxHeightProperty, maxHeightProperty); + Bind(TopmostProperty, topmostProperty); + return Disposable.Create(() => { - if (_presenterSubscription != null) - { - _presenterSubscription.Dispose(); - _presenterSubscription = null; - } - - Presenter?.ApplyTemplate(); - Presenter?.GetObservable(ContentPresenter.ChildProperty) - .Subscribe(SetTemplatedParentAndApplyChildTemplates); - } + foreach (var x in bindings) + x.Dispose(); + }); } - private void SetTemplatedParentAndApplyChildTemplates(IControl control) + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) { - if (control != null) + using (BeginAutoSizing()) { - var templatedParent = Parent.TemplatedParent; - - if (control.TemplatedParent == null) - { - control.SetValue(TemplatedParentProperty, templatedParent); - } - - control.ApplyTemplate(); - - if (!(control is IPresenter) && control.TemplatedParent == templatedParent) - { - foreach (IControl child in control.GetVisualChildren()) - { - SetTemplatedParentAndApplyChildTemplates(child); - } - } + _positionerParameters.Size = finalSize; + UpdatePosition(); } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); } } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index e1b3061b54..c6119e89dc 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 188685f796..c8c15bc079 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -333,6 +333,11 @@ namespace Avalonia.Controls.Primitives case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: SelectedIndex = IndexOf(Items, SelectedItem); + + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) + { + SelectedIndex = 0; + } break; } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index a61757e628..ec0dbd124c 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index c96fea6c25..21a7dd68f8 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Metadata; namespace Avalonia.Controls.Primitives diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs new file mode 100644 index 0000000000..b7229eb121 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia.Controls.Primitives +{ + public class VisualLayerManager : Decorator + { + private const int AdornerZIndex = int.MaxValue - 100; + private const int OverlayZIndex = int.MaxValue - 99; + private IStyleHost _styleRoot; + private readonly List _layers = new List(); + + + public bool IsPopup { get; set; } + + public AdornerLayer AdornerLayer + { + get + { + var rv = FindLayer(); + if (rv == null) + AddLayer(rv = new AdornerLayer(), AdornerZIndex); + return rv; + } + } + + public OverlayLayer OverlayLayer + { + get + { + if (IsPopup) + return null; + var rv = FindLayer(); + if(rv == null) + AddLayer(rv = new OverlayLayer(), OverlayZIndex); + return rv; + } + } + + T FindLayer() where T : class + { + foreach (var layer in _layers) + if (layer is T match) + return match; + return null; + } + + void AddLayer(Control layer, int zindex) + { + _layers.Add(layer); + ((ISetLogicalParent)layer).SetParent(this); + layer.ZIndex = zindex; + VisualChildren.Add(layer); + if (((ILogical)this).IsAttachedToLogicalTree) + ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot)); + InvalidateArrange(); + } + + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _styleRoot = e.Root; + + foreach (var l in _layers) + ((ILogical)l).NotifyAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _styleRoot = null; + base.OnDetachedFromLogicalTree(e); + foreach (var l in _layers) + ((ILogical)l).NotifyDetachedFromLogicalTree(e); + } + + + protected override Size MeasureOverride(Size availableSize) + { + foreach (var l in _layers) + l.Measure(availableSize); + return base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var l in _layers) + l.Arrange(new Rect(finalSize)); + return base.ArrangeOverride(finalSize); + } + } +} diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a0f51099cd..29e3a17f74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -33,8 +34,8 @@ namespace Avalonia.Controls static ProgressBar() { - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); @@ -120,4 +121,4 @@ namespace Avalonia.Controls UpdateIndicator(Bounds.Size); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs new file mode 100644 index 0000000000..04d859c742 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -0,0 +1,54 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + internal class ItemTemplateWrapper + { + private readonly IDataTemplate _dataTemplate; + + public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; + + public IControl GetElement(IControl parent, object data) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + IControl element = null; + + if (recyclePool != null) + { + // try to get an element from the recycle pool. + element = recyclePool.TryGetElement(string.Empty, parent); + } + + if (element == null) + { + // no element was found in recycle pool, create a new element + element = selectedTemplate.Build(data); + + // Associate template with element + element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); + } + + return element; + } + + public void RecycleElement(IControl parent, IControl element) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + if (recyclePool == null) + { + // No Recycle pool in the template, create one. + recyclePool = new RecyclePool(); + RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); + } + + recyclePool.PutElement(element, "" /* key */, parent); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs new file mode 100644 index 0000000000..257c1b2399 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -0,0 +1,724 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + /// Represents a data-driven collection control that incorporates a flexible layout system, + /// custom views, and virtualization. + /// + public class ItemsRepeater : Panel + { + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemTemplateProperty = + ItemsControl.ItemTemplateProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty VerticalCacheLengthProperty = + AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); + + private static readonly AttachedProperty VirtualizationInfoProperty = + AvaloniaProperty.RegisterAttached("VirtualizationInfo"); + + internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); + internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); + + private readonly ViewManager _viewManager; + private readonly ViewportManager _viewportManager; + private IEnumerable _items; + private VirtualizingLayoutContext _layoutContext; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private bool _isLayoutInProgress; + private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; + private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; + private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + + /// + /// Initializes a new instance of the class. + /// + public ItemsRepeater() + { + _viewManager = new ViewManager(this); + _viewportManager = new ViewportManager(this); + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + OnLayoutChanged(null, Layout); + } + + static ItemsRepeater() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + } + + /// + /// Gets or sets the layout used to size and position elements in the ItemsRepeater. + /// + /// + /// The layout used to size and position elements. The default is a StackLayout with + /// vertical orientation. + /// + public AttachedLayout Layout + { + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); + } + + /// + /// Gets or sets an object source used to generate the content of the ItemsRepeater. + /// + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + /// + /// Gets or sets the template used to display each item. + /// + public IDataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling horizontally. + /// + public double HorizontalCacheLength + { + get => GetValue(HorizontalCacheLengthProperty); + set => SetValue(HorizontalCacheLengthProperty, value); + } + + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling vertically. + /// + public double VerticalCacheLength + { + get => GetValue(VerticalCacheLengthProperty); + set => SetValue(VerticalCacheLengthProperty, value); + } + + /// + /// Gets a standardized view of the supported interactions between a given Items object and + /// the ItemsRepeater control and its associated components. + /// + public ItemsSourceView ItemsSourceView { get; private set; } + + internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal Point LayoutOrigin { get; set; } + internal object LayoutState { get; set; } + internal IControl MadeAnchor => _viewportManager.MadeAnchor; + internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow(); + internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor; + + private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; + + private LayoutContext LayoutContext + { + get + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } + + /// + /// Occurs each time an element is cleared and made available to be re-used. + /// + /// + /// This event is raised immediately each time an element is cleared, such as when it falls + /// outside the range of realized items. Elements are cleared when they become available + /// for re-use. + /// + public event EventHandler ElementClearing; + + /// + /// Occurs for each realized when the index for the item it + /// represents has changed. + /// + /// + /// When you use ItemsRepeater to build a more complex control that supports specific + /// interactions on the child elements (such as selection or click), it is useful to be + /// able to keep an up-to-date identifier for the backing data item. + /// + /// This event is raised for each realized IControl where the index for the item it + /// represents has changed. For example, when another item is added or removed in the data + /// source, the index for items that come after in the ordering will be impacted. + /// + public event EventHandler ElementIndexChanged; + + /// + /// Occurs each time an element is prepared for use. + /// + /// + /// The prepared element might be newly created or an existing element that is being re- + /// used. + /// + public event EventHandler ElementPrepared; + + /// + /// Retrieves the index of the item from the data source that corresponds to the specified + /// . + /// + /// + /// The element that corresponds to the item to get the index of. + /// + /// + /// The index of the item from the data source that corresponds to the specified UIElement, + /// or -1 if the element is not supported. + /// + public int GetElementIndex(IControl element) => GetElementIndexImpl(element); + + /// + /// Retrieves the realized UIElement that corresponds to the item at the specified index in + /// the data source. + /// + /// The index of the item. + /// + /// he UIElement that corresponds to the item at the specified index if the item is + /// realized, or null if the item is not realized. + /// + public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + + internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + + internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + + internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + { + var value = element.GetValue(VirtualizationInfoProperty); + return value; + } + + internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) + { + if (TryGetVirtualizationInfo(element) != null) + { + throw new InvalidOperationException("VirtualizationInfo already created."); + } + + var result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + return result; + } + + internal static VirtualizationInfo GetVirtualizationInfo(IControl element) + { + var result = element.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + } + + return result; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _viewportManager.OnOwnerMeasuring(); + + _isLayoutInProgress = true; + + try + { + _viewManager.PrunePinnedElements(); + var extent = new Rect(); + var desiredSize = new Size(); + var layout = Layout; + + if (layout != null) + { + var layoutContext = GetLayoutContext(); + + desiredSize = layout.Measure(layoutContext, availableSize); + extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); + + // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not + // call GetElementAt(index). + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + + if (virtInfo.Owner == ElementOwner.Layout && + virtInfo.AutoRecycleCandidate && + !virtInfo.KeepAlive) + { + ClearElementImpl(element); + } + } + } + + _viewportManager.SetLayoutExtent(extent); + return desiredSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _isLayoutInProgress = true; + + try + { + var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; + + // The view manager might clear elements during this call. + // That's why we call it before arranging cleared elements + // off screen. + _viewManager.OnOwnerArranged(); + + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + virtInfo.KeepAlive = false; + + if (virtInfo.Owner == ElementOwner.ElementFactory || + virtInfo.Owner == ElementOwner.PinnedPool) + { + // Toss it away. And arrange it with size 0 so that XYFocus won't use it. + element.Arrange(new Rect( + ClearedElementsArrangePosition.X - element.DesiredSize.Width, + ClearedElementsArrangePosition.Y - element.DesiredSize.Height, + 0, + 0)); + } + else + { + var newBounds = element.Bounds; + virtInfo.ArrangeBounds = newBounds; + } + } + + _viewportManager.OnOwnerArranged(); + + return arrangeSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + InvalidateMeasure(); + _viewportManager.ResetScrollers(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewportManager.ResetScrollers(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var property = args.Property; + + if (property == ItemsProperty) + { + var newValue = (IEnumerable)args.NewValue; + var newDataSource = newValue as ItemsSourceView; + if (newValue != null && newDataSource == null) + { + newDataSource = new ItemsSourceView(newValue); + } + + OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); + } + else if (property == ItemTemplateProperty) + { + OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + } + else if (property == LayoutProperty) + { + OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); + } + else if (property == HorizontalCacheLengthProperty) + { + _viewportManager.HorizontalCacheLength = (double)args.NewValue; + } + else if (property == VerticalCacheLengthProperty) + { + _viewportManager.VerticalCacheLength = (double)args.NewValue; + } + else + { + base.OnPropertyChanged(args); + } + } + + internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + { + var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + return element; + } + + internal void ClearElementImpl(IControl element) + { + // Clearing an element due to a collection change + // is more strict in that pinned elements will be forcibly + // unpinned and sent back to the view generator. + var isClearedDueToCollectionChange = + _processingItemsSourceChange != null && + (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); + + _viewManager.ClearElement(element, isClearedDueToCollectionChange); + _viewportManager.OnElementCleared(element); + } + + private int GetElementIndexImpl(IControl element) + { + var virtInfo = TryGetVirtualizationInfo(element); + return _viewManager.GetElementIndex(virtInfo); + } + + private IControl GetElementFromIndexImpl(int index) + { + IControl result = null; + + var children = Children; + for (var i = 0; i < children.Count && result == null; ++i) + { + var element = children[i]; + var virtInfo = TryGetVirtualizationInfo(element); + if (virtInfo?.IsRealized == true && virtInfo.Index == index) + { + result = element; + } + } + + return result; + } + + private IControl GetOrCreateElementImpl(int index) + { + if (index >= 0 && index >= ItemsSourceView.Count) + { + throw new ArgumentException("Argument index is invalid.", "index"); + } + + if (_isLayoutInProgress) + { + throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout."); + } + + var element = GetElementFromIndexImpl(index); + bool isAnchorOutsideRealizedRange = element == null; + + if (isAnchorOutsideRealizedRange) + { + if (Layout == null) + { + throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); + } + + element = (IControl)GetLayoutContext().GetOrCreateElementAt(index); + element.Measure(Size.Infinity); + } + + _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange); + InvalidateMeasure(); + + return element; + } + + internal void OnElementPrepared(IControl element, int index) + { + _viewportManager.OnElementPrepared(element); + if (ElementPrepared != null) + { + if (_elementPreparedArgs == null) + { + _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); + } + else + { + _elementPreparedArgs.Update(element, index); + } + + ElementPrepared(this, _elementPreparedArgs); + } + } + + internal void OnElementClearing(IControl element) + { + if (ElementClearing != null) + { + if (_elementClearingArgs == null) + { + _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); + } + else + { + _elementClearingArgs.Update(element); + } + + ElementClearing(this, _elementClearingArgs); + } + } + + internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) + { + if (ElementIndexChanged != null) + { + if (_elementIndexChangedArgs == null) + { + _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); + } + else + { + _elementIndexChangedArgs.Update(element, oldIndex, newIndex); + } + + ElementIndexChanged(this, _elementIndexChangedArgs); + } + } + + private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); + } + + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + + if (oldValue != null) + { + oldValue.CollectionChanged -= OnItemsSourceViewChanged; + } + + if (newValue != null) + { + newValue.CollectionChanged += OnItemsSourceViewChanged; + } + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + + InvalidateMeasure(); + } + } + + private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue) + { + if (_isLayoutInProgress && oldValue != null) + { + throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout."); + } + + // Since the ItemTemplate has changed, we need to re-evaluate all the items that + // have already been created and are now in the tree. The easiest way to do that + // would be to do a reset.. Note that this has to be done before we change the template + // so that the cleared elements go back into the old template. + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } + } + else if (Layout is NonVirtualizingLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + } + + ItemTemplateShim = new ItemTemplateWrapper(newValue); + + InvalidateMeasure(); + } + + private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue) + { + if (_isLayoutInProgress) + { + throw new InvalidOperationException("Layout cannot be changed during layout."); + } + + _viewManager.OnLayoutChanging(); + + if (oldValue != null) + { + oldValue.UninitializeForContext(LayoutContext); + oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; + oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + // Walk through all the elements and make sure they are cleared + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + + LayoutState = null; + } + + if (newValue != null) + { + newValue.InitializeForContext(LayoutContext); + newValue.MeasureInvalidated += InvalidateMeasureForLayout; + newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + } + + bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; + _viewportManager.OnLayoutChanged(isVirtualizingLayout); + InvalidateMeasure(); + } + + private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_isLayoutInProgress) + { + // Bad things will follow if the data changes while we are in the middle of a layout pass. + throw new InvalidOperationException("Changes in data source are not allowed during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source."); + } + + _processingItemsSourceChange = args; + + try + { + _viewManager.OnItemsSourceChanged(sender, args); + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); + } + else + { + // NonVirtualizingLayout + InvalidateMeasure(); + } + } + } + finally + { + _processingItemsSourceChange = null; + } + } + + private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); + + private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); + + private VirtualizingLayoutContext GetLayoutContext() + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs new file mode 100644 index 0000000000..75d50e52a6 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs @@ -0,0 +1,24 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementClearingEventArgs : EventArgs + { + internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + + /// + /// Gets the element that is being cleared for re-use. + /// + public IControl Element { get; private set; } + + internal void Update(IControl element) => Element = element; + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs new file mode 100644 index 0000000000..7ca68140b2 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -0,0 +1,44 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs + { + internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + + /// + /// Get the element for which the index changed. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the element after the change. + /// + public int NewIndex { get; private set; } + + /// + /// Gets the index of the element before the change. + /// + public int OldIndex { get; private set; } + + internal void Update(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs new file mode 100644 index 0000000000..5a30dbcf2a --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs @@ -0,0 +1,35 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementPreparedEventArgs + { + internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) + { + Element = element; + Index = index; + } + + /// + /// Gets the prepared element. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the item the element was prepared for. + /// + public int Index { get; private set; } + + internal void Update(IControl element, int index) + { + Element = element; + Index = index; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs new file mode 100644 index 0000000000..02ead7ef36 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs @@ -0,0 +1,145 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Avalonia.Controls +{ + /// + /// Represents a standardized view of the supported interactions between a given ItemsSource + /// object and an control. + /// + /// + /// Components written to work with ItemsRepeater should consume the + /// via ItemsSourceView since this provides a normalized + /// view of the Items. That way, each component does not need to know if the source is an + /// IEnumerable, an IList, or something else. + /// + public class ItemsSourceView : INotifyCollectionChanged, IDisposable + { + private readonly IList _inner; + private INotifyCollectionChanged _notifyCollectionChanged; + private int _cachedSize = -1; + + /// + /// Initializes a new instance of the ItemsSourceView class for the specified data source. + /// + /// The data source. + public ItemsSourceView(IEnumerable source) + { + Contract.Requires(source != null); + + if (source is IList list) + { + _inner = list; + } + else if (source is IEnumerable objectEnumerable) + { + _inner = new List(objectEnumerable); + } + else + { + _inner = new List(source.Cast()); + } + + ListenToCollectionChanges(); + } + + /// + /// Gets the number of items in the collection. + /// + public int Count + { + get + { + if (_cachedSize == -1) + { + _cachedSize = _inner.Count; + } + + return _cachedSize; + } + } + + /// + /// Gets a value that indicates whether the items source can provide a unique key for each item. + /// + /// + /// TODO: Not yet implemented in Avalonia. + /// + public bool HasKeyIndexMapping => false; + + /// + /// Occurs when the collection has changed to indicate the reason for the change and which items changed. + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + public void Dispose() + { + if (_notifyCollectionChanged != null) + { + _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + } + } + + /// + /// Retrieves the item at the specified index. + /// + /// The index. + /// the item. + public object GetAt(int index) => _inner[index]; + + /// + /// Retrieves the index of the item that has the specified unique identifier (key). + /// + /// The index. + /// The key + /// + /// TODO: Not yet implemented in Avalonia. + /// + public string KeyFromIndex(int index) + { + throw new NotImplementedException(); + } + + /// + /// Retrieves the unique identifier (key) for the item at the specified index. + /// + /// The key. + /// The index. + /// + /// TODO: Not yet implemented in Avalonia. + /// + public int IndexFromKey(string key) + { + throw new NotImplementedException(); + } + + protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) + { + _cachedSize = _inner.Count; + CollectionChanged?.Invoke(this, args); + } + + private void ListenToCollectionChanges() + { + if (_inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + _notifyCollectionChanged = incc; + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnItemsSourceChanged(e); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs new file mode 100644 index 0000000000..4e5950bdc5 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -0,0 +1,106 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + internal class RecyclePool + { + public static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + + private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); + private readonly Dictionary> _elements = new Dictionary>(); + + public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate) + { + s_pools.TryGetValue(dataTemplate, out var result); + return result; + } + + public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value); + + public void PutElement(IControl element, string key, IControl owner) + { + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + var elementInfo = new ElementInfo(element, ownerAsPanel); + + if (!_elements.TryGetValue(key, out var pool)) + { + pool = new List(); + _elements.Add(key, pool); + } + + pool.Add(elementInfo); + } + + public IControl TryGetElement(string key, IControl owner) + { + if (_elements.TryGetValue(key, out var elements)) + { + if (elements.Count > 0) + { + // Prefer an element from the same owner or with no owner so that we don't incur + // the enter/leave cost during recycling. + // TODO: prioritize elements with the same owner to those without an owner. + var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault(); + elements.Remove(elementInfo); + + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel) + { + // Element is still under its parent. remove it from its parent. + var panel = elementInfo.Owner; + if (panel != null) + { + int childIndex = panel.Children.IndexOf(elementInfo.Element); + if (childIndex == -1) + { + throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection."); + } + + panel.Children.RemoveAt(childIndex); + } + } + + return elementInfo.Element; + } + } + + return null; + } + + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) + { + if (owner is IPanel panel) + { + return panel; + } + else if (owner != null) + { + throw new InvalidOperationException("Owner must be IPanel or null."); + } + + return null; + } + + private class ElementInfo + { + public ElementInfo(IControl element, IPanel owner) + { + Element = element; + Owner = owner; + } + + public IControl Element { get; } + public IPanel Owner { get;} + } + } +} diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs new file mode 100644 index 0000000000..977d9d794c --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -0,0 +1,65 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + internal class RepeaterLayoutContext : VirtualizingLayoutContext + { + private readonly ItemsRepeater _owner; + + public RepeaterLayoutContext(ItemsRepeater owner) + { + _owner = owner; + } + + protected override Point LayoutOriginCore + { + get => _owner.LayoutOrigin; + set => _owner.LayoutOrigin = value; + } + + protected override object LayoutStateCore + { + get => _owner.LayoutState; + set => _owner.LayoutState = value; + } + + protected override int RecommendedAnchorIndexCore + { + get + { + int anchorIndex = -1; + var anchor = _owner.SuggestedAnchor; + if (anchor != null) + { + anchorIndex = _owner.GetElementIndex(anchor); + } + + return anchorIndex; + } + } + + protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; + + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) + { + return _owner.GetElementImpl( + index, + (options & ElementRealizationOptions.ForceCreate) != 0, + (options & ElementRealizationOptions.SuppressAutoRecycle) != 0); + } + + protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); + + protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); + + protected override Rect RealizationRectCore() => _owner.RealizationWindow; + } +} diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs new file mode 100644 index 0000000000..775aa3f113 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs @@ -0,0 +1,54 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls +{ + internal class UniqueIdElementPool : IEnumerable> + { + private readonly Dictionary _elementMap = new Dictionary(); + private readonly ItemsRepeater _owner; + + public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner; + + public void Add(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var key = virtInfo.UniqueId; + + if (_elementMap.ContainsKey(key)) + { + throw new InvalidOperationException($"The unique id provided ({key}) is not unique."); + } + + _elementMap.Add(key, element); + } + + public IControl Remove(int index) + { + // Check if there is already a element in the mapping and if so, use it. + string key = _owner.ItemsSourceView.KeyFromIndex(index); + + if (_elementMap.TryGetValue(key, out var element)) + { + _elementMap.Remove(key); + } + + return element; + } + + public void Clear() + { + _elementMap.Clear(); + } + + public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs new file mode 100644 index 0000000000..833e708e9e --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -0,0 +1,682 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal sealed class ViewManager + { + private const int FirstRealizedElementIndexDefault = int.MaxValue; + private const int LastRealizedElementIndexDefault = int.MinValue; + + private readonly ItemsRepeater _owner; + private readonly List _pinnedPool = new List(); + private readonly UniqueIdElementPool _resetPool; + private IControl _lastFocusedElement; + private bool _isDataSourceStableResetPending; + private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + private bool _eventsSubscribed; + + public ViewManager(ItemsRepeater owner) + { + _owner = owner; + _resetPool = new UniqueIdElementPool(owner); + } + + public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle) + { + var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index); + if (element == null) + { + // check if this is the anchor made through repeater in preparation + // for a bring into view. + var madeAnchor = _owner.MadeAnchor; + if (madeAnchor != null) + { + var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); + if (anchorVirtInfo.Index == index) + { + element = madeAnchor; + } + } + } + if (element == null) { element = GetElementFromUniqueIdResetPool(index); }; + if (element == null) { element = GetElementFromPinnedElements(index); } + if (element == null) { element = GetElementFromElementFactory(index); } + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (suppressAutoRecycle) + { + virtInfo.AutoRecycleCandidate = false; + } + else + { + virtInfo.AutoRecycleCandidate = true; + virtInfo.KeepAlive = true; + } + + return element; + } + + public void ClearElement(IControl element, bool isClearedDueToCollectionChange) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var index = virtInfo.Index; + bool cleared = + ClearElementToUniqueIdResetPool(element, virtInfo) || + ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange); + + if (!cleared) + { + ClearElementToElementFactory(element); + } + + // Both First and Last indices need to be valid or default. + if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout) + { + // First and last were pointing to the same element and that is going away. + InvalidateRealizedIndicesHeldByLayout(); + } + else if (index == _firstRealizedElementIndexHeldByLayout) + { + // The FirstElement is going away, shrink the range by one. + ++_firstRealizedElementIndexHeldByLayout; + } + else if (index == _lastRealizedElementIndexHeldByLayout) + { + // Last element is going away, shrink the range by one at the end. + --_lastRealizedElementIndexHeldByLayout; + } + else + { + // Index is either outside the range we are keeping track of or inside the range. + // In both these cases, we just keep the range we have. If this clear was due to + // a collection change, then in the CollectionChanged event, we will invalidate these guys. + } + } + + public void ClearElementToElementFactory(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var clearedIndex = virtInfo.Index; + _owner.OnElementClearing(element); + _owner.ItemTemplateShim.RecycleElement(_owner, element); + + virtInfo.MoveOwnershipToElementFactory(); + + if (_lastFocusedElement == element) + { + // Focused element is going away. Remove the tracked last focused element + // and pick a reasonable next focus if we can find one within the layout + // realized elements. + MoveFocusFromClearedIndex(clearedIndex); + } + + } + + private void MoveFocusFromClearedIndex(int clearedIndex) + { + IControl focusedChild = null; + var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); + if (focusCandidate != null) + { + focusCandidate.Focus(); + _lastFocusedElement = focusedChild; + + // Add pin to hold the focused element. + UpdatePin(focusedChild, true /* addPin */); + } + else + { + // We could not find a candiate. + _lastFocusedElement = null; + } + } + + IControl FindFocusCandidate(int clearedIndex, IControl focusedChild) + { + // Walk through all the children and find elements with index before and after the cleared index. + // Note that during a delete the next element would now have the same index. + int previousIndex = int.MinValue; + int nextIndex = int.MaxValue; + IControl nextElement = null; + IControl previousElement = null; + + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + int currentIndex = virtInfo.Index; + if (currentIndex < clearedIndex) + { + if (currentIndex > previousIndex) + { + previousIndex = currentIndex; + previousElement = child; + } + } + else if (currentIndex >= clearedIndex) + { + // Note that we use >= above because if we deleted the focused element, + // the next element would have the same index now. + if (currentIndex < nextIndex) + { + nextIndex = currentIndex; + nextElement = child; + } + } + } + } + + // TODO: Find the next element if one exists, if not use the previous element. + // If the container itself is not focusable, find a descendent that is. + + return nextElement; + } + + public int GetElementIndex(VirtualizationInfo virtInfo) + { + if (virtInfo == null) + { + throw new ArgumentException("Element is not a child of this ItemsRepeater."); + } + + return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; + } + + public void PrunePinnedElements() + { + EnsureEventSubscriptions(); + + // Go through pinned elements and make sure they still have + // a reason to be pinned. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (!virtInfo.IsPinned) + { + _pinnedPool.RemoveAt(i); + --i; + + // Pinning was the only thing keeping this element alive. + ClearElementToElementFactory(elementInfo.PinnedElement); + } + } + } + + public void UpdatePin(IControl element, bool addPin) + { + var parent = element.VisualParent; + var child = (IVisual)element; + + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child); + if (virtInfo.IsRealized) + { + if (addPin) + { + virtInfo.AddPin(); + } + else if (virtInfo.IsPinned) + { + if (virtInfo.RemovePin() == 0) + { + // ElementFactory is invoked during the measure pass. + // We will clear the element then. + repeater.InvalidateMeasure(); + } + } + } + } + + child = parent; + parent = child.VisualParent; + } + } + + public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args) + { + // Note: For items that have been removed, the index will not be touched. It will hold + // the old index before it was removed. It is not valid anymore. + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + { + var newIndex = args.NewStartingIndex; + var newCount = args.NewItems.Count; + EnsureFirstLastRealizedIndices(); + if (newIndex <= _lastRealizedElementIndexHeldByLayout) + { + _lastRealizedElementIndexHeldByLayout += newCount; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + else + { + // Indices held by layout are not affected + // We could still have items in the pinned elements that need updates. This is usually a very small vector. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + var element = elementInfo.PinnedElement; + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true. + // Two cases here + // case 1: oldCount == newCount + // indices are not affected. nothing to do here. + // case 2: oldCount != newCount + // Replaced with less or more items. This is like an insert or remove + // depending on the counts. + var oldStartIndex = args.OldStartingIndex; + var newStartingIndex = args.NewStartingIndex; + var oldCount = args.OldItems.Count; + var newCount = args.NewItems.Count; + if (oldStartIndex != newStartingIndex) + { + throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); + } + + if (oldCount == 0) + { + throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); + } + + if (newCount == 0) + { + throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead."); + } + + int countChange = newCount - oldCount; + if (countChange != 0) + { + // countChange > 0 : countChange items were added + // countChange < 0 : -countChange items were removed + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (dataIndex >= oldStartIndex + oldCount) + { + UpdateElementIndex(element, virtInfo, dataIndex + countChange); + } + } + } + + EnsureFirstLastRealizedIndices(); + _lastRealizedElementIndexHeldByLayout += countChange; + } + break; + } + + case NotifyCollectionChangedAction.Remove: + { + var oldStartIndex = args.OldStartingIndex; + var oldCount = args.OldItems.Count; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) + { + // If we are doing the mapping, remove the element who's data was removed. + _owner.ClearElementImpl(element); + } + else if (dataIndex >= (oldStartIndex + oldCount)) + { + UpdateElementIndex(element, virtInfo, dataIndex - oldCount); + } + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + + case NotifyCollectionChangedAction.Reset: + if (_owner.ItemsSourceView.HasKeyIndexMapping) + { + _isDataSourceStableResetPending = true; + } + + // Walk through all the elements and make sure they are cleared, they will go into + // the stable id reset pool. + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + { + _owner.ClearElementImpl(element); + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + } + + private void EnsureFirstLastRealizedIndices() + { + if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault) + { + // This will ensure that the indexes are updated. + GetElementIfAlreadyHeldByLayout(0); + } + } + + public void OnLayoutChanging() + { + if (_owner.ItemsSourceView?.HasKeyIndexMapping == true) + { + _isDataSourceStableResetPending = true; + } + } + + public void OnOwnerArranged() + { + if (_isDataSourceStableResetPending) + { + _isDataSourceStableResetPending = false; + + foreach (var entry in _resetPool) + { + // TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource. + // Focused element is getting cleared. Need to figure out semantics on where + // focus should go when the focused element is removed from the data collection. + ClearElement(entry.Value, true /* isClearedDueToCollectionChange */); + } + + _resetPool.Clear(); + } + } + + // We optimize for the case where index is not realized to return null as quickly as we can. + // Flow layouts manage containers on their own and will never ask for an index that is already realized. + // If an index that is realized is requested by the layout, we unfortunately have to walk the + // children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing + // and non-virtualizing hosts. + private IControl GetElementIfAlreadyHeldByLayout(int index) + { + IControl element = null; + + bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; + bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); + + if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) + { + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + // Only give back elements held by layout. If someone else is holding it, they will be served by other methods. + int childIndex = virtInfo.Index; + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex); + if (virtInfo.Index == index) + { + element = child; + // If we have valid first/last indices, we don't have to walk the rest, but if we + // do not, then we keep walking through the entire children collection to get accurate + // indices once. + if (!cachedFirstLastIndicesInvalid) + { + break; + } + } + } + } + } + + return element; + } + + private IControl GetElementFromUniqueIdResetPool(int index) + { + IControl element = null; + // See if you can get it from the reset pool. + if (_isDataSourceStableResetPending) + { + element = _resetPool.Remove(index); + if (element != null) + { + // Make sure that the index is updated to the current one + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); + UpdateElementIndex(element, virtInfo, index); + } + } + + return element; + } + + private IControl GetElementFromPinnedElements(int index) + { + IControl element = null; + + // See if you can find something among the pinned elements. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (virtInfo.Index == index) + { + _pinnedPool.RemoveAt(i); + element = elementInfo.PinnedElement; + elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); + break; + } + } + + return element; + } + + private IControl GetElementFromElementFactory(int index) + { + // The view generator is the provider of last resort. + + var itemTemplateFactory = _owner.ItemTemplateShim; + if (itemTemplateFactory == null) + { + // If no ItemTemplate was provided, use a default + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + itemTemplateFactory = _owner.ItemTemplateShim; + } + + var data = _owner.ItemsSourceView.GetAt(index); + var element = itemTemplateFactory.GetElement(_owner, data); + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (virtInfo == null) + { + virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); + } + + // Prepare the element + element.DataContext = data; + + virtInfo.MoveOwnershipToLayoutFromElementFactory( + index, + /* uniqueId: */ + _owner.ItemsSourceView.HasKeyIndexMapping ? + _owner.ItemsSourceView.KeyFromIndex(index) : + string.Empty); + + // The view generator is the only provider that prepares the element. + var repeater = _owner; + + // Add the element to the children collection here before raising OnElementPrepared so + // that handlers can walk up the tree in case they want to find their IndexPath in the + // nested case. + var children = repeater.Children; + if (element.VisualParent != repeater) + { + children.Add(element); + } + + repeater.OnElementPrepared(element, index); + + // Update realized indices + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); + + return element; + } + + private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private void UpdateFocusedElement() + { + IControl focusedElement = null; + + var child = FocusManager.Instance.Current; + + if (child != null) + { + var parent = child.VisualParent; + var owner = _owner; + + // Find out if the focused element belongs to one of our direct + // children. + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var element = child as IControl; + if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized) + { + focusedElement = element; + } + + break; + } + + child = parent as IInputElement; + parent = child.VisualParent; + } + } + + // If the focused element has changed, + // we need to unpin the old one and pin the new one. + if (_lastFocusedElement != focusedElement) + { + if (_lastFocusedElement != null) + { + UpdatePin(_lastFocusedElement, false /* addPin */); + } + + if (focusedElement != null) + { + UpdatePin(focusedElement, true /* addPin */); + } + + _lastFocusedElement = focusedElement; + } + } + + private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement(); + + private void EnsureEventSubscriptions() + { + if (!_eventsSubscribed) + { + _owner.GotFocus += OnFocusChanged; + _owner.LostFocus += OnFocusChanged; + } + } + + private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index) + { + var oldIndex = virtInfo.Index; + if (oldIndex != index) + { + virtInfo.UpdateIndex(index); + _owner.OnElementIndexChanged(element, oldIndex, index); + } + } + + private void InvalidateRealizedIndicesHeldByLayout() + { + _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + } + + private struct PinnedElementInfo + { + public PinnedElementInfo(IControl element) + { + PinnedElement = element; + VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element); + } + + public IControl PinnedElement { get; } + public VirtualizationInfo VirtualizationInfo { get; } + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs new file mode 100644 index 0000000000..10c11889d0 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -0,0 +1,501 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Layout; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal class ViewportManager + { + private const double CacheBufferPerSideInflationPixelDelta = 40.0; + private readonly ItemsRepeater _owner; + private bool _ensuredScroller; + private IScrollAnchorProvider _scroller; + private IControl _makeAnchorElement; + private bool _isAnchorOutsideRealizedRange; + private Task _cacheBuildAction; + private Rect _visibleWindow; + private Rect _layoutExtent; + // This is the expected shift by the layout. + private Point _expectedViewportShift; + // This is what is pending and not been accounted for. + // Sometimes the scrolling surface cannot service a shift (for example + // it is already at the top and cannot shift anymore.) + private Point _pendingViewportShift; + // Unshiftable shift amount that this view manager can + // handle on its own to fake it to the layout as if the shift + // actually happened. This can happen in cases where no scrollviewer + // in the parent chain can scroll in the shift direction. + private Point _unshiftableShift; + private double _maximumHorizontalCacheLength = 0.0; + private double _maximumVerticalCacheLength = 0.0; + private double _horizontalCacheBufferPerSide; + private double _verticalCacheBufferPerSide; + private bool _isBringIntoViewInProgress; + // For non-virtualizing layouts, we do not need to keep + // updating viewports and invalidating measure often. So when + // a non virtualizing layout is used, we stop doing all that work. + bool _managingViewportDisabled; + private IDisposable _effectiveViewportChangedRevoker; + private bool _layoutUpdatedSubscribed; + + public ViewportManager(ItemsRepeater owner) + { + _owner = owner; + } + + public IControl SuggestedAnchor + { + get + { + // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick. + var suggestedAnchor = _makeAnchorElement; + var owner = _owner; + + if (suggestedAnchor == null) + { + var anchorElement = _scroller?.CurrentAnchor; + + if (anchorElement != null) + { + // We can't simply return anchorElement because, in case of nested Repeaters, it may not + // be a direct child of ours, or even an indirect child. We need to walk up the tree starting + // from anchorElement to figure out what child of ours (if any) to use as the suggested element. + var child = anchorElement; + var parent = child.VisualParent as IControl; + + while (parent != null) + { + if (parent == owner) + { + suggestedAnchor = child; + break; + } + + child = parent; + parent = parent.VisualParent as IControl; + } + } + } + + return suggestedAnchor; + } + } + + public bool HasScroller => _scroller != null; + + public IControl MadeAnchor => _makeAnchorElement; + + public double HorizontalCacheLength + { + get => _maximumHorizontalCacheLength; + set + { + if (_maximumHorizontalCacheLength != value) + { + ValidateCacheLength(value); + _maximumHorizontalCacheLength = value; + } + } + } + + public double VerticalCacheLength + { + get => _maximumVerticalCacheLength; + set + { + if (_maximumVerticalCacheLength != value) + { + ValidateCacheLength(value); + _maximumVerticalCacheLength = value; + } + } + } + + private Rect GetLayoutVisibleWindowDiscardAnchor() + { + var visibleWindow = _visibleWindow; + + if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutVisibleWindow() + { + var visibleWindow = _visibleWindow; + + if (_makeAnchorElement != null) + { + // The anchor is not necessarily laid out yet. Its position should default + // to zero and the layout origin is expected to change once layout is done. + // Until then, we need a window that's going to protect the anchor from + // getting recycled. + visibleWindow = visibleWindow.WithX(0).WithY(0); + } + else if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutRealizationWindow() + { + var realizationWindow = GetLayoutVisibleWindow(); + if (HasScroller) + { + realizationWindow = new Rect( + realizationWindow.X - _horizontalCacheBufferPerSide, + realizationWindow.Y - _verticalCacheBufferPerSide, + realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0, + realizationWindow.Height + _verticalCacheBufferPerSide * 2.0); + } + + return realizationWindow; + } + + public void SetLayoutExtent(Rect extent) + { + _expectedViewportShift = new Point( + _expectedViewportShift.X + _layoutExtent.X - extent.X, + _expectedViewportShift.Y + _layoutExtent.Y - extent.Y); + + // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. + if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) + { + // There are cases where we might be expecting a shift but not get it. We will + // be waiting for the effective viewport event but if the scroll viewer is not able + // to perform the shift (perhaps because it cannot scroll in negative offset), + // then we will end up not realizing elements in the visible + // window. To avoid this, we register to layout updated for this layout pass. If we + // get an effective viewport, we know we have a new viewport and we unregister from + // layout updated. If we get the layout updated handler, then we know that the + // scroller was unable to perform the shift and we invalidate measure and unregister + // from the layout updated event. + if (!_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated += OnLayoutUpdated; + _layoutUpdatedSubscribed = true; + } + } + + _layoutExtent = extent; + _pendingViewportShift = _expectedViewportShift; + + // We just finished a measure pass and have a new extent. + // Let's make sure the scrollers will run its arrange so that they track the anchor. + ((IControl)_scroller)?.InvalidateArrange(); + } + + public Point GetOrigin() => _layoutExtent.TopLeft; + + public void OnLayoutChanged(bool isVirtualizing) + { + _managingViewportDisabled = !isVirtualizing; + + _layoutExtent = default; + _expectedViewportShift = default; + _pendingViewportShift = default; + _unshiftableShift = default; + + _effectiveViewportChangedRevoker?.Dispose(); + + if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + } + + public void OnElementPrepared(IControl element) + { + // If we have an anchor element, we do not want the + // scroll anchor provider to start anchoring some other element. + ////element.CanBeScrollAnchor(true); + } + + public void OnElementCleared(ILayoutable element) + { + ////element.CanBeScrollAnchor(false); + } + + public void OnOwnerMeasuring() + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + EnsureScroller(); + } + + public void OnOwnerArranged() + { + _expectedViewportShift = default; + + if (!_managingViewportDisabled) + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + // EnsureScroller(); + + if (HasScroller) + { + double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0; + double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0; + + bool continueBuildingCache = + _horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide || + _verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide; + + if (continueBuildingCache) + { + _horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + _verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + + _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); + _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); + } + } + } + } + + private void OnLayoutUpdated(object sender, EventArgs args) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + if (_managingViewportDisabled) + { + return; + } + + // We were expecting a viewport shift but we never got one and we are not going to in this + // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and + // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer + // that can scroll in the direction where the shift is expected. + if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) + { + // Assume this is never going to come. + _unshiftableShift = new Point( + _unshiftableShift.X + _pendingViewportShift.X, + _unshiftableShift.Y + _pendingViewportShift.Y); + _pendingViewportShift = default; + _expectedViewportShift = default; + + TryInvalidateMeasure(); + } + } + + public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) + { + _makeAnchorElement = anchor; + _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; + } + + public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) + { + if (!_managingViewportDisabled) + { + // During the time between a bring into view request and the element coming into view we do not + // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the + // element that is being brought into view. We can do this by making just that element as a potential + // anchor candidate and ensure no other element of this repeater is an anchor candidate. + // Once the layout pass is done and we render the frame, the element will be in frame and we can + // switch back to letting the anchor provider pick a suitable anchor. + + // get the targetChild - i.e the immediate child of this repeater that is being brought into view. + // Note that the element being brought into view could be a descendant. + var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); + + // Make sure that only the target child can be the anchor during the bring into view operation. + foreach (var child in _owner.Children) + { + ////if (child.CanBeScrollAnchor && child != targetChild) + ////{ + //// child.CanBeScrollAnchor = false; + ////} + } + + // Register to rendering event to go back to how things were before where any child can be the anchor. + _isBringIntoViewInProgress = true; + ////if (!m_renderingToken) + ////{ + //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; + //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); + ////} + } + } + + private IControl GetImmediateChildOfRepeater(IControl descendant) + { + var targetChild = descendant; + var parent = descendant.Parent; + while (parent != null && parent != _owner) + { + targetChild = parent; + parent = (IControl)parent.VisualParent; + } + + if (parent == null) + { + throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); + } + + return targetChild; + } + + public void ResetScrollers() + { + _scroller = null; + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = null; + _ensuredScroller = false; + } + + private void OnEffectiveViewportChanged(TransformedBounds? bounds) + { + if (!bounds.HasValue) + { + return; + } + + var globalClip = bounds.Value.Clip; + var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; + var clip = globalClip.TransformToAABB(transform); + var effectiveViewport = clip.Intersect(bounds.Value.Bounds); + + UpdateViewport(effectiveViewport); + + _pendingViewportShift = default; + _unshiftableShift = default; + if (_visibleWindow.IsEmpty) + { + // We got cleared. + _layoutExtent = default; + } + + // We got a new viewport, we dont need to wait for layout updated anymore to + // see if our request for a pending shift was handled. + if (_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + } + } + + private void EnsureScroller() + { + if (!_ensuredScroller) + { + ResetScrollers(); + + var parent = _owner.GetVisualParent(); + while (parent != null) + { + if (parent is IScrollAnchorProvider scroller) + { + _scroller = scroller; + break; + } + + parent = parent.VisualParent; + } + + if (_scroller == null) + { + // We usually update the viewport in the post arrange handler. But, since we don't have + // a scroller, let's do it now. + UpdateViewport(Rect.Empty); + } + else if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + + _ensuredScroller = true; + } + } + + private void UpdateViewport(Rect viewport) + { + var currentVisibleWindow = viewport; + + if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && + -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) + { + // We got cleared. + _visibleWindow = default; + } + else + { + _visibleWindow = currentVisibleWindow; + } + + TryInvalidateMeasure(); + } + + private static void ValidateCacheLength(double cacheLength) + { + if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) + { + throw new ArgumentException("The maximum cache length must be equal or superior to zero."); + } + } + + private void TryInvalidateMeasure() + { + // Don't invalidate measure if we have an invalid window. + if (!_visibleWindow.IsEmpty) + { + // We invalidate measure instead of just invalidating arrange because + // we don't invalidate measure in UpdateViewport if the view is changing to + // avoid layout cycles. + _owner.InvalidateMeasure(); + } + } + + private IDisposable SubscribeToEffectiveViewportChanged(IControl control) + { + // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - + // we can get this from TransformedBounds, but this property is updated after layout has + // run, resulting in the UI being updated too late when scrolling quickly. We can + // partially remedey this by triggering also on Bounds changes, but this won't work so + // well for nested ItemsRepeaters. + // + // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially + // for this case): we need to implement that in Avalonia. + return control.GetObservable(Visual.TransformedBoundsProperty) + .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) + .Skip(1) + .Subscribe(OnEffectiveViewportChanged); + } + + private class ScrollerInfo + { + public ScrollerInfo(ScrollViewer scroller) + { + Scroller = scroller; + } + + public ScrollViewer Scroller { get; } + } + }; +} diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs new file mode 100644 index 0000000000..eb30c1b7cf --- /dev/null +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -0,0 +1,118 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Controls +{ + internal enum ElementOwner + { + // All elements are originally owned by the view generator. + ElementFactory, + // Ownership is transferred to the layout when it calls GetElement. + Layout, + // Ownership is transferred to the pinned pool if the element is cleared (outside of + // a 'remove' collection change of course). + PinnedPool, + // Ownership is transfered to the reset pool if the element is cleared by a reset and + // the data source supports unique ids. + UniqueIdResetPool, + // Ownership is transfered to the animator if the element is cleared due to a + // 'remove'-like collection change. + Animator + } + + internal class VirtualizationInfo + { + private int _pinCounter; + private object _data; + + public Rect ArrangeBounds { get; set; } + public bool AutoRecycleCandidate { get; set; } + public int Index { get; private set; } + public bool IsPinned => _pinCounter > 0; + public bool IsHeldByLayout => Owner == ElementOwner.Layout; + public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; + public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; + public bool KeepAlive { get; set; } + public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; + public string UniqueId { get; private set; } + + public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) + { + Owner = ElementOwner.Layout; + Index = index; + UniqueId = uniqueId; + } + + public void MoveOwnershipToLayoutFromUniqueIdResetPool() + { + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToLayoutFromPinnedPool() + { + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToElementFactory() + { + Owner = ElementOwner.ElementFactory; + _pinCounter = 0; + Index = -1; + UniqueId = string.Empty; + ArrangeBounds = ItemsRepeater.InvalidRect; + } + + public void MoveOwnershipToUniqueIdResetPoolFromLayout() + { + Owner = ElementOwner.UniqueIdResetPool; + // Keep the pinCounter the same. If the container survives the reset + // it can go on being pinned as if nothing happened. + } + + public void MoveOwnershipToAnimator() + { + // During a unique id reset, some elements might get removed. + // Their ownership will go from the UniqueIdResetPool to the Animator. + // The common path though is for ownership to go from Layout to Animator. + Owner = ElementOwner.Animator; + Index = -1; + _pinCounter = 0; + } + + public void MoveOwnershipToPinnedPool() + { + Owner = ElementOwner.PinnedPool; + } + + public int AddPin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't pin an unrealized element."); + } + + return ++_pinCounter; + } + + public int RemovePin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't unpin an unrealized element."); + } + + if (!IsPinned) + { + throw new InvalidOperationException("UnpinElement was called more often than PinElement."); + } + + return --_pinCounter; + } + + public void UpdateIndex(int newIndex) => Index = newIndex; + } +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 264b1fd2ce..c9b5cbb75b 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// /// A control scrolls its content if the content is bigger than the space available. /// - public class ScrollViewer : ContentControl, IScrollable + public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// /// Defines the property. @@ -333,6 +333,9 @@ namespace Avalonia.Controls get { return _viewport.Height; } } + /// + IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement + /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// @@ -373,6 +376,16 @@ namespace Avalonia.Controls control.SetValue(VerticalScrollBarVisibilityProperty, value); } + void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + + void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) { var maxX = Math.Max(extent.Width - viewport.Width, 0); diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index bc4733296b..9eaa246434 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..bd3441078d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -1,8 +1,9 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Linq; using Avalonia.Input; using Avalonia.Layout; @@ -17,13 +18,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackLayout.SpacingProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackLayout.OrientationProperty.AddOwner(); /// /// Initializes static members of the class. @@ -155,106 +156,122 @@ namespace Avalonia.Controls } /// - /// Measures the control. + /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). + /// Children in this dimension are encouraged to be as large as they like. In the other dimension, + /// StackPanel will assume the maximum size of its children. /// - /// The available size. - /// The desired size of the control. + /// Constraint + /// Desired size protected override Size MeasureOverride(Size availableSize) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + Size stackDesiredSize = new Size(); + var children = Children; + Size layoutSlotSize = availableSize; + bool fHorizontal = (Orientation == Orientation.Horizontal); + double spacing = Spacing; + bool hasVisibleChild = false; - if (Orientation == Orientation.Vertical) + // + // Initialize child sizing and iterator data + // Allow children as much size as they want along the stack. + // + if (fHorizontal) { - childAvailableWidth = availableSize.Width; - - if (!double.IsNaN(Width)) - { - childAvailableWidth = Width; - } - - childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth); - childAvailableWidth = Math.Max(childAvailableWidth, MinWidth); + layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity); } else { - childAvailableHeight = availableSize.Height; + layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); + } - if (!double.IsNaN(Height)) - { - childAvailableHeight = Height; - } + // + // Iterate through children. + // While we still supported virtualization, this was hidden in a child iterator (see source history). + // + for (int i = 0, count = children.Count; i < count; ++i) + { + // Get next child. + var child = children[i]; - childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight); - childAvailableHeight = Math.Max(childAvailableHeight, MinHeight); - } + if (child == null) + { continue; } - double measuredWidth = 0; - double measuredHeight = 0; - double spacing = Spacing; - bool hasVisibleChild = Children.Any(c => c.IsVisible); + bool isVisible = child.IsVisible; - foreach (Control child in Children) - { - child.Measure(new Size(childAvailableWidth, childAvailableHeight)); - Size size = child.DesiredSize; + if (isVisible && !hasVisibleChild) + { + hasVisibleChild = true; + } - if (Orientation == Orientation.Vertical) + // Measure the child. + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; + + // Accumulate child size. + if (fHorizontal) { - measuredHeight += size.Height + (child.IsVisible ? spacing : 0); - measuredWidth = Math.Max(measuredWidth, size.Width); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width); + stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { - measuredWidth += size.Width + (child.IsVisible ? spacing : 0); - measuredHeight = Math.Max(measuredHeight, size.Height); + stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height); } } - if (Orientation == Orientation.Vertical) + if (fHorizontal) { - measuredHeight -= (hasVisibleChild ? spacing : 0); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); } else - { - measuredWidth -= (hasVisibleChild ? spacing : 0); + { + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return new Size(measuredWidth, measuredHeight).Constrain(availableSize); + return stackDesiredSize; } - /// + /// + /// Content arrangement. + /// + /// Arrange size protected override Size ArrangeOverride(Size finalSize) { - var orientation = Orientation; + var children = Children; + bool fHorizontal = (Orientation == Orientation.Horizontal); + Rect rcChild = new Rect(finalSize); + double previousChildSize = 0.0; var spacing = Spacing; - var finalRect = new Rect(finalSize); - var pos = 0.0; - foreach (Control child in Children) + // + // Arrange and Position Children. + // + for (int i = 0, count = children.Count; i < count; ++i) { - if (!child.IsVisible) - { - continue; - } + var child = children[i]; - double childWidth = child.DesiredSize.Width; - double childHeight = child.DesiredSize.Height; + if (child == null) + { continue; } - if (orientation == Orientation.Vertical) + if (fHorizontal) { - var rect = new Rect(0, pos, childWidth, childHeight) - .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top); - ArrangeChild(child, rect, finalSize, orientation); - pos += childHeight + spacing; + rcChild = rcChild.WithX(rcChild.X + previousChildSize); + previousChildSize = child.DesiredSize.Width; + rcChild = rcChild.WithWidth(previousChildSize); + rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); + previousChildSize += spacing; } else { - var rect = new Rect(pos, 0, childWidth, childHeight) - .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment); - ArrangeChild(child, rect, finalSize, orientation); - pos += childWidth + spacing; + rcChild = rcChild.WithY(rcChild.Y + previousChildSize); + previousChildSize = child.DesiredSize.Height; + rcChild = rcChild.WithHeight(previousChildSize); + rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); + previousChildSize += spacing; } + + ArrangeChild(child, rcChild, finalSize, Orientation); } return finalSize; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 6b0c48b97b..b9603b91ed 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,12 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive; using System.Reactive.Linq; using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Metadata; namespace Avalonia.Controls @@ -106,6 +103,14 @@ namespace Avalonia.Controls FontWeightProperty, FontSizeProperty, FontStyleProperty); + + Observable.Merge( + TextProperty.Changed, + TextAlignmentProperty.Changed, + FontSizeProperty.Changed, + FontStyleProperty.Changed, + FontWeightProperty.Changed + ).AddClassHandler((x,_) => x.OnTextPropertiesChanged()); } /// @@ -114,18 +119,6 @@ namespace Avalonia.Controls public TextBlock() { _text = string.Empty; - - Observable.Merge( - this.GetObservable(TextProperty).Select(_ => Unit.Default), - this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), - this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), - this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), - this.GetObservable(FontWeightProperty).Select(_ => Unit.Default)) - .Subscribe(_ => - { - InvalidateFormattedText(); - InvalidateMeasure(); - }); } /// @@ -408,5 +401,11 @@ namespace Avalonia.Controls InvalidateFormattedText(); InvalidateMeasure(); } + + private void OnTextPropertiesChanged() + { + InvalidateFormattedText(); + InvalidateMeasure(); + } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 28d1ba5e0f..5fe1e3804b 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -4,6 +4,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -60,7 +61,7 @@ namespace Avalonia.Controls private static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private PopupRoot _popup; + private IPopupHost _popup; /// /// Initializes static members of the class. @@ -234,19 +235,20 @@ namespace Avalonia.Controls { Close(); - _popup = new PopupRoot { Content = this, }; + _popup = OverlayPopupHost.CreatePopupHost(control, null); + _popup.SetChild(this); ((ISetLogicalParent)_popup).SetParent(control); - _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, - GetHorizontalOffset(control), GetVerticalOffset(control)); + + _popup.ConfigurePosition(control, GetPlacement(control), + new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); _popup.Show(); - _popup.SnapInsideScreenEdges(); } private void Close() { if (_popup != null) { - _popup.Content = null; + _popup.SetChild(null); _popup.Hide(); _popup = null; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d2793fe0dd..ef43746665 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -135,6 +135,12 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); + + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); + MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); + MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); + MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); + } /// @@ -155,6 +161,7 @@ namespace Avalonia.Controls impl.Closing = HandleClosing; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); } /// @@ -239,6 +246,44 @@ namespace Avalonia.Controls set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } } + /// + /// Gets or sets the window position in screen coordinates. + /// + public PixelPoint Position + { + get { return PlatformImpl?.Position ?? PixelPoint.Origin; } + set + { + PlatformImpl?.Move(value); + } + } + + /// + /// Starts moving a window with left button being held. Should be called from left mouse button press event handler + /// + public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); + + /// + /// Starts resizing a window. This function is used if an application has window resizing controls. + /// Should be called from left mouse button press event handler + /// + public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge); + + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(finalSize); + } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); + } + /// Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 40c9fc94d2..a47c55f87c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -49,10 +49,6 @@ namespace Avalonia.Controls IsVisibleProperty.OverrideDefaultValue(false); IsVisibleProperty.Changed.AddClassHandler(x => x.IsVisibleChanged); - MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); - MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); - MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); - MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); } @@ -67,7 +63,6 @@ namespace Avalonia.Controls impl.Activated = HandleActivated; impl.Deactivated = HandleDeactivated; impl.PositionChanged = HandlePositionChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); } /// @@ -96,19 +91,6 @@ namespace Avalonia.Controls get { return _isActive; } private set { SetAndRaise(IsActiveProperty, ref _isActive, value); } } - - /// - /// Gets or sets the window position in screen coordinates. - /// - public PixelPoint Position - { - get { return PlatformImpl?.Position ?? PixelPoint.Origin; } - set - { - if (PlatformImpl is IWindowBaseImpl impl) - impl.Position = value; - } - } public Screens Screens { get; private set; } @@ -208,21 +190,6 @@ namespace Avalonia.Controls return Disposable.Create(() => AutoSizing = false); } - /// - /// Carries out the arrange pass of the window. - /// - /// The final window size. - /// The parameter unchanged. - protected override Size ArrangeOverride(Size finalSize) - { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(finalSize); - } - - return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); - } - /// /// Ensures that the window is initialized. /// @@ -318,16 +285,5 @@ namespace Avalonia.Controls } } } - - /// - /// Starts moving a window with left button being held. Should be called from left mouse button press event handler - /// - public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); - - /// - /// Starts resizing a window. This function is used if an application has window resizing controls. - /// Should be called from left mouse button press event handler - /// - public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge); } } diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 6f53d853c7..3acf341c35 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; using static System.Math; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index dc01bcb07e..40524ad4b7 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.DesignerSupport.Remote RenderIfNeeded(); } + public void Move(PixelPoint point) + { + + } + public void SetMinMaxSize(Size minSize, Size maxSize) { } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 3b6d071583..dcfcd42c04 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -40,8 +40,6 @@ namespace Avalonia.DesignerSupport.Remote return s_lastWindow; } - public IPopupImpl CreatePopup() => new WindowStub(); - public static void Initialize(IAvaloniaRemoteTransportConnection transport) { s_transport = transport; @@ -54,7 +52,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(instance) .Bind().ToConstant(threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 9c547279d6..4ce0da60a2 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -5,6 +5,7 @@ using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; @@ -13,7 +14,7 @@ using Avalonia.Rendering; namespace Avalonia.DesignerSupport.Remote { - class WindowStub : IPopupImpl, IWindowImpl + class WindowStub : IWindowImpl, IPopupImpl { public Action Deactivated { get; set; } public Action Activated { get; set; } @@ -29,10 +30,23 @@ namespace Avalonia.DesignerSupport.Remote public Func Closing { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); + public IPopupImpl CreatePopup() => new WindowStub(this); + public PixelPoint Position { get; set; } public Action PositionChanged { get; set; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + + public WindowStub(IWindowImpl parent = null) + { + if (parent != null) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, + (_, size, __) => + { + Resize(size); + })); + } + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); public void Dispose() { @@ -77,6 +91,11 @@ namespace Avalonia.DesignerSupport.Remote { } + public void Move(PixelPoint point) + { + + } + public IScreenImpl Screen { get; } = new ScreenStub(); public void SetMinMaxSize(Size minSize, Size maxSize) @@ -110,6 +129,8 @@ namespace Avalonia.DesignerSupport.Remote public void SetTopmost(bool value) { } + + public IPopupPositioner PopupPositioner { get; } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index a538516c1a..1df0f3a097 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -1,24 +1,24 @@ - + - - - - - - - + + + + + + + - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - - + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index ccb6151ada..ddd3e29e43 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -18,22 +17,27 @@ using Avalonia.VisualTree; namespace Avalonia { - public static class DevToolsExtensions - { - public static void AttachDevTools(this TopLevel control) - { - Avalonia.Diagnostics.DevTools.Attach(control); - } - } + public static class DevToolsExtensions + { + public static void AttachDevTools(this TopLevel control) + { + Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12)); + } + + public static void AttachDevTools(this TopLevel control, KeyGesture gesture) + { + Diagnostics.DevTools.Attach(control, gesture); + } + } } namespace Avalonia.Diagnostics { - public class DevTools : UserControl + public class DevTools : UserControl { - private static Dictionary s_open = new Dictionary(); - private static HashSet s_visualTreeRoots = new HashSet(); - private IDisposable _keySubscription; + private static readonly Dictionary s_open = new Dictionary(); + private static readonly HashSet s_visualTreeRoots = new HashSet(); + private readonly IDisposable _keySubscription; public DevTools(IControl root) { @@ -49,51 +53,49 @@ namespace Avalonia.Diagnostics // HACK: needed for XAMLIL, will fix that later public DevTools() { - } public IControl Root { get; } - public static IDisposable Attach(TopLevel control) + public static IDisposable Attach(TopLevel control, KeyGesture gesture) { + void PreviewKeyDown(object sender, KeyEventArgs e) + { + if (gesture.Matches(e)) + { + OpenDevTools(control); + } + } + return control.AddHandler( KeyDownEvent, - WindowPreviewKeyDown, + PreviewKeyDown, RoutingStrategies.Tunnel); } - private static void WindowPreviewKeyDown(object sender, KeyEventArgs e) + private static void OpenDevTools(TopLevel control) { - if (e.Key == Key.F12) + if (s_open.TryGetValue(control, out var devToolsWindow)) + { + devToolsWindow.Activate(); + } + else { - var control = (TopLevel)sender; - var devToolsWindow = default(Window); + var devTools = new DevTools(control); - if (s_open.TryGetValue(control, out devToolsWindow)) + devToolsWindow = new Window { - devToolsWindow.Activate(); - } - else - { - var devTools = new DevTools(control); - - devToolsWindow = new Window - { - Width = 1024, - Height = 512, - Content = devTools, - DataTemplates = - { - new ViewLocator(), - }, - Title = "Avalonia DevTools" - }; - - devToolsWindow.Closed += devTools.DevToolsClosed; - s_open.Add(control, devToolsWindow); - MarkAsDevTool(devToolsWindow); - devToolsWindow.Show(); - } + Width = 1024, + Height = 512, + Content = devTools, + DataTemplates = { new ViewLocator() }, + Title = "Avalonia DevTools" + }; + + devToolsWindow.Closed += devTools.DevToolsClosed; + s_open.Add(control, devToolsWindow); + MarkAsDevTool(devToolsWindow); + devToolsWindow.Show(); } } @@ -118,7 +120,6 @@ namespace Avalonia.Diagnostics if ((e.Modifiers) == modifiers) { - var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) .FirstOrDefault(); diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs index aab50a13dd..464187a048 100644 --- a/src/Avalonia.Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs @@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models { Contract.Requires(handler != null); - this.Handler = handler; - this.Handled = handled; - this.Route = route; + Handler = handler; + Handled = handled; + Route = route; } public object Handler { get; } @@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models { return named.Name + " (" + Handler.GetType().Name + ")"; } + return Handler.GetType().Name; } } diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index cda511909a..a66703301d 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -7,7 +7,7 @@ using Avalonia.Controls.Templates; namespace Avalonia.Diagnostics { - public class ViewLocator : IDataTemplate + internal class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs index d723890196..4b832f7ce6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IEnumerable Classes - { - get; - private set; - } - - public IEnumerable Properties - { - get; - private set; - } + public IEnumerable Properties { get; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index bc80ab0550..9f524a21eb 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Controls; diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs index 0674918400..7e38749a6f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventOwnerTreeNode : EventTreeNodeBase { - private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[] + private static readonly RoutedEvent[] s_defaultEvents = { - Button.ClickEvent, - InputElement.KeyDownEvent, - InputElement.KeyUpEvent, - InputElement.TextInputEvent, - InputElement.PointerReleasedEvent, - InputElement.PointerPressedEvent, + Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent, + InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent }; public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm) : base(null, type.Name) { - this.Children = new AvaloniaList(events.OrderBy(e => e.Name) + Children = new AvaloniaList(events.OrderBy(e => e.Name) .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) })); - this.IsExpanded = true; + IsExpanded = true; } public override bool? IsEnabled @@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels if (base.IsEnabled != value) { base.IsEnabled = value; + if (_updateChildren && value != null) { foreach (var child in Children) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs index 7ece790310..36f1904253 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; using Avalonia.Threading; @@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventTreeNode : EventTreeNodeBase { - private RoutedEvent _event; - private EventsViewModel _parentViewModel; + private readonly RoutedEvent _event; + private readonly EventsViewModel _parentViewModel; private bool _isRegistered; private FiredEvent _currentEvent; @@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(@event != null); Contract.Requires(vm != null); - this._event = @event; - this._parentViewModel = vm; + _event = @event; + _parentViewModel = vm; } public override bool? IsEnabled diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs index 146a8cea8e..4be4d8f74e 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels private bool _isExpanded; private bool? _isEnabled = false; - public EventTreeNodeBase(EventTreeNodeBase parent, string text) + protected EventTreeNodeBase(EventTreeNodeBase parent, string text) { - this.Parent = parent; - this.Text = text; + Parent = parent; + Text = text; } public IAvaloniaReadOnlyList Children @@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public virtual bool? IsEnabled { - get { return _isEnabled; } - set { RaiseAndSetIfChanged(ref _isEnabled, value); } + get => _isEnabled; + set => RaiseAndSetIfChanged(ref _isEnabled, value); } public EventTreeNodeBase Parent @@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Text { get; - private set; } internal void UpdateChecked() @@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels { if (Children == null) return false; + bool? value = false; + for (int i = 0; i < Children.Count; i++) { if (i == 0) diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs index 049280c390..daf8ebd0f6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.ObjectModel; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; @@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class FiredEvent : ViewModelBase { - private RoutedEventArgs _eventArgs; + private readonly RoutedEventArgs _eventArgs; private EventChainLink _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) @@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(eventArgs != null); Contract.Requires(originator != null); - this._eventArgs = eventArgs; - this.Originator = originator; + _eventArgs = eventArgs; + Originator = originator; AddToChain(originator); } @@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels if (IsHandled) { return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + - $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; } + return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; } } @@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels public EventChainLink HandledBy { - get { return _handledBy; } + get => _handledBy; set { if (_handledBy != value) diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs index 638cf6c88f..0b9bd85b4f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels public static LogicalTreeNode[] Create(object control) { - var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; + return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs index 2609b74ce0..523be406c8 100644 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs @@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels Value = diagnostic.Value ?? "(null)"; Priority = (diagnostic.Priority != BindingPriority.Unset) ? diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? "Inherited" : "Unset"; + diagnostic.Property.Inherits ? + "Inherited" : + "Unset"; Diagnostic = diagnostic.Diagnostic; }); } @@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels public string Priority { - get { return _priority; } - private set { RaiseAndSetIfChanged(ref _priority, value); } + get => _priority; + private set => RaiseAndSetIfChanged(ref _priority, value); } public string Diagnostic { - get { return _diagnostic; } - private set { RaiseAndSetIfChanged(ref _diagnostic, value); } + get => _diagnostic; + private set => RaiseAndSetIfChanged(ref _diagnostic, value); } public object Value { - get { return _value; } - private set { RaiseAndSetIfChanged(ref _value, value); } + get => _value; + private set => RaiseAndSetIfChanged(ref _value, value); } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..902eb81bd9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs @@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => styleable.Classes.CollectionChanged += x, + x => styleable.Classes.CollectionChanged -= x) + .TakeUntil(styleable.StyleDetach); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) @@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels public string Classes { - get { return _classes; } - private set { RaiseAndSetIfChanged(ref _classes, value); } + get => _classes; + private set => RaiseAndSetIfChanged(ref _classes, value); } public IVisual Visual @@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public TreeNode Parent @@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Type { get; - private set; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index 6b294c98bd..b2b1aaa723 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -23,7 +23,7 @@ namespace Avalonia.Diagnostics.ViewModels public TreeNode SelectedNode { - get { return _selected; } + get => _selected; set { if (RaiseAndSetIfChanged(ref _selected, value)) @@ -35,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels public ControlDetailsViewModel Details { - get { return _details; } - private set { RaiseAndSetIfChanged(ref _details, value); } + get => _details; + private set => RaiseAndSetIfChanged(ref _details, value); } public TreeNode FindNode(IControl control) @@ -66,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels { control = control.GetVisualParent(); } - } + } if (node != null) { @@ -90,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels { return node; } - else + + foreach (var child in node.Children) { - foreach (var child in node.Children) - { - var result = FindNode(child, control); + var result = FindNode(child, control); - if (result != null) - { - return result; - } + if (result != null) + { + return result; } } diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs index 00660754c0..a6ff4dd853 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace Avalonia.Diagnostics.ViewModels { - public class ViewModelBase : INotifyPropertyChanged + internal class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs index 8c070261d9..47ef91507a 100644 --- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs @@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels } } - public bool IsInTemplate { get; private set; } + public bool IsInTemplate { get; } public static VisualTreeNode[] Create(object control) { - var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; + return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 868bc774bb..fb867ab55e 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.Views { private static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); + private SimpleGrid _grid; public ControlDetailsView() @@ -25,7 +26,7 @@ namespace Avalonia.Diagnostics.Views public ControlDetailsViewModel ViewModel { - get { return GetValue(ViewModelProperty); } + get => GetValue(ViewModelProperty); private set { SetValue(ViewModelProperty, value); @@ -37,13 +38,7 @@ namespace Avalonia.Diagnostics.Views { Func> pt = PropertyTemplate; - Content = new ScrollViewer - { - Content = _grid = new SimpleGrid - { - [GridRepeater.TemplateProperty] = pt, - } - }; + Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; } private IEnumerable PropertyTemplate(object i) @@ -57,7 +52,7 @@ namespace Avalonia.Diagnostics.Views Margin = margin, Text = property.Name, TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), + [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() }; yield return new TextBlock @@ -66,14 +61,14 @@ namespace Avalonia.Diagnostics.Views TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) - .ToBinding(), + .ToBinding() }; yield return new TextBlock { Margin = margin, TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), + [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() }; } } diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml index 8d4d37f7b3..406dd433a2 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml @@ -2,53 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" x:Class="Avalonia.Diagnostics.Views.EventsView"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -