Merge branch 'master' into box-shadow
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="1.68.2" />
|
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
|
||||
<PackageReference Include="SkiaSharp" Version="1.68.2.1" />
|
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -12,6 +12,7 @@ using Nuke.Common.ProjectModel;
|
||||
using Nuke.Common.Tooling;
|
||||
using Nuke.Common.Tools.DotNet;
|
||||
using Nuke.Common.Tools.MSBuild;
|
||||
using Nuke.Common.Tools.Npm;
|
||||
using Nuke.Common.Utilities;
|
||||
using Nuke.Common.Utilities.Collections;
|
||||
using static Nuke.Common.EnvironmentInfo;
|
||||
@@ -121,8 +122,21 @@ partial class Build : NukeBuild
|
||||
EnsureCleanDirectory(Parameters.TestResultsRoot);
|
||||
});
|
||||
|
||||
Target CompileHtmlPreviewer => _ => _
|
||||
.DependsOn(Clean)
|
||||
.Executes(() =>
|
||||
{
|
||||
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
|
||||
|
||||
NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir));
|
||||
NpmTasks.NpmRun(c => c
|
||||
.SetWorkingDirectory(webappDir)
|
||||
.SetCommand("dist"));
|
||||
});
|
||||
|
||||
Target Compile => _ => _
|
||||
.DependsOn(Clean)
|
||||
.DependsOn(CompileHtmlPreviewer)
|
||||
.Executes(() =>
|
||||
{
|
||||
if (Parameters.IsRunningOnWindows)
|
||||
|
||||
@@ -74,11 +74,11 @@
|
||||
</StackPanel.DataTemplates>
|
||||
<StackPanel Margin="18" Spacing="4" Width="200">
|
||||
<TextBlock FontSize="16" Text="Multiple"/>
|
||||
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
|
||||
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="18" Spacing="4" Width="200">
|
||||
<TextBlock FontSize="16" Text="Multiple"/>
|
||||
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
|
||||
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
|
||||
</StackPanel>
|
||||
<ContentControl Content="{Binding SelectedItems[0]}">
|
||||
<ContentControl.DataTemplates>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using ReactiveUI;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace BindingDemo.ViewModels
|
||||
{
|
||||
@@ -27,7 +28,7 @@ namespace BindingDemo.ViewModels
|
||||
Detail = "Item " + x + " details",
|
||||
}));
|
||||
|
||||
SelectedItems = new ObservableCollection<TestItem>();
|
||||
Selection = new SelectionModel();
|
||||
|
||||
ShuffleItems = ReactiveCommand.Create(() =>
|
||||
{
|
||||
@@ -56,7 +57,7 @@ namespace BindingDemo.ViewModels
|
||||
}
|
||||
|
||||
public ObservableCollection<TestItem> Items { get; }
|
||||
public ObservableCollection<TestItem> SelectedItems { get; }
|
||||
public SelectionModel Selection { get; }
|
||||
public ReactiveCommand<Unit, Unit> ShuffleItems { get; }
|
||||
|
||||
public string BooleanString
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="16">
|
||||
<StackPanel Orientation="Vertical" Spacing="8">
|
||||
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
|
||||
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
|
||||
|
||||
<Button Command="{Binding AddItemCommand}">Add</Button>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="16">
|
||||
<StackPanel Orientation="Vertical" Spacing="8">
|
||||
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
|
||||
<TreeView Items="{Binding Items}" Selection="{Binding Selection}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
|
||||
<TreeView.ItemTemplate>
|
||||
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||
<TextBlock Text="{Binding Header}"/>
|
||||
|
||||
@@ -28,21 +28,22 @@ namespace ControlCatalog.Pages
|
||||
{
|
||||
Node root = new Node();
|
||||
Items = root.Children;
|
||||
SelectedItems = new ObservableCollection<Node>();
|
||||
Selection = new SelectionModel();
|
||||
|
||||
AddItemCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
|
||||
Node parentItem = Selection.SelectedItems.Count > 0 ?
|
||||
(Node)Selection.SelectedItems[0] : root;
|
||||
parentItem.AddNewItem();
|
||||
});
|
||||
|
||||
RemoveItemCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
while (SelectedItems.Count > 0)
|
||||
while (Selection.SelectedItems.Count > 0)
|
||||
{
|
||||
Node lastItem = SelectedItems[0];
|
||||
Node lastItem = (Node)Selection.SelectedItems[0];
|
||||
RecursiveRemove(Items, lastItem);
|
||||
SelectedItems.Remove(lastItem);
|
||||
Selection.DeselectAt(Selection.SelectedIndices[0]);
|
||||
}
|
||||
|
||||
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
|
||||
@@ -67,7 +68,7 @@ namespace ControlCatalog.Pages
|
||||
|
||||
public ObservableCollection<Node> Items { get; }
|
||||
|
||||
public ObservableCollection<Node> SelectedItems { get; }
|
||||
public SelectionModel Selection { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
|
||||
|
||||
@@ -78,7 +79,7 @@ namespace ControlCatalog.Pages
|
||||
get => _selectionMode;
|
||||
set
|
||||
{
|
||||
SelectedItems.Clear();
|
||||
Selection.ClearSelection();
|
||||
this.RaiseAndSetIfChanged(ref _selectionMode, value);
|
||||
}
|
||||
}
|
||||
@@ -109,7 +110,7 @@ namespace ControlCatalog.Pages
|
||||
|
||||
public override string ToString() => Header;
|
||||
|
||||
private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
|
||||
private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<ListBox Name="listBox"
|
||||
Items="{Binding Items}"
|
||||
SelectedItems="{Binding SelectedItems}"
|
||||
Selection="{Binding Selection}"
|
||||
SelectionMode="Multiple"
|
||||
VirtualizationMode="{Binding VirtualizationMode}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"
|
||||
|
||||
@@ -48,8 +48,7 @@ namespace VirtualizationDemo.ViewModels
|
||||
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
|
||||
}
|
||||
|
||||
public AvaloniaList<ItemViewModel> SelectedItems { get; }
|
||||
= new AvaloniaList<ItemViewModel>();
|
||||
public SelectionModel Selection { get; } = new SelectionModel();
|
||||
|
||||
public AvaloniaList<ItemViewModel> Items
|
||||
{
|
||||
@@ -138,9 +137,9 @@ namespace VirtualizationDemo.ViewModels
|
||||
{
|
||||
var index = Items.Count;
|
||||
|
||||
if (SelectedItems.Count > 0)
|
||||
if (Selection.SelectedIndices.Count > 0)
|
||||
{
|
||||
index = Items.IndexOf(SelectedItems[0]);
|
||||
index = Selection.SelectedIndex.GetAt(0);
|
||||
}
|
||||
|
||||
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
|
||||
@@ -148,9 +147,9 @@ namespace VirtualizationDemo.ViewModels
|
||||
|
||||
private void Remove()
|
||||
{
|
||||
if (SelectedItems.Count > 0)
|
||||
if (Selection.SelectedItems.Count > 0)
|
||||
{
|
||||
Items.RemoveAll(SelectedItems);
|
||||
Items.RemoveAll(Selection.SelectedItems.Cast<ItemViewModel>().ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +163,7 @@ namespace VirtualizationDemo.ViewModels
|
||||
|
||||
private void SelectItem(int index)
|
||||
{
|
||||
SelectedItems.Clear();
|
||||
SelectedItems.Add(Items[index]);
|
||||
Selection.SelectedIndex = new IndexPath(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ namespace Avalonia.Controls
|
||||
if (OwningGrid != null)
|
||||
{
|
||||
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
|
||||
if (e.MouseButton == MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
if (!e.Handled)
|
||||
//if (!e.Handled && OwningGrid.IsTabStop)
|
||||
|
||||
@@ -457,7 +457,7 @@ namespace Avalonia.Controls
|
||||
|
||||
private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left)
|
||||
if (OwningColumn == null || e.Handled || !IsEnabled || e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -786,7 +786,7 @@ namespace Avalonia.Controls
|
||||
|
||||
private void DataGridRow_PointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
if(e.MouseButton != MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ namespace Avalonia.Controls
|
||||
//TODO TabStop
|
||||
private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
if (OwningGrid != null && e.MouseButton == MouseButton.Left)
|
||||
if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled)
|
||||
{
|
||||
|
||||
@@ -164,7 +164,7 @@ namespace Avalonia.Controls.Primitives
|
||||
//TODO TabStop
|
||||
private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if(e.MouseButton != MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace Avalonia.Controls
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
if (e.MouseButton == MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
IsPressed = true;
|
||||
e.Handled = true;
|
||||
|
||||
@@ -149,7 +149,8 @@ namespace Avalonia.Controls.Primitives
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
if (e.MouseButton == MouseButton.Left)
|
||||
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
CalendarLeftMouseButtonDown?.Invoke(this, e);
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ namespace Avalonia.Controls.Primitives
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
if (e.MouseButton == MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
CalendarDayButtonMouseDown?.Invoke(this, e);
|
||||
}
|
||||
|
||||
|
||||
@@ -839,7 +839,7 @@ namespace Avalonia.Controls
|
||||
}
|
||||
private void Calendar_PointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.MouseButton == MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -306,9 +306,9 @@ namespace Avalonia.Controls
|
||||
{
|
||||
var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
|
||||
|
||||
if (container == null && SelectedItems.Count > 0)
|
||||
if (container == null && SelectedIndex != -1)
|
||||
{
|
||||
ScrollIntoView(SelectedItems[0]);
|
||||
ScrollIntoView(Selection.SelectedIndex);
|
||||
container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,9 +94,13 @@ namespace Avalonia.Controls.Generators
|
||||
/// <returns>The container, or null of not found.</returns>
|
||||
public IControl ContainerFromItem(object item)
|
||||
{
|
||||
IControl result;
|
||||
_itemToContainer.TryGetValue(item, out result);
|
||||
return result;
|
||||
if (item != null)
|
||||
{
|
||||
_itemToContainer.TryGetValue(item, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -106,9 +110,13 @@ namespace Avalonia.Controls.Generators
|
||||
/// <returns>The item, or null of not found.</returns>
|
||||
public object ItemFromContainer(IControl container)
|
||||
{
|
||||
object result;
|
||||
_containerToItem.TryGetValue(container, out result);
|
||||
return result;
|
||||
if (container != null)
|
||||
{
|
||||
_containerToItem.TryGetValue(container, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the selected items for a control.
|
||||
/// </summary>
|
||||
public interface ISelectionModel : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the anchor index.
|
||||
/// </summary>
|
||||
IndexPath AnchorIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the index of the first selected item.
|
||||
/// </summary>
|
||||
IndexPath SelectedIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the indexes of the selected items.
|
||||
/// </summary>
|
||||
IReadOnlyList<IndexPath> SelectedIndices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first selected item.
|
||||
/// </summary>
|
||||
object SelectedItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected items.
|
||||
/// </summary>
|
||||
IReadOnlyList<object> SelectedItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the model represents a single or multiple selection.
|
||||
/// </summary>
|
||||
bool SingleSelect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to always keep an item selected where possible.
|
||||
/// </summary>
|
||||
bool AutoSelect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection that contains the items that can be selected.
|
||||
/// </summary>
|
||||
object Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the children of a selection are required.
|
||||
/// </summary>
|
||||
event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the selection has changed.
|
||||
/// </summary>
|
||||
event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Clears the selection.
|
||||
/// </summary>
|
||||
void ClearSelection();
|
||||
|
||||
/// <summary>
|
||||
/// Deselects an item.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item.</param>
|
||||
void Deselect(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Deselects an item.
|
||||
/// </summary>
|
||||
/// <param name="groupIndex">The index of the item group.</param>
|
||||
/// <param name="itemIndex">The index of the item in the group.</param>
|
||||
void Deselect(int groupIndex, int itemIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Deselects an item.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item.</param>
|
||||
void DeselectAt(IndexPath index);
|
||||
|
||||
/// <summary>
|
||||
/// Deselects a range of items.
|
||||
/// </summary>
|
||||
/// <param name="start">The start index of the range.</param>
|
||||
/// <param name="end">The end index of the range.</param>
|
||||
void DeselectRange(IndexPath start, IndexPath end);
|
||||
|
||||
/// <summary>
|
||||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The end index of the range.</param>
|
||||
void DeselectRangeFromAnchor(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="endGroupIndex">
|
||||
/// The index of the item group that represents the end of the selection.
|
||||
/// </param>
|
||||
/// <param name="endItemIndex">
|
||||
/// The index of the item in the group that represents the end of the selection.
|
||||
/// </param>
|
||||
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The end index of the range.</param>
|
||||
void DeselectRangeFromAnchorTo(IndexPath index);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the object and clears the selection.
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an item is selected.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item</param>
|
||||
bool IsSelected(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an item is selected.
|
||||
/// </summary>
|
||||
/// <param name="groupIndex">The index of the item group.</param>
|
||||
/// <param name="itemIndex">The index of the item in the group.</param>
|
||||
bool IsSelected(int groupIndex, int itemIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an item is selected.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item</param>
|
||||
public bool IsSelectedAt(IndexPath index);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an item or its descendents are selected.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item</param>
|
||||
/// <returns>
|
||||
/// True if the item and all its descendents are selected, false if the item and all its
|
||||
/// descendents are deselected, or null if a combination of selected and deselected.
|
||||
/// </returns>
|
||||
bool? IsSelectedWithPartial(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an item or its descendents are selected.
|
||||
/// </summary>
|
||||
/// <param name="groupIndex">The index of the item group.</param>
|
||||
/// <param name="itemIndex">The index of the item in the group.</param>
|
||||
/// <returns>
|
||||
/// True if the item and all its descendents are selected, false if the item and all its
|
||||
/// descendents are deselected, or null if a combination of selected and deselected.
|
||||
/// </returns>
|
||||
bool? IsSelectedWithPartial(int groupIndex, int itemIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an item or its descendents are selected.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item</param>
|
||||
/// <returns>
|
||||
/// True if the item and all its descendents are selected, false if the item and all its
|
||||
/// descendents are deselected, or null if a combination of selected and deselected.
|
||||
/// </returns>
|
||||
bool? IsSelectedWithPartialAt(IndexPath index);
|
||||
|
||||
/// <summary>
|
||||
/// Selects an item.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item</param>
|
||||
void Select(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Selects an item.
|
||||
/// </summary>
|
||||
/// <param name="groupIndex">The index of the item group.</param>
|
||||
/// <param name="itemIndex">The index of the item in the group.</param>
|
||||
void Select(int groupIndex, int itemIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Selects an item.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item</param>
|
||||
void SelectAt(IndexPath index);
|
||||
|
||||
/// <summary>
|
||||
/// Selects all items.
|
||||
/// </summary>
|
||||
void SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// Selects a range of items.
|
||||
/// </summary>
|
||||
/// <param name="start">The start index of the range.</param>
|
||||
/// <param name="end">The end index of the range.</param>
|
||||
void SelectRange(IndexPath start, IndexPath end);
|
||||
|
||||
/// <summary>
|
||||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The end index of the range.</param>
|
||||
void SelectRangeFromAnchor(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="endGroupIndex">
|
||||
/// The index of the item group that represents the end of the selection.
|
||||
/// </param>
|
||||
/// <param name="endItemIndex">
|
||||
/// The index of the item in the group that represents the end of the selection.
|
||||
/// </param>
|
||||
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The end index of the range.</param>
|
||||
void SelectRangeFromAnchorTo(IndexPath index);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The anchor index.</param>
|
||||
void SetAnchorIndex(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="AnchorIndex"/>.
|
||||
/// </summary>
|
||||
/// <param name="groupIndex">The index of the item group.</param>
|
||||
/// <param name="index">The index of the item in the group.</param>
|
||||
void SetAnchorIndex(int groupIndex, int index);
|
||||
|
||||
/// <summary>
|
||||
/// Begins a batch update of the selection.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
|
||||
IDisposable Update();
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,11 @@ namespace Avalonia.Controls
|
||||
{
|
||||
var source = Source;
|
||||
|
||||
if (source != null)
|
||||
if (source != null && Bounds.Width > 0 && Bounds.Height > 0)
|
||||
{
|
||||
Rect viewPort = new Rect(Bounds.Size);
|
||||
Size sourceSize = source.Size;
|
||||
|
||||
Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
|
||||
Size scaledSize = sourceSize * scale;
|
||||
Rect destRect = viewPort
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
public readonly struct IndexPath : IComparable<IndexPath>, IEquatable<IndexPath>
|
||||
{
|
||||
public static readonly IndexPath Unselected = default;
|
||||
|
||||
private readonly int _index;
|
||||
private readonly int[]? _path;
|
||||
|
||||
public IndexPath(int index)
|
||||
{
|
||||
_index = index + 1;
|
||||
_path = null;
|
||||
}
|
||||
|
||||
public IndexPath(int groupIndex, int itemIndex)
|
||||
{
|
||||
_index = 0;
|
||||
_path = new[] { groupIndex, itemIndex };
|
||||
}
|
||||
|
||||
public IndexPath(IEnumerable<int>? indices)
|
||||
{
|
||||
if (indices != null)
|
||||
{
|
||||
_index = 0;
|
||||
_path = indices.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
_index = 0;
|
||||
_path = null;
|
||||
}
|
||||
}
|
||||
|
||||
private IndexPath(int[] basePath, int index)
|
||||
{
|
||||
basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
|
||||
|
||||
_index = 0;
|
||||
_path = new int[basePath.Length + 1];
|
||||
Array.Copy(basePath, _path, basePath.Length);
|
||||
_path[basePath.Length] = index;
|
||||
}
|
||||
|
||||
public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1);
|
||||
|
||||
public int GetAt(int index)
|
||||
{
|
||||
if (index >= GetSize())
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
return _path?[index] ?? (_index - 1);
|
||||
}
|
||||
|
||||
public int CompareTo(IndexPath other)
|
||||
{
|
||||
var rhsPath = other;
|
||||
int compareResult = 0;
|
||||
int lhsCount = GetSize();
|
||||
int rhsCount = rhsPath.GetSize();
|
||||
|
||||
if (lhsCount == 0 || rhsCount == 0)
|
||||
{
|
||||
// one of the paths are empty, compare based on size
|
||||
compareResult = (lhsCount - rhsCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// both paths are non-empty, but can be of different size
|
||||
for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++)
|
||||
{
|
||||
if (GetAt(i) < rhsPath.GetAt(i))
|
||||
{
|
||||
compareResult = -1;
|
||||
break;
|
||||
}
|
||||
else if (GetAt(i) > rhsPath.GetAt(i))
|
||||
{
|
||||
compareResult = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if both match upto min(lhsCount, rhsCount), compare based on size
|
||||
compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult;
|
||||
}
|
||||
|
||||
if (compareResult != 0)
|
||||
{
|
||||
compareResult = compareResult > 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
return compareResult;
|
||||
}
|
||||
|
||||
public IndexPath CloneWithChildIndex(int childIndex)
|
||||
{
|
||||
if (_path != null)
|
||||
{
|
||||
return new IndexPath(_path, childIndex);
|
||||
}
|
||||
else if (_index != 0)
|
||||
{
|
||||
return new IndexPath(_index - 1, childIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new IndexPath(childIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_path != null)
|
||||
{
|
||||
return "R" + string.Join(".", _path);
|
||||
}
|
||||
else if (_index != 0)
|
||||
{
|
||||
return "R" + (_index - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "R";
|
||||
}
|
||||
}
|
||||
|
||||
public static IndexPath CreateFrom(int index) => new IndexPath(index);
|
||||
|
||||
public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex);
|
||||
|
||||
public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices);
|
||||
|
||||
public override bool Equals(object obj) => obj is IndexPath other && Equals(other);
|
||||
|
||||
public bool Equals(IndexPath other) => CompareTo(other) == 0;
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = -504981047;
|
||||
|
||||
if (_path != null)
|
||||
{
|
||||
foreach (var i in _path)
|
||||
{
|
||||
hashCode = hashCode * -1521134295 + i.GetHashCode();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hashCode = hashCode * -1521134295 + _index.GetHashCode();
|
||||
}
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; }
|
||||
public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; }
|
||||
public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; }
|
||||
public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; }
|
||||
public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; }
|
||||
public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; }
|
||||
public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; }
|
||||
public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
internal readonly struct IndexRange : IEquatable<IndexRange>
|
||||
{
|
||||
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue);
|
||||
|
||||
public IndexRange(int begin, int end)
|
||||
{
|
||||
// Accept out of order begin/end pairs, just swap them.
|
||||
if (begin > end)
|
||||
{
|
||||
int temp = begin;
|
||||
begin = end;
|
||||
end = temp;
|
||||
}
|
||||
|
||||
Begin = begin;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public int Begin { get; }
|
||||
public int End { get; }
|
||||
public int Count => (End - Begin) + 1;
|
||||
|
||||
public bool Contains(int index) => index >= Begin && index <= End;
|
||||
|
||||
public bool Split(int splitIndex, out IndexRange before, out IndexRange after)
|
||||
{
|
||||
bool afterIsValid;
|
||||
|
||||
before = new IndexRange(Begin, splitIndex);
|
||||
|
||||
if (splitIndex < End)
|
||||
{
|
||||
after = new IndexRange(splitIndex + 1, End);
|
||||
afterIsValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
after = new IndexRange();
|
||||
afterIsValid = false;
|
||||
}
|
||||
|
||||
return afterIsValid;
|
||||
}
|
||||
|
||||
public bool Intersects(IndexRange other)
|
||||
{
|
||||
return (Begin <= other.End) && (End >= other.Begin);
|
||||
}
|
||||
|
||||
public bool Adjacent(IndexRange other)
|
||||
{
|
||||
return Begin == other.End + 1 || End == other.Begin - 1;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is IndexRange range && Equals(range);
|
||||
}
|
||||
|
||||
public bool Equals(IndexRange other)
|
||||
{
|
||||
return Begin == other.Begin && End == other.End;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = 1903003160;
|
||||
hashCode = hashCode * -1521134295 + Begin.GetHashCode();
|
||||
hashCode = hashCode * -1521134295 + End.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{Begin}..{End}]";
|
||||
|
||||
public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right);
|
||||
public static bool operator !=(IndexRange left, IndexRange right) => !(left == right);
|
||||
|
||||
public static int Add(
|
||||
IList<IndexRange> ranges,
|
||||
IndexRange range,
|
||||
IList<IndexRange>? added = null)
|
||||
{
|
||||
var result = 0;
|
||||
|
||||
for (var i = 0; i < ranges.Count && range != s_invalid; ++i)
|
||||
{
|
||||
var existing = ranges[i];
|
||||
|
||||
if (range.Intersects(existing) || range.Adjacent(existing))
|
||||
{
|
||||
if (range.Begin < existing.Begin)
|
||||
{
|
||||
var add = new IndexRange(range.Begin, existing.Begin - 1);
|
||||
ranges[i] = new IndexRange(range.Begin, existing.End);
|
||||
added?.Add(add);
|
||||
result += add.Count;
|
||||
}
|
||||
|
||||
range = range.End <= existing.End ?
|
||||
s_invalid :
|
||||
new IndexRange(existing.End + 1, range.End);
|
||||
}
|
||||
else if (range.End < existing.Begin)
|
||||
{
|
||||
ranges.Insert(i, range);
|
||||
added?.Add(range);
|
||||
result += range.Count;
|
||||
range = s_invalid;
|
||||
}
|
||||
}
|
||||
|
||||
if (range != s_invalid)
|
||||
{
|
||||
ranges.Add(range);
|
||||
added?.Add(range);
|
||||
result += range.Count;
|
||||
}
|
||||
|
||||
MergeRanges(ranges);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int Remove(
|
||||
IList<IndexRange> ranges,
|
||||
IndexRange range,
|
||||
IList<IndexRange>? removed = null)
|
||||
{
|
||||
var result = 0;
|
||||
|
||||
for (var i = 0; i < ranges.Count; ++i)
|
||||
{
|
||||
var existing = ranges[i];
|
||||
|
||||
if (range.Intersects(existing))
|
||||
{
|
||||
if (range.Begin <= existing.Begin && range.End >= existing.End)
|
||||
{
|
||||
ranges.RemoveAt(i--);
|
||||
removed?.Add(existing);
|
||||
result += existing.Count;
|
||||
}
|
||||
else if (range.Begin > existing.Begin && range.End >= existing.End)
|
||||
{
|
||||
ranges[i] = new IndexRange(existing.Begin, range.Begin - 1);
|
||||
removed?.Add(new IndexRange(range.Begin, existing.End));
|
||||
result += existing.End - (range.Begin - 1);
|
||||
}
|
||||
else if (range.Begin > existing.Begin && range.End < existing.End)
|
||||
{
|
||||
ranges[i] = new IndexRange(existing.Begin, range.Begin - 1);
|
||||
ranges.Insert(++i, new IndexRange(range.End + 1, existing.End));
|
||||
removed?.Add(range);
|
||||
result += range.Count;
|
||||
}
|
||||
else if (range.End <= existing.End)
|
||||
{
|
||||
var remove = new IndexRange(existing.Begin, range.End);
|
||||
ranges[i] = new IndexRange(range.End + 1, existing.End);
|
||||
removed?.Add(remove);
|
||||
result += remove.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IEnumerable<IndexRange> Subtract(
|
||||
IndexRange lhs,
|
||||
IEnumerable<IndexRange> rhs)
|
||||
{
|
||||
var result = new List<IndexRange> { lhs };
|
||||
|
||||
foreach (var range in rhs)
|
||||
{
|
||||
Remove(result, range);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IEnumerable<int> EnumerateIndices(IEnumerable<IndexRange> ranges)
|
||||
{
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
for (var i = range.Begin; i <= range.End; ++i)
|
||||
{
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetCount(IEnumerable<IndexRange> ranges)
|
||||
{
|
||||
var result = 0;
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
result += (range.End - range.Begin) + 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void MergeRanges(IList<IndexRange> ranges)
|
||||
{
|
||||
for (var i = ranges.Count - 2; i >= 0; --i)
|
||||
{
|
||||
var r = ranges[i];
|
||||
var r1 = ranges[i + 1];
|
||||
|
||||
if (r.Intersects(r1) || r.End == r1.Begin - 1)
|
||||
{
|
||||
ranges[i] = new IndexRange(r.Begin, r1.End);
|
||||
ranges.RemoveAt(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,12 @@ namespace Avalonia.Controls
|
||||
public static readonly new DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
|
||||
SelectingItemsControl.SelectedItemsProperty;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Selection"/> property.
|
||||
/// </summary>
|
||||
public static readonly new DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
|
||||
SelectingItemsControl.SelectionProperty;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="SelectionMode"/> property.
|
||||
/// </summary>
|
||||
@@ -70,6 +76,15 @@ namespace Avalonia.Controls
|
||||
set => base.SelectedItems = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a model holding the current selection.
|
||||
/// </summary>
|
||||
public new ISelectionModel Selection
|
||||
{
|
||||
get => base.Selection;
|
||||
set => base.Selection = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selection mode.
|
||||
/// </summary>
|
||||
@@ -95,12 +110,12 @@ namespace Avalonia.Controls
|
||||
/// <summary>
|
||||
/// Selects all items in the <see cref="ListBox"/>.
|
||||
/// </summary>
|
||||
public new void SelectAll() => base.SelectAll();
|
||||
public void SelectAll() => Selection.SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// Deselects all items in the <see cref="ListBox"/>.
|
||||
/// </summary>
|
||||
public new void UnselectAll() => base.UnselectAll();
|
||||
public void UnselectAll() => Selection.ClearSelection();
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IItemContainerGenerator CreateItemContainerGenerator()
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Rendering;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -338,8 +339,9 @@ namespace Avalonia.Controls.Platform
|
||||
protected internal virtual void PointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var item = GetMenuItem(e.Source as IControl);
|
||||
var visual = (IVisual)sender;
|
||||
|
||||
if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == true)
|
||||
if (e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed && item?.HasSubMenu == true)
|
||||
{
|
||||
if (item.IsSubMenuOpen)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,6 @@ namespace Avalonia.Controls.Presenters
|
||||
|
||||
void ItemsChanged(NotifyCollectionChangedEventArgs e);
|
||||
|
||||
void ScrollIntoView(object item);
|
||||
void ScrollIntoView(int index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,8 +275,8 @@ namespace Avalonia.Controls.Presenters
|
||||
/// <summary>
|
||||
/// Scrolls the specified item into view.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public virtual void ScrollIntoView(object item)
|
||||
/// <param name="index">The index of the item.</param>
|
||||
public virtual void ScrollIntoView(int index)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -64,18 +64,13 @@ namespace Avalonia.Controls.Presenters
|
||||
/// <summary>
|
||||
/// Scrolls the specified item into view.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public override void ScrollIntoView(object item)
|
||||
/// <param name="index">The index of the item.</param>
|
||||
public override void ScrollIntoView(int index)
|
||||
{
|
||||
if (Items != null)
|
||||
if (index != -1)
|
||||
{
|
||||
var index = Items.IndexOf(item);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
|
||||
container?.BringIntoView();
|
||||
}
|
||||
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
|
||||
container?.BringIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,20 +286,15 @@ namespace Avalonia.Controls.Presenters
|
||||
break;
|
||||
}
|
||||
|
||||
return ScrollIntoView(newItemIndex);
|
||||
return ScrollIntoViewCore(newItemIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ScrollIntoView(object item)
|
||||
public override void ScrollIntoView(int index)
|
||||
{
|
||||
if (Items != null)
|
||||
if (index != -1)
|
||||
{
|
||||
var index = Items.IndexOf(item);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
ScrollIntoView(index);
|
||||
}
|
||||
ScrollIntoViewCore(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +506,7 @@ namespace Avalonia.Controls.Presenters
|
||||
/// </summary>
|
||||
/// <param name="index">The item index.</param>
|
||||
/// <returns>The container that was brought into view.</returns>
|
||||
private IControl ScrollIntoView(int index)
|
||||
private IControl ScrollIntoViewCore(int index)
|
||||
{
|
||||
var panel = VirtualizingPanel;
|
||||
var generator = Owner.ItemContainerGenerator;
|
||||
|
||||
@@ -128,9 +128,9 @@ namespace Avalonia.Controls.Presenters
|
||||
_scrollInvalidated?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public override void ScrollIntoView(object item)
|
||||
public override void ScrollIntoView(int index)
|
||||
{
|
||||
Virtualizer?.ScrollIntoView(item);
|
||||
Virtualizer?.ScrollIntoView(index);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void ScrollIntoView(object item)
|
||||
public virtual void ScrollIntoView(int index)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -115,7 +115,7 @@ namespace Avalonia.Controls.Remote.Server
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastReceivedFrame = lastFrame.SequenceId;
|
||||
_lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame);
|
||||
}
|
||||
Dispatcher.UIThread.Post(RenderIfNeeded);
|
||||
}
|
||||
@@ -298,6 +298,8 @@ namespace Avalonia.Controls.Remote.Server
|
||||
Width = width,
|
||||
Height = height,
|
||||
Stride = width * bpp,
|
||||
DpiX = _dpi.X,
|
||||
DpiY = _dpi.Y
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Avalonia.Controls
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
if (e.MouseButton == MouseButton.Left)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
StartTimer();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ namespace Avalonia.Controls
|
||||
/// <returns>the item.</returns>
|
||||
public object GetAt(int index) => _inner[index];
|
||||
|
||||
public int IndexOf(object item) => _inner.IndexOf(item);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the index of the item that has the specified unique identifier (key).
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
public interface ISelectedItemInfo
|
||||
{
|
||||
public IndexPath Path { get; }
|
||||
}
|
||||
|
||||
internal class SelectedItems<TValue, Tinfo> : IReadOnlyList<TValue>
|
||||
where Tinfo : ISelectedItemInfo
|
||||
{
|
||||
private readonly List<Tinfo> _infos;
|
||||
private readonly Func<List<Tinfo>, int, TValue> _getAtImpl;
|
||||
|
||||
public SelectedItems(
|
||||
List<Tinfo> infos,
|
||||
int count,
|
||||
Func<List<Tinfo>, int, TValue> getAtImpl)
|
||||
{
|
||||
_infos = infos;
|
||||
_getAtImpl = getAtImpl;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public TValue this[int index] => _getAtImpl(_infos, index);
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public IEnumerator<TValue> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < Count; ++i)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,848 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia.Controls.Utils;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
public class SelectionModel : ISelectionModel, IDisposable
|
||||
{
|
||||
private readonly SelectionNode _rootNode;
|
||||
private bool _singleSelect;
|
||||
private bool _autoSelect;
|
||||
private int _operationCount;
|
||||
private IReadOnlyList<IndexPath>? _selectedIndicesCached;
|
||||
private IReadOnlyList<object?>? _selectedItemsCached;
|
||||
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
|
||||
|
||||
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
|
||||
|
||||
public SelectionModel()
|
||||
{
|
||||
_rootNode = new SelectionNode(this, null);
|
||||
SharedLeafNode = new SelectionNode(this, null);
|
||||
}
|
||||
|
||||
public object? Source
|
||||
{
|
||||
get => _rootNode.Source;
|
||||
set
|
||||
{
|
||||
if (_rootNode.Source != value)
|
||||
{
|
||||
var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0;
|
||||
|
||||
if (_rootNode.Source != null)
|
||||
{
|
||||
if (_rootNode.Source != null)
|
||||
{
|
||||
using (var operation = new Operation(this))
|
||||
{
|
||||
ClearSelection(resetAnchor: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_rootNode.Source = value;
|
||||
ApplyAutoSelect();
|
||||
|
||||
RaisePropertyChanged("Source");
|
||||
|
||||
if (raiseChanged)
|
||||
{
|
||||
var e = new SelectionModelSelectionChangedEventArgs(
|
||||
null,
|
||||
SelectedIndices,
|
||||
null,
|
||||
SelectedItems);
|
||||
OnSelectionChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool SingleSelect
|
||||
{
|
||||
get => _singleSelect;
|
||||
set
|
||||
{
|
||||
if (_singleSelect != value)
|
||||
{
|
||||
_singleSelect = value;
|
||||
var selectedIndices = SelectedIndices;
|
||||
|
||||
if (value && selectedIndices != null && selectedIndices.Count > 0)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
|
||||
// We want to be single select, so make sure there is only
|
||||
// one selected item.
|
||||
var firstSelectionIndexPath = selectedIndices[0];
|
||||
ClearSelection(resetAnchor: true);
|
||||
SelectWithPathImpl(firstSelectionIndexPath, select: true);
|
||||
SelectedIndex = firstSelectionIndexPath;
|
||||
}
|
||||
|
||||
RaisePropertyChanged("SingleSelect");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RetainSelectionOnReset
|
||||
{
|
||||
get => _rootNode.RetainSelectionOnReset;
|
||||
set => _rootNode.RetainSelectionOnReset = value;
|
||||
}
|
||||
|
||||
public bool AutoSelect
|
||||
{
|
||||
get => _autoSelect;
|
||||
set
|
||||
{
|
||||
if (_autoSelect != value)
|
||||
{
|
||||
_autoSelect = value;
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IndexPath AnchorIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
IndexPath anchor = default;
|
||||
|
||||
if (_rootNode.AnchorIndex >= 0)
|
||||
{
|
||||
var path = new List<int>();
|
||||
SelectionNode? current = _rootNode;
|
||||
|
||||
while (current?.AnchorIndex >= 0)
|
||||
{
|
||||
path.Add(current.AnchorIndex);
|
||||
current = current.GetAt(current.AnchorIndex, false);
|
||||
}
|
||||
|
||||
anchor = new IndexPath(path);
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
SelectionTreeHelper.TraverseIndexPath(
|
||||
_rootNode,
|
||||
value,
|
||||
realizeChildren: true,
|
||||
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth));
|
||||
}
|
||||
else
|
||||
{
|
||||
_rootNode.AnchorIndex = -1;
|
||||
}
|
||||
|
||||
RaisePropertyChanged("AnchorIndex");
|
||||
}
|
||||
}
|
||||
|
||||
public IndexPath SelectedIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
IndexPath selectedIndex = default;
|
||||
var selectedIndices = SelectedIndices;
|
||||
|
||||
if (selectedIndices?.Count > 0)
|
||||
{
|
||||
selectedIndex = selectedIndices[0];
|
||||
}
|
||||
|
||||
return selectedIndex;
|
||||
}
|
||||
set
|
||||
{
|
||||
var isSelected = IsSelectedWithPartialAt(value);
|
||||
|
||||
if (!IsSelectedAt(value) || SelectedItems.Count > 1)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
ClearSelection(resetAnchor: true);
|
||||
SelectWithPathImpl(value, select: true);
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object? SelectedItem
|
||||
{
|
||||
get
|
||||
{
|
||||
object? item = null;
|
||||
var selectedItems = SelectedItems;
|
||||
|
||||
if (selectedItems?.Count > 0)
|
||||
{
|
||||
item = selectedItems[0];
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<object?> SelectedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedItemsCached == null)
|
||||
{
|
||||
var selectedInfos = new List<SelectedItemInfo>();
|
||||
var count = 0;
|
||||
|
||||
if (_rootNode.Source != null)
|
||||
{
|
||||
SelectionTreeHelper.Traverse(
|
||||
_rootNode,
|
||||
realizeChildren: false,
|
||||
currentInfo =>
|
||||
{
|
||||
if (currentInfo.Node.SelectedCount > 0)
|
||||
{
|
||||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
|
||||
count += currentInfo.Node.SelectedCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Instead of creating a dumb vector that takes up the space for all the selected items,
|
||||
// we create a custom IReadOnlyList implementation that calls back using a delegate to find
|
||||
// the selected item at a particular index. This avoid having to create the storage and copying
|
||||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
||||
// easier to consume flat vector view of objects.
|
||||
var selectedItems = new SelectedItems<object?, SelectedItemInfo> (
|
||||
selectedInfos,
|
||||
count,
|
||||
(infos, index) =>
|
||||
{
|
||||
var currentIndex = 0;
|
||||
object? item = null;
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var node = info.Node;
|
||||
|
||||
if (node != null)
|
||||
{
|
||||
var currentCount = node.SelectedCount;
|
||||
|
||||
if (index >= currentIndex && index < currentIndex + currentCount)
|
||||
{
|
||||
var targetIndex = node.SelectedIndices[index - currentIndex];
|
||||
item = node.ItemsSourceView!.GetAt(targetIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
currentIndex += currentCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Selection has changed since SelectedItems property was read.");
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
_selectedItemsCached = selectedItems;
|
||||
}
|
||||
|
||||
return _selectedItemsCached;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<IndexPath> SelectedIndices
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedIndicesCached == null)
|
||||
{
|
||||
var selectedInfos = new List<SelectedItemInfo>();
|
||||
var count = 0;
|
||||
|
||||
SelectionTreeHelper.Traverse(
|
||||
_rootNode,
|
||||
false,
|
||||
currentInfo =>
|
||||
{
|
||||
if (currentInfo.Node.SelectedCount > 0)
|
||||
{
|
||||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
|
||||
count += currentInfo.Node.SelectedCount;
|
||||
}
|
||||
});
|
||||
|
||||
// Instead of creating a dumb vector that takes up the space for all the selected indices,
|
||||
// we create a custom VectorView implimentation that calls back using a delegate to find
|
||||
// the IndexPath at a particular index. This avoid having to create the storage and copying
|
||||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
||||
// easier to consume flat vector view of IndexPaths.
|
||||
var indices = new SelectedItems<IndexPath, SelectedItemInfo>(
|
||||
selectedInfos,
|
||||
count,
|
||||
(infos, index) => // callback for GetAt(index)
|
||||
{
|
||||
var currentIndex = 0;
|
||||
IndexPath path = default;
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var node = info.Node;
|
||||
|
||||
if (node != null)
|
||||
{
|
||||
var currentCount = node.SelectedCount;
|
||||
if (index >= currentIndex && index < currentIndex + currentCount)
|
||||
{
|
||||
int targetIndex = node.SelectedIndices[index - currentIndex];
|
||||
path = info.Path.CloneWithChildIndex(targetIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
currentIndex += currentCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Selection has changed since SelectedIndices property was read.");
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
});
|
||||
|
||||
_selectedIndicesCached = indices;
|
||||
}
|
||||
|
||||
return _selectedIndicesCached;
|
||||
}
|
||||
}
|
||||
|
||||
internal SelectionNode SharedLeafNode { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ClearSelection(resetAnchor: false);
|
||||
_rootNode.Cleanup();
|
||||
_rootNode.Dispose();
|
||||
_selectedIndicesCached = null;
|
||||
_selectedItemsCached = null;
|
||||
}
|
||||
|
||||
public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index);
|
||||
|
||||
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index);
|
||||
|
||||
public void Select(int index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectImpl(index, select: true);
|
||||
}
|
||||
|
||||
public void Select(int groupIndex, int itemIndex)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectWithGroupImpl(groupIndex, itemIndex, select: true);
|
||||
}
|
||||
|
||||
public void SelectAt(IndexPath index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectWithPathImpl(index, select: true);
|
||||
}
|
||||
|
||||
public void Deselect(int index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectImpl(index, select: false);
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
|
||||
public void Deselect(int groupIndex, int itemIndex)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectWithGroupImpl(groupIndex, itemIndex, select: false);
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
|
||||
public void DeselectAt(IndexPath index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectWithPathImpl(index, select: false);
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
|
||||
public bool IsSelected(int index) => _rootNode.IsSelected(index);
|
||||
|
||||
public bool IsSelected(int grouIndex, int itemIndex)
|
||||
{
|
||||
return IsSelectedAt(new IndexPath(grouIndex, itemIndex));
|
||||
}
|
||||
|
||||
public bool IsSelectedAt(IndexPath index)
|
||||
{
|
||||
var path = index;
|
||||
SelectionNode? node = _rootNode;
|
||||
|
||||
for (int i = 0; i < path.GetSize() - 1; i++)
|
||||
{
|
||||
var childIndex = path.GetAt(i);
|
||||
node = node.GetAt(childIndex, realizeChild: false);
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return node.IsSelected(index.GetAt(index.GetSize() - 1));
|
||||
}
|
||||
|
||||
public bool? IsSelectedWithPartial(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new ArgumentException("Index must be >= 0", nameof(index));
|
||||
}
|
||||
|
||||
var isSelected = _rootNode.IsSelectedWithPartial(index);
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
public bool? IsSelectedWithPartial(int groupIndex, int itemIndex)
|
||||
{
|
||||
if (groupIndex < 0)
|
||||
{
|
||||
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex));
|
||||
}
|
||||
|
||||
if (itemIndex < 0)
|
||||
{
|
||||
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex));
|
||||
}
|
||||
|
||||
var isSelected = (bool?)false;
|
||||
var childNode = _rootNode.GetAt(groupIndex, realizeChild: false);
|
||||
|
||||
if (childNode != null)
|
||||
{
|
||||
isSelected = childNode.IsSelectedWithPartial(itemIndex);
|
||||
}
|
||||
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
public bool? IsSelectedWithPartialAt(IndexPath index)
|
||||
{
|
||||
var path = index;
|
||||
var isRealized = true;
|
||||
SelectionNode? node = _rootNode;
|
||||
|
||||
for (int i = 0; i < path.GetSize() - 1; i++)
|
||||
{
|
||||
var childIndex = path.GetAt(i);
|
||||
node = node.GetAt(childIndex, realizeChild: false);
|
||||
|
||||
if (node == null)
|
||||
{
|
||||
isRealized = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var isSelected = (bool?)false;
|
||||
|
||||
if (isRealized)
|
||||
{
|
||||
var size = path.GetSize();
|
||||
if (size == 0)
|
||||
{
|
||||
isSelected = SelectionNode.ConvertToNullableBool(node!.EvaluateIsSelectedBasedOnChildrenNodes());
|
||||
}
|
||||
else
|
||||
{
|
||||
isSelected = node!.IsSelectedWithPartial(path.GetAt(size - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
public void SelectRangeFromAnchor(int index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeFromAnchorImpl(index, select: true);
|
||||
}
|
||||
|
||||
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true);
|
||||
}
|
||||
|
||||
public void SelectRangeFromAnchorTo(IndexPath index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeImpl(AnchorIndex, index, select: true);
|
||||
}
|
||||
|
||||
public void DeselectRangeFromAnchor(int index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeFromAnchorImpl(index, select: false);
|
||||
}
|
||||
|
||||
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */);
|
||||
}
|
||||
|
||||
public void DeselectRangeFromAnchorTo(IndexPath index)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeImpl(AnchorIndex, index, select: false);
|
||||
}
|
||||
|
||||
public void SelectRange(IndexPath start, IndexPath end)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeImpl(start, end, select: true);
|
||||
}
|
||||
|
||||
public void DeselectRange(IndexPath start, IndexPath end)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectRangeImpl(start, end, select: false);
|
||||
}
|
||||
|
||||
public void SelectAll()
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
|
||||
SelectionTreeHelper.Traverse(
|
||||
_rootNode,
|
||||
realizeChildren: true,
|
||||
info =>
|
||||
{
|
||||
if (info.Node.DataCount > 0)
|
||||
{
|
||||
info.Node.SelectAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
ClearSelection(resetAnchor: true);
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
|
||||
public IDisposable Update() => new Operation(this);
|
||||
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
RaisePropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
private void RaisePropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public void OnSelectionInvalidatedDueToCollectionChange(
|
||||
bool selectionInvalidated,
|
||||
IReadOnlyList<object?>? removedItems)
|
||||
{
|
||||
SelectionModelSelectionChangedEventArgs? e = null;
|
||||
|
||||
if (selectionInvalidated)
|
||||
{
|
||||
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
|
||||
}
|
||||
|
||||
OnSelectionChanged(e);
|
||||
ApplyAutoSelect();
|
||||
}
|
||||
|
||||
internal IObservable<object?>? ResolvePath(object data, IndexPath dataIndexPath)
|
||||
{
|
||||
IObservable<object?>? resolved = null;
|
||||
|
||||
// Raise ChildrenRequested event if there is a handler
|
||||
if (ChildrenRequested != null)
|
||||
{
|
||||
if (_childrenRequestedEventArgs == null)
|
||||
{
|
||||
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_childrenRequestedEventArgs.Initialize(data, dataIndexPath, false);
|
||||
}
|
||||
|
||||
ChildrenRequested(this, _childrenRequestedEventArgs);
|
||||
resolved = _childrenRequestedEventArgs.Children;
|
||||
|
||||
// Clear out the values in the args so that it cannot be used after the event handler call.
|
||||
_childrenRequestedEventArgs.Initialize(null, default, true);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void ClearSelection(bool resetAnchor)
|
||||
{
|
||||
SelectionTreeHelper.Traverse(
|
||||
_rootNode,
|
||||
realizeChildren: false,
|
||||
info => info.Node.Clear());
|
||||
|
||||
if (resetAnchor)
|
||||
{
|
||||
AnchorIndex = default;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
|
||||
{
|
||||
_selectedIndicesCached = null;
|
||||
_selectedItemsCached = null;
|
||||
|
||||
// Raise SelectionChanged event
|
||||
if (e != null)
|
||||
{
|
||||
SelectionChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(SelectedIndex));
|
||||
RaisePropertyChanged(nameof(SelectedIndices));
|
||||
|
||||
if (_rootNode.Source != null)
|
||||
{
|
||||
RaisePropertyChanged(nameof(SelectedItem));
|
||||
RaisePropertyChanged(nameof(SelectedItems));
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectImpl(int index, bool select)
|
||||
{
|
||||
if (_singleSelect)
|
||||
{
|
||||
ClearSelection(resetAnchor: true);
|
||||
}
|
||||
|
||||
var selected = _rootNode.Select(index, select);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
AnchorIndex = new IndexPath(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
|
||||
{
|
||||
if (_singleSelect)
|
||||
{
|
||||
ClearSelection(resetAnchor: true);
|
||||
}
|
||||
|
||||
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true);
|
||||
var selected = childNode!.Select(itemIndex, select);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
AnchorIndex = new IndexPath(groupIndex, itemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectWithPathImpl(IndexPath index, bool select)
|
||||
{
|
||||
bool selected = false;
|
||||
|
||||
if (_singleSelect)
|
||||
{
|
||||
ClearSelection(resetAnchor: true);
|
||||
}
|
||||
|
||||
SelectionTreeHelper.TraverseIndexPath(
|
||||
_rootNode,
|
||||
index,
|
||||
true,
|
||||
(currentNode, path, depth, childIndex) =>
|
||||
{
|
||||
if (depth == path.GetSize() - 1)
|
||||
{
|
||||
selected = currentNode.Select(childIndex, select);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
AnchorIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectRangeFromAnchorImpl(int index, bool select)
|
||||
{
|
||||
int anchorIndex = 0;
|
||||
var anchor = AnchorIndex;
|
||||
|
||||
if (anchor != null)
|
||||
{
|
||||
anchorIndex = anchor.GetAt(0);
|
||||
}
|
||||
|
||||
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
|
||||
}
|
||||
|
||||
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select)
|
||||
{
|
||||
var startGroupIndex = 0;
|
||||
var startItemIndex = 0;
|
||||
var anchorIndex = AnchorIndex;
|
||||
|
||||
if (anchorIndex != null)
|
||||
{
|
||||
startGroupIndex = anchorIndex.GetAt(0);
|
||||
startItemIndex = anchorIndex.GetAt(1);
|
||||
}
|
||||
|
||||
// Make sure start > end
|
||||
if (startGroupIndex > endGroupIndex ||
|
||||
(startGroupIndex == endGroupIndex && startItemIndex > endItemIndex))
|
||||
{
|
||||
int temp = startGroupIndex;
|
||||
startGroupIndex = endGroupIndex;
|
||||
endGroupIndex = temp;
|
||||
temp = startItemIndex;
|
||||
startItemIndex = endItemIndex;
|
||||
endItemIndex = temp;
|
||||
}
|
||||
|
||||
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
|
||||
{
|
||||
var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!;
|
||||
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
|
||||
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
|
||||
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select)
|
||||
{
|
||||
var winrtStart = start;
|
||||
var winrtEnd = end;
|
||||
|
||||
// Make sure start <= end
|
||||
if (winrtEnd.CompareTo(winrtStart) == -1)
|
||||
{
|
||||
var temp = winrtStart;
|
||||
winrtStart = winrtEnd;
|
||||
winrtEnd = temp;
|
||||
}
|
||||
|
||||
// Note: Since we do not know the depth of the tree, we have to walk to each leaf
|
||||
SelectionTreeHelper.TraverseRangeRealizeChildren(
|
||||
_rootNode,
|
||||
winrtStart,
|
||||
winrtEnd,
|
||||
info =>
|
||||
{
|
||||
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
|
||||
});
|
||||
}
|
||||
|
||||
private void BeginOperation()
|
||||
{
|
||||
if (_operationCount++ == 0)
|
||||
{
|
||||
_rootNode.BeginOperation();
|
||||
}
|
||||
}
|
||||
|
||||
private void EndOperation()
|
||||
{
|
||||
if (_operationCount == 0)
|
||||
{
|
||||
throw new AvaloniaInternalException("No selection operation in progress.");
|
||||
}
|
||||
|
||||
SelectionModelSelectionChangedEventArgs? e = null;
|
||||
|
||||
if (--_operationCount == 0)
|
||||
{
|
||||
var changes = new List<SelectionNodeOperation>();
|
||||
_rootNode.EndOperation(changes);
|
||||
|
||||
if (changes.Count > 0)
|
||||
{
|
||||
var changeSet = new SelectionModelChangeSet(changes);
|
||||
e = changeSet.CreateEventArgs();
|
||||
}
|
||||
}
|
||||
|
||||
OnSelectionChanged(e);
|
||||
_rootNode.Cleanup();
|
||||
}
|
||||
|
||||
private void ApplyAutoSelect()
|
||||
{
|
||||
if (AutoSelect)
|
||||
{
|
||||
_selectedIndicesCached = null;
|
||||
|
||||
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
|
||||
{
|
||||
using var operation = new Operation(this);
|
||||
SelectImpl(0, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SelectedItemInfo : ISelectedItemInfo
|
||||
{
|
||||
public SelectedItemInfo(SelectionNode node, IndexPath path)
|
||||
{
|
||||
Node = node;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public SelectionNode Node { get; }
|
||||
public IndexPath Path { get; }
|
||||
public int Count => Node.SelectedCount;
|
||||
}
|
||||
|
||||
private struct Operation : IDisposable
|
||||
{
|
||||
private readonly SelectionModel _manager;
|
||||
public Operation(SelectionModel manager) => (_manager = manager).BeginOperation();
|
||||
public void Dispose() => _manager.EndOperation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
internal class SelectionModelChangeSet
|
||||
{
|
||||
private readonly List<SelectionNodeOperation> _changes;
|
||||
|
||||
public SelectionModelChangeSet(List<SelectionNodeOperation> changes)
|
||||
{
|
||||
_changes = changes;
|
||||
}
|
||||
|
||||
public SelectionModelSelectionChangedEventArgs CreateEventArgs()
|
||||
{
|
||||
var deselectedIndexCount = 0;
|
||||
var selectedIndexCount = 0;
|
||||
var deselectedItemCount = 0;
|
||||
var selectedItemCount = 0;
|
||||
|
||||
foreach (var change in _changes)
|
||||
{
|
||||
deselectedIndexCount += change.DeselectedCount;
|
||||
selectedIndexCount += change.SelectedCount;
|
||||
|
||||
if (change.Items != null)
|
||||
{
|
||||
deselectedItemCount += change.DeselectedCount;
|
||||
selectedItemCount += change.SelectedCount;
|
||||
}
|
||||
}
|
||||
|
||||
var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
|
||||
_changes,
|
||||
deselectedIndexCount,
|
||||
GetDeselectedIndexAt);
|
||||
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
|
||||
_changes,
|
||||
selectedIndexCount,
|
||||
GetSelectedIndexAt);
|
||||
var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>(
|
||||
_changes,
|
||||
deselectedItemCount,
|
||||
GetDeselectedItemAt);
|
||||
var selectedItems = new SelectedItems<object?, SelectionNodeOperation>(
|
||||
_changes,
|
||||
selectedItemCount,
|
||||
GetSelectedItemAt);
|
||||
|
||||
return new SelectionModelSelectionChangedEventArgs(
|
||||
deselectedIndices,
|
||||
selectedIndices,
|
||||
deselectedItems,
|
||||
selectedItems);
|
||||
}
|
||||
|
||||
private IndexPath GetDeselectedIndexAt(
|
||||
List<SelectionNodeOperation> infos,
|
||||
int index)
|
||||
{
|
||||
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
|
||||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
|
||||
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
|
||||
}
|
||||
|
||||
private IndexPath GetSelectedIndexAt(
|
||||
List<SelectionNodeOperation> infos,
|
||||
int index)
|
||||
{
|
||||
static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
|
||||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
|
||||
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
|
||||
}
|
||||
|
||||
private object? GetDeselectedItemAt(
|
||||
List<SelectionNodeOperation> infos,
|
||||
int index)
|
||||
{
|
||||
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
|
||||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
|
||||
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
|
||||
}
|
||||
|
||||
private object? GetSelectedItemAt(
|
||||
List<SelectionNodeOperation> infos,
|
||||
int index)
|
||||
{
|
||||
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
|
||||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
|
||||
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
|
||||
}
|
||||
|
||||
private IndexPath GetIndexAt(
|
||||
List<SelectionNodeOperation> infos,
|
||||
int index,
|
||||
Func<SelectionNodeOperation, int> getCount,
|
||||
Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
|
||||
{
|
||||
var currentIndex = 0;
|
||||
IndexPath path = default;
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var currentCount = getCount(info);
|
||||
|
||||
if (index >= currentIndex && index < currentIndex + currentCount)
|
||||
{
|
||||
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
|
||||
path = info.Path.CloneWithChildIndex(targetIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
currentIndex += currentCount;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private object? GetItemAt(
|
||||
List<SelectionNodeOperation> infos,
|
||||
int index,
|
||||
Func<SelectionNodeOperation, int> getCount,
|
||||
Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
|
||||
{
|
||||
var currentIndex = 0;
|
||||
object? item = null;
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var currentCount = getCount(info);
|
||||
|
||||
if (index >= currentIndex && index < currentIndex + currentCount)
|
||||
{
|
||||
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
|
||||
item = info.Items?.GetAt(targetIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
currentIndex += currentCount;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private int GetIndexAt(List<IndexRange>? ranges, int index)
|
||||
{
|
||||
var currentIndex = 0;
|
||||
|
||||
if (ranges != null)
|
||||
{
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var currentCount = (range.End - range.Begin) + 1;
|
||||
|
||||
if (index >= currentIndex && index < currentIndex + currentCount)
|
||||
{
|
||||
return range.Begin + (index - currentIndex);
|
||||
}
|
||||
|
||||
currentIndex += currentCount;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
|
||||
/// </summary>
|
||||
public class SelectionModelChildrenRequestedEventArgs : EventArgs
|
||||
{
|
||||
private object? _source;
|
||||
private IndexPath _sourceIndexPath;
|
||||
private bool _throwOnAccess;
|
||||
|
||||
internal SelectionModelChildrenRequestedEventArgs(
|
||||
object source,
|
||||
IndexPath sourceIndexPath,
|
||||
bool throwOnAccess)
|
||||
{
|
||||
source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
Initialize(source, sourceIndexPath, throwOnAccess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an observable which produces the children of the <see cref="Source"/>
|
||||
/// object.
|
||||
/// </summary>
|
||||
public IObservable<object?>? Children { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object whose children are being requested.
|
||||
/// </summary>
|
||||
public object Source
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_throwOnAccess)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
|
||||
}
|
||||
|
||||
return _source!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the object whose children are being requested.
|
||||
/// </summary>
|
||||
public IndexPath SourceIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_throwOnAccess)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
|
||||
}
|
||||
|
||||
return _sourceIndexPath;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Initialize(
|
||||
object? source,
|
||||
IndexPath sourceIndexPath,
|
||||
bool throwOnAccess)
|
||||
{
|
||||
if (!throwOnAccess && source == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
_source = source;
|
||||
_sourceIndexPath = sourceIndexPath;
|
||||
_throwOnAccess = throwOnAccess;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
public class SelectionModelSelectionChangedEventArgs : EventArgs
|
||||
{
|
||||
public SelectionModelSelectionChangedEventArgs(
|
||||
IReadOnlyList<IndexPath>? deselectedIndices,
|
||||
IReadOnlyList<IndexPath>? selectedIndices,
|
||||
IReadOnlyList<object?>? deselectedItems,
|
||||
IReadOnlyList<object?>? selectedItems)
|
||||
{
|
||||
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>();
|
||||
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>();
|
||||
DeselectedItems = deselectedItems ?? Array.Empty<object?>();
|
||||
SelectedItems= selectedItems ?? Array.Empty<object?>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the indices of the items that were removed from the selection.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IndexPath> DeselectedIndices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the indices of the items that were added to the selection.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IndexPath> SelectedIndices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items that were removed from the selection.
|
||||
/// </summary>
|
||||
public IReadOnlyList<object?> DeselectedItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items that were added to the selection.
|
||||
/// </summary>
|
||||
public IReadOnlyList<object?> SelectedItems { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks nested selection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SelectionNode is the internal tree data structure that we keep track of for selection in
|
||||
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
|
||||
/// collection changes and keeps the selected indices up to date. This can either be a leaf
|
||||
/// node or a non leaf node.
|
||||
/// </remarks>
|
||||
internal class SelectionNode : IDisposable
|
||||
{
|
||||
private readonly SelectionModel _manager;
|
||||
private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>();
|
||||
private readonly SelectionNode? _parent;
|
||||
private readonly List<IndexRange> _selected = new List<IndexRange>();
|
||||
private readonly List<int> _selectedIndicesCached = new List<int>();
|
||||
private IDisposable? _childrenSubscription;
|
||||
private SelectionNodeOperation? _operation;
|
||||
private object? _source;
|
||||
private bool _selectedIndicesCacheIsValid;
|
||||
private bool _retainSelectionOnReset;
|
||||
private List<object?>? _selectedItems;
|
||||
|
||||
public SelectionNode(SelectionModel manager, SelectionNode? parent)
|
||||
{
|
||||
_manager = manager;
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public int AnchorIndex { get; set; } = -1;
|
||||
|
||||
public bool RetainSelectionOnReset
|
||||
{
|
||||
get => _retainSelectionOnReset;
|
||||
set
|
||||
{
|
||||
if (_retainSelectionOnReset != value)
|
||||
{
|
||||
_retainSelectionOnReset = value;
|
||||
|
||||
if (_retainSelectionOnReset)
|
||||
{
|
||||
_selectedItems = new List<object?>();
|
||||
PopulateSelectedItemsFromSelectedIndices();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedItems = null;
|
||||
}
|
||||
|
||||
foreach (var child in _childrenNodes)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
child.RetainSelectionOnReset = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object? Source
|
||||
{
|
||||
get => _source;
|
||||
set
|
||||
{
|
||||
if (_source != value)
|
||||
{
|
||||
if (_source != null)
|
||||
{
|
||||
ClearSelection();
|
||||
ClearChildNodes();
|
||||
UnhookCollectionChangedHandler();
|
||||
}
|
||||
|
||||
_source = value;
|
||||
|
||||
// Setup ItemsSourceView
|
||||
var newDataSource = value as ItemsSourceView;
|
||||
|
||||
if (value != null && newDataSource == null)
|
||||
{
|
||||
newDataSource = new ItemsSourceView((IEnumerable)value);
|
||||
}
|
||||
|
||||
ItemsSourceView = newDataSource;
|
||||
|
||||
PopulateSelectedItemsFromSelectedIndices();
|
||||
HookupCollectionChangedHandler();
|
||||
OnSelectionChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ItemsSourceView? ItemsSourceView { get; private set; }
|
||||
public int DataCount => ItemsSourceView?.Count ?? 0;
|
||||
public int ChildrenNodeCount => _childrenNodes.Count;
|
||||
public int RealizedChildrenNodeCount { get; private set; }
|
||||
|
||||
public IndexPath IndexPath
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = new List<int>(); ;
|
||||
var parent = _parent;
|
||||
var child = this;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
var childNodes = parent._childrenNodes;
|
||||
var index = childNodes.IndexOf(child);
|
||||
|
||||
// We are walking up to the parent, so the path will be backwards
|
||||
path.Insert(0, index);
|
||||
child = parent;
|
||||
parent = parent._parent;
|
||||
}
|
||||
|
||||
return new IndexPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
// For a genuine tree view, we dont know which node is leaf until we
|
||||
// actually walk to it, so currently the tree builds up to the leaf. I don't
|
||||
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
|
||||
// an explosion of node objects. However, I'm still creating the m_childrenNodes
|
||||
// collection unfortunately.
|
||||
public SelectionNode? GetAt(int index, bool realizeChild)
|
||||
{
|
||||
SelectionNode? child = null;
|
||||
|
||||
if (realizeChild)
|
||||
{
|
||||
if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
if (_childrenNodes.Count == 0)
|
||||
{
|
||||
if (ItemsSourceView != null)
|
||||
{
|
||||
for (int i = 0; i < ItemsSourceView.Count; i++)
|
||||
{
|
||||
_childrenNodes.Add(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_childrenNodes[index] == null)
|
||||
{
|
||||
var childData = ItemsSourceView!.GetAt(index);
|
||||
IObservable<object?>? resolver = null;
|
||||
|
||||
if (childData != null)
|
||||
{
|
||||
var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
|
||||
resolver = _manager.ResolvePath(childData, childDataIndexPath);
|
||||
}
|
||||
|
||||
if (resolver != null)
|
||||
{
|
||||
child = new SelectionNode(_manager, parent: this);
|
||||
child.SetChildrenObservable(resolver);
|
||||
}
|
||||
else if (childData is IEnumerable<object> || childData is IList)
|
||||
{
|
||||
child = new SelectionNode(_manager, parent: this);
|
||||
child.Source = childData;
|
||||
}
|
||||
else
|
||||
{
|
||||
child = _manager.SharedLeafNode;
|
||||
}
|
||||
|
||||
if (_operation != null && child != _manager.SharedLeafNode)
|
||||
{
|
||||
child.BeginOperation();
|
||||
}
|
||||
|
||||
_childrenNodes[index] = child;
|
||||
RealizedChildrenNodeCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
child = _childrenNodes[index];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_childrenNodes.Count > 0)
|
||||
{
|
||||
child = _childrenNodes[index];
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
public void SetChildrenObservable(IObservable<object?> resolver)
|
||||
{
|
||||
_childrenSubscription = resolver.Subscribe(x => Source = x);
|
||||
}
|
||||
|
||||
public int SelectedCount { get; private set; }
|
||||
|
||||
public bool IsSelected(int index)
|
||||
{
|
||||
var isSelected = false;
|
||||
|
||||
foreach (var range in _selected)
|
||||
{
|
||||
if (range.Contains(index))
|
||||
{
|
||||
isSelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
// True -> Selected
|
||||
// False -> Not Selected
|
||||
// Null -> Some descendents are selected and some are not
|
||||
public bool? IsSelectedWithPartial()
|
||||
{
|
||||
var isSelected = (bool?)false;
|
||||
|
||||
if (_parent != null)
|
||||
{
|
||||
var parentsChildren = _parent._childrenNodes;
|
||||
|
||||
var myIndexInParent = parentsChildren.IndexOf(this);
|
||||
|
||||
if (myIndexInParent != -1)
|
||||
{
|
||||
isSelected = _parent.IsSelectedWithPartial(myIndexInParent);
|
||||
}
|
||||
}
|
||||
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
// True -> Selected
|
||||
// False -> Not Selected
|
||||
// Null -> Some descendents are selected and some are not
|
||||
public bool? IsSelectedWithPartial(int index)
|
||||
{
|
||||
SelectionState selectionState;
|
||||
|
||||
if (_childrenNodes.Count == 0 || // no nodes realized
|
||||
_childrenNodes.Count <= index || // target node is not realized
|
||||
_childrenNodes[index] == null || // target node is not realized
|
||||
_childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node.
|
||||
{
|
||||
// Ask parent if the target node is selected.
|
||||
selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected;
|
||||
}
|
||||
else
|
||||
{
|
||||
// targetNode is the node representing the index. This node is the parent.
|
||||
// targetNode is a non-leaf node, containing one or many children nodes. Evaluate
|
||||
// based on children of targetNode.
|
||||
var targetNode = _childrenNodes[index];
|
||||
selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes();
|
||||
}
|
||||
|
||||
return ConvertToNullableBool(selectionState);
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => SelectedCount > 0 ? SelectedIndices[0] : -1;
|
||||
set
|
||||
{
|
||||
if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value)))
|
||||
{
|
||||
ClearSelection();
|
||||
|
||||
if (value != -1)
|
||||
{
|
||||
Select(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> SelectedIndices
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_selectedIndicesCacheIsValid)
|
||||
{
|
||||
_selectedIndicesCacheIsValid = true;
|
||||
|
||||
foreach (var range in _selected)
|
||||
{
|
||||
for (int index = range.Begin; index <= range.End; index++)
|
||||
{
|
||||
// Avoid duplicates
|
||||
if (!_selectedIndicesCached.Contains(index))
|
||||
{
|
||||
_selectedIndicesCached.Add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the list for easy consumption
|
||||
_selectedIndicesCached.Sort();
|
||||
}
|
||||
|
||||
return _selectedIndicesCached;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<object> SelectedItems
|
||||
{
|
||||
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_childrenSubscription?.Dispose();
|
||||
ItemsSourceView?.Dispose();
|
||||
ClearChildNodes();
|
||||
UnhookCollectionChangedHandler();
|
||||
}
|
||||
|
||||
public void BeginOperation()
|
||||
{
|
||||
if (_operation != null)
|
||||
{
|
||||
throw new AvaloniaInternalException("Selection operation already in progress.");
|
||||
}
|
||||
|
||||
_operation = new SelectionNodeOperation(this);
|
||||
|
||||
for (var i = 0; i < _childrenNodes.Count; ++i)
|
||||
{
|
||||
var child = _childrenNodes[i];
|
||||
|
||||
if (child != null && child != _manager.SharedLeafNode)
|
||||
{
|
||||
child.BeginOperation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EndOperation(List<SelectionNodeOperation> changes)
|
||||
{
|
||||
if (_operation == null)
|
||||
{
|
||||
throw new AvaloniaInternalException("No selection operation in progress.");
|
||||
}
|
||||
|
||||
if (_operation.HasChanges)
|
||||
{
|
||||
changes.Add(_operation);
|
||||
}
|
||||
|
||||
_operation = null;
|
||||
|
||||
for (var i = 0; i < _childrenNodes.Count; ++i)
|
||||
{
|
||||
var child = _childrenNodes[i];
|
||||
|
||||
if (child != null && child != _manager.SharedLeafNode)
|
||||
{
|
||||
child.EndOperation(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Cleanup()
|
||||
{
|
||||
var result = SelectedCount == 0;
|
||||
|
||||
for (var i = 0; i < _childrenNodes.Count; ++i)
|
||||
{
|
||||
var child = _childrenNodes[i];
|
||||
|
||||
if (child != null)
|
||||
{
|
||||
if (child.Cleanup())
|
||||
{
|
||||
child.Dispose();
|
||||
_childrenNodes[i] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Select(int index, bool select)
|
||||
{
|
||||
return Select(index, select, raiseOnSelectionChanged: true);
|
||||
}
|
||||
|
||||
public bool ToggleSelect(int index)
|
||||
{
|
||||
return Select(index, !IsSelected(index));
|
||||
}
|
||||
|
||||
public void SelectAll()
|
||||
{
|
||||
if (ItemsSourceView != null)
|
||||
{
|
||||
var size = ItemsSourceView.Count;
|
||||
|
||||
if (size > 0)
|
||||
{
|
||||
SelectRange(new IndexRange(0, size - 1), select: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() => ClearSelection();
|
||||
|
||||
public bool SelectRange(IndexRange range, bool select)
|
||||
{
|
||||
if (IsValidIndex(range.Begin) && IsValidIndex(range.End))
|
||||
{
|
||||
if (select)
|
||||
{
|
||||
AddRange(range, raiseOnSelectionChanged: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveRange(range, raiseOnSelectionChanged: true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HookupCollectionChangedHandler()
|
||||
{
|
||||
if (ItemsSourceView != null)
|
||||
{
|
||||
ItemsSourceView.CollectionChanged += OnSourceListChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnhookCollectionChangedHandler()
|
||||
{
|
||||
if (ItemsSourceView != null)
|
||||
{
|
||||
ItemsSourceView.CollectionChanged -= OnSourceListChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidIndex(int index)
|
||||
{
|
||||
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count);
|
||||
}
|
||||
|
||||
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
|
||||
{
|
||||
var selected = new List<IndexRange>();
|
||||
|
||||
SelectedCount += IndexRange.Add(_selected, addRange, selected);
|
||||
|
||||
if (selected.Count > 0)
|
||||
{
|
||||
_operation?.Selected(selected);
|
||||
|
||||
if (_selectedItems != null && ItemsSourceView != null)
|
||||
{
|
||||
for (var i = addRange.Begin; i <= addRange.End; ++i)
|
||||
{
|
||||
_selectedItems.Add(ItemsSourceView!.GetAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (raiseOnSelectionChanged)
|
||||
{
|
||||
OnSelectionChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
|
||||
{
|
||||
var removed = new List<IndexRange>();
|
||||
|
||||
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
|
||||
|
||||
if (removed.Count > 0)
|
||||
{
|
||||
_operation?.Deselected(removed);
|
||||
|
||||
if (_selectedItems != null)
|
||||
{
|
||||
for (var i = removeRange.Begin; i <= removeRange.End; ++i)
|
||||
{
|
||||
_selectedItems.Remove(ItemsSourceView!.GetAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (raiseOnSelectionChanged)
|
||||
{
|
||||
OnSelectionChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
// Deselect all items
|
||||
if (_selected.Count > 0)
|
||||
{
|
||||
_operation?.Deselected(_selected);
|
||||
_selected.Clear();
|
||||
OnSelectionChanged();
|
||||
}
|
||||
|
||||
_selectedItems?.Clear();
|
||||
SelectedCount = 0;
|
||||
AnchorIndex = -1;
|
||||
}
|
||||
|
||||
private void ClearChildNodes()
|
||||
{
|
||||
foreach (var child in _childrenNodes)
|
||||
{
|
||||
if (child != null && child != _manager.SharedLeafNode)
|
||||
{
|
||||
child.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
RealizedChildrenNodeCount = 0;
|
||||
}
|
||||
|
||||
private bool Select(int index, bool select, bool raiseOnSelectionChanged)
|
||||
{
|
||||
if (IsValidIndex(index))
|
||||
{
|
||||
// Ignore duplicate selection calls
|
||||
if (IsSelected(index) == select)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var range = new IndexRange(index, index);
|
||||
|
||||
if (select)
|
||||
{
|
||||
AddRange(range, raiseOnSelectionChanged);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveRange(range, raiseOnSelectionChanged);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
bool selectionInvalidated = false;
|
||||
List<object?>? removed = null;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
{
|
||||
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
|
||||
break;
|
||||
}
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
{
|
||||
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
|
||||
break;
|
||||
}
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
{
|
||||
if (_selectedItems == null)
|
||||
{
|
||||
ClearSelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
removed = RecreateSelectionFromSelectedItems();
|
||||
}
|
||||
|
||||
selectionInvalidated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
{
|
||||
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
|
||||
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionInvalidated)
|
||||
{
|
||||
OnSelectionChanged();
|
||||
}
|
||||
|
||||
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
|
||||
}
|
||||
|
||||
private bool OnItemsAdded(int index, int count)
|
||||
{
|
||||
var selectionInvalidated = false;
|
||||
|
||||
// Update ranges for leaf items
|
||||
var toAdd = new List<IndexRange>();
|
||||
|
||||
for (int i = 0; i < _selected.Count; i++)
|
||||
{
|
||||
var range = _selected[i];
|
||||
|
||||
// The range is after the inserted items, need to shift the range right
|
||||
if (range.End >= index)
|
||||
{
|
||||
int begin = range.Begin;
|
||||
|
||||
// If the index left of newIndex is inside the range,
|
||||
// Split the range and remember the left piece to add later
|
||||
if (range.Contains(index - 1))
|
||||
{
|
||||
range.Split(index - 1, out var before, out _);
|
||||
toAdd.Add(before);
|
||||
begin = index;
|
||||
}
|
||||
|
||||
// Shift the range to the right
|
||||
_selected[i] = new IndexRange(begin + count, range.End + count);
|
||||
selectionInvalidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the left sides of the split ranges
|
||||
_selected.AddRange(toAdd);
|
||||
|
||||
// Update for non-leaf if we are tracking non-leaf nodes
|
||||
if (_childrenNodes.Count > 0)
|
||||
{
|
||||
selectionInvalidated = true;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
_childrenNodes.Insert(index, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the anchor
|
||||
if (AnchorIndex >= index)
|
||||
{
|
||||
AnchorIndex += count;
|
||||
}
|
||||
|
||||
// Check if adding a node invalidated an ancestors
|
||||
// selection state. For example if parent was selected before
|
||||
// adding a new item makes the parent partially selected now.
|
||||
if (!selectionInvalidated)
|
||||
{
|
||||
var parent = _parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
var isSelected = parent.IsSelectedWithPartial();
|
||||
|
||||
// If a parent is selected, then it will become partially selected.
|
||||
// If it is not selected or partially selected - there is no change.
|
||||
if (isSelected == true)
|
||||
{
|
||||
selectionInvalidated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent._parent;
|
||||
}
|
||||
}
|
||||
|
||||
return selectionInvalidated;
|
||||
}
|
||||
|
||||
private (bool, List<object?>) OnItemsRemoved(int index, IList items)
|
||||
{
|
||||
var selectionInvalidated = false;
|
||||
var removed = new List<object?>();
|
||||
var count = items.Count;
|
||||
|
||||
// Remove the items from the selection for leaf
|
||||
if (ItemsSourceView!.Count > 0)
|
||||
{
|
||||
bool isSelected = false;
|
||||
|
||||
for (int i = 0; i <= count - 1; i++)
|
||||
{
|
||||
if (IsSelected(index + i))
|
||||
{
|
||||
isSelected = true;
|
||||
removed.Add(items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
var removeRange = new IndexRange(index, index + count - 1);
|
||||
SelectedCount -= IndexRange.Remove(_selected, removeRange);
|
||||
selectionInvalidated = true;
|
||||
|
||||
if (_selectedItems != null)
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
_selectedItems.Remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _selected.Count; i++)
|
||||
{
|
||||
var range = _selected[i];
|
||||
|
||||
// The range is after the removed items, need to shift the range left
|
||||
if (range.End > index)
|
||||
{
|
||||
// Shift the range to the left
|
||||
_selected[i] = new IndexRange(range.Begin - count, range.End - count);
|
||||
selectionInvalidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update for non-leaf if we are tracking non-leaf nodes
|
||||
if (_childrenNodes.Count > 0)
|
||||
{
|
||||
selectionInvalidated = true;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (_childrenNodes[index] != null)
|
||||
{
|
||||
removed.AddRange(_childrenNodes[index]!.SelectedItems);
|
||||
RealizedChildrenNodeCount--;
|
||||
_childrenNodes[index]!.Dispose();
|
||||
}
|
||||
_childrenNodes.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Adjust the anchor
|
||||
if (AnchorIndex >= index)
|
||||
{
|
||||
AnchorIndex -= count;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No more items in the list, clear
|
||||
ClearSelection();
|
||||
RealizedChildrenNodeCount = 0;
|
||||
selectionInvalidated = true;
|
||||
}
|
||||
|
||||
// Check if removing a node invalidated an ancestors
|
||||
// selection state. For example if parent was partially selected before
|
||||
// removing an item, it could be selected now.
|
||||
if (!selectionInvalidated)
|
||||
{
|
||||
var parent = _parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
var isSelected = parent.IsSelectedWithPartial();
|
||||
// If a parent is partially selected, then it will become selected.
|
||||
// If it is selected or not selected - there is no change.
|
||||
if (!isSelected.HasValue)
|
||||
{
|
||||
selectionInvalidated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent._parent;
|
||||
}
|
||||
}
|
||||
|
||||
return (selectionInvalidated, removed);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
_selectedIndicesCacheIsValid = false;
|
||||
_selectedIndicesCached.Clear();
|
||||
}
|
||||
|
||||
public static bool? ConvertToNullableBool(SelectionState isSelected)
|
||||
{
|
||||
bool? result = null; // PartialySelected
|
||||
|
||||
if (isSelected == SelectionState.Selected)
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
else if (isSelected == SelectionState.NotSelected)
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public SelectionState EvaluateIsSelectedBasedOnChildrenNodes()
|
||||
{
|
||||
var selectionState = SelectionState.NotSelected;
|
||||
int realizedChildrenNodeCount = RealizedChildrenNodeCount;
|
||||
int selectedCount = SelectedCount;
|
||||
|
||||
if (realizedChildrenNodeCount != 0 || selectedCount != 0)
|
||||
{
|
||||
// There are realized children or some selected leaves.
|
||||
int dataCount = DataCount;
|
||||
if (realizedChildrenNodeCount == 0 && selectedCount > 0)
|
||||
{
|
||||
// All nodes are leaves under it - we didn't create children nodes as an optimization.
|
||||
// See if all/some or none of the leaves are selected.
|
||||
selectionState = dataCount != selectedCount ?
|
||||
SelectionState.PartiallySelected :
|
||||
dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected;
|
||||
}
|
||||
else
|
||||
{
|
||||
// There are child nodes, walk them individually and evaluate based on each child
|
||||
// being selected/not selected or partially selected.
|
||||
selectedCount = 0;
|
||||
int notSelectedCount = 0;
|
||||
for (int i = 0; i < ChildrenNodeCount; i++)
|
||||
{
|
||||
var child = GetAt(i, realizeChild: false);
|
||||
|
||||
if (child != null)
|
||||
{
|
||||
// child is realized, ask it.
|
||||
var isChildSelected = IsSelectedWithPartial(i);
|
||||
if (isChildSelected == null)
|
||||
{
|
||||
selectionState = SelectionState.PartiallySelected;
|
||||
break;
|
||||
}
|
||||
else if (isChildSelected == true)
|
||||
{
|
||||
selectedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
notSelectedCount++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not realized.
|
||||
if (IsSelected(i))
|
||||
{
|
||||
selectedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
notSelectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCount > 0 && notSelectedCount > 0)
|
||||
{
|
||||
selectionState = SelectionState.PartiallySelected;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionState != SelectionState.PartiallySelected)
|
||||
{
|
||||
if (selectedCount != 0 && selectedCount != dataCount)
|
||||
{
|
||||
selectionState = SelectionState.PartiallySelected;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectionState;
|
||||
}
|
||||
|
||||
private void PopulateSelectedItemsFromSelectedIndices()
|
||||
{
|
||||
if (_selectedItems != null)
|
||||
{
|
||||
_selectedItems.Clear();
|
||||
|
||||
foreach (var i in SelectedIndices)
|
||||
{
|
||||
_selectedItems.Add(ItemsSourceView!.GetAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<object?> RecreateSelectionFromSelectedItems()
|
||||
{
|
||||
var removed = new List<object?>();
|
||||
|
||||
_selected.Clear();
|
||||
SelectedCount = 0;
|
||||
|
||||
for (var i = 0; i < _selectedItems!.Count; ++i)
|
||||
{
|
||||
var item = _selectedItems[i];
|
||||
var index = ItemsSourceView!.IndexOf(item);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
IndexRange.Add(_selected, new IndexRange(index, index));
|
||||
++SelectedCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
removed.Add(item);
|
||||
_selectedItems.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
public enum SelectionState
|
||||
{
|
||||
Selected,
|
||||
NotSelected,
|
||||
PartiallySelected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls
|
||||
{
|
||||
internal class SelectionNodeOperation : ISelectedItemInfo
|
||||
{
|
||||
private readonly SelectionNode _owner;
|
||||
private List<IndexRange>? _selected;
|
||||
private List<IndexRange>? _deselected;
|
||||
private int _selectedCount = -1;
|
||||
private int _deselectedCount = -1;
|
||||
|
||||
public SelectionNodeOperation(SelectionNode owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0;
|
||||
public List<IndexRange>? SelectedRanges => _selected;
|
||||
public List<IndexRange>? DeselectedRanges => _deselected;
|
||||
public IndexPath Path => _owner.IndexPath;
|
||||
public ItemsSourceView? Items => _owner.ItemsSourceView;
|
||||
|
||||
public int SelectedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedCount == -1)
|
||||
{
|
||||
_selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0;
|
||||
}
|
||||
|
||||
return _selectedCount;
|
||||
}
|
||||
}
|
||||
|
||||
public int DeselectedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_deselectedCount == -1)
|
||||
{
|
||||
_deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0;
|
||||
}
|
||||
|
||||
return _deselectedCount;
|
||||
}
|
||||
}
|
||||
|
||||
public void Selected(IndexRange range)
|
||||
{
|
||||
Add(range, ref _selected, _deselected);
|
||||
_selectedCount = -1;
|
||||
}
|
||||
|
||||
public void Selected(IEnumerable<IndexRange> ranges)
|
||||
{
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
Selected(range);
|
||||
}
|
||||
}
|
||||
|
||||
public void Deselected(IndexRange range)
|
||||
{
|
||||
Add(range, ref _deselected, _selected);
|
||||
_deselectedCount = -1;
|
||||
}
|
||||
|
||||
public void Deselected(IEnumerable<IndexRange> ranges)
|
||||
{
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
Deselected(range);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
IndexRange range,
|
||||
ref List<IndexRange>? add,
|
||||
List<IndexRange>? remove)
|
||||
{
|
||||
if (remove != null)
|
||||
{
|
||||
var removed = new List<IndexRange>();
|
||||
IndexRange.Remove(remove, range, removed);
|
||||
var selected = IndexRange.Subtract(range, removed);
|
||||
|
||||
if (selected.Any())
|
||||
{
|
||||
add ??= new List<IndexRange>();
|
||||
|
||||
foreach (var r in selected)
|
||||
{
|
||||
IndexRange.Add(add, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
add ??= new List<IndexRange>();
|
||||
IndexRange.Add(add, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ namespace Avalonia.Controls
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse)
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse)
|
||||
{
|
||||
e.Handled = UpdateSelectionFromEventSource(e.Source);
|
||||
}
|
||||
|
||||
+299
-368
@@ -2,11 +2,12 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls.Generators;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Utils;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -42,15 +43,29 @@ namespace Avalonia.Controls
|
||||
o => o.SelectedItems,
|
||||
(o, v) => o.SelectedItems = v);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Selection"/> property.
|
||||
/// </summary>
|
||||
public static readonly DirectProperty<TreeView, ISelectionModel> SelectionProperty =
|
||||
SelectingItemsControl.SelectionProperty.AddOwner<TreeView>(
|
||||
o => o.Selection,
|
||||
(o, v) => o.Selection = v);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="SelectionMode"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
|
||||
ListBox.SelectionModeProperty.AddOwner<TreeView>();
|
||||
|
||||
private static readonly IList Empty = Array.Empty<object>();
|
||||
/// <summary>
|
||||
/// Defines the <see cref="SelectionChanged"/> property.
|
||||
/// </summary>
|
||||
public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
|
||||
SelectingItemsControl.SelectionChangedEvent;
|
||||
|
||||
private object _selectedItem;
|
||||
private IList _selectedItems;
|
||||
private ISelectionModel _selection;
|
||||
private readonly SelectedItemsSync _selectedItems;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="TreeView"/> class.
|
||||
@@ -60,6 +75,13 @@ namespace Avalonia.Controls
|
||||
// HACK: Needed or SelectedItem property will not be found in Release build.
|
||||
}
|
||||
|
||||
public TreeView()
|
||||
{
|
||||
// Setting Selection to null causes a default SelectionModel to be created.
|
||||
Selection = null;
|
||||
_selectedItems = new SelectedItemsSync(Selection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the control's selection changes.
|
||||
/// </summary>
|
||||
@@ -84,8 +106,6 @@ namespace Avalonia.Controls
|
||||
set => SetValue(AutoScrollToSelectedItemProperty, value);
|
||||
}
|
||||
|
||||
private bool _syncingSelectedItems;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selection mode.
|
||||
/// </summary>
|
||||
@@ -95,61 +115,102 @@ namespace Avalonia.Controls
|
||||
set => SetValue(SelectionModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected item.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Gets or sets the selected item.
|
||||
/// </summary>
|
||||
public object SelectedItem
|
||||
{
|
||||
get => _selectedItem;
|
||||
set
|
||||
{
|
||||
var selectedItems = SelectedItems;
|
||||
|
||||
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
if (selectedItems.Count != 1 || selectedItems[0] != value)
|
||||
{
|
||||
_syncingSelectedItems = true;
|
||||
SelectSingleItem(value);
|
||||
_syncingSelectedItems = false;
|
||||
}
|
||||
}
|
||||
else if (SelectedItems.Count > 0)
|
||||
{
|
||||
SelectedItems.Clear();
|
||||
}
|
||||
}
|
||||
get => Selection.SelectedItem;
|
||||
set => Selection.SelectedIndex = IndexFromItem(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected items.
|
||||
/// Gets or sets the selected items.
|
||||
/// </summary>
|
||||
public IList SelectedItems
|
||||
protected IList SelectedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedItems == null)
|
||||
{
|
||||
_selectedItems = new AvaloniaList<object>();
|
||||
SubscribeToSelectedItems();
|
||||
}
|
||||
|
||||
return _selectedItems;
|
||||
}
|
||||
get => _selectedItems.GetOrCreateItems();
|
||||
set => _selectedItems.SetItems(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a model holding the current selection.
|
||||
/// </summary>
|
||||
public ISelectionModel Selection
|
||||
{
|
||||
get => _selection;
|
||||
set
|
||||
{
|
||||
if (value?.IsFixedSize == true || value?.IsReadOnly == true)
|
||||
value ??= new SelectionModel
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Cannot use a fixed size or read-only collection as SelectedItems.");
|
||||
}
|
||||
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
|
||||
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
|
||||
RetainSelectionOnReset = true,
|
||||
};
|
||||
|
||||
UnsubscribeFromSelectedItems();
|
||||
_selectedItems = value ?? new AvaloniaList<object>();
|
||||
SubscribeToSelectedItems();
|
||||
if (_selection != value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
|
||||
}
|
||||
else if (value.Source != null && value.Source != Items)
|
||||
{
|
||||
throw new ArgumentException("Selection has invalid Source.");
|
||||
}
|
||||
|
||||
List<object> oldSelection = null;
|
||||
|
||||
if (_selection != null)
|
||||
{
|
||||
oldSelection = Selection.SelectedItems.ToList();
|
||||
_selection.PropertyChanged -= OnSelectionModelPropertyChanged;
|
||||
_selection.SelectionChanged -= OnSelectionModelSelectionChanged;
|
||||
_selection.ChildrenRequested -= OnSelectionModelChildrenRequested;
|
||||
MarkContainersUnselected();
|
||||
}
|
||||
|
||||
_selection = value;
|
||||
|
||||
if (_selection != null)
|
||||
{
|
||||
_selection.Source = Items;
|
||||
_selection.PropertyChanged += OnSelectionModelPropertyChanged;
|
||||
_selection.SelectionChanged += OnSelectionModelSelectionChanged;
|
||||
_selection.ChildrenRequested += OnSelectionModelChildrenRequested;
|
||||
|
||||
if (_selection.SingleSelect)
|
||||
{
|
||||
SelectionMode &= ~SelectionMode.Multiple;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectionMode |= SelectionMode.Multiple;
|
||||
}
|
||||
|
||||
if (_selection.AutoSelect)
|
||||
{
|
||||
SelectionMode |= SelectionMode.AlwaysSelected;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectionMode &= ~SelectionMode.AlwaysSelected;
|
||||
}
|
||||
|
||||
UpdateContainerSelection();
|
||||
|
||||
var selectedItem = SelectedItem;
|
||||
|
||||
if (_selectedItem != selectedItem)
|
||||
{
|
||||
RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
|
||||
_selectedItem = selectedItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,186 +243,12 @@ namespace Avalonia.Controls
|
||||
/// Note that this method only selects nodes currently visible due to their parent nodes
|
||||
/// being expanded: it does not expand nodes.
|
||||
/// </remarks>
|
||||
public void SelectAll()
|
||||
{
|
||||
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
|
||||
}
|
||||
public void SelectAll() => Selection.SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// Deselects all items in the <see cref="TreeView"/>.
|
||||
/// </summary>
|
||||
public void UnselectAll()
|
||||
{
|
||||
SelectedItems.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
|
||||
/// </summary>
|
||||
private void SubscribeToSelectedItems()
|
||||
{
|
||||
if (_selectedItems is INotifyCollectionChanged incc)
|
||||
{
|
||||
incc.CollectionChanged += SelectedItemsCollectionChanged;
|
||||
}
|
||||
|
||||
SelectedItemsCollectionChanged(
|
||||
_selectedItems,
|
||||
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
private void SelectSingleItem(object item)
|
||||
{
|
||||
SelectedItems.Clear();
|
||||
SelectedItems.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
|
||||
/// </summary>
|
||||
/// <param name="sender">The event sender.</param>
|
||||
/// <param name="e">The event args.</param>
|
||||
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
IList added = null;
|
||||
IList removed = null;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
|
||||
SelectedItemsAdded(e.NewItems.Cast<object>().ToArray());
|
||||
|
||||
if (AutoScrollToSelectedItem)
|
||||
{
|
||||
var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]);
|
||||
|
||||
container?.BringIntoView();
|
||||
}
|
||||
|
||||
added = e.NewItems;
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
|
||||
if (!_syncingSelectedItems)
|
||||
{
|
||||
if (SelectedItems.Count == 0)
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var selectedIndex = SelectedItems.IndexOf(_selectedItem);
|
||||
|
||||
if (selectedIndex == -1)
|
||||
{
|
||||
var old = _selectedItem;
|
||||
_selectedItem = SelectedItems[0];
|
||||
|
||||
RaisePropertyChanged(SelectedItemProperty, old, _selectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in e.OldItems)
|
||||
{
|
||||
MarkItemSelected(item, false);
|
||||
}
|
||||
|
||||
removed = e.OldItems;
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
|
||||
foreach (IControl container in ItemContainerGenerator.Index.Containers)
|
||||
{
|
||||
MarkContainerSelected(container, false);
|
||||
}
|
||||
|
||||
if (SelectedItems.Count > 0)
|
||||
{
|
||||
SelectedItemsAdded(SelectedItems);
|
||||
|
||||
added = SelectedItems;
|
||||
}
|
||||
else if (!_syncingSelectedItems)
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
|
||||
foreach (var item in e.OldItems)
|
||||
{
|
||||
MarkItemSelected(item, false);
|
||||
}
|
||||
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
MarkItemSelected(item, true);
|
||||
}
|
||||
|
||||
if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
|
||||
{
|
||||
var oldItem = SelectedItem;
|
||||
var item = SelectedItems[0];
|
||||
_selectedItem = item;
|
||||
RaisePropertyChanged(SelectedItemProperty, oldItem, item);
|
||||
}
|
||||
|
||||
added = e.NewItems;
|
||||
removed = e.OldItems;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (added?.Count > 0 || removed?.Count > 0)
|
||||
{
|
||||
var changed = new SelectionChangedEventArgs(
|
||||
SelectingItemsControl.SelectionChangedEvent,
|
||||
removed ?? Empty,
|
||||
added ?? Empty);
|
||||
RaiseEvent(changed);
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkItemSelected(object item, bool selected)
|
||||
{
|
||||
var container = ItemContainerGenerator.Index.ContainerFromItem(item);
|
||||
|
||||
MarkContainerSelected(container, selected);
|
||||
}
|
||||
|
||||
private void SelectedItemsAdded(IList items)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (object item in items)
|
||||
{
|
||||
MarkItemSelected(item, true);
|
||||
}
|
||||
|
||||
if (SelectedItem == null && !_syncingSelectedItems)
|
||||
{
|
||||
SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
|
||||
/// </summary>
|
||||
private void UnsubscribeFromSelectedItems()
|
||||
{
|
||||
if (_selectedItems is INotifyCollectionChanged incc)
|
||||
{
|
||||
incc.CollectionChanged -= SelectedItemsCollectionChanged;
|
||||
}
|
||||
}
|
||||
public void UnselectAll() => Selection.ClearSelection();
|
||||
|
||||
(bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element,
|
||||
NavigationDirection direction)
|
||||
@@ -445,6 +332,72 @@ namespace Avalonia.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The event args.</param>
|
||||
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
|
||||
{
|
||||
var container = ContainerFromIndex(Selection.AnchorIndex);
|
||||
|
||||
if (container != null)
|
||||
{
|
||||
container.BringIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The event args.</param>
|
||||
private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
|
||||
{
|
||||
void Mark(IndexPath index, bool selected)
|
||||
{
|
||||
var container = ContainerFromIndex(index);
|
||||
|
||||
if (container != null)
|
||||
{
|
||||
MarkContainerSelected(container, selected);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var i in e.SelectedIndices)
|
||||
{
|
||||
Mark(i, true);
|
||||
}
|
||||
|
||||
foreach (var i in e.DeselectedIndices)
|
||||
{
|
||||
Mark(i, false);
|
||||
}
|
||||
|
||||
var newSelectedItem = SelectedItem;
|
||||
|
||||
if (newSelectedItem != _selectedItem)
|
||||
{
|
||||
RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
|
||||
_selectedItem = newSelectedItem;
|
||||
}
|
||||
|
||||
var ev = new SelectionChangedEventArgs(
|
||||
SelectionChangedEvent,
|
||||
e.DeselectedItems.ToList(),
|
||||
e.SelectedItems.ToList());
|
||||
RaiseEvent(ev);
|
||||
}
|
||||
|
||||
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
|
||||
{
|
||||
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl;
|
||||
e.Children = container?.GetObservable(ItemsProperty);
|
||||
}
|
||||
|
||||
private TreeViewItem GetContainerInDirection(
|
||||
TreeViewItem from,
|
||||
NavigationDirection direction,
|
||||
@@ -498,6 +451,12 @@ namespace Avalonia.Controls
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
Selection.Source = Items;
|
||||
base.ItemsChanged(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
@@ -519,6 +478,18 @@ namespace Avalonia.Controls
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
|
||||
{
|
||||
base.OnPropertyChanged(property, oldValue, newValue, priority);
|
||||
|
||||
if (property == SelectionModeProperty)
|
||||
{
|
||||
var mode = newValue.GetValueOrDefault<SelectionMode>();
|
||||
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
|
||||
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the selection for an item based on user interaction.
|
||||
/// </summary>
|
||||
@@ -534,9 +505,9 @@ namespace Avalonia.Controls
|
||||
bool toggleModifier = false,
|
||||
bool rightButton = false)
|
||||
{
|
||||
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
|
||||
var index = IndexFromContainer((TreeViewItem)container);
|
||||
|
||||
if (item == null)
|
||||
if (index.GetSize() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -553,41 +524,48 @@ namespace Avalonia.Controls
|
||||
var multi = (mode & SelectionMode.Multiple) != 0;
|
||||
var range = multi && selectedContainer != null && rangeModifier;
|
||||
|
||||
if (rightButton)
|
||||
if (!select)
|
||||
{
|
||||
if (!SelectedItems.Contains(item))
|
||||
Selection.DeselectAt(index);
|
||||
}
|
||||
else if (rightButton)
|
||||
{
|
||||
if (!Selection.IsSelectedAt(index))
|
||||
{
|
||||
SelectSingleItem(item);
|
||||
Selection.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
else if (!toggle && !range)
|
||||
{
|
||||
SelectSingleItem(item);
|
||||
Selection.SelectedIndex = index;
|
||||
}
|
||||
else if (multi && range)
|
||||
{
|
||||
SynchronizeItems(
|
||||
SelectedItems,
|
||||
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
|
||||
using var operation = Selection.Update();
|
||||
var anchor = Selection.AnchorIndex;
|
||||
|
||||
if (anchor.GetSize() == 0)
|
||||
{
|
||||
anchor = new IndexPath(0);
|
||||
}
|
||||
|
||||
Selection.ClearSelection();
|
||||
Selection.AnchorIndex = anchor;
|
||||
Selection.SelectRangeFromAnchorTo(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
var i = SelectedItems.IndexOf(item);
|
||||
|
||||
if (i != -1)
|
||||
if (Selection.IsSelectedAt(index))
|
||||
{
|
||||
SelectedItems.Remove(item);
|
||||
Selection.DeselectAt(index);
|
||||
}
|
||||
else if (multi)
|
||||
{
|
||||
Selection.SelectAt(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (multi)
|
||||
{
|
||||
SelectedItems.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedItem = item;
|
||||
}
|
||||
Selection.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,117 +588,6 @@ namespace Avalonia.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find which node is first in hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="treeView">Search root.</param>
|
||||
/// <param name="nodeA">Nodes to find.</param>
|
||||
/// <param name="nodeB">Node to find.</param>
|
||||
/// <returns>Found first node.</returns>
|
||||
private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB)
|
||||
{
|
||||
return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
|
||||
}
|
||||
|
||||
private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator,
|
||||
TreeViewItem nodeA,
|
||||
TreeViewItem nodeB)
|
||||
{
|
||||
IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
|
||||
|
||||
foreach (ItemContainerInfo container in containers)
|
||||
{
|
||||
TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
|
||||
|
||||
if (node != null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null;
|
||||
|
||||
if (match != null)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
|
||||
/// The range is inclusive.
|
||||
/// </summary>
|
||||
/// <param name="from">From container.</param>
|
||||
/// <param name="to">To container.</param>
|
||||
private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to)
|
||||
{
|
||||
var items = new List<object>();
|
||||
|
||||
if (from == null || to == null)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
TreeViewItem firstItem = FindFirstNode(this, from, to);
|
||||
|
||||
if (firstItem == null)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
bool wasReversed = false;
|
||||
|
||||
if (firstItem == to)
|
||||
{
|
||||
var temp = from;
|
||||
|
||||
from = to;
|
||||
to = temp;
|
||||
|
||||
wasReversed = true;
|
||||
}
|
||||
|
||||
TreeViewItem node = from;
|
||||
|
||||
while (node != to)
|
||||
{
|
||||
var item = ItemContainerGenerator.Index.ItemFromContainer(node);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
node = GetContainerInDirection(node, NavigationDirection.Down, true);
|
||||
}
|
||||
|
||||
var toItem = ItemContainerGenerator.Index.ItemFromContainer(to);
|
||||
|
||||
if (toItem != null)
|
||||
{
|
||||
items.Add(toItem);
|
||||
}
|
||||
|
||||
if (wasReversed)
|
||||
{
|
||||
items.Reverse();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the selection based on an event that may have originated in a container that
|
||||
/// belongs to the control.
|
||||
@@ -826,26 +693,90 @@ namespace Avalonia.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes a list of objects equal another (though doesn't preserve order).
|
||||
/// </summary>
|
||||
/// <param name="items">The items collection.</param>
|
||||
/// <param name="desired">The desired items.</param>
|
||||
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
|
||||
private void MarkContainersUnselected()
|
||||
{
|
||||
var list = items.Cast<object>().ToList();
|
||||
var toRemove = list.Except(desired).ToList();
|
||||
var toAdd = desired.Except(list).ToList();
|
||||
|
||||
foreach (var i in toRemove)
|
||||
foreach (var container in ItemContainerGenerator.Index.Containers)
|
||||
{
|
||||
items.Remove(i);
|
||||
MarkContainerSelected(container, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateContainerSelection()
|
||||
{
|
||||
var index = ItemContainerGenerator.Index;
|
||||
|
||||
foreach (var container in index.Containers)
|
||||
{
|
||||
var i = IndexFromContainer((TreeViewItem)container);
|
||||
|
||||
MarkContainerSelected(
|
||||
container,
|
||||
Selection.IsSelectedAt(i) != false);
|
||||
}
|
||||
}
|
||||
|
||||
private static IndexPath IndexFromContainer(TreeViewItem container)
|
||||
{
|
||||
var result = new List<int>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (container.Level == 0)
|
||||
{
|
||||
var treeView = container.FindAncestorOfType<TreeView>();
|
||||
|
||||
if (treeView == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container));
|
||||
result.Reverse();
|
||||
return new IndexPath(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parent = container.FindAncestorOfType<TreeViewItem>();
|
||||
|
||||
if (parent == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
result.Add(parent.ItemContainerGenerator.IndexFromContainer(container));
|
||||
container = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IndexPath IndexFromItem(object item)
|
||||
{
|
||||
var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem;
|
||||
|
||||
if (container != null)
|
||||
{
|
||||
return IndexFromContainer(container);
|
||||
}
|
||||
|
||||
foreach (var i in toAdd)
|
||||
return default;
|
||||
}
|
||||
|
||||
private TreeViewItem ContainerFromIndex(IndexPath index)
|
||||
{
|
||||
TreeViewItem treeViewItem = null;
|
||||
|
||||
for (var i = 0; i < index.GetSize(); ++i)
|
||||
{
|
||||
items.Add(i);
|
||||
var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator;
|
||||
treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem;
|
||||
|
||||
if (treeViewItem == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return treeViewItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using Avalonia.Collections;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
|
||||
/// </summary>
|
||||
internal class SelectedItemsSync
|
||||
{
|
||||
private IList? _items;
|
||||
private bool _updatingItems;
|
||||
private bool _updatingModel;
|
||||
|
||||
public SelectedItemsSync(ISelectionModel model)
|
||||
{
|
||||
model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
Model = model;
|
||||
}
|
||||
|
||||
public ISelectionModel Model { get; private set; }
|
||||
|
||||
public IList GetOrCreateItems()
|
||||
{
|
||||
if (_items == null)
|
||||
{
|
||||
var items = new AvaloniaList<object>(Model.SelectedItems);
|
||||
items.CollectionChanged += ItemsCollectionChanged;
|
||||
Model.SelectionChanged += SelectionModelSelectionChanged;
|
||||
_items = items;
|
||||
}
|
||||
|
||||
return _items;
|
||||
}
|
||||
|
||||
public void SetItems(IList? items)
|
||||
{
|
||||
items ??= new AvaloniaList<object>();
|
||||
|
||||
if (items.IsFixedSize)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Cannot assign fixed size selection to SelectedItems.");
|
||||
}
|
||||
|
||||
if (_items is INotifyCollectionChanged incc)
|
||||
{
|
||||
incc.CollectionChanged -= ItemsCollectionChanged;
|
||||
}
|
||||
|
||||
if (_items == null)
|
||||
{
|
||||
Model.SelectionChanged += SelectionModelSelectionChanged;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_updatingModel = true;
|
||||
_items = items;
|
||||
|
||||
using (Model.Update())
|
||||
{
|
||||
Model.ClearSelection();
|
||||
Add(items);
|
||||
}
|
||||
|
||||
if (_items is INotifyCollectionChanged incc2)
|
||||
{
|
||||
incc2.CollectionChanged += ItemsCollectionChanged;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updatingModel = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetModel(ISelectionModel model)
|
||||
{
|
||||
model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (_items != null)
|
||||
{
|
||||
Model.SelectionChanged -= SelectionModelSelectionChanged;
|
||||
Model = model;
|
||||
Model.SelectionChanged += SelectionModelSelectionChanged;
|
||||
|
||||
try
|
||||
{
|
||||
_updatingItems = true;
|
||||
_items.Clear();
|
||||
|
||||
foreach (var i in model.SelectedItems)
|
||||
{
|
||||
_items.Add(i);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updatingItems = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (_updatingItems)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_items == null)
|
||||
{
|
||||
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
|
||||
}
|
||||
|
||||
void Remove()
|
||||
{
|
||||
foreach (var i in e.OldItems)
|
||||
{
|
||||
var index = IndexOf(Model.Source, i);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
Model.Deselect(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var operation = Model.Update();
|
||||
|
||||
_updatingModel = true;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
Add(e.NewItems);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
Remove();
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
Remove();
|
||||
Add(e.NewItems);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
Model.ClearSelection();
|
||||
Add(_items);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updatingModel = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Add(IList newItems)
|
||||
{
|
||||
foreach (var i in newItems)
|
||||
{
|
||||
var index = IndexOf(Model.Source, i);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
Model.Select(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
|
||||
{
|
||||
if (_updatingModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_items == null)
|
||||
{
|
||||
throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var deselected = e.DeselectedItems.ToList();
|
||||
var selected = e.SelectedItems.ToList();
|
||||
|
||||
_updatingItems = true;
|
||||
|
||||
foreach (var i in deselected)
|
||||
{
|
||||
_items.Remove(i);
|
||||
}
|
||||
|
||||
foreach (var i in selected)
|
||||
{
|
||||
_items.Add(i);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updatingItems = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int IndexOf(object source, object item)
|
||||
{
|
||||
if (source is IList l)
|
||||
{
|
||||
return l.IndexOf(item);
|
||||
}
|
||||
else if (source is ItemsSourceView v)
|
||||
{
|
||||
return v.IndexOf(item);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Avalonia.Controls.Utils
|
||||
{
|
||||
internal static class SelectionTreeHelper
|
||||
{
|
||||
public static void TraverseIndexPath(
|
||||
SelectionNode root,
|
||||
IndexPath path,
|
||||
bool realizeChildren,
|
||||
Action<SelectionNode, IndexPath, int, int> nodeAction)
|
||||
{
|
||||
var node = root;
|
||||
|
||||
for (int depth = 0; depth < path.GetSize(); depth++)
|
||||
{
|
||||
int childIndex = path.GetAt(depth);
|
||||
nodeAction(node, path, depth, childIndex);
|
||||
|
||||
if (depth < path.GetSize() - 1)
|
||||
{
|
||||
node = node.GetAt(childIndex, realizeChildren)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Traverse(
|
||||
SelectionNode root,
|
||||
bool realizeChildren,
|
||||
Action<TreeWalkNodeInfo> nodeAction)
|
||||
{
|
||||
var pendingNodes = new List<TreeWalkNodeInfo>();
|
||||
var current = new IndexPath(null);
|
||||
|
||||
pendingNodes.Add(new TreeWalkNodeInfo(root, current));
|
||||
|
||||
while (pendingNodes.Count > 0)
|
||||
{
|
||||
var nextNode = pendingNodes.Last();
|
||||
pendingNodes.RemoveAt(pendingNodes.Count - 1);
|
||||
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
|
||||
for (int i = count - 1; i >= 0; i--)
|
||||
{
|
||||
var child = nextNode.Node.GetAt(i, realizeChildren);
|
||||
var childPath = nextNode.Path.CloneWithChildIndex(i);
|
||||
if (child != null)
|
||||
{
|
||||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node));
|
||||
}
|
||||
}
|
||||
|
||||
// Queue the children first and then perform the action. This way
|
||||
// the action can remove the children in the action if necessary
|
||||
nodeAction(nextNode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TraverseRangeRealizeChildren(
|
||||
SelectionNode root,
|
||||
IndexPath start,
|
||||
IndexPath end,
|
||||
Action<TreeWalkNodeInfo> nodeAction)
|
||||
{
|
||||
var pendingNodes = new List<TreeWalkNodeInfo>();
|
||||
var current = start;
|
||||
|
||||
// Build up the stack to account for the depth first walk up to the
|
||||
// start index path.
|
||||
TraverseIndexPath(
|
||||
root,
|
||||
start,
|
||||
true,
|
||||
(node, path, depth, childIndex) =>
|
||||
{
|
||||
var currentPath = StartPath(path, depth);
|
||||
bool isStartPath = IsSubSet(start, currentPath);
|
||||
bool isEndPath = IsSubSet(end, currentPath);
|
||||
|
||||
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
|
||||
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1;
|
||||
|
||||
for (int i = endIndex; i >= startIndex; i--)
|
||||
{
|
||||
var child = node.GetAt(i, realizeChild: true);
|
||||
if (child != null)
|
||||
{
|
||||
var childPath = currentPath.CloneWithChildIndex(i);
|
||||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// From the start index path, do a depth first walk as long as the
|
||||
// current path is less than the end path.
|
||||
while (pendingNodes.Count > 0)
|
||||
{
|
||||
var info = pendingNodes.Last();
|
||||
pendingNodes.RemoveAt(pendingNodes.Count - 1);
|
||||
int depth = info.Path.GetSize();
|
||||
bool isStartPath = IsSubSet(start, info.Path);
|
||||
bool isEndPath = IsSubSet(end, info.Path);
|
||||
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
|
||||
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1;
|
||||
for (int i = endIndex; i >= startIndex; i--)
|
||||
{
|
||||
var child = info.Node.GetAt(i, realizeChild: true);
|
||||
if (child != null)
|
||||
{
|
||||
var childPath = info.Path.CloneWithChildIndex(i);
|
||||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node));
|
||||
}
|
||||
}
|
||||
|
||||
nodeAction(info);
|
||||
|
||||
if (info.Path.CompareTo(end) == 0)
|
||||
{
|
||||
// We reached the end index path. stop iterating.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSubSet(IndexPath path, IndexPath subset)
|
||||
{
|
||||
var subsetSize = subset.GetSize();
|
||||
if (path.GetSize() < subsetSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < subsetSize; i++)
|
||||
{
|
||||
if (path.GetAt(i) != subset.GetAt(i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IndexPath StartPath(IndexPath path, int length)
|
||||
{
|
||||
var subPath = new List<int>();
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
subPath.Add(path.GetAt(i));
|
||||
}
|
||||
|
||||
return new IndexPath(subPath);
|
||||
}
|
||||
|
||||
public struct TreeWalkNodeInfo
|
||||
{
|
||||
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent)
|
||||
{
|
||||
node = node ?? throw new ArgumentNullException(nameof(node));
|
||||
|
||||
Node = node;
|
||||
Path = indexPath;
|
||||
ParentNode = parent;
|
||||
}
|
||||
|
||||
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath)
|
||||
{
|
||||
node = node ?? throw new ArgumentNullException(nameof(node));
|
||||
|
||||
Node = node;
|
||||
Path = indexPath;
|
||||
ParentNode = null;
|
||||
}
|
||||
|
||||
public SelectionNode Node { get; }
|
||||
public IndexPath Path { get; }
|
||||
public SelectionNode? ParentNode { get; }
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -675,7 +675,9 @@ namespace Avalonia.Controls
|
||||
|
||||
if (o != n)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
RaisePropertyChanged(HasSystemDecorationsProperty, o, n);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
-->
|
||||
<Version>0.7.0</Version>
|
||||
<NoWarn>CS1591</NoWarn>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Remote\HtmlTransport\webapp\build\**\*.gz" />
|
||||
<EmbeddedResource Condition="'$(Configuration)' == 'Debug'" Remove="Remote\HtmlTransport\webapp\build\**\*.map.gz"/>
|
||||
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
|
||||
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
|
||||
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
|
||||
|
||||
@@ -35,5 +35,7 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
|
||||
public void Start() => _inner?.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Remote.Protocol;
|
||||
using Avalonia.Remote.Protocol.Designer;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Avalonia.DesignerSupport.Remote
|
||||
{
|
||||
class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod
|
||||
{
|
||||
private string _path;
|
||||
private string _lastContents;
|
||||
private bool _disposed;
|
||||
|
||||
public FileWatcherTransport(Uri file)
|
||||
{
|
||||
_path = file.LocalPath;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
void Dump(object o, string pad)
|
||||
{
|
||||
foreach (var p in o.GetType().GetProperties())
|
||||
{
|
||||
Console.Write($"{pad}{p.Name}: ");
|
||||
var v = p.GetValue(o);
|
||||
if (v == null || v.GetType().IsPrimitive || v is string || v is Guid)
|
||||
Console.WriteLine(v);
|
||||
else
|
||||
{
|
||||
Console.WriteLine();
|
||||
Dump(v, pad + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task Send(object data)
|
||||
{
|
||||
Console.WriteLine(data.GetType().Name);
|
||||
Dump(data, " ");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
|
||||
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
|
||||
{
|
||||
add
|
||||
{
|
||||
_onMessage+=value;
|
||||
}
|
||||
remove { _onMessage -= value; }
|
||||
}
|
||||
|
||||
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
|
||||
public void Start()
|
||||
{
|
||||
UpdaterThread();
|
||||
}
|
||||
|
||||
// I couldn't get FileSystemWatcher working on Linux, so I came up with this abomination
|
||||
async void UpdaterThread()
|
||||
{
|
||||
while (!_disposed)
|
||||
{
|
||||
var data = File.ReadAllText(_path);
|
||||
if (data != _lastContents)
|
||||
{
|
||||
Console.WriteLine("Triggering XAML update");
|
||||
_lastContents = data;
|
||||
_onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data });
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
public string PreviewerMethod => RemoteDesignerEntryPoint.Methods.Html;
|
||||
}
|
||||
|
||||
interface ITransportWithEnforcedMethod
|
||||
{
|
||||
string PreviewerMethod { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Remote.Protocol;
|
||||
using Avalonia.Remote.Protocol.Viewport;
|
||||
|
||||
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
|
||||
{
|
||||
public class HtmlWebSocketTransport : IAvaloniaRemoteTransportConnection
|
||||
{
|
||||
private readonly IAvaloniaRemoteTransportConnection _signalTransport;
|
||||
private readonly SimpleWebSocketHttpServer _simpleServer;
|
||||
private readonly Dictionary<string, byte[]> _resources;
|
||||
private SimpleWebSocket _pendingSocket;
|
||||
private bool _disposed;
|
||||
private object _lock = new object();
|
||||
private AutoResetEvent _wakeup = new AutoResetEvent(false);
|
||||
private FrameMessage _lastFrameMessage = null;
|
||||
private FrameMessage _lastSentFrameMessage = null;
|
||||
private RequestViewportResizeMessage _lastViewportRequest;
|
||||
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
|
||||
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException;
|
||||
|
||||
private static readonly Dictionary<string, string> Mime = new Dictionary<string, string>
|
||||
{
|
||||
["html"] = "text/html", ["htm"] = "text/html", ["js"] = "text/javascript", ["css"] = "text/css"
|
||||
};
|
||||
|
||||
private static readonly byte[] NotFound = Encoding.UTF8.GetBytes("404 - Not Found");
|
||||
|
||||
|
||||
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
|
||||
{
|
||||
if (listenUri.Scheme != "http")
|
||||
throw new ArgumentException("listenUri");
|
||||
|
||||
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
|
||||
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
|
||||
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary(
|
||||
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
|
||||
r =>
|
||||
{
|
||||
|
||||
using (var s =
|
||||
new GZipStream(typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceStream(r),
|
||||
CompressionMode.Decompress))
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
s.CopyTo(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
});
|
||||
|
||||
_signalTransport = signalTransport;
|
||||
var address = IPAddress.Parse(listenUri.Host);
|
||||
|
||||
_simpleServer = new SimpleWebSocketHttpServer(address, listenUri.Port);
|
||||
_simpleServer.Listen();
|
||||
Task.Run(AcceptWorker);
|
||||
Task.Run(SocketWorker);
|
||||
_signalTransport.Send(new HtmlTransportStartedMessage { Uri = "http://" + address + ":" + listenUri.Port + "/" });
|
||||
}
|
||||
|
||||
async void AcceptWorker()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
|
||||
using (var req = await _simpleServer.AcceptAsync())
|
||||
{
|
||||
|
||||
if (!req.IsWebsocketRequest)
|
||||
{
|
||||
|
||||
var key = req.Path == "/" ? "index.html" : req.Path.TrimStart('/').Replace('/', '.');
|
||||
if (_resources.TryGetValue(key, out var data))
|
||||
{
|
||||
var ext = Path.GetExtension(key).Substring(1);
|
||||
string mime = null;
|
||||
if (ext == null || !Mime.TryGetValue(ext, out mime))
|
||||
mime = "application/octet-stream";
|
||||
await req.RespondAsync(200, data, mime);
|
||||
}
|
||||
else
|
||||
{
|
||||
await req.RespondAsync(404, NotFound, "text/plain");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var socket = await req.AcceptWebSocket();
|
||||
SocketReceiveWorker(socket);
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingSocket?.Dispose();
|
||||
_pendingSocket = socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async void SocketReceiveWorker(SimpleWebSocket socket)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var msg = await socket.ReceiveMessage().ConfigureAwait(false);
|
||||
if(msg == null)
|
||||
return;
|
||||
if (msg.IsText)
|
||||
{
|
||||
var s = Encoding.UTF8.GetString(msg.Data);
|
||||
var parts = s.Split(':');
|
||||
if (parts[0] == "frame-received")
|
||||
_onMessage?.Invoke(this, new FrameReceivedMessage { SequenceId = long.Parse(parts[1]) });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.Error.WriteLine(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
async void SocketWorker()
|
||||
{
|
||||
try
|
||||
{
|
||||
SimpleWebSocket socket = null;
|
||||
while (true)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
socket?.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
FrameMessage sendNow = null;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_pendingSocket != null)
|
||||
{
|
||||
socket?.Dispose();
|
||||
socket = _pendingSocket;
|
||||
_pendingSocket = null;
|
||||
_lastSentFrameMessage = null;
|
||||
}
|
||||
|
||||
if (_lastFrameMessage != _lastSentFrameMessage)
|
||||
_lastSentFrameMessage = sendNow = _lastFrameMessage;
|
||||
}
|
||||
|
||||
if (sendNow != null && socket != null)
|
||||
{
|
||||
await socket.SendMessage(
|
||||
$"frame:{sendNow.SequenceId}:{sendNow.Width}:{sendNow.Height}:{sendNow.Stride}:{sendNow.DpiX}:{sendNow.DpiY}");
|
||||
await socket.SendMessage(false, sendNow.Data);
|
||||
}
|
||||
|
||||
_wakeup.WaitOne(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.Error.WriteLine(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pendingSocket?.Dispose();
|
||||
_simpleServer.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public Task Send(object data)
|
||||
{
|
||||
if (data is FrameMessage frame)
|
||||
{
|
||||
_lastFrameMessage = frame;
|
||||
_wakeup.Set();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
if (data is RequestViewportResizeMessage req)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return _signalTransport.Send(data);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_onMessage?.Invoke(this, new Avalonia.Remote.Protocol.Viewport.ClientSupportedPixelFormatsMessage
|
||||
{
|
||||
Formats = new []{PixelFormat.Rgba8888}
|
||||
});
|
||||
_signalTransport.Start();
|
||||
}
|
||||
|
||||
#region Forward
|
||||
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
|
||||
{
|
||||
add
|
||||
{
|
||||
bool subscribeToInner;
|
||||
lock (_lock)
|
||||
{
|
||||
subscribeToInner = _onMessage == null;
|
||||
_onMessage += value;
|
||||
}
|
||||
|
||||
if (subscribeToInner)
|
||||
_signalTransport.OnMessage += OnSignalTransportMessage;
|
||||
}
|
||||
remove
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_onMessage -= value;
|
||||
if (_onMessage == null)
|
||||
_signalTransport.OnMessage -= OnSignalTransportMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSignalTransportMessage(IAvaloniaRemoteTransportConnection signal, object message) => _onMessage?.Invoke(this, message);
|
||||
|
||||
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException
|
||||
{
|
||||
add
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var subscribeToInner = _onException == null;
|
||||
_onException += value;
|
||||
if (subscribeToInner)
|
||||
_signalTransport.OnException += OnSignalTransportException;
|
||||
}
|
||||
}
|
||||
remove
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_onException -= value;
|
||||
if(_onException==null)
|
||||
_signalTransport.OnException -= OnSignalTransportException;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSignalTransportException(IAvaloniaRemoteTransportConnection arg1, Exception ex)
|
||||
{
|
||||
_onException?.Invoke(this, ex);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
|
||||
{
|
||||
public class SimpleWebSocketHttpServer : IDisposable
|
||||
{
|
||||
private readonly IPAddress _address;
|
||||
private readonly int _port;
|
||||
private TcpListener _listener;
|
||||
|
||||
public async Task<SimpleWebSocketHttpRequest> AcceptAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var res = await AcceptAsyncImpl();
|
||||
if (res != null)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
async Task<SimpleWebSocketHttpRequest> AcceptAsyncImpl()
|
||||
{
|
||||
if (_listener == null)
|
||||
throw new InvalidOperationException("Currently not listening");
|
||||
var socket = await _listener.AcceptSocketAsync();
|
||||
var stream = new NetworkStream(socket);
|
||||
bool error = true;
|
||||
async Task<string> ReadLineAsync()
|
||||
{
|
||||
var readBuffer = new byte[1];
|
||||
var lineBuffer = new byte[1024];
|
||||
for (var c = 0; c < 1024; c++)
|
||||
{
|
||||
if (await stream.ReadAsync(readBuffer, 0, 1) == 0)
|
||||
throw new EndOfStreamException();
|
||||
if (readBuffer[0] == 10)
|
||||
{
|
||||
if (c == 0)
|
||||
return "";
|
||||
if (lineBuffer[c - 1] == 13)
|
||||
c--;
|
||||
if (c == 0)
|
||||
return "";
|
||||
|
||||
return Encoding.UTF8.GetString(lineBuffer, 0, c);
|
||||
}
|
||||
lineBuffer[c] = readBuffer[0];
|
||||
}
|
||||
|
||||
throw new InvalidDataException("Header is too large");
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
string line = null;
|
||||
try
|
||||
{
|
||||
|
||||
line = await ReadLineAsync();
|
||||
var sp = line.Split(' ');
|
||||
if (sp.Length != 3 || !sp[2].StartsWith("HTTP") || sp[0] != "GET")
|
||||
return null;
|
||||
var path = sp[1];
|
||||
|
||||
while (true)
|
||||
{
|
||||
line = await ReadLineAsync();
|
||||
if (line == "")
|
||||
break;
|
||||
sp = line.Split(new[] {':'}, 2);
|
||||
headers[sp[0]] = sp[1].TrimStart();
|
||||
}
|
||||
|
||||
error = false;
|
||||
|
||||
return new SimpleWebSocketHttpRequest(stream, path, headers);
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = true;
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (error)
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void Listen()
|
||||
{
|
||||
var listener = new TcpListener(_address, _port);
|
||||
listener.Start();
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public SimpleWebSocketHttpServer(IPAddress address, int port)
|
||||
{
|
||||
_address = address;
|
||||
_port = port;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener?.Stop();
|
||||
_listener = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SimpleWebSocketHttpRequest : IDisposable
|
||||
{
|
||||
public Dictionary<string, string> Headers { get; }
|
||||
public string Path { get; }
|
||||
private NetworkStream _stream;
|
||||
public bool IsWebsocketRequest { get; }
|
||||
public IReadOnlyList<string> WebSocketProtocols { get; }
|
||||
private string _websocketKey;
|
||||
|
||||
public SimpleWebSocketHttpRequest(NetworkStream stream, string path, Dictionary<string, string> headers)
|
||||
{
|
||||
Path = path;
|
||||
Headers = headers;
|
||||
|
||||
_stream = stream;
|
||||
if (headers.TryGetValue("Connection", out var h)
|
||||
&& h.Contains("Upgrade")
|
||||
&& headers.TryGetValue("Upgrade", out h) &&
|
||||
h == "websocket"
|
||||
&& headers.TryGetValue("Sec-WebSocket-Key", out _websocketKey))
|
||||
{
|
||||
IsWebsocketRequest = true;
|
||||
if (headers.TryGetValue("Sec-WebSocket-Protocol", out h))
|
||||
WebSocketProtocols = h.Split(',').Select(x => x.Trim()).ToArray();
|
||||
else WebSocketProtocols = new string[0];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RespondAsync(int code, byte[] data, string contentType)
|
||||
{
|
||||
var headers = Encoding.UTF8.GetBytes($"HTTP/1.1 {code} {(HttpStatusCode)code}\r\nConnection: close\r\nContent-Type: {contentType}\r\nContent-Length: {data.Length}\r\n\r\n");
|
||||
await _stream.WriteAsync(headers, 0, headers.Length);
|
||||
await _stream.WriteAsync(data, 0, data.Length);
|
||||
_stream.Dispose();
|
||||
_stream = null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task<SimpleWebSocket> AcceptWebSocket(string protocol = null)
|
||||
{
|
||||
|
||||
var handshakeSource = _websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
string handshake;
|
||||
using (var sha1 = SHA1.Create())
|
||||
handshake = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(handshakeSource)));
|
||||
var headers =
|
||||
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "
|
||||
+ handshake + "\r\n";
|
||||
if (protocol != null)
|
||||
headers += protocol + "\r\n";
|
||||
headers += "\r\n";
|
||||
var bheaders = Encoding.UTF8.GetBytes(headers);
|
||||
await _stream.WriteAsync(bheaders, 0, bheaders.Length);
|
||||
|
||||
var s = _stream;
|
||||
_stream = null;
|
||||
return new SimpleWebSocket(s);
|
||||
}
|
||||
|
||||
public void Dispose() => _stream?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class SimpleWebSocket : IDisposable
|
||||
{
|
||||
class AsyncLock
|
||||
{
|
||||
private object _syncRoot = new object();
|
||||
private Queue<TaskCompletionSource<IDisposable>> _queue = new Queue<TaskCompletionSource<IDisposable>>();
|
||||
private bool _locked;
|
||||
public Task<IDisposable> LockAsync()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (!_locked)
|
||||
{
|
||||
_locked = true;
|
||||
return Task.FromResult<IDisposable>(new Lock(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
var tcs = new TaskCompletionSource<IDisposable>();
|
||||
_queue.Enqueue(tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Unlock()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_queue.Count != 0)
|
||||
_queue.Dequeue().SetResult(new Lock(this));
|
||||
else
|
||||
_locked = false;
|
||||
}
|
||||
}
|
||||
|
||||
class Lock : IDisposable
|
||||
{
|
||||
private AsyncLock _parent;
|
||||
private object _syncRoot = new object();
|
||||
|
||||
public Lock(AsyncLock parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_parent == null)
|
||||
return;
|
||||
var p = _parent;
|
||||
_parent = null;
|
||||
p.Unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream _stream;
|
||||
private AsyncLock _sendLock = new AsyncLock();
|
||||
private AsyncLock _recvLock = new AsyncLock();
|
||||
private const int WebsocketInitialHeaderLength = 2;
|
||||
private const int WebsocketLen16Length = 4;
|
||||
private const int WebsocketLen64Length = 10;
|
||||
|
||||
private const int WebsocketLen16Code = 126;
|
||||
private const int WebsocketLen64Code = 127;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
struct WebSocketHeader
|
||||
{
|
||||
[FieldOffset(0)] public byte Mask;
|
||||
[FieldOffset(1)] public byte Length8;
|
||||
[FieldOffset(2)] public ushort Length16;
|
||||
[FieldOffset(2)] public ulong Length64;
|
||||
}
|
||||
|
||||
readonly byte[] _sendHeaderBuffer = new byte[10];
|
||||
readonly MemoryStream _receiveFrameStream = new MemoryStream();
|
||||
readonly MemoryStream _receiveMessageStream = new MemoryStream();
|
||||
private FrameType _currentMessageFrameType;
|
||||
|
||||
enum FrameType
|
||||
{
|
||||
Continue = 0x0,
|
||||
Text = 0x1,
|
||||
Binary = 0x2,
|
||||
Close = 0x8,
|
||||
Ping = 0x9,
|
||||
Pong = 0xA
|
||||
}
|
||||
|
||||
internal SimpleWebSocket(Stream stream)
|
||||
{
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
}
|
||||
|
||||
public Task SendMessage(string text)
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(text);
|
||||
return SendMessage(true, data);
|
||||
}
|
||||
public Task SendMessage(bool isText, byte[] data) => SendMessage(isText, data, 0, data.Length);
|
||||
|
||||
|
||||
public Task SendMessage(bool isText, byte[] data, int offset, int length)
|
||||
=> SendFrame(isText ? FrameType.Text : FrameType.Binary, data, offset, length);
|
||||
|
||||
async Task SendFrame(FrameType type, byte[] data, int offset, int length)
|
||||
{
|
||||
using (var l = await _sendLock.LockAsync())
|
||||
{
|
||||
var header = new WebSocketHeader();
|
||||
|
||||
int headerLength;
|
||||
if (data.Length <= 125)
|
||||
{
|
||||
headerLength = WebsocketInitialHeaderLength;
|
||||
header.Length8 = (byte) length;
|
||||
}
|
||||
else if (length <= 0xffff)
|
||||
{
|
||||
headerLength = WebsocketLen16Length;
|
||||
header.Length8 = WebsocketLen16Code;
|
||||
header.Length16 = (ushort) IPAddress.HostToNetworkOrder((short) (ushort) length);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
headerLength = WebsocketLen64Length;
|
||||
header.Length8 = WebsocketLen64Code;
|
||||
header.Length64 = (ulong) IPAddress.HostToNetworkOrder((long) length);
|
||||
}
|
||||
|
||||
var endOfMessage = true;
|
||||
header.Mask = (byte) (((endOfMessage ? 1u : 0u) << 7) | ((byte) (type) & 0xf));
|
||||
unsafe
|
||||
{
|
||||
Marshal.Copy(new IntPtr(&header), _sendHeaderBuffer, 0, headerLength);
|
||||
}
|
||||
|
||||
await _stream.WriteAsync(_sendHeaderBuffer, 0, headerLength);
|
||||
await _stream.WriteAsync(data, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
struct Frame
|
||||
{
|
||||
public byte[] Data;
|
||||
public bool EndOfMessage;
|
||||
public FrameType FrameType;
|
||||
}
|
||||
|
||||
byte[] _recvHeaderBuffer = new byte[8];
|
||||
byte[] _maskBuffer = new byte[4];
|
||||
async Task<Frame> ReadFrame()
|
||||
{
|
||||
_receiveFrameStream.Position = 0;
|
||||
_receiveFrameStream.SetLength(0);
|
||||
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
|
||||
var masked = (_recvHeaderBuffer[1] & 0x80) != 0;
|
||||
var len0 = (_recvHeaderBuffer[1] & 0x7F);
|
||||
var endOfMessage = (_recvHeaderBuffer[0] & 0x80) != 0;
|
||||
var frameType = (FrameType) (_recvHeaderBuffer[0] & 0xf);
|
||||
int length;
|
||||
if (len0 <= 125)
|
||||
length = len0;
|
||||
else if (len0 == WebsocketLen16Code)
|
||||
{
|
||||
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
|
||||
length = (ushort) IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_recvHeaderBuffer, 0));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await ReadExact(_stream, _recvHeaderBuffer, 0, 8);
|
||||
length = (int) (ulong) IPAddress.NetworkToHostOrder((long) BitConverter.ToUInt64(_recvHeaderBuffer, 0));
|
||||
}
|
||||
|
||||
if (masked)
|
||||
await ReadExact(_stream, _maskBuffer, 0, 4);
|
||||
await ReadExact(_stream, _receiveFrameStream, length);
|
||||
var data = _receiveFrameStream.ToArray();
|
||||
if(masked)
|
||||
for (var c = 0; c < data.Length; c++)
|
||||
data[c] = (byte) (data[c] ^ _maskBuffer[c % 4]);
|
||||
|
||||
return new Frame
|
||||
{
|
||||
Data = data,
|
||||
EndOfMessage = endOfMessage,
|
||||
FrameType = frameType
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public async Task<SimpleWebSocketMessage> ReceiveMessage()
|
||||
{
|
||||
using (await _recvLock.LockAsync())
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var frame = await ReadFrame();
|
||||
|
||||
if (frame.FrameType == FrameType.Close)
|
||||
return null;
|
||||
if (frame.FrameType == FrameType.Ping)
|
||||
await SendFrame(FrameType.Pong, frame.Data, 0, frame.Data.Length);
|
||||
if (frame.FrameType == FrameType.Text || frame.FrameType == FrameType.Binary)
|
||||
{
|
||||
var isText = frame.FrameType == FrameType.Text;
|
||||
if (_receiveMessageStream.Length == 0 && frame.EndOfMessage)
|
||||
return new SimpleWebSocketMessage
|
||||
{
|
||||
IsText = isText,
|
||||
Data = frame.Data
|
||||
};
|
||||
|
||||
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
|
||||
_currentMessageFrameType = frame.FrameType;
|
||||
}
|
||||
if (frame.FrameType == FrameType.Continue)
|
||||
{
|
||||
frame.FrameType = _currentMessageFrameType;
|
||||
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
|
||||
if (frame.EndOfMessage)
|
||||
{
|
||||
var isText = frame.FrameType == FrameType.Text;
|
||||
var data = _receiveMessageStream.ToArray();
|
||||
_receiveMessageStream.Position = 0;
|
||||
_receiveMessageStream.SetLength(0);
|
||||
return new SimpleWebSocketMessage
|
||||
{
|
||||
IsText = isText,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
byte[] _readExactBuffer = new byte[4096];
|
||||
async Task ReadExact(Stream from, MemoryStream to, int length)
|
||||
{
|
||||
while (length>0)
|
||||
{
|
||||
var toRead = Math.Min(length, _readExactBuffer.Length);
|
||||
var read = await from.ReadAsync(_readExactBuffer, 0, toRead);
|
||||
to.Write(_readExactBuffer, 0, read);
|
||||
if (read <= 0)
|
||||
throw new EndOfStreamException();
|
||||
length -= read;
|
||||
}
|
||||
}
|
||||
|
||||
async Task ReadExact(Stream from, byte[] to, int offset, int length)
|
||||
{
|
||||
while (length > 0)
|
||||
{
|
||||
var read = await from.ReadAsync(to, offset, length);
|
||||
if (read <= 0)
|
||||
throw new EndOfStreamException();
|
||||
length -= read;
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SimpleWebSocketMessage
|
||||
{
|
||||
public bool IsText { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
|
||||
public string AsString()
|
||||
{
|
||||
return Encoding.UTF8.GetString(Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
build
|
||||
node_modules
|
||||
+8878
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "simple",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"webpack-ver": "cross-env NODE_ENV=production webpack --version",
|
||||
"dist": "cross-env NODE_ENV=production webpack --display-error-details",
|
||||
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"awesome-typescript-loader": "^5.0.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"cross-env": "^5.1.6",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"style-loader": "^0.21.0",
|
||||
"to-string-loader": "^1.1.5",
|
||||
"tsconfig-paths-webpack-plugin": "^3.2.0",
|
||||
"typescript": "^2.9.2",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "~4.16.3",
|
||||
"webpack-cli": "~2.1.3",
|
||||
"webpack-livereload-plugin": "~2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react": "^16.3.14",
|
||||
"@types/react-dom": "^16.0.5",
|
||||
"mobx": "4.3.0",
|
||||
"mobx-react": "^5.1.2",
|
||||
"react": "^16.3.2",
|
||||
"react-dom": "^16.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {PreviewerFrame, PreviewerServerConnection} from "src/PreviewerServerConnection";
|
||||
import * as React from "react";
|
||||
|
||||
interface PreviewerPresenterProps {
|
||||
conn: PreviewerServerConnection;
|
||||
}
|
||||
|
||||
export class PreviewerPresenter extends React.Component<PreviewerPresenterProps> {
|
||||
private canvasRef: React.RefObject<HTMLCanvasElement>;
|
||||
|
||||
constructor(props: PreviewerPresenterProps) {
|
||||
super(props);
|
||||
this.state = {width: 1, height: 1};
|
||||
this.canvasRef = React.createRef()
|
||||
this.componentDidUpdate({
|
||||
conn: null!
|
||||
}, this.state);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.updateCanvas(this.canvasRef.current, this.props.conn.currentFrame);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<PreviewerPresenterProps>, prevState: Readonly<{}>, snapshot?: any): void {
|
||||
if(prevProps.conn != this.props.conn)
|
||||
{
|
||||
if(prevProps.conn)
|
||||
prevProps.conn.removeFrameListener(this.frameHandler);
|
||||
if(this.props.conn)
|
||||
this.props.conn.addFrameListener(this.frameHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private frameHandler = (frame: PreviewerFrame)=>{
|
||||
this.updateCanvas(this.canvasRef.current, frame);
|
||||
};
|
||||
|
||||
|
||||
updateCanvas(canvas: HTMLCanvasElement | null, frame: PreviewerFrame | null) {
|
||||
if (!canvas)
|
||||
return;
|
||||
if (frame == null){
|
||||
canvas.width = canvas.height = 1;
|
||||
canvas.getContext('2d')!.clearRect(0,0,1,1);
|
||||
}
|
||||
else {
|
||||
canvas.width = frame.data.width;
|
||||
canvas.height = frame.data.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.putImageData(frame.data, 0,0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <canvas ref={this.canvasRef}/>
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
export interface PreviewerFrame {
|
||||
data: ImageData;
|
||||
dpiX: number;
|
||||
dpiY: number;
|
||||
}
|
||||
|
||||
export class PreviewerServerConnection {
|
||||
private nextFrame = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
stride: 0,
|
||||
dpiX: 0,
|
||||
dpiY: 0,
|
||||
sequenceId: "0"
|
||||
};
|
||||
|
||||
public currentFrame: PreviewerFrame | null;
|
||||
private handlers = new Set<(frame: PreviewerFrame | null) => void>();
|
||||
private conn: WebSocket;
|
||||
|
||||
public addFrameListener(listener: (frame: PreviewerFrame | null) => void) {
|
||||
this.handlers.add(listener);
|
||||
if (this.currentFrame)
|
||||
listener(this.currentFrame);
|
||||
}
|
||||
|
||||
public removeFrameListener(listener: (frame: PreviewerFrame | null) => void) {
|
||||
this.handlers.delete(listener);
|
||||
}
|
||||
|
||||
constructor(uri: string) {
|
||||
this.currentFrame = null;
|
||||
var conn = this.conn = new WebSocket(uri);
|
||||
conn.binaryType = 'arraybuffer';
|
||||
|
||||
const onMessage = this.onMessage;
|
||||
conn.onmessage = msg => onMessage(msg);
|
||||
|
||||
const onClose = () => this.setFrame(null);
|
||||
conn.onclose = () => onClose();
|
||||
conn.onerror = (err: Event) => {
|
||||
onClose();
|
||||
console.log("Connection error: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
private onMessage = (msg: MessageEvent) => {
|
||||
if (typeof msg.data == 'string' || msg.data instanceof String) {
|
||||
const parts = msg.data.split(':');
|
||||
if (parts[0] == 'frame') {
|
||||
this.nextFrame = {
|
||||
sequenceId: parts[1],
|
||||
width: parseInt(parts[2]),
|
||||
height: parseInt(parts[3]),
|
||||
stride: parseInt(parts[4]),
|
||||
dpiX: parseInt(parts[5]),
|
||||
dpiY: parseInt(parts[6])
|
||||
}
|
||||
}
|
||||
} else if (msg.data instanceof ArrayBuffer) {
|
||||
const arr = new Uint8ClampedArray(msg.data, 0);
|
||||
const imageData = new ImageData(arr, this.nextFrame.width, this.nextFrame.height);
|
||||
this.conn.send('frame-received:' + this.nextFrame.sequenceId);
|
||||
this.setFrame({
|
||||
data: imageData,
|
||||
dpiX: this.nextFrame.dpiX,
|
||||
dpiY: this.nextFrame.dpiY
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private setFrame(frame: PreviewerFrame | null) {
|
||||
this.currentFrame = frame;
|
||||
this.handlers.forEach(h => h(this.currentFrame));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Avalonia XAML previewer web edition</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<center>Loading...</center>
|
||||
</div>
|
||||
<noscript>Javascript is required</noscript>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from "react";
|
||||
import {PreviewerPresenter} from './FramePresenter'
|
||||
import {PreviewerServerConnection} from "src/PreviewerServerConnection";
|
||||
import * as ReactDOM from "react-dom";
|
||||
|
||||
const loc = window.location;
|
||||
const conn = new PreviewerServerConnection((loc.protocol === "https:" ? "wss" : "ws") + "://" + loc.host + "/ws");
|
||||
|
||||
const App = function(){
|
||||
return <div style={{width: '100%'}}>
|
||||
<PreviewerPresenter conn={conn}/>
|
||||
</div>
|
||||
};
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById("app"));
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build/dist",
|
||||
"module": "esnext",
|
||||
"target": "es5",
|
||||
"lib": ["es6", "dom"],
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": false,
|
||||
"baseUrl": ".",
|
||||
"experimentalDecorators": true,
|
||||
"paths": {
|
||||
"*": ["./node_modules/@types/*", "./node_modules/*"],
|
||||
"src/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"scripts",
|
||||
"acceptance-tests",
|
||||
"webpack",
|
||||
"jest",
|
||||
"src/setupTests.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const LiveReloadPlugin = require('webpack-livereload-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const prod = process.env.NODE_ENV == 'production';
|
||||
|
||||
class Printer {
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString()));
|
||||
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
bundle: './src/index.tsx'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
publicPath: '/',
|
||||
filename: '[name].[chunkhash].js'
|
||||
},
|
||||
performance: { hints: false },
|
||||
mode: prod ? "production" : "development",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: "pre",
|
||||
test: /\.js$/,
|
||||
loader: "source-map-loader",
|
||||
exclude: [
|
||||
path.resolve(__dirname, 'node_modules/mobx-state-router')
|
||||
]
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: 'awesome-typescript-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(jpg|png)$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 25000,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'fonts/', // where the fonts will go
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: 'assets/[name].[hash:8].[ext]',
|
||||
},
|
||||
}]
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
devtool: "source-map",
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, 'node_modules')],
|
||||
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })],
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
alias: {
|
||||
'src': path.resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
plugins:
|
||||
[
|
||||
new Printer(),
|
||||
new CleanWebpackPlugin([path.resolve(__dirname, 'build')]),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].[chunkhash]h" +
|
||||
".css",
|
||||
chunkFilename: "[id].[chunkhash].css"
|
||||
}),
|
||||
new LiveReloadPlugin({appendScriptTag: !prod}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, './src/index.html'),
|
||||
filename: 'index.html' //relative to root of the application
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
// relative path from src
|
||||
//{ from: './src/favicon.ico' },
|
||||
//{ from: './src/assets' }
|
||||
]),
|
||||
new CompressionPlugin({
|
||||
test: /(\?.*)?$/i
|
||||
})
|
||||
]
|
||||
};
|
||||
module.exports = config;
|
||||
@@ -5,6 +5,7 @@ using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.DesignerSupport.Remote.HtmlTransport;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Remote.Protocol;
|
||||
using Avalonia.Remote.Protocol.Designer;
|
||||
@@ -24,15 +25,16 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
{
|
||||
public string AppPath { get; set; }
|
||||
public Uri Transport { get; set; }
|
||||
public Uri HtmlMethodListenUri { get; set; }
|
||||
public string Method { get; set; } = Methods.AvaloniaRemote;
|
||||
public string SessionId { get; set; } = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
static class Methods
|
||||
internal static class Methods
|
||||
{
|
||||
public const string AvaloniaRemote = "avalonia-remote";
|
||||
public const string Win32 = "win32";
|
||||
|
||||
public const string Html = "html";
|
||||
}
|
||||
|
||||
static Exception Die(string error)
|
||||
@@ -52,6 +54,19 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
{
|
||||
Console.Error.WriteLine("Usage: --transport transport_spec --session-id sid --method method app");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("--transport: transport used for communication with the IDE");
|
||||
Console.Error.WriteLine(" 'tcp-bson' (e. g. 'tcp-bson://127.0.0.1:30243/') - TCP-based transport with BSON serialization of messages defined in Avalonia.Remote.Protocol");
|
||||
Console.Error.WriteLine(" 'file' (e. g. 'file://C://my/file.xaml' - pseudo-transport that triggers XAML updates on file changes, useful as a standalone previewer tool, always uses http preview method");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("--session-id: session id to be sent to IDE process");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("--method: the way the XAML is displayed");
|
||||
Console.Error.WriteLine(" 'avalonia-remote' - binary image is sent via transport connection in FrameMessage");
|
||||
Console.Error.WriteLine(" 'win32' - XAML is displayed in win32 window (handle could be obtained from UpdateXamlResultMessage), IDE is responsible to use user32!SetParent");
|
||||
Console.Error.WriteLine(" 'html' - Previewer starts an HTML server and displays XAML previewer as a web page");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("--html-url - endpoint for HTML method to listen on, e. g. http://127.0.0.1:8081");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Example: --transport tcp-bson://127.0.0.1:30243/ --session-id 123 --method avalonia-remote MyApp.exe");
|
||||
Console.Error.Flush();
|
||||
return Die(null);
|
||||
@@ -74,6 +89,8 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
next = a => rv.Transport = new Uri(a, UriKind.Absolute);
|
||||
else if (arg == "--method")
|
||||
next = a => rv.Method = a;
|
||||
else if (arg == "--html-url")
|
||||
next = a => rv.HtmlMethodListenUri = new Uri(a, UriKind.Absolute);
|
||||
else if (arg == "--session-id")
|
||||
next = a => rv.SessionId = a;
|
||||
else if (rv.AppPath == null)
|
||||
@@ -89,6 +106,9 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
{
|
||||
PrintUsage();
|
||||
}
|
||||
|
||||
if (next != null)
|
||||
PrintUsage();
|
||||
return rv;
|
||||
}
|
||||
|
||||
@@ -98,27 +118,40 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
{
|
||||
return new BsonTcpTransport().Connect(IPAddress.Parse(transport.Host), transport.Port).Result;
|
||||
}
|
||||
|
||||
if (transport.Scheme == "file")
|
||||
{
|
||||
return new FileWatcherTransport(transport);
|
||||
}
|
||||
PrintUsage();
|
||||
return null;
|
||||
}
|
||||
|
||||
interface IAppInitializer
|
||||
{
|
||||
Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
|
||||
IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
|
||||
}
|
||||
|
||||
|
||||
class AppInitializer<T> : IAppInitializer where T : AppBuilderBase<T>, new()
|
||||
{
|
||||
public Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport,
|
||||
public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport,
|
||||
CommandLineArgs args, object obj)
|
||||
{
|
||||
var builder = (AppBuilderBase<T>) obj;
|
||||
var builder = (AppBuilderBase<T>)obj;
|
||||
if (args.Method == Methods.AvaloniaRemote)
|
||||
builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport));
|
||||
if (args.Method == Methods.Html)
|
||||
{
|
||||
transport = new HtmlWebSocketTransport(transport,
|
||||
args.HtmlMethodListenUri ?? new Uri("http://localhost:5000"));
|
||||
builder.UseWindowingSubsystem(() =>
|
||||
PreviewerWindowingPlatform.Initialize(transport));
|
||||
}
|
||||
|
||||
if (args.Method == Methods.Win32)
|
||||
builder.UseWindowingSubsystem("Avalonia.Win32");
|
||||
builder.SetupWithoutStarting();
|
||||
return builder.Instance;
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +161,8 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
{
|
||||
var args = ParseCommandLineArgs(cmdline);
|
||||
var transport = CreateTransport(args.Transport);
|
||||
if (transport is ITransportWithEnforcedMethod enforcedMethod)
|
||||
args.Method = enforcedMethod.PreviewerMethod;
|
||||
var asm = Assembly.LoadFile(System.IO.Path.GetFullPath(args.AppPath));
|
||||
var entryPoint = asm.EntryPoint;
|
||||
if (entryPoint == null)
|
||||
@@ -141,12 +176,14 @@ namespace Avalonia.DesignerSupport.Remote
|
||||
var appBuilder = builderMethod.Invoke(null, null);
|
||||
Log($"Initializing application in design mode");
|
||||
var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer<>).MakeGenericType(appBuilder.GetType()));
|
||||
var app = initializer.GetConfiguredApp(transport, args, appBuilder);
|
||||
transport = initializer.ConfigureApp(transport, args, appBuilder);
|
||||
s_transport = transport;
|
||||
transport.OnMessage += OnTransportMessage;
|
||||
transport.OnException += (t, e) => Die(e.ToString());
|
||||
transport.Start();
|
||||
Log("Sending StartDesignerSessionMessage");
|
||||
transport.Send(new StartDesignerSessionMessage {SessionId = args.SessionId});
|
||||
|
||||
Dispatcher.UIThread.MainLoop(CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace Avalonia.Dialogs
|
||||
|
||||
if (indexOfPreselected > 1)
|
||||
{
|
||||
_filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]);
|
||||
_filesView.ScrollIntoView(indexOfPreselected - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ namespace Avalonia.Dialogs
|
||||
: dialog is OpenFolderDialog ? "Select directory"
|
||||
: throw new ArgumentException(nameof(dialog)));
|
||||
|
||||
var directory = dialog.InitialDirectory;
|
||||
var directory = dialog.Directory;
|
||||
|
||||
if (directory == null || !Directory.Exists(directory))
|
||||
{
|
||||
|
||||
@@ -52,8 +52,10 @@ namespace Avalonia.Input
|
||||
Data = data;
|
||||
_target = target;
|
||||
_targetLocation = targetLocation;
|
||||
Modifiers = (InputModifiers)keyModifiers;
|
||||
KeyModifiers = keyModifiers;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Modifiers = (InputModifiers)keyModifiers;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -186,8 +186,9 @@ namespace Avalonia.Input
|
||||
private static void OnPreviewPointerPressed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var ev = (PointerPressedEventArgs)e;
|
||||
var visual = (IVisual)sender;
|
||||
|
||||
if (sender == e.Source && ev.MouseButton == MouseButton.Left)
|
||||
if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Avalonia.Input
|
||||
{
|
||||
@@ -71,12 +72,13 @@ namespace Avalonia.Input
|
||||
if (ev.Route == RoutingStrategies.Bubble)
|
||||
{
|
||||
var e = (PointerPressedEventArgs)ev;
|
||||
var visual = (IVisual)ev.Source;
|
||||
|
||||
if (e.ClickCount <= 1)
|
||||
{
|
||||
s_lastPress = new WeakReference<IInteractive>(e.Source);
|
||||
}
|
||||
else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton == MouseButton.Left)
|
||||
else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
|
||||
{
|
||||
|
||||
@@ -128,10 +128,10 @@ namespace Avalonia.Input
|
||||
_obsoleteClickCount = obsoleteClickCount;
|
||||
}
|
||||
|
||||
[Obsolete("Use DoubleTapped or DoubleRightTapped event instead")]
|
||||
[Obsolete("Use DoubleTapped event or Gestures.DoubleRightTapped attached event")]
|
||||
public int ClickCount => _obsoleteClickCount;
|
||||
|
||||
[Obsolete("Use PointerUpdateKind")]
|
||||
[Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")]
|
||||
public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton();
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ namespace Avalonia.Input
|
||||
/// </summary>
|
||||
public MouseButton InitialPressMouseButton { get; }
|
||||
|
||||
[Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see https://github.com/AvaloniaUI/Avalonia/wiki/Pointer-events-in-0.9 for more details", true)]
|
||||
[Obsolete("Use InitialPressMouseButton")]
|
||||
public MouseButton MouseButton => InitialPressMouseButton;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ namespace Avalonia.Input.Raw
|
||||
Location = location;
|
||||
Data = data;
|
||||
Effects = effects;
|
||||
Modifiers = (InputModifiers)modifiers;
|
||||
KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Modifiers = (InputModifiers)modifiers;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Avalonia.Native
|
||||
_native.OpenFileDialog(nativeParent,
|
||||
events, ofd.AllowMultiple,
|
||||
ofd.Title ?? "",
|
||||
ofd.InitialDirectory ?? "",
|
||||
ofd.Directory ?? "",
|
||||
ofd.InitialFileName ?? "",
|
||||
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions)));
|
||||
}
|
||||
@@ -37,7 +37,7 @@ namespace Avalonia.Native
|
||||
_native.SaveFileDialog(nativeParent,
|
||||
events,
|
||||
dialog.Title ?? "",
|
||||
dialog.InitialDirectory ?? "",
|
||||
dialog.Directory ?? "",
|
||||
dialog.InitialFileName ?? "",
|
||||
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions)));
|
||||
}
|
||||
@@ -51,7 +51,7 @@ namespace Avalonia.Native
|
||||
|
||||
var nativeParent = GetNativeWindow(parent);
|
||||
|
||||
_native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? "");
|
||||
_native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.Directory ?? "");
|
||||
|
||||
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); });
|
||||
}
|
||||
|
||||
@@ -144,5 +144,9 @@ namespace Avalonia.Remote.Protocol
|
||||
|
||||
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage;
|
||||
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
|
||||
public void Start()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ namespace Avalonia.Remote.Protocol
|
||||
Task Send(object data);
|
||||
event Action<IAvaloniaRemoteTransportConnection, object> OnMessage;
|
||||
event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
|
||||
void Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,5 +98,7 @@ namespace Avalonia.Remote.Protocol
|
||||
add => _onException.Add(value);
|
||||
remove => _onException.Remove(value);
|
||||
}
|
||||
|
||||
public void Start() => _conn.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Avalonia.Remote.Protocol
|
||||
{
|
||||
[AvaloniaRemoteMessageGuid("53778004-78fa-4381-8ec3-176a6f2328b6")]
|
||||
public class HtmlTransportStartedMessage
|
||||
{
|
||||
public string Uri { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,8 @@
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public int Stride { get; set; }
|
||||
public double DpiX { get; set; }
|
||||
public double DpiY { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Avalonia.X11.NativeDialogs
|
||||
() => ShowDialog(dialog.Title, platformImpl,
|
||||
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
|
||||
(dialog as OpenFileDialog)?.AllowMultiple ?? false,
|
||||
Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
|
||||
Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory,
|
||||
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters));
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Avalonia.X11.NativeDialogs
|
||||
return await await RunOnGlibThread(async () =>
|
||||
{
|
||||
var res = await ShowDialog(dialog.Title, platformImpl,
|
||||
GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null);
|
||||
GtkFileChooserAction.SelectFolder, false, dialog.Directory, null);
|
||||
return res?.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ namespace Avalonia.Win32
|
||||
frm.SetFileTypes((uint)filters.Count, filters.ToArray());
|
||||
frm.SetFileTypeIndex(0);
|
||||
|
||||
if (dialog.InitialDirectory != null)
|
||||
if (dialog.Directory != null)
|
||||
{
|
||||
UnmanagedMethods.IShellItem directoryShellItem;
|
||||
Guid riid = UnmanagedMethods.ShellIds.IShellItem;
|
||||
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
|
||||
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
|
||||
{
|
||||
frm.SetFolder(directoryShellItem);
|
||||
frm.SetDefaultFolder(directoryShellItem);
|
||||
@@ -114,21 +114,21 @@ namespace Avalonia.Win32
|
||||
options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | DefaultDialogOptions);
|
||||
frm.SetOptions(options);
|
||||
|
||||
if (dialog.InitialDirectory != null)
|
||||
if (dialog.Directory != null)
|
||||
{
|
||||
UnmanagedMethods.IShellItem directoryShellItem;
|
||||
Guid riid = UnmanagedMethods.ShellIds.IShellItem;
|
||||
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
|
||||
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
|
||||
{
|
||||
frm.SetFolder(directoryShellItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (dialog.DefaultDirectory != null)
|
||||
if (dialog.Directory != null)
|
||||
{
|
||||
UnmanagedMethods.IShellItem directoryShellItem;
|
||||
Guid riid = UnmanagedMethods.ShellIds.IShellItem;
|
||||
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
|
||||
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
|
||||
{
|
||||
frm.SetDefaultFolder(directoryShellItem);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Avalonia.Controls.UnitTests;
|
||||
using Avalonia.Controls.UnitTests;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.UnitTests;
|
||||
using Xunit;
|
||||
|
||||
[assembly: ExportAvaloniaModule("DefaultModule", typeof(AppBuilderTests.DefaultModule))]
|
||||
[assembly: ExportAvaloniaModule("RenderingModule", typeof(AppBuilderTests.Direct2DModule), ForRenderingSubsystem = "Direct2D1")]
|
||||
@@ -16,6 +10,7 @@ using Avalonia.UnitTests;
|
||||
|
||||
namespace Avalonia.Controls.UnitTests
|
||||
{
|
||||
using AppBuilder = Avalonia.UnitTests.AppBuilder;
|
||||
|
||||
public class AppBuilderTests
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
|
||||
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
|
||||
|
||||
@@ -9,6 +9,7 @@ using Avalonia.UnitTests;
|
||||
using Avalonia.VisualTree;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using MouseButton = Avalonia.Input.MouseButton;
|
||||
|
||||
namespace Avalonia.Controls.UnitTests
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
first.Day == second.Day;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip ="FIX ME ASAP")]
|
||||
public void SelectedDatesChanged_Should_Fire_When_SelectedDate_Set()
|
||||
{
|
||||
bool handled = false;
|
||||
|
||||
@@ -265,7 +265,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selected_Item_Changes_To_NextAvailable_Item_If_SelectedItem_Is_Removed_From_Middle()
|
||||
public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle()
|
||||
{
|
||||
var items = new ObservableCollection<string>
|
||||
{
|
||||
@@ -288,8 +288,8 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
items.RemoveAt(1);
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Equal("FooBar", target.SelectedItem);
|
||||
Assert.Equal(0, target.SelectedIndex);
|
||||
Assert.Equal("Foo", target.SelectedItem);
|
||||
}
|
||||
|
||||
private Control CreateTemplate(Carousel control, INameScope scope)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
first.Day == second.Day;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "FIX ME ASAP")]
|
||||
public void SelectedDateChanged_Should_Fire_When_SelectedDate_Set()
|
||||
{
|
||||
using (UnitTestApplication.Start(Services))
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.Controls.UnitTests
|
||||
{
|
||||
public class IndexPathTests
|
||||
{
|
||||
[Fact]
|
||||
public void Simple_Index()
|
||||
{
|
||||
var a = new IndexPath(1);
|
||||
|
||||
Assert.Equal(1, a.GetSize());
|
||||
Assert.Equal(1, a.GetAt(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_Paths()
|
||||
{
|
||||
var a = new IndexPath(1);
|
||||
var b = new IndexPath(1);
|
||||
|
||||
Assert.True(a == b);
|
||||
Assert.False(a != b);
|
||||
Assert.True(a.Equals(b));
|
||||
Assert.Equal(0, a.CompareTo(b));
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unequal_Paths()
|
||||
{
|
||||
var a = new IndexPath(1);
|
||||
var b = new IndexPath(2);
|
||||
|
||||
Assert.False(a == b);
|
||||
Assert.True(a != b);
|
||||
Assert.False(a.Equals(b));
|
||||
Assert.Equal(-1, a.CompareTo(b));
|
||||
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equal_Null_Path()
|
||||
{
|
||||
var a = new IndexPath(null);
|
||||
var b = new IndexPath(null);
|
||||
|
||||
Assert.True(a == b);
|
||||
Assert.False(a != b);
|
||||
Assert.True(a.Equals(b));
|
||||
Assert.Equal(0, a.CompareTo(b));
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unequal_Null_Path()
|
||||
{
|
||||
var a = new IndexPath(null);
|
||||
var b = new IndexPath(2);
|
||||
|
||||
Assert.False(a == b);
|
||||
Assert.True(a != b);
|
||||
Assert.False(a.Equals(b));
|
||||
Assert.Equal(-1, a.CompareTo(b));
|
||||
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_Is_Null_Path()
|
||||
{
|
||||
var a = new IndexPath(null);
|
||||
var b = default(IndexPath);
|
||||
|
||||
Assert.True(a == b);
|
||||
Assert.False(a != b);
|
||||
Assert.True(a.Equals(b));
|
||||
Assert.Equal(0, a.CompareTo(b));
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Null_Equality()
|
||||
{
|
||||
var a = new IndexPath(null);
|
||||
var b = new IndexPath(1);
|
||||
|
||||
// Implementing operator == on a struct automatically implements an operator which
|
||||
// accepts null, so make sure this does something useful.
|
||||
Assert.True(a == null);
|
||||
Assert.False(a != null);
|
||||
Assert.False(b == null);
|
||||
Assert.True(b != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.Controls.UnitTests
|
||||
{
|
||||
public class IndexRangeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_Should_Add_Range_To_Empty_List()
|
||||
{
|
||||
var ranges = new List<IndexRange>();
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected);
|
||||
|
||||
Assert.Equal(5, result);
|
||||
Assert.Equal(new[] { new IndexRange(0, 4) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(0, 4) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Add_Non_Intersecting_Range_At_End()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(0, 4) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected);
|
||||
|
||||
Assert.Equal(3, result);
|
||||
Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Add_Non_Intersecting_Range_At_Beginning()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected);
|
||||
|
||||
Assert.Equal(5, result);
|
||||
Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(0, 4) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Add_Non_Intersecting_Range_In_Middle()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(0, 4), new IndexRange(14, 16) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected);
|
||||
|
||||
Assert.Equal(3, result);
|
||||
Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10), new IndexRange(14, 16) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Add_Intersecting_Range_Start()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(6, 9), selected);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Equal(new[] { new IndexRange(6, 10) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(6, 7) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Add_Intersecting_Range_End()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(9, 12), selected);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 12) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(11, 12) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Add_Intersecting_Range_Both()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(6, 12), selected);
|
||||
|
||||
Assert.Equal(4, result);
|
||||
Assert.Equal(new[] { new IndexRange(6, 12) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 12) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Join_Two_Intersecting_Ranges()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(8, 14), selected);
|
||||
|
||||
Assert.Equal(1, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 14) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(11, 11) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Join_Two_Intersecting_Ranges_And_Add_Ranges()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(6, 18), selected);
|
||||
|
||||
Assert.Equal(7, result);
|
||||
Assert.Equal(new[] { new IndexRange(6, 18) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 11), new IndexRange(15, 18) }, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Should_Not_Add_Already_Selected_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var selected = new List<IndexRange>();
|
||||
var result = IndexRange.Add(ranges, new IndexRange(9, 10), selected);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10) }, ranges);
|
||||
Assert.Empty(selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Entire_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected);
|
||||
|
||||
Assert.Equal(3, result);
|
||||
Assert.Empty(ranges);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Start_Of_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 12) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected);
|
||||
|
||||
Assert.Equal(3, result);
|
||||
Assert.Equal(new[] { new IndexRange(11, 12) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_End_Of_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 12) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(10, 12), deselected);
|
||||
|
||||
Assert.Equal(3, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 9) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(10, 12) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Overlapping_End_Of_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 12) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(10, 14), deselected);
|
||||
|
||||
Assert.Equal(3, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 9) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(10, 12) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Middle_Of_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(10, 20) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(12, 16), deselected);
|
||||
|
||||
Assert.Equal(5, result);
|
||||
Assert.Equal(new[] { new IndexRange(10, 11), new IndexRange(17, 20) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(12, 16) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Multiple_Ranges()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(6, 15), deselected);
|
||||
|
||||
Assert.Equal(6, result);
|
||||
Assert.Equal(new[] { new IndexRange(16, 18) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 14) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Multiple_And_Partial_Ranges_1()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(9, 15), deselected);
|
||||
|
||||
Assert.Equal(5, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(16, 18) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 14) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Multiple_And_Partial_Ranges_2()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(8, 13), deselected);
|
||||
|
||||
Assert.Equal(5, result);
|
||||
Assert.Equal(new[] { new IndexRange(14, 14), new IndexRange(16, 18) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 13) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Multiple_And_Partial_Ranges_3()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(9, 13), deselected);
|
||||
|
||||
Assert.Equal(4, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(14, 14), new IndexRange(16, 18) }, ranges);
|
||||
Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 13) }, deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Should_Do_Nothing_For_Unselected_Range()
|
||||
{
|
||||
var ranges = new List<IndexRange> { new IndexRange(8, 10) };
|
||||
var deselected = new List<IndexRange>();
|
||||
var result = IndexRange.Remove(ranges, new IndexRange(2, 4), deselected);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(new[] { new IndexRange(8, 10) }, ranges);
|
||||
Assert.Empty(deselected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stress_Test()
|
||||
{
|
||||
const int iterations = 100;
|
||||
var random = new Random(0);
|
||||
var selection = new List<IndexRange>();
|
||||
var expected = new List<int>();
|
||||
|
||||
IndexRange Generate()
|
||||
{
|
||||
var start = random.Next(100);
|
||||
return new IndexRange(start, start + random.Next(20));
|
||||
}
|
||||
|
||||
for (var i = 0; i < iterations; ++i)
|
||||
{
|
||||
var toAdd = random.Next(5);
|
||||
|
||||
for (var j = 0; j < toAdd; ++j)
|
||||
{
|
||||
var range = Generate();
|
||||
IndexRange.Add(selection, range);
|
||||
|
||||
for (var k = range.Begin; k <= range.End; ++k)
|
||||
{
|
||||
if (!expected.Contains(k))
|
||||
{
|
||||
expected.Add(k);
|
||||
}
|
||||
}
|
||||
|
||||
var actual = IndexRange.EnumerateIndices(selection).ToList();
|
||||
expected.Sort();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
var toRemove = random.Next(5);
|
||||
|
||||
for (var j = 0; j < toRemove; ++j)
|
||||
{
|
||||
var range = Generate();
|
||||
IndexRange.Remove(selection, range);
|
||||
|
||||
for (var k = range.Begin; k <= range.End; ++k)
|
||||
{
|
||||
expected.Remove(k);
|
||||
}
|
||||
|
||||
var actual = IndexRange.EnumerateIndices(selection).ToList();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
selection.Clear();
|
||||
expected.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -573,7 +573,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
|
||||
target.Arrange(Rect.Empty);
|
||||
|
||||
// Check for issue #591: this should not throw.
|
||||
target.ScrollIntoView(items[0]);
|
||||
target.ScrollIntoView(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +727,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
|
||||
|
||||
var last = (target.Items as IList)[10];
|
||||
|
||||
target.ScrollIntoView(last);
|
||||
target.ScrollIntoView(10);
|
||||
|
||||
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
|
||||
Assert.Same(target.Panel.Children[9].DataContext, last);
|
||||
@@ -744,12 +744,12 @@ namespace Avalonia.Controls.UnitTests.Presenters
|
||||
|
||||
var last = (target.Items as IList)[10];
|
||||
|
||||
target.ScrollIntoView(last);
|
||||
target.ScrollIntoView(10);
|
||||
|
||||
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
|
||||
Assert.Same(target.Panel.Children[9].DataContext, last);
|
||||
|
||||
target.ScrollIntoView(last);
|
||||
target.ScrollIntoView(10);
|
||||
|
||||
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
|
||||
Assert.Same(target.Panel.Children[9].DataContext, last);
|
||||
|
||||
@@ -536,37 +536,19 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
Assert.Equal(items[1], target.SelectedItem);
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
|
||||
SelectionChangedEventArgs receivedArgs = null;
|
||||
|
||||
target.SelectionChanged += (_, args) => receivedArgs = args;
|
||||
|
||||
var removed = items[1];
|
||||
|
||||
items.RemoveAt(1);
|
||||
|
||||
Assert.Null(target.SelectedItem);
|
||||
Assert.Equal(-1, target.SelectedIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Moving_Selected_Item_Should_Update_Selection()
|
||||
{
|
||||
var items = new AvaloniaList<Item>
|
||||
{
|
||||
new Item(),
|
||||
new Item(),
|
||||
};
|
||||
|
||||
var target = new SelectingItemsControl
|
||||
{
|
||||
Items = items,
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.SelectedIndex = 0;
|
||||
|
||||
Assert.Equal(items[0], target.SelectedItem);
|
||||
Assert.Equal(0, target.SelectedIndex);
|
||||
|
||||
items.Move(0, 1);
|
||||
|
||||
Assert.Equal(items[1], target.SelectedItem);
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Empty(receivedArgs.AddedItems);
|
||||
Assert.Equal(new[] { removed }, receivedArgs.RemovedItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1089,8 +1071,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
|
||||
items[1] = "Qux";
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Equal("Qux", target.SelectedItem);
|
||||
Assert.Equal(-1, target.SelectedIndex);
|
||||
Assert.Null(target.SelectedItem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+2
-2
@@ -75,8 +75,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.SelectedIndex = 2;
|
||||
items.RemoveAt(2);
|
||||
|
||||
Assert.Equal(2, target.SelectedIndex);
|
||||
Assert.Equal("qux", target.SelectedItem);
|
||||
Assert.Equal(0, target.SelectedIndex);
|
||||
Assert.Equal("foo", target.SelectedItem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+340
-22
@@ -70,8 +70,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
[Fact]
|
||||
public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex()
|
||||
{
|
||||
// Note that we don't need SelectionMode = Multiple here. Multiple selections can always
|
||||
// be made in code.
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar", "baz" },
|
||||
@@ -337,7 +335,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
"qiz",
|
||||
"lol",
|
||||
},
|
||||
SelectionMode = SelectionMode.Multiple,
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
@@ -370,7 +367,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.SelectedIndex = 3;
|
||||
target.SelectRange(1);
|
||||
|
||||
Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems.Cast<object>().ToList());
|
||||
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.Cast<object>().ToList());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -672,7 +669,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift);
|
||||
|
||||
var panel = target.Presenter.Panel;
|
||||
|
||||
@@ -680,6 +677,57 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctrl_Selecting_Raises_SelectionChanged_Events()
|
||||
{
|
||||
var target = new ListBox
|
||||
{
|
||||
Template = Template(),
|
||||
Items = new[] { "Foo", "Bar", "Baz", "Qux" },
|
||||
SelectionMode = SelectionMode.Multiple,
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
SelectionChangedEventArgs receivedArgs = null;
|
||||
|
||||
target.SelectionChanged += (_, args) => receivedArgs = args;
|
||||
|
||||
void VerifyAdded(string selection)
|
||||
{
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal(new[] { selection }, receivedArgs.AddedItems);
|
||||
Assert.Empty(receivedArgs.RemovedItems);
|
||||
}
|
||||
|
||||
void VerifyRemoved(string selection)
|
||||
{
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal(new[] { selection }, receivedArgs.RemovedItems);
|
||||
Assert.Empty(receivedArgs.AddedItems);
|
||||
}
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
|
||||
|
||||
VerifyAdded("Bar");
|
||||
|
||||
receivedArgs = null;
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control);
|
||||
|
||||
VerifyAdded("Baz");
|
||||
|
||||
receivedArgs = null;
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control);
|
||||
|
||||
VerifyAdded("Qux");
|
||||
|
||||
receivedArgs = null;
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control);
|
||||
|
||||
VerifyRemoved("Bar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection()
|
||||
{
|
||||
@@ -693,14 +741,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Equal("Bar", target.SelectedItem);
|
||||
Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems);
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(2, target.SelectedIndex);
|
||||
Assert.Equal("Baz", target.SelectedItem);
|
||||
@@ -720,12 +768,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Equal("Bar", target.SelectedItem);
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Equal("Bar", target.SelectedItem);
|
||||
@@ -744,7 +792,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control);
|
||||
|
||||
var panel = target.Presenter.Panel;
|
||||
|
||||
@@ -765,7 +813,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift);
|
||||
|
||||
var panel = target.Presenter.Panel;
|
||||
|
||||
@@ -786,7 +834,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift);
|
||||
|
||||
var panel = target.Presenter.Panel;
|
||||
|
||||
@@ -794,6 +842,52 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shift_Selecting_Raises_SelectionChanged_Events()
|
||||
{
|
||||
var target = new ListBox
|
||||
{
|
||||
Template = Template(),
|
||||
Items = new[] { "Foo", "Bar", "Baz", "Qux" },
|
||||
SelectionMode = SelectionMode.Multiple,
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
SelectionChangedEventArgs receivedArgs = null;
|
||||
|
||||
target.SelectionChanged += (_, args) => receivedArgs = args;
|
||||
|
||||
void VerifyAdded(params string[] selection)
|
||||
{
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal(selection, receivedArgs.AddedItems);
|
||||
Assert.Empty(receivedArgs.RemovedItems);
|
||||
}
|
||||
|
||||
void VerifyRemoved(string selection)
|
||||
{
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal(new[] { selection }, receivedArgs.RemovedItems);
|
||||
Assert.Empty(receivedArgs.AddedItems);
|
||||
}
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
|
||||
|
||||
VerifyAdded("Bar");
|
||||
|
||||
receivedArgs = null;
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift);
|
||||
|
||||
VerifyAdded("Baz" ,"Qux");
|
||||
|
||||
receivedArgs = null;
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift);
|
||||
|
||||
VerifyRemoved("Qux");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order()
|
||||
{
|
||||
@@ -810,15 +904,15 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
|
||||
Assert.Equal(new[] { "Foo" }, target.SelectedItems);
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems);
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems);
|
||||
}
|
||||
@@ -842,6 +936,30 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
Assert.Equal("Foo", target.SelectedItem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectAll_Raises_SelectionChanged_Event()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Template = Template(),
|
||||
Items = new[] { "Foo", "Bar", "Baz" },
|
||||
SelectionMode = SelectionMode.Multiple,
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
SelectionChangedEventArgs receivedArgs = null;
|
||||
|
||||
target.SelectionChanged += (_, args) => receivedArgs = args;
|
||||
|
||||
target.SelectAll();
|
||||
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal(target.Items, receivedArgs.AddedItems);
|
||||
Assert.Empty(receivedArgs.RemovedItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnselectAll_Clears_SelectedIndex_And_SelectedItem()
|
||||
{
|
||||
@@ -993,7 +1111,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.SelectAll();
|
||||
items[1] = "Qux";
|
||||
|
||||
Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems);
|
||||
Assert.Equal(new[] { "Foo", "Baz" }, target.SelectedItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1056,7 +1174,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Shift);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift);
|
||||
|
||||
Assert.Equal(2, target.SelectedItems.Count);
|
||||
|
||||
@@ -1106,7 +1224,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Shift);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift);
|
||||
|
||||
Assert.Equal(1, target.SelectedItems.Count);
|
||||
}
|
||||
@@ -1126,11 +1244,200 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Control);
|
||||
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(1, target.SelectedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Adding_To_Selection_Should_Set_SelectedIndex()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Selection.Select(1);
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
var oldSelection = target.Selection;
|
||||
|
||||
target.Selection = null;
|
||||
|
||||
Assert.NotNull(target.Selection);
|
||||
Assert.NotSame(oldSelection, target.Selection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
var selection = new SelectionModel { Source = new[] { "baz" } };
|
||||
Assert.Throws<ArgumentException>(() => target.Selection = selection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_Source()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
var selection = new SelectionModel();
|
||||
target.Selection = selection;
|
||||
|
||||
Assert.Same(target.Items, selection.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
var selection = new SelectionModel { Source = target.Items };
|
||||
selection.Select(1);
|
||||
target.Selection = selection;
|
||||
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems);
|
||||
Assert.Equal(new[] { 1 }, SelectedContainers(target));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar", "baz" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
var selection = new SelectionModel { Source = target.Items };
|
||||
selection.SelectRange(new IndexPath(0), new IndexPath(2));
|
||||
target.Selection = selection;
|
||||
|
||||
Assert.Equal(0, target.SelectedIndex);
|
||||
Assert.Equal(new[] { "foo", "bar", "baz" }, target.Selection.SelectedItems);
|
||||
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reassigning_Selection_Should_Clear_Selection()
|
||||
{
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = new[] { "foo", "bar" },
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Selection.Select(1);
|
||||
target.Selection = new SelectionModel();
|
||||
|
||||
Assert.Equal(-1, target.SelectedIndex);
|
||||
Assert.Null(target.SelectedItem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_Selection_Should_Set_Item_IsSelected()
|
||||
{
|
||||
var items = new[]
|
||||
{
|
||||
new ListBoxItem(),
|
||||
new ListBoxItem(),
|
||||
new ListBoxItem(),
|
||||
};
|
||||
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = items,
|
||||
Template = Template(),
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
var selection = new SelectionModel { Source = items };
|
||||
selection.SelectRange(new IndexPath(0), new IndexPath(1));
|
||||
target.Selection = selection;
|
||||
|
||||
Assert.True(items[0].IsSelected);
|
||||
Assert.True(items[1].IsSelected);
|
||||
Assert.False(items[2].IsSelected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assigning_Selection_Should_Raise_SelectionChanged()
|
||||
{
|
||||
var items = new[] { "foo", "bar", "baz" };
|
||||
|
||||
var target = new TestSelector
|
||||
{
|
||||
Items = items,
|
||||
Template = Template(),
|
||||
SelectedItem = "bar",
|
||||
};
|
||||
|
||||
var raised = 0;
|
||||
|
||||
target.SelectionChanged += (s, e) =>
|
||||
{
|
||||
if (raised == 0)
|
||||
{
|
||||
Assert.Empty(e.AddedItems.Cast<object>());
|
||||
Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast<object>());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(new[] { "foo", "baz" }, e.AddedItems.Cast<object>());
|
||||
Assert.Empty(e.RemovedItems.Cast<object>());
|
||||
}
|
||||
|
||||
++raised;
|
||||
};
|
||||
|
||||
target.ApplyTemplate();
|
||||
target.Presenter.ApplyTemplate();
|
||||
|
||||
|
||||
var selection = new SelectionModel { Source = items };
|
||||
selection.Select(0);
|
||||
selection.Select(2);
|
||||
target.Selection = selection;
|
||||
|
||||
Assert.Equal(2, raised);
|
||||
}
|
||||
|
||||
private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
|
||||
{
|
||||
return target.Presenter.Panel.Children
|
||||
@@ -1154,20 +1461,31 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
public static readonly new AvaloniaProperty<IList> SelectedItemsProperty =
|
||||
SelectingItemsControl.SelectedItemsProperty;
|
||||
|
||||
public TestSelector()
|
||||
{
|
||||
SelectionMode = SelectionMode.Multiple;
|
||||
}
|
||||
|
||||
public new IList SelectedItems
|
||||
{
|
||||
get { return base.SelectedItems; }
|
||||
set { base.SelectedItems = value; }
|
||||
}
|
||||
|
||||
public new ISelectionModel Selection
|
||||
{
|
||||
get => base.Selection;
|
||||
set => base.Selection = value;
|
||||
}
|
||||
|
||||
public new SelectionMode SelectionMode
|
||||
{
|
||||
get { return base.SelectionMode; }
|
||||
set { base.SelectionMode = value; }
|
||||
}
|
||||
|
||||
public new void SelectAll() => base.SelectAll();
|
||||
public new void UnselectAll() => base.UnselectAll();
|
||||
public void SelectAll() => Selection.SelectAll();
|
||||
public void UnselectAll() => Selection.ClearSelection();
|
||||
public void SelectRange(int index) => UpdateSelection(index, true, true);
|
||||
public void Toggle(int index) => UpdateSelection(index, true, false, true);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Removing_Selected_Should_Select_Next()
|
||||
public void Removing_Selected_Should_Select_First()
|
||||
{
|
||||
var items = new ObservableCollection<TabItem>()
|
||||
{
|
||||
@@ -96,10 +96,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
Assert.Same(items[1], target.SelectedItem);
|
||||
items.RemoveAt(1);
|
||||
|
||||
// Assert for former element [2] now [1] == "3rd"
|
||||
Assert.Equal(1, target.SelectedIndex);
|
||||
Assert.Same(items[1], target.SelectedItem);
|
||||
Assert.Same("3rd", ((TabItem)target.SelectedItem).Name);
|
||||
Assert.Equal(0, target.SelectedIndex);
|
||||
Assert.Same(items[0], target.SelectedItem);
|
||||
Assert.Same("first", ((TabItem)target.SelectedItem).Name);
|
||||
}
|
||||
|
||||
private Control CreateTabStripTemplate(TabStrip parent, INameScope scope)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Removal_Should_Set_Next_Tab()
|
||||
public void Removal_Should_Set_First_Tab()
|
||||
{
|
||||
var collection = new ObservableCollection<TabItem>()
|
||||
{
|
||||
@@ -123,11 +123,9 @@ namespace Avalonia.Controls.UnitTests
|
||||
target.SelectedItem = collection[1];
|
||||
collection.RemoveAt(1);
|
||||
|
||||
// compare with former [2] now [1] == "3rd"
|
||||
Assert.Same(collection[1], target.SelectedItem);
|
||||
Assert.Same(collection[0], target.SelectedItem);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Core;
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -168,7 +167,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
Assert.True(container.IsSelected);
|
||||
|
||||
_mouse.Click(container, modifiers: InputModifiers.Control);
|
||||
_mouse.Click(container, modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Null(target.SelectedItem);
|
||||
Assert.False(container.IsSelected);
|
||||
@@ -203,7 +202,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
Assert.True(container1.IsSelected);
|
||||
|
||||
_mouse.Click(container2, modifiers: InputModifiers.Control);
|
||||
_mouse.Click(container2, modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(item2, target.SelectedItem);
|
||||
Assert.False(container1.IsSelected);
|
||||
@@ -235,18 +234,18 @@ namespace Avalonia.Controls.UnitTests
|
||||
var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
|
||||
var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
|
||||
|
||||
ClickContainer(item1Container, InputModifiers.Control);
|
||||
ClickContainer(item1Container, KeyModifiers.Control);
|
||||
Assert.True(item1Container.IsSelected);
|
||||
|
||||
ClickContainer(item2Container, InputModifiers.Control);
|
||||
ClickContainer(item2Container, KeyModifiers.Control);
|
||||
Assert.True(item2Container.IsSelected);
|
||||
|
||||
Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType<Node>());
|
||||
Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType<Node>());
|
||||
|
||||
ClickContainer(item1Container, InputModifiers.Control);
|
||||
ClickContainer(item1Container, KeyModifiers.Control);
|
||||
Assert.False(item1Container.IsSelected);
|
||||
|
||||
Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
|
||||
Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType<Node>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -274,11 +273,11 @@ namespace Avalonia.Controls.UnitTests
|
||||
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
|
||||
Assert.True(fromContainer.IsSelected);
|
||||
|
||||
ClickContainer(toContainer, InputModifiers.Shift);
|
||||
ClickContainer(toContainer, KeyModifiers.Shift);
|
||||
AssertChildrenSelected(target, rootNode);
|
||||
}
|
||||
|
||||
@@ -307,11 +306,11 @@ namespace Avalonia.Controls.UnitTests
|
||||
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
|
||||
Assert.True(fromContainer.IsSelected);
|
||||
|
||||
ClickContainer(toContainer, InputModifiers.Shift);
|
||||
ClickContainer(toContainer, KeyModifiers.Shift);
|
||||
AssertChildrenSelected(target, rootNode);
|
||||
}
|
||||
|
||||
@@ -340,12 +339,12 @@ namespace Avalonia.Controls.UnitTests
|
||||
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
|
||||
ClickContainer(toContainer, InputModifiers.Shift);
|
||||
ClickContainer(toContainer, KeyModifiers.Shift);
|
||||
AssertChildrenSelected(target, rootNode);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
|
||||
Assert.True(fromContainer.IsSelected);
|
||||
|
||||
@@ -661,8 +660,8 @@ namespace Avalonia.Controls.UnitTests
|
||||
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(toContainer, InputModifiers.Shift);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
ClickContainer(toContainer, KeyModifiers.Shift);
|
||||
|
||||
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
|
||||
var selectAllGesture = keymap.SelectAll.First();
|
||||
@@ -707,8 +706,8 @@ namespace Avalonia.Controls.UnitTests
|
||||
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(toContainer, InputModifiers.Shift);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
ClickContainer(toContainer, KeyModifiers.Shift);
|
||||
|
||||
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
|
||||
var selectAllGesture = keymap.SelectAll.First();
|
||||
@@ -746,11 +745,11 @@ namespace Avalonia.Controls.UnitTests
|
||||
target.SelectAll();
|
||||
|
||||
AssertChildrenSelected(target, tree[0]);
|
||||
Assert.Equal(5, target.SelectedItems.Count);
|
||||
Assert.Equal(5, target.Selection.SelectedItems.Count);
|
||||
|
||||
_mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right);
|
||||
|
||||
Assert.Equal(5, target.SelectedItems.Count);
|
||||
Assert.Equal(5, target.Selection.SelectedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -779,14 +778,14 @@ namespace Avalonia.Controls.UnitTests
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then);
|
||||
|
||||
ClickContainer(fromContainer, InputModifiers.None);
|
||||
ClickContainer(toContainer, InputModifiers.Shift);
|
||||
ClickContainer(fromContainer, KeyModifiers.None);
|
||||
ClickContainer(toContainer, KeyModifiers.Shift);
|
||||
|
||||
Assert.Equal(2, target.SelectedItems.Count);
|
||||
Assert.Equal(2, target.Selection.SelectedItems.Count);
|
||||
|
||||
_mouse.Click(thenContainer, MouseButton.Right);
|
||||
|
||||
Assert.Equal(1, target.SelectedItems.Count);
|
||||
Assert.Equal(1, target.Selection.SelectedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -814,9 +813,9 @@ namespace Avalonia.Controls.UnitTests
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
_mouse.Click(fromContainer);
|
||||
_mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Shift);
|
||||
_mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift);
|
||||
|
||||
Assert.Equal(1, target.SelectedItems.Count);
|
||||
Assert.Equal(1, target.Selection.SelectedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -844,9 +843,9 @@ namespace Avalonia.Controls.UnitTests
|
||||
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
|
||||
|
||||
_mouse.Click(fromContainer);
|
||||
_mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Control);
|
||||
_mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control);
|
||||
|
||||
Assert.Equal(1, target.SelectedItems.Count);
|
||||
Assert.Equal(1, target.Selection.SelectedItems.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -964,7 +963,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
ApplyTemplates(target);
|
||||
|
||||
_mouse.Click(GetItem(target, 0));
|
||||
_mouse.Click(GetItem(target, 1), modifiers: InputModifiers.Shift);
|
||||
_mouse.Click(GetItem(target, 1), modifiers: KeyModifiers.Shift);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1171,7 +1170,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
}
|
||||
}
|
||||
|
||||
void ClickContainer(IControl container, InputModifiers modifiers)
|
||||
void ClickContainer(IControl container, KeyModifiers modifiers)
|
||||
{
|
||||
_mouse.Click(container, modifiers: modifiers);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ namespace Avalonia.Controls.UnitTests.Utils
|
||||
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
|
||||
.Bind<IStyler>().ToConstant(styler.Object);
|
||||
|
||||
var gesture1 = new KeyGesture(Key.A, InputModifiers.Control);
|
||||
var gesture2 = new KeyGesture(Key.B, InputModifiers.Control);
|
||||
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
|
||||
var gesture2 = new KeyGesture(Key.B, KeyModifiers.Control);
|
||||
|
||||
var tl = new Window();
|
||||
var button = new Button();
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls.Utils;
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.Controls.UnitTests.Utils
|
||||
{
|
||||
public class SelectedItemsSyncTests
|
||||
{
|
||||
[Fact]
|
||||
public void Initial_Items_Are_From_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
Assert.Equal(new[] { "bar", "baz" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selecting_On_Model_Adds_Item()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
target.Model.Select(0);
|
||||
|
||||
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selecting_Duplicate_On_Model_Adds_Item()
|
||||
{
|
||||
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
target.Model.Select(4);
|
||||
|
||||
Assert.Equal(new[] { "bar", "baz", "bar" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deselecting_On_Model_Removes_Item()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
target.Model.Deselect(1);
|
||||
|
||||
Assert.Equal(new[] { "baz" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deselecting_Duplicate_On_Model_Removes_Item()
|
||||
{
|
||||
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
target.Model.Select(4);
|
||||
target.Model.Deselect(4);
|
||||
|
||||
Assert.Equal(new[] { "baz", "bar" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reassigning_Model_Resets_Items()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
var newModel = new SelectionModel { Source = target.Model.Source };
|
||||
newModel.Select(0);
|
||||
newModel.Select(1);
|
||||
|
||||
target.SetModel(newModel);
|
||||
|
||||
Assert.Equal(new[] { "foo", "bar" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reassigning_Model_Tracks_New_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
var newModel = new SelectionModel { Source = target.Model.Source };
|
||||
target.SetModel(newModel);
|
||||
|
||||
newModel.Select(0);
|
||||
newModel.Select(1);
|
||||
|
||||
Assert.Equal(new[] { "foo", "bar" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Adding_To_Items_Selects_On_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
items.Add("foo");
|
||||
|
||||
Assert.Equal(
|
||||
new[] { new IndexPath(0), new IndexPath(1), new IndexPath(2) },
|
||||
target.Model.SelectedIndices);
|
||||
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Removing_From_Items_Deselects_On_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
items.Remove("baz");
|
||||
|
||||
Assert.Equal(new[] { new IndexPath(1) }, target.Model.SelectedIndices);
|
||||
Assert.Equal(new[] { "bar" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replacing_Item_Updates_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
items[0] = "foo";
|
||||
|
||||
Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
|
||||
Assert.Equal(new[] { "foo", "baz" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clearing_Items_Updates_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
items.Clear();
|
||||
|
||||
Assert.Empty(target.Model.SelectedIndices);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setting_Items_Updates_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var oldItems = target.GetOrCreateItems();
|
||||
|
||||
var newItems = new AvaloniaList<string> { "foo", "baz" };
|
||||
target.SetItems(newItems);
|
||||
|
||||
Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
|
||||
Assert.Same(newItems, target.GetOrCreateItems());
|
||||
Assert.NotSame(oldItems, target.GetOrCreateItems());
|
||||
Assert.Equal(new[] { "foo", "baz" }, newItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setting_Items_Subscribes_To_Model()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var items = new AvaloniaList<string> { "foo", "baz" };
|
||||
|
||||
target.SetItems(items);
|
||||
target.Model.Select(1);
|
||||
|
||||
Assert.Equal(new[] { "foo", "baz", "bar" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setting_Items_To_Null_Creates_Empty_Items()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var oldItems = target.GetOrCreateItems();
|
||||
|
||||
target.SetItems(null);
|
||||
|
||||
var newItems = Assert.IsType<AvaloniaList<object>>(target.GetOrCreateItems());
|
||||
|
||||
Assert.NotSame(oldItems, newItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handles_Null_Model_Source()
|
||||
{
|
||||
var model = new SelectionModel();
|
||||
model.Select(1);
|
||||
|
||||
var target = new SelectedItemsSync(model);
|
||||
var items = target.GetOrCreateItems();
|
||||
|
||||
Assert.Empty(items);
|
||||
|
||||
model.Select(2);
|
||||
model.Source = new[] { "foo", "bar", "baz" };
|
||||
|
||||
Assert.Equal(new[] { "bar", "baz" }, items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Does_Not_Accept_Fixed_Size_Items()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
target.SetItems(new[] { "foo", "bar", "baz" }));
|
||||
}
|
||||
|
||||
private static SelectedItemsSync CreateTarget(
|
||||
IEnumerable<string> items = null)
|
||||
{
|
||||
items ??= new[] { "foo", "bar", "baz" };
|
||||
|
||||
var model = new SelectionModel { Source = items };
|
||||
model.SelectRange(new IndexPath(1), new IndexPath(2));
|
||||
|
||||
var target = new SelectedItemsSync(model);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace Avalonia.Input.UnitTests
|
||||
{
|
||||
public class MouseDeviceTests
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
[Fact]
|
||||
public void Capture_Is_Transferred_To_Parent_When_Control_Removed()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ namespace Avalonia.Input.UnitTests
|
||||
|
||||
Assert.Same(root, target.Captured);
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
[Fact]
|
||||
public void MouseMove_Should_Update_IsPointerOver()
|
||||
@@ -207,7 +209,9 @@ namespace Avalonia.Input.UnitTests
|
||||
|
||||
SendMouseMove(inputManager, root, new Point(11, 11));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var result = root.MouseDevice.GetPosition(root.Child);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Assert.Equal(new Point(1, 11), result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,25 @@ namespace Avalonia.UnitTests
|
||||
{
|
||||
public class MouseTestHelper
|
||||
{
|
||||
private Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
|
||||
private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
|
||||
private ulong _nextStamp = 1;
|
||||
private ulong Timestamp() => _nextStamp++;
|
||||
|
||||
private InputModifiers _pressedButtons;
|
||||
private RawInputModifiers _pressedButtons;
|
||||
public IInputElement Captured => _pointer.Captured;
|
||||
|
||||
InputModifiers Convert(MouseButton mouseButton)
|
||||
=> (mouseButton == MouseButton.Left ? InputModifiers.LeftMouseButton
|
||||
: mouseButton == MouseButton.Middle ? InputModifiers.MiddleMouseButton
|
||||
: mouseButton == MouseButton.Right ? InputModifiers.RightMouseButton : InputModifiers.None);
|
||||
|
||||
int ButtonCount(PointerPointProperties props)
|
||||
private RawInputModifiers Convert(MouseButton mouseButton)
|
||||
{
|
||||
return mouseButton switch
|
||||
{
|
||||
MouseButton.Left => RawInputModifiers.LeftMouseButton,
|
||||
MouseButton.Right => RawInputModifiers.RightMouseButton,
|
||||
MouseButton.Middle => RawInputModifiers.MiddleMouseButton,
|
||||
_ => RawInputModifiers.None,
|
||||
};
|
||||
}
|
||||
|
||||
private int ButtonCount(PointerPointProperties props)
|
||||
{
|
||||
var rv = 0;
|
||||
if (props.IsLeftButtonPressed)
|
||||
@@ -32,17 +38,14 @@ namespace Avalonia.UnitTests
|
||||
|
||||
private MouseButton _pressedButton;
|
||||
|
||||
KeyModifiers GetModifiers(InputModifiers modifiers) =>
|
||||
(KeyModifiers)((int)modifiers & (int)RawInputModifiers.KeyboardMask);
|
||||
|
||||
public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
|
||||
InputModifiers modifiers = default, int clickCount = 1)
|
||||
KeyModifiers modifiers = default, int clickCount = 1)
|
||||
{
|
||||
Down(target, target, mouseButton, position, modifiers, clickCount);
|
||||
}
|
||||
|
||||
public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
|
||||
Point position = default, InputModifiers modifiers = default, int clickCount = 1)
|
||||
Point position = default, KeyModifiers modifiers = default, int clickCount = 1)
|
||||
{
|
||||
_pressedButtons |= Convert(mouseButton);
|
||||
var props = new PointerPointProperties((RawInputModifiers)_pressedButtons,
|
||||
@@ -57,23 +60,23 @@ namespace Avalonia.UnitTests
|
||||
_pressedButton = mouseButton;
|
||||
_pointer.Capture((IInputElement)target);
|
||||
source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
|
||||
GetModifiers(modifiers), clickCount));
|
||||
modifiers, clickCount));
|
||||
}
|
||||
}
|
||||
|
||||
public void Move(IInteractive target, in Point position, InputModifiers modifiers = default) => Move(target, target, position, modifiers);
|
||||
public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default)
|
||||
public void Move(IInteractive target, in Point position, KeyModifiers modifiers = default) => Move(target, target, position, modifiers);
|
||||
public void Move(IInteractive target, IInteractive source, in Point position, KeyModifiers modifiers = default)
|
||||
{
|
||||
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position,
|
||||
Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), GetModifiers(modifiers)));
|
||||
Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), modifiers));
|
||||
}
|
||||
|
||||
public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
|
||||
InputModifiers modifiers = default)
|
||||
KeyModifiers modifiers = default)
|
||||
=> Up(target, target, mouseButton, position, modifiers);
|
||||
|
||||
public void Up(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
|
||||
Point position = default, InputModifiers modifiers = default)
|
||||
Point position = default, KeyModifiers modifiers = default)
|
||||
{
|
||||
var conv = Convert(mouseButton);
|
||||
_pressedButtons = (_pressedButtons | conv) ^ conv;
|
||||
@@ -85,7 +88,7 @@ namespace Avalonia.UnitTests
|
||||
if (ButtonCount(props) == 0)
|
||||
{
|
||||
target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
|
||||
Timestamp(), props, GetModifiers(modifiers), _pressedButton));
|
||||
Timestamp(), props, modifiers, _pressedButton));
|
||||
_pointer.Capture(null);
|
||||
}
|
||||
else
|
||||
@@ -93,10 +96,10 @@ namespace Avalonia.UnitTests
|
||||
}
|
||||
|
||||
public void Click(IInteractive target, MouseButton button = MouseButton.Left, Point position = default,
|
||||
InputModifiers modifiers = default)
|
||||
KeyModifiers modifiers = default)
|
||||
=> Click(target, target, button, position, modifiers);
|
||||
public void Click(IInteractive target, IInteractive source, MouseButton button = MouseButton.Left,
|
||||
Point position = default, InputModifiers modifiers = default)
|
||||
Point position = default, KeyModifiers modifiers = default)
|
||||
{
|
||||
Down(target, source, button, position, modifiers);
|
||||
Up(target, source, button, position, modifiers);
|
||||
|
||||
@@ -360,7 +360,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
|
||||
|
||||
var result = initial.CloneScene();
|
||||
sceneBuilder.Update(result, border);
|
||||
|
||||
|
||||
var borderNode = (VisualNode)result.Root.Children[0];
|
||||
Assert.Same(border, borderNode.Visual);
|
||||
|
||||
@@ -880,16 +880,24 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
|
||||
{
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
|
||||
{
|
||||
var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>());
|
||||
var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>(
|
||||
x => x.PixelSize == new PixelSize(100, 100) &&
|
||||
x.Dpi == new Vector(96, 96)));
|
||||
|
||||
Image img;
|
||||
var tree = new TestRoot
|
||||
{
|
||||
Child = img = new Image
|
||||
{
|
||||
Source = new Bitmap(bitmap)
|
||||
Source = new Bitmap(bitmap),
|
||||
Height = 100,
|
||||
Width = 100
|
||||
}
|
||||
};
|
||||
|
||||
tree.Measure(Size.Infinity);
|
||||
tree.Arrange(new Rect(new Size(100, 100)));
|
||||
|
||||
Assert.Equal(2, bitmap.RefCount);
|
||||
IRef<IDrawOperation> operation;
|
||||
|
||||
@@ -912,16 +920,24 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
|
||||
{
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
|
||||
{
|
||||
var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>());
|
||||
var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>(
|
||||
x => x.PixelSize == new PixelSize(100, 100) &&
|
||||
x.Dpi == new Vector(96, 96)));
|
||||
|
||||
Image img;
|
||||
var tree = new TestRoot
|
||||
{
|
||||
Child = img = new Image
|
||||
{
|
||||
Source = new Bitmap(bitmap)
|
||||
Source = new Bitmap(bitmap),
|
||||
Width = 100,
|
||||
Height = 100
|
||||
}
|
||||
};
|
||||
|
||||
tree.Measure(Size.Infinity);
|
||||
tree.Arrange(new Rect(new Size(100, 100)));
|
||||
|
||||
var scene = new Scene(tree);
|
||||
var sceneBuilder = new SceneBuilder();
|
||||
sceneBuilder.UpdateAll(scene);
|
||||
|
||||
Reference in New Issue
Block a user