Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make switching between flat and tree layouts easier. #277

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions samples/TreeDataGridDemo/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
DockPanel.Dock="Right">
Cell Selection
</CheckBox>
<CheckBox IsChecked="{Binding Files.FlatList}"
Margin="4 0 0 0"
DockPanel.Dock="Right">
Flat
</CheckBox>
<TextBox Text="{Binding Files.SelectedPath, Mode=OneWay}"
Margin="4 0 0 0"
VerticalContentAlignment="Center"
Expand Down
192 changes: 137 additions & 55 deletions samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;
Expand All @@ -20,6 +19,9 @@ namespace TreeDataGridDemo.ViewModels
public class FilesPageViewModel : ReactiveObject
{
private static IconConverter? s_iconConverter;
private readonly HierarchicalTreeDataGridSource<FileTreeNodeModel>? _treeSource;
private FlatTreeDataGridSource<FileTreeNodeModel>? _flatSource;
private ITreeDataGridSource<FileTreeNodeModel> _source;
private bool _cellSelection;
private FileTreeNodeModel? _root;
private string _selectedDrive;
Expand All @@ -38,61 +40,17 @@ public FilesPageViewModel()
_selectedDrive = Drives.FirstOrDefault() ?? "/";
}

Source = new HierarchicalTreeDataGridSource<FileTreeNodeModel>(Array.Empty<FileTreeNodeModel>())
{
Columns =
{
new CheckBoxColumn<FileTreeNodeModel>(
null,
x => x.IsChecked,
(o, v) => o.IsChecked = v,
options: new()
{
CanUserResizeColumn = false,
}),
new HierarchicalExpanderColumn<FileTreeNodeModel>(
new TemplateColumn<FileTreeNodeModel>(
"Name",
"FileNameCell",
"FileNameEditCell",
new GridLength(1, GridUnitType.Star),
new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Name),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Name),
IsTextSearchEnabled = true,
TextSearchValueSelector = x => x.Name
}),
x => x.Children,
x => x.HasChildren,
x => x.IsExpanded),
new TextColumn<FileTreeNodeModel, long?>(
"Size",
x => x.Size,
options: new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Size),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Size),
}),
new TextColumn<FileTreeNodeModel, DateTimeOffset?>(
"Modified",
x => x.Modified,
options: new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Modified),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Modified),
}),
}
};

Source.RowSelection!.SingleSelect = false;
Source.RowSelection.SelectionChanged += SelectionChanged;
_source = _treeSource = CreateTreeSource();

this.WhenAnyValue(x => x.SelectedDrive)
.Subscribe(x =>
{
_root = new FileTreeNodeModel(_selectedDrive, isDirectory: true, isRoot: true);
Source.Items = new[] { _root };

if (_treeSource is not null)
_treeSource.Items = new[] { _root };
else if (_flatSource is not null)
_flatSource.Items = _root.Children;
});
}

Expand All @@ -115,6 +73,16 @@ public bool CellSelection

public IList<string> Drives { get; }

public bool FlatList
{
get => Source != _treeSource;
set
{
if (value != FlatList)
Source = value ? _flatSource ??= CreateFlatSource() : _treeSource!;
}
}

public string SelectedDrive
{
get => _selectedDrive;
Expand All @@ -127,7 +95,11 @@ public string SelectedDrive
set => SetSelectedPath(value);
}

public HierarchicalTreeDataGridSource<FileTreeNodeModel> Source { get; }
public ITreeDataGridSource<FileTreeNodeModel> Source
{
get => _source;
private set => this.RaiseAndSetIfChanged(ref _source, value);
}

public static IMultiValueConverter FileIconConverter
{
Expand All @@ -151,11 +123,115 @@ public static IMultiValueConverter FileIconConverter
}
}

