From 3b42b52ff4e2489adeefff16e248aa1229de9a8c Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Mon, 11 Jul 2022 19:07:20 -0600 Subject: [PATCH] Improve sorting --- Source/LibationWinForms/AvaloniaUI/App.axaml | 2 +- .../LibationWinForms/AvaloniaUI/App.axaml.cs | 7 +- .../AvaloniaUI/Assets/DataGridTheme.xaml | 658 ++++++++++++++++++ .../DataGridCheckBoxColumnExt.axaml.cs | 27 +- .../ViewModels/GridEntryBindingList2.cs | 103 +-- .../ViewModels/ItemsRepeaterPageViewModel.cs | 160 ----- .../ViewModels/MainWindowViewModel.cs | 47 -- .../ViewModels/ProductsDisplayViewModel.cs | 8 +- .../MainWindow.BackupCounts.axaml.cs | 2 +- .../MainWindow.QuickFilters.axaml.cs | 4 +- .../MainWindow.RemoveBooks.axaml.cs | 5 +- .../MainWindow/MainWindow.ScanAuto.axaml.cs | 4 +- .../MainWindow/MainWindow.ScanManual.axaml.cs | 19 +- .../Views/MainWindow/MainWindow.axaml | 4 +- .../Views/MainWindow/MainWindow.axaml.cs | 51 +- .../AvaloniaUI/Views/ProductsDisplay2.axaml | 23 +- .../Views/ProductsDisplay2.axaml.cs | 129 +++- .../LibationWinForms/LibationWinForms.csproj | 1 + Source/LibationWinForms/Program.cs | 9 +- 19 files changed, 852 insertions(+), 411 deletions(-) create mode 100644 Source/LibationWinForms/AvaloniaUI/Assets/DataGridTheme.xaml delete mode 100644 Source/LibationWinForms/AvaloniaUI/ViewModels/ItemsRepeaterPageViewModel.cs delete mode 100644 Source/LibationWinForms/AvaloniaUI/ViewModels/MainWindowViewModel.cs diff --git a/Source/LibationWinForms/AvaloniaUI/App.axaml b/Source/LibationWinForms/AvaloniaUI/App.axaml index dadc8c76..3526c826 100644 --- a/Source/LibationWinForms/AvaloniaUI/App.axaml +++ b/Source/LibationWinForms/AvaloniaUI/App.axaml @@ -11,6 +11,6 @@ - + \ No newline at end of file diff --git a/Source/LibationWinForms/AvaloniaUI/App.axaml.cs b/Source/LibationWinForms/AvaloniaUI/App.axaml.cs index 44aab666..2351216d 100644 --- a/Source/LibationWinForms/AvaloniaUI/App.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/App.axaml.cs @@ -16,10 +16,9 @@ namespace LibationWinForms.AvaloniaUI { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow - { - - }; + var mainWindow = new MainWindow(); + desktop.MainWindow = mainWindow; + mainWindow.OnLoad(); } base.OnFrameworkInitializationCompleted(); diff --git a/Source/LibationWinForms/AvaloniaUI/Assets/DataGridTheme.xaml b/Source/LibationWinForms/AvaloniaUI/Assets/DataGridTheme.xaml new file mode 100644 index 00000000..c10afcf4 --- /dev/null +++ b/Source/LibationWinForms/AvaloniaUI/Assets/DataGridTheme.xaml @@ -0,0 +1,658 @@ + + + 0.6 + 0.8 + 12,0,12,0 + + M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z + M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z + M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs index f70a91c0..be8157a0 100644 --- a/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs @@ -1,32 +1,35 @@ - -using Avalonia; using Avalonia.Controls; -using Avalonia.Data; using Avalonia.Interactivity; -using Avalonia.Styling; using LibationWinForms.AvaloniaUI.ViewModels; -using System; namespace LibationWinForms.AvaloniaUI.Controls { + /// The purpose of this extension is to immediately commit any check state changes to the viewmodel public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn { - protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) - { - return base.PrepareCellForEdit(editingElement, editingEventArgs); - } protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem) { var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox; ele.Checked += EditingElement_Checked; + ele.Unchecked += EditingElement_Checked; + ele.Indeterminate += EditingElement_Checked; return ele; } private void EditingElement_Checked(object sender, RoutedEventArgs e) { - var cbox = sender as CheckBox; - var gEntry = cbox.DataContext as GridEntry2; - gEntry.Remove = cbox.IsChecked; + if (sender is CheckBox cbox && cbox.DataContext is GridEntry2 gentry) + { + gentry.Remove = cbox.IsChecked; + FindDataGridParent(cbox)?.CommitEdit(DataGridEditingUnit.Cell, false); + } + } + + DataGrid? FindDataGridParent(IControl? control) + { + if (control?.Parent is null) return null; + else if (control?.Parent is DataGrid dg) return dg; + else return FindDataGridParent(control?.Parent); } } } diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs index 1de83350..8e7570b4 100644 --- a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs +++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs @@ -25,71 +25,45 @@ namespace LibationWinForms.AvaloniaUI.ViewModels */ public class GridEntryBindingList2 : ObservableCollection { - public GridEntryBindingList2(IEnumerable enumeration) : base(new List(enumeration)) - { - foreach (var item in enumeration) - item.PropertyChanged += Item_PropertyChanged; - } + public GridEntryBindingList2(IEnumerable enumeration) + : base(new List(enumeration)) { } + public GridEntryBindingList2(List list) + : base(list) { } + + public List InternalList => Items as List; /// All items in the list, including those filtered out. public List AllItems() => Items.Concat(FilterRemoved).ToList(); /// When true, itms will not be checked filtered by search criteria on item changed public bool SuspendFilteringOnUpdate { get; set; } public string Filter { get => FilterString; set => ApplyFilter(value); } - protected MemberComparer Comparer { get; } = new(); /// Items that were removed from the base list due to filtering private readonly List FilterRemoved = new(); private string FilterString; private SearchResultSet SearchResults; - private bool isSorted; #region Items Management public new void Remove(GridEntry2 entry) { - entry.PropertyChanged -= Item_PropertyChanged; FilterRemoved.Add(entry); base.Remove(entry); } - protected override void RemoveItem(int index) - { - var item = Items[index]; - item.PropertyChanged -= Item_PropertyChanged; - base.RemoveItem(index); - } - - protected override void ClearItems() - { - foreach (var item in Items) - item.PropertyChanged -= Item_PropertyChanged; - base.ClearItems(); - } - protected override void InsertItem(int index, GridEntry2 item) { - item.PropertyChanged += Item_PropertyChanged; FilterRemoved.Remove(item); base.InsertItem(index, item); } - private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e) - { - //Don't audo-sort Remove column or else Avalonia will crash. - if (isSorted && e.PropertyName == Comparer.PropertyName && e.PropertyName != nameof(GridEntry.Remove)) - { - Sort(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - return; - } - } - #endregion #region Filtering + public void ResetCollection() + => OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + private void ApplyFilter(string filterString) { if (filterString != FilterString) @@ -125,18 +99,6 @@ namespace LibationWinForms.AvaloniaUI.ViewModels } } - if (isSorted) - Sort(); - else - { - //No user sort is applied, so do default sorting by DateAdded, descending - Comparer.PropertyName = nameof(GridEntry.DateAdded); - Comparer.Direction = ListSortDirection.Descending; - Sort(); - } - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - FilterString = null; SearchResults = null; } @@ -182,52 +144,5 @@ namespace LibationWinForms.AvaloniaUI.ViewModels } #endregion - - #region Sorting - - public void DoSortCore(string propertyName) - { - if (isSorted && Comparer.PropertyName == propertyName) - { - Comparer.Direction = ~Comparer.Direction & ListSortDirection.Descending; - } - else - { - Comparer.PropertyName = propertyName; - Comparer.Direction = ListSortDirection.Descending; - } - - Sort(); - - isSorted = true; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - protected void Sort() - { - var itemsList = (List)Items; - - var children = itemsList.BookEntries().Where(i => i.Parent is not null).ToList(); - - var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList(); - - 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); - - foreach (var parent in children.Select(c => c.Parent).Distinct()) - { - var pIndex = itemsList.IndexOf(parent); - - //children should always be sorted by series index. - foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c.SeriesIndex)) - itemsList.Insert(++pIndex, c); - } - } - - #endregion } } diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/ItemsRepeaterPageViewModel.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/ItemsRepeaterPageViewModel.cs deleted file mode 100644 index 22f14569..00000000 --- a/Source/LibationWinForms/AvaloniaUI/ViewModels/ItemsRepeaterPageViewModel.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Collections.ObjectModel; -using Avalonia.Media; -using ReactiveUI; - -namespace LibationWinForms.AvaloniaUI.ViewModels -{ - public class ProcessQueueItems : ObservableCollection - { - public ProcessQueueItems(IEnumerable items) :base(items) { } - - public void MoveFirst(ItemsRepeaterPageViewModel.Item item) - { - var index = Items.IndexOf(item); - if (index < 1) return; - - Move(index, 0); - } - public void MoveUp(ItemsRepeaterPageViewModel.Item item) - { - var index = Items.IndexOf(item); - if (index < 1) return; - - Move(index, index - 1); - } - public void MoveDown(ItemsRepeaterPageViewModel.Item item) - { - var index = Items.IndexOf(item); - if (index < 0 || index > Items.Count - 2) return; - - Move(index, index + 1); - } - - public void MoveLast(ItemsRepeaterPageViewModel.Item item) - { - var index = Items.IndexOf(item); - if (index < 0 || index > Items.Count - 2) return; - - Move(index, Items.Count - 1); - } - } - - - public class ItemsRepeaterPageViewModel : ViewModelBase - { - private int _newItemIndex = 1; - private int _newGenerationIndex = 0; - private ProcessQueueItems _items; - - public ItemsRepeaterPageViewModel() - { - _items = CreateItems(); - } - - public ProcessQueueItems Items - { - get => _items; - set => this.RaiseAndSetIfChanged(ref _items, value); - } - - public Item? SelectedItem { get; set; } - - public void AddItem() - { - var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1; - Items.Insert(index + 1, new Item(index + 1, $"New Item {_newItemIndex++}")); - } - - public void RemoveItem() - { - if (SelectedItem is not null) - { - Items.Remove(SelectedItem); - SelectedItem = null; - } - else if (Items.Count > 0) - { - Items.RemoveAt(Items.Count - 1); - } - } - - public void RandomizeHeights() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetItems() - { - Items = CreateItems(); - } - - private ProcessQueueItems CreateItems() - { - var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]"; - - _newGenerationIndex++; - - return new ProcessQueueItems( - Enumerable.Range(1, 100).Select(i => new Item(i, $"Item {i.ToString()} {suffix}"))); - } - - public class Item : ViewModelBase - { - private double _height = double.NaN; - static Random rnd = new Random(); - - public Item(int index, string text) - { - Index = index; - Text = text; - Narrator = "Narrator " + index; - Author = "Author " + index; - Title = "Book " + index + ": This is a book title.\r\nThis is line 2 of the book title"; - - Progress = rnd.Next(0, 101); - ETA = "ETA: 01:14"; - - IsDownloading = rnd.Next(0, 2) == 0; - - if (!IsDownloading) - IsFinished = rnd.Next(0, 2) == 0; - - if (IsDownloading) - Title += "\r\nDOWNLOADING"; - else if (IsFinished) - Title += "\r\nFINISHED"; - else - Title += "\r\nQUEUED"; - } - - public bool IsFinished { get; } - public bool IsDownloading { get; } - public bool Queued => !IsFinished && !IsDownloading; - - - public int Index { get; } - public string Text { get; } - public string ETA { get; } - public string Narrator { get; } - public string Author { get; } - public string Title { get; } - public int Progress { get; } - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } - } -} diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/MainWindowViewModel.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 8f281313..00000000 --- a/Source/LibationWinForms/AvaloniaUI/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,47 +0,0 @@ -using ApplicationServices; -using Avalonia.Collections; -using DataLayer; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Text; - -namespace LibationWinForms.AvaloniaUI.ViewModels -{ - public class MainWindowViewModel : ViewModelBase - { - public string Greeting => "Welcome to Avalonia!"; - public GridEntryBindingList2 People { get; set; } - public MainWindowViewModel(IEnumerable dbBooks) - { - var geList = dbBooks - .Where(lb => lb.Book.IsProduct()) - .Select(b => new LibraryBookEntry2(b)) - .Cast() - .ToList(); - - var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()); - - var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList(); - - foreach (var parent in seriesBooks) - { - var seriesEpisodes = episodes.FindChildren(parent); - - if (!seriesEpisodes.Any()) continue; - - var seriesEntry = new SeriesEntrys2(parent, seriesEpisodes); - - geList.Add(seriesEntry); - geList.AddRange(seriesEntry.Children); - } - - People = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded)); - People.CollapseAll(); - - } - } - -} diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs index a804b9db..d0cd021f 100644 --- a/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/ProductsDisplayViewModel.cs @@ -12,8 +12,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels { public class ProductsDisplayViewModel : ViewModelBase { - public string Greeting => "Welcome to Avalonia!"; - public GridEntryBindingList2 People { get; set; } + public GridEntryBindingList2 GridEntries { get; set; } public ProductsDisplayViewModel(IEnumerable dbBooks) { var geList = dbBooks @@ -38,9 +37,8 @@ namespace LibationWinForms.AvaloniaUI.ViewModels geList.AddRange(seriesEntry.Children); } - People = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded)); - People.CollapseAll(); + GridEntries = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded)); + GridEntries.CollapseAll(); } } - } diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.BackupCounts.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.BackupCounts.axaml.cs index 0cfbad94..ace398d6 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.BackupCounts.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.BackupCounts.axaml.cs @@ -20,7 +20,7 @@ namespace LibationWinForms.AvaloniaUI.Views beginPdfBackupsToolStripMenuItem.Format(0); pdfsCountsLbl.Text = "| [Calculating backed up PDFs]"; - Opened += setBackupCounts; + Load += setBackupCounts; LibraryCommands.LibrarySizeChanged += setBackupCounts; LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.QuickFilters.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.QuickFilters.axaml.cs index 061a9f58..58a725d0 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.QuickFilters.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.QuickFilters.axaml.cs @@ -14,8 +14,8 @@ namespace LibationWinForms.AvaloniaUI.Views { private void Configure_QuickFilters() { - Opened += updateFirstFilterIsDefaultToolStripMenuItem; - Opened += updateFiltersMenu; + Load += updateFirstFilterIsDefaultToolStripMenuItem; + Load += updateFiltersMenu; QuickFilters.UseDefaultChanged += updateFirstFilterIsDefaultToolStripMenuItem; QuickFilters.Updated += updateFiltersMenu; } diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.RemoveBooks.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.RemoveBooks.axaml.cs index 1043eb25..e078ad25 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.RemoveBooks.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.RemoveBooks.axaml.cs @@ -1,11 +1,7 @@ using AudibleUtilities; -using Avalonia.Controls; using LibationWinForms.Dialogs; using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace LibationWinForms.AvaloniaUI.Views { @@ -16,6 +12,7 @@ namespace LibationWinForms.AvaloniaUI.Views { removeBooksBtn.IsVisible = false; doneRemovingBtn.IsVisible = false; + removeLibraryBooksToolStripMenuItem.Click += removeLibraryBooksToolStripMenuItem_Click; } public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanAuto.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanAuto.axaml.cs index 0f3a0f3a..d325a734 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanAuto.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanAuto.axaml.cs @@ -46,9 +46,9 @@ namespace LibationWinForms.AvaloniaUI.Views }; // load init state to menu checkbox - Opened += updateAutoScanLibraryToolStripMenuItem; + Load += updateAutoScanLibraryToolStripMenuItem; // if enabled: begin on load - Opened += startAutoScan; + Load += startAutoScan; // if new 'default' account is added, run autoscan AccountsSettingsPersister.Saving += accountsPreSave; diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanManual.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanManual.axaml.cs index bf012a24..47efab3d 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanManual.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.ScanManual.axaml.cs @@ -16,7 +16,7 @@ namespace LibationWinForms.AvaloniaUI.Views { private void Configure_ScanManual() { - Opened += refreshImportMenu; + Load += refreshImportMenu; AccountsSettingsPersister.Saved += refreshImportMenu; } @@ -33,8 +33,21 @@ namespace LibationWinForms.AvaloniaUI.Views scanLibraryOfSomeAccountsToolStripMenuItem.IsVisible = count > 1; removeLibraryBooksToolStripMenuItem.IsVisible = count > 0; - removeSomeAccountsToolStripMenuItem.IsVisible = count > 1; - removeAllAccountsToolStripMenuItem.IsVisible = count > 1; + + //Avalonia will not fire the Click event for a MenuItem with children, + //so if only 1 account, remove the children. Otherwise add children + //for multiple accounts. + removeLibraryBooksToolStripMenuItem.Items = null; + + if (count > 1) + { + removeLibraryBooksToolStripMenuItem.Items = + new List + { + removeSomeAccountsToolStripMenuItem, + removeAllAccountsToolStripMenuItem + }; + } } public void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml index 8cfe327b..633f312b 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml @@ -90,11 +90,11 @@ + Name="productsDisplay" /> diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs index d2254916..681fb3b7 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs @@ -43,58 +43,13 @@ namespace LibationWinForms.AvaloniaUI.Views Configure_NonUI(); { + this.Load += (_, _) => productsDisplay.Display(); LibraryCommands.LibrarySizeChanged += (_, __) => Dispatcher.UIThread.Post(() => productsDisplay.Display()); } } - /* - MenuItem importToolStripMenuItem; - MenuItem autoScanLibraryToolStripMenuItem; - CheckBox autoScanLibraryToolStripMenuItemCheckbox; - MenuItem noAccountsYetAddAccountToolStripMenuItem; - MenuItem scanLibraryToolStripMenuItem; - MenuItem scanLibraryOfAllAccountsToolStripMenuItem; - MenuItem scanLibraryOfSomeAccountsToolStripMenuItem; - MenuItem removeLibraryBooksToolStripMenuItem; - MenuItem removeAllAccountsToolStripMenuItem; - MenuItem removeSomeAccountsToolStripMenuItem; - MenuItem liberateToolStripMenuItem; - MenuItem beginBookBackupsToolStripMenuItem; - MenuItem beginPdfBackupsToolStripMenuItem; - MenuItem convertAllM4bToMp3ToolStripMenuItem; - MenuItem liberateVisibleToolStripMenuItem_LiberateMenu; - MenuItem exportToolStripMenuItem; - MenuItem exportLibraryToolStripMenuItem; - MenuItem quickFiltersToolStripMenuItem; - MenuItem firstFilterIsDefaultToolStripMenuItem; - CheckBox firstFilterIsDefaultToolStripMenuItem_Checkbox; - MenuItem editQuickFiltersToolStripMenuItem; - MenuItem visibleBooksToolStripMenuItem; - MenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu; - MenuItem replaceTagsToolStripMenuItem; - MenuItem setDownloadedToolStripMenuItem; - MenuItem removeToolStripMenuItem; - MenuItem settingsToolStripMenuItem; - MenuItem accountsToolStripMenuItem; - MenuItem basicSettingsToolStripMenuItem; - MenuItem aboutToolStripMenuItem; + public event EventHandler Load; - - StackPanel scanningToolStripMenuItem; - TextBlock scanningToolStripMenuItem_Text; - - Button filterHelpBtn; - Button addQuickFilterBtn; - TextBox filterSearchTb; - Button filterBtn; - Button toggleQueueHideBtn; - - StackPanel removeBooksButtonsPanel; - Button removeBooksBtn; - - SplitView splitContainer1; - ProductsDisplay2 productsDisplay; - ProcessQueueControl2 processBookQueue1; - */ + public void OnLoad() => Load?.Invoke(this, EventArgs.Empty); private void FindAllControls() { diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml index 9065dd7f..ed3449e4 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml +++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml @@ -4,14 +4,16 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views" xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls" - mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="700" + mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="400" x:Class="LibationWinForms.AvaloniaUI.Views.ProductsDisplay2"> + + - + - + @@ -24,8 +26,9 @@ + - + @@ -103,17 +106,17 @@ - + - + - + @@ -127,17 +130,17 @@ - + - + - + diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs index 865065eb..ae2c344c 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs @@ -4,11 +4,14 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using DataLayer; +using Dinah.Core.DataBinding; using FileLiberator; using LibationFileManager; using LibationWinForms.AvaloniaUI.ViewModels; using System; +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; @@ -20,6 +23,7 @@ namespace LibationWinForms.AvaloniaUI.Views public event EventHandler VisibleCountChanged; public event EventHandler RemovableCountChanged; public event EventHandler LiberateClicked; + public event EventHandler InitialLoaded; private ProductsDisplayViewModel _viewModel; private GridEntryBindingList2 bindingList => productsGrid.Items as GridEntryBindingList2; @@ -42,11 +46,10 @@ namespace LibationWinForms.AvaloniaUI.Views productsGrid.CanUserSortColumns = true; removeGVColumn = productsGrid.Columns[0]; - - var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true); - productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(dbBooks); - - this.AttachedToVisualTree +=(_, _) => VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); + } + public override void EndInit() + { + base.EndInit(); } private void InitializeComponent() @@ -54,20 +57,106 @@ namespace LibationWinForms.AvaloniaUI.Views AvaloniaXamlLoader.Load(this); } + private class RowComparer : IComparer + { + private static readonly System.Reflection.PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + private static readonly System.Reflection.PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + public DataGridColumn Column { get; init; } + public string PropertyName { get; init; } + public ListSortDirection? SortDirection { get; set; } + + /// + /// This compare method 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. + /// + public int Compare(object x, object y) + { + if (x is null) return -1; + if (y is null) return 1; + if (x is null && y is null) return 0; + + var geA = (GridEntry2)x; + var geB = (GridEntry2)y; + + SortDirection ??= GetSortOrder(Column); + + SeriesEntrys2 parentA = null; + SeriesEntrys2 parentB = null; + + if (geA is LibraryBookEntry2 lbA && lbA.Parent is SeriesEntrys2 seA) + parentA = seA; + if (geB is LibraryBookEntry2 lbB && lbB.Parent is SeriesEntrys2 seB) + parentB = seB; + + //both a and b are standalone + if (parentA is null && parentB is null) + return Compare(geA, geB); + + //a is a standalone, 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 Compare(geA, parentB); + } + + //a is a child, b is a standalone + 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 Compare(parentA, geB); + } + + //both are children of the same series, always present in order of series index, ascending + if (parentA == parentB) + return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1); + + //a and b are children of different series. + return Compare(parentA, parentB); + } + + private static ListSortDirection? GetSortOrder(DataGridColumn column) + => CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(column)) as ListSortDirection?; + + private int Compare(GridEntry2 x, GridEntry2 y) + { + var val1 = x.GetMemberValue(PropertyName); + var val2 = y.GetMemberValue(PropertyName); + + return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); + } + } + + Dictionary ColumnComparers = new(); + DataGridColumn CurrentSortColumn; + private void Dg1_Sorting(object sender, DataGridColumnEventArgs e) { - bindingList.DoSortCore(e.Column.SortMemberPath); - e.Handled = true; + if (!ColumnComparers.ContainsKey(e.Column)) + ColumnComparers[e.Column] = new RowComparer + { + Column = e.Column, + PropertyName = e.Column.SortMemberPath + }; + + //Force the comparer to get the current sort order. We can't + //retrieve it from inside this event handler because Avalonia + //doesn't set the property until after this event. + ColumnComparers[e.Column].SortDirection = null; + + e.Column.CustomSortComparer = ColumnComparers[e.Column]; + CurrentSortColumn = e.Column; } #region Button controls - public void Remove_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) - { - productsGrid.CommitEdit(DataGridEditingUnit.Cell, true); - RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true)); - } - public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) { var button = args.Source as Button; @@ -228,6 +317,12 @@ namespace LibationWinForms.AvaloniaUI.Views { // don't return early if lib size == 0. this will not update correctly if all books are removed var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true); + if (productsGrid.DataContext is null) + { + productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(dbBooks); + InitialLoaded?.Invoke(this, EventArgs.Empty); + VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); + } UpdateGrid(dbBooks); } catch (Exception ex) @@ -387,6 +482,14 @@ namespace LibationWinForms.AvaloniaUI.Views if (visibleCount != bindingList.Count) VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); + + //Re-sort after filtering + if (CurrentSortColumn is null) + bindingList.InternalList.Sort((i1, i2) => i2.DateAdded.CompareTo(i1.DateAdded)); + else + CurrentSortColumn?.Sort(ColumnComparers[CurrentSortColumn].SortDirection.Value); + + bindingList.ResetCollection(); } #endregion diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj index 4316a2c1..579d53ad 100644 --- a/Source/LibationWinForms/LibationWinForms.csproj +++ b/Source/LibationWinForms/LibationWinForms.csproj @@ -55,6 +55,7 @@ + diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index 31876365..e271d278 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -19,6 +19,8 @@ namespace LibationWinForms [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)] static extern bool AllocConsole(); + static bool UseAvaloniaUI = true; + [STAThread] static void Main() { @@ -76,9 +78,10 @@ namespace LibationWinForms // global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd postLoggingGlobalExceptionHandling(); - - BuildAvaloniaApp().StartWithClassicDesktopLifetime(null); - //System.Windows.Forms.Application.Run(new Form1()); + if (UseAvaloniaUI) + BuildAvaloniaApp().StartWithClassicDesktopLifetime(null); + else + System.Windows.Forms.Application.Run(new Form1()); } public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure()