diff --git a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs index e8bf1a6c..67dd5ab7 100644 --- a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs @@ -58,6 +58,7 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient @@ -75,6 +76,41 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient public bool HasMultipleItemsSelected => SelectedVisibleCount > 1; + /// + /// Indicates whether all mail items are currently selected. + /// Counts all mail items including those in threads, regardless of thread expansion state. + /// + public bool IsAllItemsSelected + { + get + { + var totalMailItems = _sourceItems.Count; + if (totalMailItems == 0) return false; + + var selectedCount = 0; + + // Count selected standalone emails (not in threads) + selectedCount += _sourceItems.Count(e => e.IsSelected && !e.IsDisplayedInThread); + + // Count selected emails in threads + foreach (var expander in _threadExpanders.Values) + { + if (expander.IsSelected) + { + // If thread is selected, all emails in the thread are considered selected + selectedCount += expander.ThreadEmails.Count; + } + else + { + // If thread is not selected, count only individually selected emails within the thread + selectedCount += expander.ThreadEmails.Count(e => e.IsSelected); + } + } + + return selectedCount == totalMailItems; + } + } + public GroupedEmailCollection() { // Create a flat collection for ItemsView with headers, expanders and emails mixed @@ -211,6 +247,10 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient /// Registers a mail item to track its selection state when added to the visible UI /// @@ -337,6 +385,9 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient - /// Selects all visible mail items in the collection. - /// Only operates on MailItemViewModel instances, skipping group headers and thread expanders. + /// Selects all mail items in the collection. + /// Includes standalone mail items and all mail items inside threads, regardless of thread expansion state. /// /// The number of items that were selected. public int SelectAll() { - var selectedCount = 0; + var initialSelectedCount = SelectedItems.Count(); - foreach (var item in Items) + // Select all standalone emails (not in threads) + foreach (var mailItem in _sourceItems.Where(e => !e.IsDisplayedInThread)) { - // Only select MailItemViewModel instances (skip GroupHeaderBase and ThreadMailItemViewModel) - if (item is MailItemViewModel mailItem) - { - if (!mailItem.IsSelected) - { - mailItem.IsSelected = true; - selectedCount++; - } - } + mailItem.IsSelected = true; + } + + // Select all thread expanders (which automatically selects all emails within them) + foreach (var expander in _threadExpanders.Values) + { + expander.IsSelected = true; } SelectedVisibleCount = _selectedVisibleItems.Count; + var finalSelectedCount = SelectedItems.Count(); + var selectedCount = finalSelectedCount - initialSelectedCount; + if (selectedCount > 0) { SelectionChanged?.Invoke(this, EventArgs.Empty); @@ -1105,29 +1162,37 @@ public partial class GroupedEmailCollection : ObservableObject, IRecipient - /// Clears the selection of all visible mail items in the collection. - /// Only operates on MailItemViewModel instances, skipping group headers and thread expanders. + /// Clears the selection of all mail items in the collection. + /// Includes standalone mail items and all mail items inside threads, regardless of thread expansion state. /// /// The number of items that were deselected. public int ClearSelections() { - var deselectedCount = 0; + var initialSelectedCount = SelectedItems.Count(); - foreach (var item in Items) + // Deselect all standalone emails (not in threads) + foreach (var mailItem in _sourceItems.Where(e => !e.IsDisplayedInThread)) { - // Only deselect MailItemViewModel instances (skip GroupHeaderBase and ThreadMailItemViewModel) - if (item is MailItemViewModel mailItem) + mailItem.IsSelected = false; + } + + // Deselect all thread expanders and individual emails in threads + foreach (var expander in _threadExpanders.Values) + { + expander.IsSelected = false; + + // Also explicitly deselect individual emails within threads + foreach (var threadEmail in expander.ThreadEmails) { - if (mailItem.IsSelected) - { - mailItem.IsSelected = false; - deselectedCount++; - } + threadEmail.IsSelected = false; } } SelectedVisibleCount = _selectedVisibleItems.Count; + var finalSelectedCount = SelectedItems.Count(); + var deselectedCount = initialSelectedCount - finalSelectedCount; + if (deselectedCount > 0) { SelectionChanged?.Invoke(this, EventArgs.Empty); diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index 87b90532..673134e6 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -57,6 +57,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, private ThrottledEventHandler _selectionChangedThrottler; + public event EventHandler ThrottledSelectionChanged; public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection(); //public ObservableCollection SelectedItems { get; set; } = []; public ObservableCollection PivotFolders { get; set; } = []; @@ -195,6 +196,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, NotifyItemSelected(); SetupTopBarActions(); }); + + ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty); }); } diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml b/Wino.Mail.WinUI/Views/MailListPage.xaml index 0566310b..4c6592bf 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml @@ -49,8 +49,15 @@ Transparent + + @@ -61,13 +68,6 @@ VerticalContentAlignment="Stretch" CanUserInvoke="UserCanInvoke" IsSelected="{x:Bind IsSelected, Mode=TwoWay}"> - - - - - - - diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs index 3b6c9037..2cd734a9 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs @@ -13,6 +13,7 @@ using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Navigation; using MoreLinq; using Windows.Foundation; +using Windows.System; using Wino.Controls; using Wino.Core.Domain; using Wino.Core.Domain.Enums; @@ -57,16 +58,20 @@ public sealed partial class MailListPage : MailListPageAbstract, { WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask)); } + + ViewModel.ThrottledSelectionChanged += ListSelectionChanged; + } + + private void ListSelectionChanged(object? sender, EventArgs e) + { + UpdateSelectAllButtonStatus(); } protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); - // Dispose all WinoListView items. - - // MailListView.Dispose(); - + ViewModel.ThrottledSelectionChanged -= ListSelectionChanged; this.Bindings.StopTracking(); RenderingFrame.Navigate(typeof(IdlePage)); @@ -79,13 +84,17 @@ public sealed partial class MailListPage : MailListPageAbstract, // Check all checkbox if all is selected. // Unhook events to prevent selection overriding. - SelectAllCheckbox.Checked -= SelectAllCheckboxChecked; - SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked; + //DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () => + //{ + // SelectAllCheckbox.Checked -= SelectAllCheckboxChecked; + // SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked; - SelectAllCheckbox.IsChecked = MailListView.CastedItemsSource?.Count() > 0 && MailListView.SelectedItems.Count == MailListView.CastedItemsSource.Count(); + // bool isAllSelected = ViewModel.MailCollection.AllItems.All(a => a.IsSelected); + // SelectAllCheckbox.IsChecked = ViewModel.MailCollection.AllItems.Count() > 0 && isAllSelected; - SelectAllCheckbox.Checked += SelectAllCheckboxChecked; - SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked; + // SelectAllCheckbox.Checked += SelectAllCheckboxChecked; + // SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked; + //}); } private void SelectionModeToggleChecked(object sender, RoutedEventArgs e) => ChangeSelectionMode(ItemsViewSelectionMode.Multiple); @@ -108,11 +117,9 @@ public sealed partial class MailListPage : MailListPageAbstract, } } - // SelectAllCheckbox.IsChecked = false; + SelectAllCheckbox.IsChecked = false; SelectionModeToggle.IsChecked = false; - // MailListView.ClearSelections(); - UpdateSelectAllButtonStatus(); ViewModel.SelectedPivotChangedCommand.Execute(null); } @@ -134,12 +141,12 @@ public sealed partial class MailListPage : MailListPageAbstract, private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e) { - // MailListView.SelectAllWino(); + ViewModel.MailCollection.SelectAll(); } private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e) { - // MailListView.ClearSelections(); + ViewModel.MailCollection.ClearSelections(); } private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args) @@ -548,29 +555,9 @@ public sealed partial class MailListPage : MailListPageAbstract, private void ListSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args) { UpdateSelectAllButtonStatus(); - SynchronizeSelectedItems(); - UpdateAdaptiveness(); } - private static object _selectedItemsLock = new object(); - private void SynchronizeSelectedItems() - { - //lock (_selectedItemsLock) - //{ - // ViewModel.SelectedItems.Clear(); - - // foreach (var item in MailListView.SelectedItems) - // { - // if (item is MailItemViewModel mailItem) - // { - // if (!mailItem.IsSelected) mailItem.IsSelected = true; - // if (!ViewModel.SelectedItems.Contains(mailItem)) ViewModel.SelectedItems.Add(mailItem); - // } - // } - //} - } - private void ThreadContainerRightTapped(object sender, RightTappedRoutedEventArgs e) { if (sender is ItemContainer container && container.Tag is ThreadMailItemViewModel expander) @@ -601,29 +588,28 @@ public sealed partial class MailListPage : MailListPageAbstract, private void MailListView_ProcessKeyboardAccelerators(UIElement sender, ProcessKeyboardAcceleratorEventArgs args) { - // ItemsView have weird logic for selection inversion. We need to handle it manually. - args.Handled = true; - - ViewModel.MailCollection.SelectAll(); - - // If not all items are selected, select all. Otherwise clear selection. - // Handle selections in the GroupedEmailCollection. - - //if (MailListView.SelectedItems.Count < MailListView.CastedItemsSource?.Count()) - //{ - // // MailListView.SelectAllWino(); - //} - //else - //{ - // // MailListView.ClearSelections(); - //} - } - - private void SingleItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args) - { - if (args.InvokedItem is MailItemViewModel mailItem) + if (args.Key == VirtualKey.Delete) { - mailItem.IsSelected = !mailItem.IsSelected; + + } + else + { + // ItemsView have weird logic for selection inversion. We need to handle it manually. + args.Handled = true; + + // TODO: If not all items are selected, select all. Otherwise clear selection. + // Handle selections in the GroupedEmailCollection. + + ViewModel.MailCollection.SelectAll(); + + //if (!ViewModel.MailCollection.IsAllItemsSelected) + //{ + + //} + //else + //{ + // ViewModel.MailCollection.ClearSelections(); + //} } } }