private FlatTreeDataGridSource<FileTreeNodeModel> CreateFlatSource()
{
var result = new FlatTreeDataGridSource<FileTreeNodeModel>(_root!.Children)
{
Columns =
{
new CheckBoxColumn<FileTreeNodeModel>(
null,
x => x.IsChecked,
(o, v) => o.IsChecked = v,
options: new()
{
CanUserResizeColumn = false,
}),
new TemplateColumn<FileTreeNodeModel>(
"Name",
"FileNameCell",
"FileNameEditCell",
new GridLength(1, GridUnitType.Star),
new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Name),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Name),
IsTextSearchEnabled = true,
TextSearchValueSelector = x => x.Name
}),
new TextColumn<FileTreeNodeModel, long?>(
"Size",
x => x.Size,
options: new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Size),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Size),
}),
new TextColumn<FileTreeNodeModel, DateTimeOffset?>(
"Modified",
x => x.Modified,
options: new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Modified),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Modified),
}),
}
};

result.RowSelection!.SingleSelect = false;
result.RowSelection.SelectionChanged += SelectionChanged;
return result;
}

private HierarchicalTreeDataGridSource<FileTreeNodeModel> CreateTreeSource()
{
var result = new HierarchicalTreeDataGridSource<FileTreeNodeModel>(Array.Empty<FileTreeNodeModel>())
{
Columns =
{
new CheckBoxColumn<FileTreeNodeModel>(
null,
x => x.IsChecked,
(o, v) => o.IsChecked = v,
options: new()
{
CanUserResizeColumn = false,
}),
new HierarchicalExpanderColumn<FileTreeNodeModel>(
new TemplateColumn<FileTreeNodeModel>(
"Name",
"FileNameCell",
"FileNameEditCell",
new GridLength(1, GridUnitType.Star),
new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Name),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Name),
IsTextSearchEnabled = true,
TextSearchValueSelector = x => x.Name
}),
x => x.Children,
x => x.HasChildren,
x => x.IsExpanded),
new TextColumn<FileTreeNodeModel, long?>(
"Size",
x => x.Size,
options: new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Size),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Size),
}),
new TextColumn<FileTreeNodeModel, DateTimeOffset?>(
"Modified",
x => x.Modified,
options: new()
{
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Modified),
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Modified),
}),
}
};

result.RowSelection!.SingleSelect = false;
result.RowSelection.SelectionChanged += SelectionChanged;
return result;
}

private void SetSelectedPath(string? value)
{
if (string.IsNullOrEmpty(value))
{
Source.RowSelection!.Clear();
GetRowSelection(Source).Clear();
return;
}

Expand Down Expand Up @@ -204,12 +280,18 @@ private void SetSelectedPath(string? value)
}
}

Source.RowSelection!.SelectedIndex = index;
GetRowSelection(Source).SelectedIndex = index;
}

private ITreeDataGridRowSelectionModel<FileTreeNodeModel> GetRowSelection(ITreeDataGridSource source)
{
return source.Selection as ITreeDataGridRowSelectionModel<FileTreeNodeModel> ??
throw new InvalidOperationException("Expected a row selection model.");
}

