From 4f85fa6ba933be7523a991b61e60ad4e9b5a641b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Mon, 27 Oct 2025 22:52:26 +0100 Subject: [PATCH] New list view items. --- Wino.Core.WinUI/Services/KeyPressService.cs | 8 +- .../Collections/WinoMailCollection.cs | 42 ++++- .../Data/MailItemViewModel.cs | 7 +- .../Data/ThreadMailItemViewModel.cs | 5 + Wino.Mail.ViewModels/MailListPageViewModel.cs | 42 +++-- .../Controls/ListView/WinoListView.cs | 59 +++++- .../Controls/ListView/WinoListViewStyles.xaml | 168 ++++++++++++++---- .../WinoMailItemContainerStyleSelector.cs | 3 +- .../WinoMailItemViewModelListViewItem.cs | 73 +------- ...WinoThreadMailItemViewModelListViewItem.cs | 114 ++++-------- Wino.Mail.WinUI/Views/MailListPage.xaml | 32 ++-- Wino.Mail.WinUI/Views/MailListPage.xaml.cs | 88 +++++++-- 12 files changed, 398 insertions(+), 243 deletions(-) diff --git a/Wino.Core.WinUI/Services/KeyPressService.cs b/Wino.Core.WinUI/Services/KeyPressService.cs index 869d1679..ce9bf66e 100644 --- a/Wino.Core.WinUI/Services/KeyPressService.cs +++ b/Wino.Core.WinUI/Services/KeyPressService.cs @@ -1,6 +1,6 @@ -using Windows.System; +using Microsoft.UI.Input; +using Windows.System; using Windows.UI.Core; -using Microsoft.UI.Xaml; using Wino.Core.Domain.Interfaces; namespace Wino.Core.WinUI.Services; @@ -8,8 +8,8 @@ namespace Wino.Core.WinUI.Services; public class KeyPressService : IKeyPressService { public bool IsCtrlKeyPressed() - => Window.Current?.CoreWindow?.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down) ?? false; + => InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); public bool IsShiftKeyPressed() - => Window.Current?.CoreWindow?.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down) ?? false; + => InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); } diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index 60b44deb..9c537862 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -724,6 +724,27 @@ public class WinoMailCollection : ObservableRecipient, IRecipient AllItemsIncludingThreads + { + get + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is ThreadMailItemViewModel threadMailItemViewModel) + { + foreach (var child in threadMailItemViewModel.ThreadEmails) + { + yield return child; + } + } + yield return item; + } + } + } + } + private IEnumerable AllItems { get @@ -752,7 +773,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient AllItems.Any() && AllItems.All(a => a.IsSelected); public bool HasSingleItemSelected => SelectedItemsCount == 1; - public async Task ExecuteWithoutRaiseSelectionChangedAsync(Action action) + public async Task ExecuteWithoutRaiseSelectionChangedAsync(Action action, bool includeThreads) { try { @@ -761,9 +782,19 @@ public class WinoMailCollection : ObservableRecipient, IRecipient { - foreach (var item in AllItems) + if (includeThreads) { - action(item); + foreach (var item in AllItemsIncludingThreads) + { + action(item); + } + } + else + { + foreach (var item in AllItems) + { + action(item); + } } }); } @@ -816,8 +847,9 @@ public class WinoMailCollection : ObservableRecipient, IRecipient ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = true); - public Task UnselectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = false); + public Task SelectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = true, true); + public Task UnselectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = false, true); + public Task CollapseAllThreadsAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => { if (a is ThreadMailItemViewModel thread) thread.IsThreadExpanded = false; }, true); private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action); diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index 37b2ba02..4bb62d0d 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -8,7 +8,7 @@ namespace Wino.Mail.ViewModels.Data; /// /// Single view model for IMailItem representation. /// -public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailListItem +public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, IMailListItem { public DateTime CreationDate => MailCopy.CreationDate; [ObservableProperty] @@ -18,6 +18,7 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM public partial bool ThumbnailUpdatedEvent { get; set; } = false; [ObservableProperty] + [NotifyPropertyChangedRecipients] public partial bool IsSelected { get; set; } [ObservableProperty] @@ -89,10 +90,6 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n); } - partial void OnIsSelectedChanged(bool oldValue, bool newValue) - { - } - public IEnumerable GetContainingIds() => [MailCopy.UniqueId]; public IEnumerable GetSelectedMailItems() diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index 03f3e7f2..9cced05f 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -17,11 +17,16 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable, [ObservableProperty] [NotifyPropertyChangedRecipients] + [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] public partial bool IsThreadExpanded { get; set; } [ObservableProperty] + [NotifyPropertyChangedRecipients] + [NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))] public partial bool IsSelected { get; set; } + public bool IsSelectedOrExpanded => IsSelected || IsThreadExpanded; + /// /// Gets the number of emails in this thread /// diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index f8c5bf06..a5bab176 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; using MoreLinq; using Nito.AsyncEx; using Serilog; @@ -39,7 +40,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient> { private bool isChangingFolder = false; @@ -221,7 +223,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, var selectedItem = MailCollection.SelectedItems.ElementAtOrDefault(0); ActiveMailItemChanged(selectedItem); } - else if (MailCollection.SelectedItemsCount > 1) + else if (MailCollection.SelectedItemsCount == 0) { ActiveMailItemChanged(null); } @@ -311,20 +313,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, { if (_activeMailItem == selectedMailItemViewModel) return; - // Don't update active mail item if Ctrl key is pressed or multi selection is enabled. - // User is probably trying to select multiple items. - // This is not the same behavior in Windows Mail, - // but it's a trash behavior. - - var isCtrlKeyPressed = _keyPressService.IsCtrlKeyPressed(); - - bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled; - - if (isMultiSelecting && StatePersistenceService.IsReaderNarrowed) - { - return; - } - _activeMailItem = selectedMailItemViewModel; Messenger.Send(new ActiveMailItemChangedEvent(_activeMailItem)); @@ -1102,6 +1090,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); + Messenger.Register>(this); } protected override void UnregisterRecipients() @@ -1115,5 +1104,26 @@ public partial class MailListPageViewModel : MailBaseViewModel, Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); + Messenger.Unregister>(this); + } + + public void Receive(PropertyChangedMessage message) + { + // Handle IsSelected property changes from MailItemViewModel + if (message.PropertyName == nameof(MailItemViewModel.IsSelected) && message.Sender is MailItemViewModel mailItemViewModel) + { + Messenger.Send(new SelectedItemsChangedMessage()); + } + else if (message.Sender is ThreadMailItemViewModel threadMailItemViewModel) + { + if (message.PropertyName == nameof(ThreadMailItemViewModel.IsSelected)) + { + // Thread selected. + } + else if (message.PropertyName == nameof(ThreadMailItemViewModel.IsThreadExpanded)) + { + // Thread expanded. + } + } } } diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs b/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs index d82a6150..05004c5e 100644 --- a/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs +++ b/Wino.Mail.WinUI/Controls/ListView/WinoListView.cs @@ -1,4 +1,5 @@ -using Microsoft.UI.Xaml; +using System.Linq; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Wino.Mail.ViewModels.Data; @@ -6,10 +7,20 @@ namespace Wino.Mail.WinUI.Controls.ListView; public partial class WinoListView : Microsoft.UI.Xaml.Controls.ListView { + public bool IsThreadListView + { + get { return (bool)GetValue(IsThreadListViewProperty); } + set { SetValue(IsThreadListViewProperty, value); } + } + + public static readonly DependencyProperty IsThreadListViewProperty = DependencyProperty.Register(nameof(IsThreadListView), typeof(bool), typeof(WinoListView), new PropertyMetadata(false)); + public bool IsAllSelected => Items.Count == SelectedItems.Count; protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { + base.PrepareContainerForItemOverride(element, item); + if (item is MailItemViewModel mailItemViewModel && element is WinoMailItemViewModelListViewItem container) { // Ensure the container's selection state matches the model's state @@ -19,18 +30,52 @@ public partial class WinoListView : Microsoft.UI.Xaml.Controls.ListView } else if (item is ThreadMailItemViewModel threadMailItemViewModel && element is WinoThreadMailItemViewModelListViewItem threadContainer) { + threadContainer.IsSelected = threadMailItemViewModel.IsSelected; threadContainer.IsThreadExpanded = threadMailItemViewModel.IsThreadExpanded; } - - base.PrepareContainerForItemOverride(element, item); } - protected override void ClearContainerForItemOverride(DependencyObject element, object item) + public WinoMailItemViewModelListViewItem? GetMailItemContainer(MailItemViewModel mailItemViewModel) { - if (element is WinoThreadMailItemViewModelListViewItem threadMailItemViewModelListViewItem) threadMailItemViewModelListViewItem.Cleanup(); - if (element is WinoMailItemViewModelListViewItem winoMailItemViewModelListViewItem) winoMailItemViewModelListViewItem.Cleanup(); + foreach (var item in Items) + { + if (item is MailItemViewModel mailItem && mailItem.Id == mailItemViewModel.Id) return ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + if (item is ThreadMailItemViewModel threadMailItem && threadMailItem.GetContainingIds().Contains(mailItemViewModel.MailCopy.UniqueId)) + { + var threadContainer = ContainerFromItem(threadMailItem) as WinoThreadMailItemViewModelListViewItem; - base.ClearContainerForItemOverride(element, item); + // Try to get the inner WinoListView. + if (threadContainer != null) + { + var innerListViewControl = threadContainer.GetWinoListViewControl(); + + return innerListViewControl?.ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem; + } + } + } + + return null; + } + + public WinoThreadMailItemViewModelListViewItem? GetThreadMailItemContainer(ThreadMailItemViewModel threadMailItemViewModel) + => ContainerFromItem(threadMailItemViewModel) as WinoThreadMailItemViewModelListViewItem; + + public void ToggleItemContainer(IMailListItem mailListItem) + { + DispatcherQueue.TryEnqueue(() => + { + if (mailListItem is MailItemViewModel mailItemViewModel) + { + var container = GetMailItemContainer(mailItemViewModel); + container?.IsSelected = mailItemViewModel.IsSelected; + } + else if (mailListItem is ThreadMailItemViewModel threadMailItemViewModel) + { + var container = GetThreadMailItemContainer(threadMailItemViewModel); + container?.IsSelected = threadMailItemViewModel.IsSelected; + container?.IsThreadExpanded = threadMailItemViewModel.IsThreadExpanded; + } + }); } public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel) diff --git a/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml b/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml index 14219189..356f1d9f 100644 --- a/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml +++ b/Wino.Mail.WinUI/Controls/ListView/WinoListViewStyles.xaml @@ -11,37 +11,76 @@ - 0) - { - innerControl.SelectedItems.Clear(); + //// 2 + //if (control.IsThreadExpanded && innerControl.SelectedItems.Count == 0 && innerControl.Items.Count > 0) + //{ + // innerControl.SelectedItems.Clear(); - // Make item selected, container might not be realized yet, so set on the model. - // It'll appear selected when container is realized. + // // Make item selected, container might not be realized yet, so set on the model. + // // It'll appear selected when container is realized. - var firstItem = innerControl.Items.FirstOrDefault() as MailItemViewModel; + // var firstItem = innerControl.Items.FirstOrDefault() as MailItemViewModel; - firstItem?.IsSelected = true; - } + // firstItem?.IsSelected = true; + //} - // 1 - expander.IsExpanded = control.IsThreadExpanded; + //// 1 + //expander.IsExpanded = control.IsThreadExpanded; - // 3 - if (!control.IsSelected) innerControl?.SelectedItems.Clear(); - } - - protected override void OnContentChanged(object oldContent, object newContent) - { - base.OnContentChanged(oldContent, newContent); - - if (oldContent is ThreadMailItemViewModel oldMailItem) - { - UnregisterSelectionCallback(oldMailItem); - } - - if (newContent is ThreadMailItemViewModel newMailItem) - { - IsSelected = newMailItem.IsSelected; - RegisterSelectionCallback(newMailItem); - } - } - - private void OnIsSelectedChanged(DependencyObject sender, DependencyProperty dp) - { - IsThreadExpanded = IsSelected; - } - - public void UnregisterSelectionCallback(ThreadMailItemViewModel mailItem) - { - mailItem.PropertyChanged -= MailPropChanged; - } - - private void MailPropChanged(object? sender, PropertyChangedEventArgs e) - { - if (sender is not ThreadMailItemViewModel mailItem) return; - - if (e.PropertyName == nameof(ThreadMailItemViewModel.IsThreadExpanded)) - { - ApplySelectionForContainer(mailItem); - } - } - - private void RegisterSelectionCallback(ThreadMailItemViewModel mailItem) - { - mailItem.PropertyChanged -= MailPropChanged; - mailItem.PropertyChanged += MailPropChanged; - } - - private void ApplySelectionForContainer(ThreadMailItemViewModel mailItem) - { - if (IsThreadExpanded != mailItem.IsThreadExpanded) IsThreadExpanded = mailItem.IsThreadExpanded; + //// 3 + //if (!control.IsSelected) innerControl?.SelectedItems.Clear(); } public WinoListView? GetWinoListViewControl() diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml b/Wino.Mail.WinUI/Views/MailListPage.xaml index c8c2ebeb..2bdf8754 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml @@ -69,7 +69,10 @@ - + + SelectionMode="Extended"> + + + + + + @@ -117,7 +129,7 @@ @@ -386,12 +398,11 @@ toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0" ChoosingItemContainer="WinoListViewChoosingItemContainer" IsItemClickEnabled="True" - ItemClick="WinoListView_ItemClick" + ItemClick="WinoListViewItemClicked" ItemContainerStyleSelector="{StaticResource WinoMailItemContainerStyleSelector}" ItemTemplateSelector="{StaticResource MailItemTemplateSelector}" ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}" ProcessKeyboardAccelerators="WinoListViewProcessKeyboardAccelerators" - SelectionChanged="WinoListViewSelectionChanged" SelectionMode="Extended"> @@ -400,7 +411,7 @@ - + @@ -412,15 +423,6 @@ - diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs index 6f65c1c0..ed19cc04 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs @@ -63,6 +63,7 @@ public sealed partial class MailListPage : MailListPageAbstract, } } + protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); @@ -162,6 +163,8 @@ public sealed partial class MailListPage : MailListPageAbstract, args.IsContainerPrepared = false; } + + private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args) { // TODO: New ItemsView implementation. @@ -496,7 +499,7 @@ public sealed partial class MailListPage : MailListPageAbstract, private void UpdateAdaptiveness() { - bool isMultiSelectionEnabled = ViewModel.IsMultiSelectionModeEnabled || KeyPressService.IsCtrlKeyPressed(); + bool isMultiSelectionEnabled = ViewModel.IsMultiSelectionModeEnabled; if (StatePersistenceService.IsReaderNarrowed) { @@ -545,22 +548,85 @@ public sealed partial class MailListPage : MailListPageAbstract, } } - private void WinoListViewSelectionChanged(object sender, SelectionChangedEventArgs e) + private async void WinoListViewItemClicked(object sender, ItemClickEventArgs e) { + if (sender is not WinoListView listView) return; - } + bool isSelectedItemFromThread = listView.IsThreadListView; + bool isCtrlPressed = KeyPressService.IsCtrlKeyPressed(); - private void WinoListView_ItemClick(object sender, ItemClickEventArgs e) - { - if (e.ClickedItem is ThreadMailItemViewModel clickedThread) + bool isClickingThreadItem = e.ClickedItem is ThreadMailItemViewModel; + + // Unselect all items. It's single selection. + if (!isCtrlPressed) { - // Only if container is selected. + await ViewModel.MailCollection.UnselectAllAsync(); - var threadContainer = MailListView.ContainerFromItem(clickedThread) as WinoThreadMailItemViewModelListViewItem; - - if (threadContainer?.IsSelected ?? false) + if (!isSelectedItemFromThread && !isClickingThreadItem) { - clickedThread.IsThreadExpanded = !clickedThread.IsThreadExpanded; + await ViewModel.MailCollection.CollapseAllThreadsAsync(); + } + } + + if (e.ClickedItem is MailItemViewModel mailListItem) + { + mailListItem.IsSelected = !mailListItem.IsSelected; + } + else if (e.ClickedItem is ThreadMailItemViewModel threadMailItemViewModel) + { + // Extended selection mode handling for threads + if (isCtrlPressed) + { + // If thread is selected and Ctrl is pressed + if (threadMailItemViewModel.IsSelected) + { + // If thread was collapsed, expand it + if (!threadMailItemViewModel.IsThreadExpanded) + { + threadMailItemViewModel.IsThreadExpanded = true; + } + else + { + // Check if all items are selected. + // If so, then unselect all items in the thread and unselect the thread itself. + if (threadMailItemViewModel.ThreadEmails.All(a => a.IsSelected)) + { + foreach (var threadEmail in threadMailItemViewModel.ThreadEmails) + { + threadEmail.IsSelected = false; + } + threadMailItemViewModel.IsSelected = false; + return; + } + else + { + // If thread was already expanded, select all items in the thread + foreach (var threadEmail in threadMailItemViewModel.ThreadEmails) + { + threadEmail.IsSelected = true; + } + } + } + } + else + { + // Thread is not selected, select and expand it. + if (!threadMailItemViewModel.IsThreadExpanded) threadMailItemViewModel.IsThreadExpanded = true; + if (!threadMailItemViewModel.IsSelected) + { + threadMailItemViewModel.IsSelected = true; + + foreach (var threadEmail in threadMailItemViewModel.ThreadEmails) + { + threadEmail.IsSelected = true; + } + } + } + } + else + { + // No Ctrl pressed, toggle expansion state (default behavior) + threadMailItemViewModel.IsThreadExpanded = !threadMailItemViewModel.IsThreadExpanded; } } }