Merge pull request #579 from Mbucari/master
Bug fixes and more shared code moved to UI base
This commit is contained in:
commit
1a95f2923b
@ -23,7 +23,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// <summary>Backing list of all grid entries</summary>
|
/// <summary>Backing list of all grid entries</summary>
|
||||||
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
||||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||||
private List<IGridEntry> FilteredInGridEntries;
|
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||||
public string FilterString { get; private set; }
|
public string FilterString { get; private set; }
|
||||||
public DataGridCollectionView GridEntries { get; private set; }
|
public DataGridCollectionView GridEntries { get; private set; }
|
||||||
|
|
||||||
@ -117,8 +117,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create the filtered-in list before adding entries to avoid a refresh
|
//Create the filtered-in list before adding entries to avoid a refresh
|
||||||
FilteredInGridEntries = QueryResults(geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)), FilterString);
|
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||||
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
|
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
|
||||||
|
|
||||||
//Add all children beneath their parent
|
//Add all children beneath their parent
|
||||||
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
||||||
@ -301,7 +301,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
if (SOURCE.Count == 0)
|
if (SOURCE.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
FilteredInGridEntries = QueryResults(SOURCE, searchString);
|
FilteredInGridEntries = SOURCE.FilterEntries(searchString);
|
||||||
|
|
||||||
await refreshGrid();
|
await refreshGrid();
|
||||||
}
|
}
|
||||||
@ -318,25 +318,11 @@ namespace LibationAvalonia.ViewModels
|
|||||||
return FilteredInGridEntries.Contains(item);
|
return FilteredInGridEntries.Contains(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<IGridEntry> QueryResults(IEnumerable<IGridEntry> entries, string searchString)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(searchString)) return null;
|
|
||||||
|
|
||||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
|
||||||
|
|
||||||
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
|
||||||
|
|
||||||
//Find all series containing children that match the search criteria
|
|
||||||
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
|
||||||
|
|
||||||
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var filterResults = QueryResults(SOURCE, FilterString);
|
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||||
|
|
||||||
if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count)
|
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||||
{
|
{
|
||||||
FilteredInGridEntries = filterResults;
|
FilteredInGridEntries = filterResults;
|
||||||
await refreshGrid();
|
await refreshGrid();
|
||||||
|
|||||||
@ -1,98 +1,28 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
/// <summary>
|
internal class RowComparer : RowComparerBase
|
||||||
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
|
|
||||||
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
|
||||||
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
|
|
||||||
/// properties when 2 items compare equal.
|
|
||||||
/// </summary>
|
|
||||||
internal class RowComparer : IComparer, IComparer<IGridEntry>, IComparer<object>
|
|
||||||
{
|
{
|
||||||
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
public DataGridColumn Column { get; init; }
|
private DataGridColumn Column { get; init; }
|
||||||
public string PropertyName { get; private set; }
|
public override string PropertyName { get; set; }
|
||||||
|
|
||||||
public RowComparer(DataGridColumn column)
|
public RowComparer(DataGridColumn column)
|
||||||
{
|
{
|
||||||
Column = column;
|
Column = column;
|
||||||
PropertyName = Column.SortMemberPath;
|
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||||
}
|
|
||||||
|
|
||||||
public int Compare(object x, object y)
|
|
||||||
{
|
|
||||||
if (x is null && y is not null) return -1;
|
|
||||||
if (x is not null && y is null) return 1;
|
|
||||||
if (x is null && y is null) return 0;
|
|
||||||
|
|
||||||
var geA = (IGridEntry)x;
|
|
||||||
var geB = (IGridEntry)y;
|
|
||||||
|
|
||||||
var sortDirection = GetSortOrder();
|
|
||||||
|
|
||||||
ISeriesEntry parentA = null;
|
|
||||||
ISeriesEntry parentB = null;
|
|
||||||
|
|
||||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
|
||||||
parentA = seA;
|
|
||||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
|
||||||
parentB = seB;
|
|
||||||
|
|
||||||
//both a and b are top-level grid entries
|
|
||||||
if (parentA is null && parentB is null)
|
|
||||||
return InternalCompare(geA, geB);
|
|
||||||
|
|
||||||
//a is top-level, b is a child
|
|
||||||
if (parentA is null && parentB is not null)
|
|
||||||
{
|
|
||||||
// b is a child of a, parent is always first
|
|
||||||
if (parentB == geA)
|
|
||||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
|
||||||
else
|
|
||||||
return InternalCompare(geA, parentB);
|
|
||||||
}
|
|
||||||
|
|
||||||
//a is a child, b is a top-level
|
|
||||||
if (parentA is not null && parentB is null)
|
|
||||||
{
|
|
||||||
// a is a child of b, parent is always first
|
|
||||||
if (parentA == geB)
|
|
||||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
|
||||||
else
|
|
||||||
return InternalCompare(parentA, geB);
|
|
||||||
}
|
|
||||||
|
|
||||||
//both are children of the same series
|
|
||||||
if (parentA == parentB)
|
|
||||||
return InternalCompare(geA, geB);
|
|
||||||
|
|
||||||
//a and b are children of different series.
|
|
||||||
return InternalCompare(parentA, parentB);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||||
private ListSortDirection? GetSortOrder()
|
protected override ListSortDirection GetSortOrder()
|
||||||
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
|
=> Column is null ? ListSortDirection.Descending
|
||||||
|
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||||
private int InternalCompare(IGridEntry x, IGridEntry y)
|
: ListSortDirection.Descending;
|
||||||
{
|
|
||||||
var val1 = x.GetMemberValue(PropertyName);
|
|
||||||
var val2 = y.GetMemberValue(PropertyName);
|
|
||||||
|
|
||||||
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Compare(IGridEntry x, IGridEntry y)
|
|
||||||
{
|
|
||||||
return Compare((object)x, y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ namespace LibationUiBase.GridView
|
|||||||
public abstract object BackgroundBrush { get; }
|
public abstract object BackgroundBrush { get; }
|
||||||
public object ButtonImage => GetLiberateIcon();
|
public object ButtonImage => GetLiberateIcon();
|
||||||
public string ToolTip => GetTooltip();
|
public string ToolTip => GetTooltip();
|
||||||
protected internal Book Book { get; internal set; }
|
private Book Book { get; }
|
||||||
|
|
||||||
private DateTime lastBookUpdate;
|
private DateTime lastBookUpdate;
|
||||||
private LiberatedStatus bookStatus;
|
private LiberatedStatus bookStatus;
|
||||||
|
|||||||
@ -154,9 +154,13 @@ namespace LibationUiBase.GridView
|
|||||||
if (udi.Book.AudibleProductId != Book.AudibleProductId)
|
if (udi.Book.AudibleProductId != Book.AudibleProductId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus),
|
if (udi.Book != LibraryBook.Book)
|
||||||
//EntryStatu's Book instance will not have the current DB state.
|
{
|
||||||
Liberate.Book = udi.Book;
|
//If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus),
|
||||||
|
//Liberate.Book and LibraryBook.Book instances will not have the current DB state.
|
||||||
|
Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
|
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
|
||||||
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
|
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using DataLayer;
|
using ApplicationServices;
|
||||||
|
using DataLayer;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -39,6 +40,26 @@ namespace LibationUiBase.GridView
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool SearchSetsDiffer(this HashSet<IGridEntry>? searchSet, HashSet<IGridEntry>? otherSet)
|
||||||
|
=> searchSet is null != otherSet is null ||
|
||||||
|
(searchSet is not null &&
|
||||||
|
otherSet is not null &&
|
||||||
|
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||||
|
|
||||||
|
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string searchString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(searchString)) return null;
|
||||||
|
|
||||||
|
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||||
|
|
||||||
|
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
||||||
|
|
||||||
|
//Find all series containing children that match the search criteria
|
||||||
|
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||||
|
|
||||||
|
return booksFilteredIn.Concat(seriesFilteredIn).ToHashSet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#nullable disable
|
#nullable disable
|
||||||
}
|
}
|
||||||
|
|||||||
91
Source/LibationUiBase/GridView/RowComparerBase.cs
Normal file
91
Source/LibationUiBase/GridView/RowComparerBase.cs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace LibationUiBase.GridView
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
|
||||||
|
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
||||||
|
/// sorted by series index, ascending.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||||
|
{
|
||||||
|
public abstract string PropertyName { get; set; }
|
||||||
|
|
||||||
|
public int Compare(object x, object y)
|
||||||
|
{
|
||||||
|
if (x is null && y is not null) return -1;
|
||||||
|
if (x is not null && y is null) return 1;
|
||||||
|
if (x is null && y is null) return 0;
|
||||||
|
|
||||||
|
var geA = (IGridEntry)x;
|
||||||
|
var geB = (IGridEntry)y;
|
||||||
|
|
||||||
|
var sortDirection = GetSortOrder();
|
||||||
|
|
||||||
|
ISeriesEntry parentA = null;
|
||||||
|
ISeriesEntry parentB = null;
|
||||||
|
|
||||||
|
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||||
|
parentA = seA;
|
||||||
|
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||||
|
parentB = seB;
|
||||||
|
|
||||||
|
//both a and b are top-level grid entries
|
||||||
|
if (parentA is null && parentB is null)
|
||||||
|
return InternalCompare(geA, geB);
|
||||||
|
|
||||||
|
//a is top-level, b is a child
|
||||||
|
if (parentA is null && parentB is not null)
|
||||||
|
{
|
||||||
|
// b is a child of a, parent is always first
|
||||||
|
if (parentB == geA)
|
||||||
|
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||||
|
else
|
||||||
|
return InternalCompare(geA, parentB);
|
||||||
|
}
|
||||||
|
|
||||||
|
//a is a child, b is a top-level
|
||||||
|
if (parentA is not null && parentB is null)
|
||||||
|
{
|
||||||
|
// a is a child of b, parent is always first
|
||||||
|
if (parentA == geB)
|
||||||
|
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||||
|
else
|
||||||
|
return InternalCompare(parentA, geB);
|
||||||
|
}
|
||||||
|
|
||||||
|
//both are children of the same series
|
||||||
|
if (parentA == parentB)
|
||||||
|
{
|
||||||
|
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
||||||
|
//the date that the series was added to the library. So when sorting by PurchaseDate
|
||||||
|
//and DateAdded, compare SeriesOrder instead..
|
||||||
|
return PropertyName switch
|
||||||
|
{
|
||||||
|
nameof(IGridEntry.DateAdded) or nameof (IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||||
|
_ => InternalCompare(geA, geB),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//a and b are children of different series.
|
||||||
|
return InternalCompare(parentA, parentB);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract ListSortDirection GetSortOrder();
|
||||||
|
|
||||||
|
private int InternalCompare(IGridEntry x, IGridEntry y)
|
||||||
|
{
|
||||||
|
var val1 = x.GetMemberValue(PropertyName);
|
||||||
|
var val2 = y.GetMemberValue(PropertyName);
|
||||||
|
|
||||||
|
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(IGridEntry x, IGridEntry y)
|
||||||
|
{
|
||||||
|
return Compare((object)x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,7 +47,7 @@ namespace LibationUiBase.GridView
|
|||||||
|
|
||||||
Children = children
|
Children = children
|
||||||
.Select(c => new LibraryBookEntry<TStatus>(c, this))
|
.Select(c => new LibraryBookEntry<TStatus>(c, this))
|
||||||
.OrderBy(c => c.SeriesIndex)
|
.OrderByDescending(c => c.SeriesOrder)
|
||||||
.ToList<ILibraryBookEntry>();
|
.ToList<ILibraryBookEntry>();
|
||||||
|
|
||||||
UpdateLibraryBook(parent);
|
UpdateLibraryBook(parent);
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Dinah.Core.DataBinding;
|
|
||||||
using LibationSearchEngine;
|
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -20,51 +18,62 @@ namespace LibationWinForms.GridView
|
|||||||
*
|
*
|
||||||
* Remove is overridden to ensure that removed items are removed from
|
* Remove is overridden to ensure that removed items are removed from
|
||||||
* the base list (visible items) as well as the FilterRemoved list.
|
* the base list (visible items) as well as the FilterRemoved list.
|
||||||
|
*
|
||||||
|
* Using BindingList.Add/Insert and BindingList.Remove will cause the
|
||||||
|
* BindingList to subscribe/unsibscribe to/from the item's PropertyChanged
|
||||||
|
* event. Adding or removing from the underlying list will not change the
|
||||||
|
* BindingList's subscription to that item.
|
||||||
*/
|
*/
|
||||||
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
|
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
|
||||||
{
|
{
|
||||||
public GridEntryBindingList() : base(new List<IGridEntry>()) { }
|
|
||||||
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration))
|
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration))
|
||||||
{
|
{
|
||||||
SearchEngineCommands.SearchEngineUpdated += (_,_) => ApplyFilter(FilterString);
|
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||||
|
ListChanged += GridEntryBindingList_ListChanged;
|
||||||
|
refreshEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <returns>All items in the list, including those filtered out.</returns>
|
/// <returns>All items in the list, including those filtered out.</returns>
|
||||||
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||||
|
|
||||||
/// <summary>All items that pass the current filter</summary>
|
/// <summary>All items that pass the current filter</summary>
|
||||||
public IEnumerable<ILibraryBookEntry> GetFilteredInItems()
|
public IEnumerable<ILibraryBookEntry> GetFilteredInItems()
|
||||||
=> SearchResults is null
|
=> FilteredInGridEntries?
|
||||||
? FilterRemoved
|
|
||||||
.OfType<ILibraryBookEntry>()
|
.OfType<ILibraryBookEntry>()
|
||||||
.Union(Items.OfType<ILibraryBookEntry>())
|
?? FilterRemoved
|
||||||
: FilterRemoved
|
|
||||||
.OfType<ILibraryBookEntry>()
|
.OfType<ILibraryBookEntry>()
|
||||||
.Join(SearchResults.Docs, o => o.Book.AudibleProductId, i => i.ProductId, (o, _) => o)
|
|
||||||
.Union(Items.OfType<ILibraryBookEntry>());
|
.Union(Items.OfType<ILibraryBookEntry>());
|
||||||
|
|
||||||
public bool SupportsFiltering => true;
|
public bool SupportsFiltering => true;
|
||||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
public string Filter
|
||||||
|
{
|
||||||
|
get => FilterString;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
FilterString = value;
|
||||||
|
|
||||||
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
|
if (Items.Count + FilterRemoved.Count == 0)
|
||||||
public bool SuspendFilteringOnUpdate { get; set; }
|
return;
|
||||||
|
|
||||||
protected MemberComparer<IGridEntry> Comparer { get; } = new();
|
FilteredInGridEntries = AllItems().FilterEntries(FilterString);
|
||||||
|
refreshEntries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RowComparer Comparer { get; } = new();
|
||||||
protected override bool SupportsSortingCore => true;
|
protected override bool SupportsSortingCore => true;
|
||||||
protected override bool SupportsSearchingCore => true;
|
protected override bool SupportsSearchingCore => true;
|
||||||
protected override bool IsSortedCore => isSorted;
|
protected override bool IsSortedCore => isSorted;
|
||||||
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
|
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
|
||||||
protected override ListSortDirection SortDirectionCore => listSortDirection;
|
protected override ListSortDirection SortDirectionCore => Comparer.SortOrder;
|
||||||
|
|
||||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||||
private readonly List<IGridEntry> FilterRemoved = new();
|
private readonly List<IGridEntry> FilterRemoved = new();
|
||||||
private string FilterString;
|
private string FilterString;
|
||||||
private SearchResultSet SearchResults;
|
|
||||||
private bool isSorted;
|
private bool isSorted;
|
||||||
private ListSortDirection listSortDirection;
|
|
||||||
private PropertyDescriptor propertyDescriptor;
|
private PropertyDescriptor propertyDescriptor;
|
||||||
|
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
|
||||||
|
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||||
|
|
||||||
#region Unused - Advanced Filtering
|
#region Unused - Advanced Filtering
|
||||||
public bool SupportsAdvancedSorting => false;
|
public bool SupportsAdvancedSorting => false;
|
||||||
@ -82,25 +91,57 @@ namespace LibationWinForms.GridView
|
|||||||
base.Remove(entry);
|
base.Remove(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyFilter(string filterString)
|
/// <summary>
|
||||||
|
/// This method should be called whenever there's been a change to the
|
||||||
|
/// set of all GridEntries that affects sort order or filter status
|
||||||
|
/// </summary>
|
||||||
|
private void refreshEntries()
|
||||||
{
|
{
|
||||||
if (filterString != FilterString)
|
if (FilteredInGridEntries is null)
|
||||||
RemoveFilter();
|
|
||||||
|
|
||||||
FilterString = filterString;
|
|
||||||
SearchResults = SearchEngineCommands.Search(filterString);
|
|
||||||
|
|
||||||
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
|
||||||
|
|
||||||
//Find all series containing children that match the search criteria
|
|
||||||
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
|
||||||
|
|
||||||
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
|
||||||
|
|
||||||
foreach (var item in filteredOut)
|
|
||||||
{
|
{
|
||||||
FilterRemoved.Add(item);
|
addRemovedItemsBack(FilterRemoved.ToList());
|
||||||
base.Remove(item);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var addBackEntries = FilterRemoved.Intersect(FilteredInGridEntries).ToList();
|
||||||
|
var toRemoveEntries = Items.Except(FilteredInGridEntries).ToList();
|
||||||
|
|
||||||
|
addRemovedItemsBack(addBackEntries);
|
||||||
|
|
||||||
|
foreach (var newRemove in toRemoveEntries)
|
||||||
|
{
|
||||||
|
FilterRemoved.Add(newRemove);
|
||||||
|
base.Remove(newRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SortInternal();
|
||||||
|
|
||||||
|
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||||
|
|
||||||
|
void addRemovedItemsBack(List<IGridEntry> addBackEntries)
|
||||||
|
{
|
||||||
|
//Add removed entries back into Items so they are displayed
|
||||||
|
//(except for episodes that are collapsed)
|
||||||
|
foreach (var addBack in addBackEntries)
|
||||||
|
{
|
||||||
|
if (addBack is ILibraryBookEntry lbe && lbe.Parent is ISeriesEntry se && !se.Liberate.Expanded)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
FilterRemoved.Remove(addBack);
|
||||||
|
Add(addBack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var filterResults = AllItems().FilterEntries(FilterString);
|
||||||
|
|
||||||
|
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||||
|
{
|
||||||
|
FilteredInGridEntries = filterResults;
|
||||||
|
refreshEntries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +159,7 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
public void CollapseItem(ISeriesEntry sEntry)
|
public void CollapseItem(ISeriesEntry sEntry)
|
||||||
{
|
{
|
||||||
foreach (var episode in sEntry.Children.Join(Items.BookEntries(), o => o, i => i, (_, i) => i).ToList())
|
foreach (var episode in sEntry.Children.Intersect(Items.BookEntries()).ToList())
|
||||||
{
|
{
|
||||||
FilterRemoved.Add(episode);
|
FilterRemoved.Add(episode);
|
||||||
base.Remove(episode);
|
base.Remove(episode);
|
||||||
@ -131,9 +172,9 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
var sindex = Items.IndexOf(sEntry);
|
var sindex = Items.IndexOf(sEntry);
|
||||||
|
|
||||||
foreach (var episode in sEntry.Children.Join(FilterRemoved.BookEntries(), o => o, i => i, (_, i) => i).ToList())
|
foreach (var episode in Comparer.OrderEntries(sEntry.Children.Intersect(FilterRemoved.BookEntries())).ToList())
|
||||||
{
|
{
|
||||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
if (FilteredInGridEntries?.Contains(episode) ?? true)
|
||||||
{
|
{
|
||||||
FilterRemoved.Remove(episode);
|
FilterRemoved.Remove(episode);
|
||||||
InsertItem(++sindex, episode);
|
InsertItem(++sindex, episode);
|
||||||
@ -144,106 +185,45 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
public void RemoveFilter()
|
public void RemoveFilter()
|
||||||
{
|
{
|
||||||
if (FilterString is null) return;
|
|
||||||
|
|
||||||
int visibleCount = Items.Count;
|
|
||||||
|
|
||||||
foreach (var item in FilterRemoved.ToList())
|
|
||||||
{
|
|
||||||
if (item is ISeriesEntry || (item is ILibraryBookEntry lbe && (lbe.Liberate.IsBook || lbe.Parent.Liberate.Expanded)))
|
|
||||||
{
|
|
||||||
FilterRemoved.Remove(item);
|
|
||||||
InsertItem(visibleCount++, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsSortedCore)
|
|
||||||
Sort();
|
|
||||||
else
|
|
||||||
//No user sort is applied, so do default sorting by DateAdded, descending
|
|
||||||
{
|
|
||||||
Comparer.PropertyName = nameof(IGridEntry.DateAdded);
|
|
||||||
Comparer.Direction = ListSortDirection.Descending;
|
|
||||||
Sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
|
||||||
|
|
||||||
FilterString = null;
|
FilterString = null;
|
||||||
SearchResults = null;
|
FilteredInGridEntries = null;
|
||||||
|
refreshEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
|
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
|
||||||
{
|
{
|
||||||
Comparer.PropertyName = property.Name;
|
Comparer.PropertyName = property.Name;
|
||||||
Comparer.Direction = direction;
|
Comparer.SortOrder = direction;
|
||||||
|
|
||||||
Sort();
|
SortInternal();
|
||||||
|
|
||||||
propertyDescriptor = property;
|
propertyDescriptor = property;
|
||||||
listSortDirection = direction;
|
|
||||||
isSorted = true;
|
isSorted = true;
|
||||||
|
|
||||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Sort()
|
private void SortInternal()
|
||||||
{
|
{
|
||||||
var itemsList = (List<IGridEntry>)Items;
|
var itemsList = (List<IGridEntry>)Items;
|
||||||
|
//User Order/OrderDescending and replace items in list instead of using List.Sort() to achieve stable sorting.
|
||||||
var children = itemsList.BookEntries().Where(i => i.Liberate.IsEpisode).ToList();
|
var sortedItems = Comparer.OrderEntries(itemsList).ToList();
|
||||||
|
|
||||||
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
|
||||||
|
|
||||||
itemsList.Clear();
|
itemsList.Clear();
|
||||||
|
|
||||||
//Only add parentless items at this stage. After these items are added in the
|
|
||||||
//correct sorting order, go back and add the children beneath their parents.
|
|
||||||
itemsList.AddRange(sortedItems);
|
itemsList.AddRange(sortedItems);
|
||||||
|
|
||||||
foreach (var parent in children.Select(c => c.Parent).Distinct())
|
|
||||||
{
|
|
||||||
var pIndex = itemsList.IndexOf(parent);
|
|
||||||
|
|
||||||
//children are sorted beneath their series parent
|
|
||||||
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c, Comparer))
|
|
||||||
itemsList.Insert(++pIndex, c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnListChanged(ListChangedEventArgs e)
|
private void GridEntryBindingList_ListChanged(object sender, ListChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.ListChangedType == ListChangedType.ItemChanged)
|
if (e.ListChangedType == ListChangedType.ItemChanged && IsSortedCore && e.PropertyDescriptor == SortPropertyCore)
|
||||||
{
|
refreshEntries();
|
||||||
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is ILibraryBookEntry lbItem)
|
|
||||||
{
|
|
||||||
SearchResults = SearchEngineCommands.Search(FilterString);
|
|
||||||
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))
|
|
||||||
{
|
|
||||||
FilterRemoved.Add(lbItem);
|
|
||||||
base.Remove(lbItem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSorted && e.PropertyDescriptor == SortPropertyCore)
|
|
||||||
{
|
|
||||||
var item = Items[e.NewIndex];
|
|
||||||
Sort();
|
|
||||||
var newIndex = Items.IndexOf(item);
|
|
||||||
|
|
||||||
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
base.OnListChanged(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RemoveSortCore()
|
protected override void RemoveSortCore()
|
||||||
{
|
{
|
||||||
isSorted = false;
|
isSorted = false;
|
||||||
propertyDescriptor = base.SortPropertyCore;
|
propertyDescriptor = base.SortPropertyCore;
|
||||||
listSortDirection = base.SortDirectionCore;
|
Comparer.SortOrder = base.SortDirectionCore;
|
||||||
|
|
||||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,7 +181,7 @@ namespace LibationWinForms.GridView
|
|||||||
geList.AddRange(seriesEntry.Children);
|
geList.AddRange(seriesEntry.Children);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
bindingList = new GridEntryBindingList(geList);
|
||||||
bindingList.CollapseAll();
|
bindingList.CollapseAll();
|
||||||
syncBindingSource.DataSource = bindingList;
|
syncBindingSource.DataSource = bindingList;
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
||||||
@ -195,8 +195,6 @@ namespace LibationWinForms.GridView
|
|||||||
string existingFilter = syncBindingSource.Filter;
|
string existingFilter = syncBindingSource.Filter;
|
||||||
Filter(null);
|
Filter(null);
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = true;
|
|
||||||
|
|
||||||
//Add absent entries to grid, or update existing entry
|
//Add absent entries to grid, or update existing entry
|
||||||
|
|
||||||
var allEntries = bindingList.AllItems().BookEntries();
|
var allEntries = bindingList.AllItems().BookEntries();
|
||||||
@ -219,8 +217,6 @@ namespace LibationWinForms.GridView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingList.SuspendFilteringOnUpdate = false;
|
|
||||||
|
|
||||||
//Re-apply filter after adding new/updating existing books to capture any changes
|
//Re-apply filter after adding new/updating existing books to capture any changes
|
||||||
Filter(existingFilter);
|
Filter(existingFilter);
|
||||||
|
|
||||||
|
|||||||
20
Source/LibationWinForms/GridView/RowComparer.cs
Normal file
20
Source/LibationWinForms/GridView/RowComparer.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using LibationUiBase.GridView;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LibationWinForms.GridView
|
||||||
|
{
|
||||||
|
internal class RowComparer : RowComparerBase
|
||||||
|
{
|
||||||
|
public ListSortDirection SortOrder { get; set; } = ListSortDirection.Descending;
|
||||||
|
public override string PropertyName { get; set; } = nameof(IGridEntry.DateAdded);
|
||||||
|
protected override ListSortDirection GetSortOrder() => SortOrder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method for ordering grid entries
|
||||||
|
/// </summary>
|
||||||
|
public IOrderedEnumerable<IGridEntry> OrderEntries(IEnumerable<IGridEntry> entries)
|
||||||
|
=> SortOrder is ListSortDirection.Descending ? entries.OrderDescending(this) : entries.Order(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user