private void SelectionChanged(object? sender, TreeSelectionModelSelectionChangedEventArgs<FileTreeNodeModel> e)
{
var selectedPath = Source.RowSelection?.SelectedItem?.Path;
var selectedPath = GetRowSelection(Source).SelectedItem?.Path;
this.RaiseAndSetIfChanged(ref _selectedPath, selectedPath, nameof(SelectedPath));

foreach (var i in e.DeselectedItems)
Expand Down
8 changes: 4 additions & 4 deletions src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public interface ITreeDataGridSource : INotifyPropertyChanged
IRows Rows { get; }

/// <summary>
/// Gets the selection model.
/// Gets or sets the selection model.
/// </summary>
ITreeDataGridSelection? Selection { get; }
ITreeDataGridSelection? Selection { get; set; }

/// <summary>
/// Gets a value indicating whether the data source is hierarchical.
Expand Down Expand Up @@ -84,8 +84,8 @@ public interface ITreeDataGridSource : INotifyPropertyChanged
public interface ITreeDataGridSource<TModel> : ITreeDataGridSource
{
/// <summary>
/// Gets the items in the data source.
/// Gets or sets the items in the data source.
/// </summary>
new IEnumerable<TModel> Items { get; }
new IEnumerable<TModel> Items { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Avalonia.Controls.Models.TreeDataGrid
/// </summary>
public class ColumnList<TModel> : NotifyingListBase<IColumn<TModel>>, IColumns
{
private bool _initialized;
private double _viewportWidth;

public event EventHandler? LayoutInvalidated;
Expand All @@ -22,6 +23,7 @@ public void AddRange(IEnumerable<IColumn<TModel>> items)
public Size CellMeasured(int columnIndex, int rowIndex, Size size)
{
var column = (IUpdateColumnLayout)this[columnIndex];
_initialized = true;
return new Size(column.CellMeasured(size.Width, rowIndex), size.Height);
}

Expand Down Expand Up @@ -103,7 +105,8 @@ public void ViewportChanged(Rect viewport)
if (_viewportWidth != viewport.Width)
{
_viewportWidth = viewport.Width;
UpdateColumnSizes();
if (_initialized)
UpdateColumnSizes();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ protected virtual void OnEffectiveViewportChanged(object? sender, EffectiveViewp
// viewport.
Viewport = e.EffectiveViewport.Size == default ?
s_invalidViewport :
e.EffectiveViewport.Intersect(new(Bounds.Size));
Intersect(e.EffectiveViewport, new(Bounds.Size));

_isWaitingForViewportUpdate = false;

Expand Down Expand Up @@ -730,6 +730,24 @@ private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs

private static bool HasInfinity(Size s) => double.IsInfinity(s.Width) || double.IsInfinity(s.Height);

private static Rect Intersect(Rect a, Rect b)
{
// Hack fix for https://github.com/AvaloniaUI/Avalonia/issues/15075
var newLeft = (a.X > b.X) ? a.X : b.X;
var newTop = (a.Y > b.Y) ? a.Y : b.Y;
var newRight = (a.Right < b.Right) ? a.Right : b.Right;
var newBottom = (a.Bottom < b.Bottom) ? a.Bottom : b.Bottom;

if ((newRight >= newLeft) && (newBottom >= newTop))
{
return new Rect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
}
else
{
return default;
}
}

private struct MeasureViewport
{
public int anchorIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,46 @@ public void Does_Not_Realize_Columns_Outside_Viewport()
Assert.True(double.IsNaN(columns[3].ActualWidth));
}

[AvaloniaFact(Timeout = 10000)]
public void Columns_Are_Correctly_Sized_After_Changing_Source()
{
// Create the initial target with 2 columns and make sure our preconditions are correct.
var (target, items) = CreateTarget(columns: new IColumn<Model>[]
{
new TextColumn<Model, int>("ID", x => x.Id, width: new GridLength(1, GridUnitType.Star)),
new TextColumn<Model, string?>("Title1", x => x.Title, options: MinWidth(50)),
});

AssertColumnIndexes(target, 0, 2);

// Create a new source and assign it to the TreeDataGrid.
var newSource = new FlatTreeDataGridSource<Model>(items)
{
Columns =
{
new TextColumn<Model, int>("ID", x => x.Id, width: new GridLength(1, GridUnitType.Star)),
new TextColumn<Model, string?>("Title1", x => x.Title, options: MinWidth(20)),
new TextColumn<Model, string?>("Title2", x => x.Title, options: MinWidth(20)),
}
};

target.Source = newSource;

// The columns should not have an ActualWidth yet.
Assert.True(double.IsNaN(newSource.Columns[0].ActualWidth));
Assert.True(double.IsNaN(newSource.Columns[1].ActualWidth));
Assert.True(double.IsNaN(newSource.Columns[2].ActualWidth));

// Do a layout pass and check that the columns have been correctly sized.
target.UpdateLayout();
AssertColumnIndexes(target, 0, 3);

var columns = (ColumnList<Model>)target.Columns!;
Assert.Equal(60, columns[0].ActualWidth);
Assert.Equal(20, columns[1].ActualWidth);
Assert.Equal(20, columns[2].ActualWidth);
}

public class RemoveItems
{
[AvaloniaFact(Timeout = 10000)]
Expand Down