New list view items.

This commit is contained in:
Burak Kaan Köse
2025-10-27 22:52:26 +01:00
parent 4eea21c4f5
commit 4f85fa6ba9
12 changed files with 398 additions and 243 deletions
+4 -4
View File
@@ -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);
}
@@ -724,6 +724,27 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
await NotifySelectionChangesAsync();
}
private IEnumerable<IMailListItem> 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<MailItemViewModel> AllItems
{
get
@@ -752,7 +773,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
public bool IsAllItemsSelected => AllItems.Any() && AllItems.All(a => a.IsSelected);
public bool HasSingleItemSelected => SelectedItemsCount == 1;
public async Task ExecuteWithoutRaiseSelectionChangedAsync(Action<MailItemViewModel> action)
public async Task ExecuteWithoutRaiseSelectionChangedAsync(Action<IMailListItem> action, bool includeThreads)
{
try
{
@@ -761,9 +782,19 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
await ExecuteUIThread(() =>
{
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<SelectedItemsC
return -1;
}
public Task SelectAllAsync() => 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);
@@ -8,7 +8,7 @@ namespace Wino.Mail.ViewModels.Data;
/// <summary>
/// Single view model for IMailItem representation.
/// </summary>
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<Guid> GetContainingIds() => [MailCopy.UniqueId];
public IEnumerable<MailItemViewModel> GetSelectedMailItems()
@@ -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;
/// <summary>
/// Gets the number of emails in this thread
/// </summary>
+26 -16
View File
@@ -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<NewMailSynchronizationRequested>,
IRecipient<AccountSynchronizerStateChanged>,
IRecipient<AccountCacheResetMessage>,
IRecipient<ThumbnailAdded>
IRecipient<ThumbnailAdded>,
IRecipient<PropertyChangedMessage<bool>>
{
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<AccountSynchronizerStateChanged>(this);
Messenger.Register<AccountCacheResetMessage>(this);
Messenger.Register<ThumbnailAdded>(this);
Messenger.Register<PropertyChangedMessage<bool>>(this);
}
protected override void UnregisterRecipients()
@@ -1115,5 +1104,26 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
Messenger.Unregister<AccountCacheResetMessage>(this);
Messenger.Unregister<ThumbnailAdded>(this);
Messenger.Unregister<PropertyChangedMessage<bool>>(this);
}
public void Receive(PropertyChangedMessage<bool> 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.
}
}
}
}
@@ -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)
@@ -11,37 +11,76 @@
<ResourceDictionary>
<!-- Thread Mail ListViewItem Style -->
<Style x:Key="DefaultThreadListViewItemStyle" TargetType="local:WinoMailItemViewModelListViewItem">
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="Background" Value="{ThemeResource ListViewItemBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ListViewItemForeground}" />
<Setter Property="TabNavigation" Value="Local" />
<Setter Property="IsHoldingEnabled" Value="True" />
<Setter Property="Padding" Value="16,0,12,0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="MinWidth" Value="{ThemeResource ListViewItemMinWidth}" />
<Setter Property="MinHeight" Value="{ThemeResource ListViewItemMinHeight}" />
<Setter Property="AllowDrop" Value="False" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="FocusVisualMargin" Value="1" />
<Setter Property="FocusVisualPrimaryBrush" Value="{ThemeResource ListViewItemFocusVisualPrimaryBrush}" />
<Setter Property="FocusVisualPrimaryThickness" Value="2" />
<Setter Property="FocusVisualSecondaryBrush" Value="{ThemeResource ListViewItemFocusVisualSecondaryBrush}" />
<Setter Property="FocusVisualSecondaryThickness" Value="1" />
<Style
x:Key="DefaultThreadListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="local:WinoThreadMailItemViewModelListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:WinoMailItemViewModelListViewItem">
<!-- Expandable Content -->
<ContentPresenter
x:Name="ThreadContent"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<ControlTemplate TargetType="local:WinoThreadMailItemViewModelListViewItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid
x:Name="RootGrid"
Grid.ColumnSpan="2"
Background="{ThemeResource ListViewItemBackground}"
CornerRadius="{ThemeResource ControlCornerRadius}" />
<Border
x:Name="SelectionIndicator"
Width="4"
Margin="0,16"
HorizontalAlignment="Center"
Background="{ThemeResource ListViewItemSelectionIndicatorBrush}"
CornerRadius="4"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Item.IsSelected, Mode=OneWay}" />
<!-- Expandable Content -->
<ContentPresenter
x:Name="ThreadContent"
Grid.Column="1"
Margin="6,0,0,0"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundPointerOver}" />
<Setter Target="ThreadContent.Foreground" Value="{ThemeResource ListViewItemForegroundPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundSelected}" />
<Setter Target="ThreadContent.Foreground" Value="{ThemeResource ListViewItemForegroundSelected}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SelectedPointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundSelectedPointerOver}" />
<Setter Target="ThreadContent.Foreground" Value="{ThemeResource ListViewItemForegroundSelected}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PressedSelected">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundSelectedPressed}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
@@ -52,7 +91,76 @@
<Style
x:Key="DefaultMailListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="local:WinoMailItemViewModelListViewItem" />
TargetType="local:WinoMailItemViewModelListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:WinoMailItemViewModelListViewItem">
<Grid
x:Name="RootGrid"
Margin="0,2"
Background="{ThemeResource ListViewItemBackground}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
x:Name="SelectionIndicator"
Width="4"
Margin="0,16"
HorizontalAlignment="Center"
Background="{ThemeResource ListViewItemSelectionIndicatorBrush}"
CornerRadius="4"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Item.IsSelected, Mode=OneWay}" />
<ContentPresenter
x:Name="MailContent"
Grid.Column="1"
Margin="6,0,0,0"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Background="Transparent"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundPointerOver}" />
<Setter Target="MailContent.Foreground" Value="{ThemeResource ListViewItemForegroundPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundSelected}" />
<Setter Target="MailContent.Foreground" Value="{ThemeResource ListViewItemForegroundSelected}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SelectedPointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundSelectedPointerOver}" />
<Setter Target="MailContent.Foreground" Value="{ThemeResource ListViewItemForegroundSelected}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PressedSelected">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundSelectedPressed}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<local:WinoMailItemContainerStyleSelector
x:Name="WinoMailItemContainerStyleSelector"
@@ -12,7 +12,8 @@ public partial class WinoMailItemContainerStyleSelector : StyleSelector
protected override Style SelectStyleCore(object item, DependencyObject container)
{
if (item is MailItemViewModel) return MailItemStyle ?? throw new Exception($"Missing style for {nameof(MailItemViewModel)}");
if (item is ThreadMailItemViewModel) return ThreadStyle ?? throw new Exception($"Missing style for {nameof(ThreadMailItemViewModel)}");
if (item is ThreadMailItemViewModel)
return ThreadStyle ?? throw new Exception($"Missing style for {nameof(ThreadMailItemViewModel)}");
return null;
}
@@ -1,86 +1,23 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml.Controls;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails;
namespace Wino.Mail.WinUI.Controls.ListView;
public partial class WinoMailItemViewModelListViewItem : ListViewItem
{
private readonly long _selectionChangeCallbackToken;
[GeneratedDependencyProperty]
public partial MailItemViewModel? Item { get; set; }
public WinoMailItemViewModelListViewItem()
{
DefaultStyleKey = typeof(WinoMailItemViewModelListViewItem);
_selectionChangeCallbackToken = RegisterPropertyChangedCallback(IsSelectedProperty, OnIsSelectedChanged);
}
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
if (oldContent is MailItemViewModel oldMailItem)
{
UnregisterSelectionCallback(oldMailItem);
}
if (newContent is MailItemViewModel newMailItem)
{
IsSelected = newMailItem.IsSelected;
RegisterSelectionCallback(newMailItem);
}
}
public void Cleanup()
{
if (Content is MailItemViewModel mailItem)
{
UnregisterSelectionCallback(mailItem);
UnregisterPropertyChangedCallback(IsSelectedProperty, _selectionChangeCallbackToken);
}
}
private void UnregisterSelectionCallback(IMailListItem mailItem)
{
mailItem.PropertyChanged -= MailPropChanged;
}
private void RegisterSelectionCallback(IMailListItem mailItem)
{
mailItem.PropertyChanged -= MailPropChanged;
mailItem.PropertyChanged += MailPropChanged;
}
// From model
private void MailPropChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is not MailItemViewModel mailItem) return;
if (e.PropertyName == nameof(MailItemViewModel.IsSelected)) ApplySelectionForContainer(mailItem);
}
// From container.
private void OnIsSelectedChanged(DependencyObject sender, DependencyProperty dp)
{
if (Content is IMailListItem mailItem)
{
ApplySelectionForModel(mailItem);
}
}
private void ApplySelectionForModel(IMailListItem mailItem)
{
mailItem.IsSelected = IsSelected;
WeakReferenceMessenger.Default.Send(new SelectedItemsChangedMessage());
}
private void ApplySelectionForContainer(IMailListItem mailItem)
{
if (IsSelected != mailItem.IsSelected)
{
IsSelected = mailItem.IsSelected;
}
if (newContent is MailItemViewModel mailItemViewModel) Item = mailItemViewModel;
}
}
@@ -1,5 +1,5 @@
using System.ComponentModel;
using System.Linq;
using System.Linq;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Controls;
@@ -10,30 +10,29 @@ namespace Wino.Mail.WinUI.Controls.ListView;
public partial class WinoThreadMailItemViewModelListViewItem : ListViewItem
{
public bool IsThreadExpanded
[GeneratedDependencyProperty]
public partial bool IsThreadExpanded { get; set; }
[GeneratedDependencyProperty]
public partial ThreadMailItemViewModel? Item { get; set; }
protected override void OnContentChanged(object oldContent, object newContent)
{
get { return (bool)GetValue(IsThreadExpandedProperty); }
set { SetValue(IsThreadExpandedProperty, value); }
base.OnContentChanged(oldContent, newContent);
if (newContent is ThreadMailItemViewModel threadMailItemViewModel) Item = threadMailItemViewModel;
}
public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(WinoThreadMailItemViewModelListViewItem), new PropertyMetadata(false, new PropertyChangedCallback(OnIsThreadExpandedChanged)));
private readonly long _selectionChangeCallbackToken;
public WinoThreadMailItemViewModelListViewItem()
{
DefaultStyleKey = typeof(WinoThreadMailItemViewModelListViewItem);
_selectionChangeCallbackToken = RegisterPropertyChangedCallback(IsSelectedProperty, OnIsSelectedChanged);
}
public void Cleanup()
partial void OnIsThreadExpandedChanged(bool newValue)
{
if (Content is ThreadMailItemViewModel mailItem)
{
UnregisterSelectionCallback(mailItem);
UnregisterPropertyChangedCallback(IsSelectedProperty, _selectionChangeCallbackToken);
}
// 1. Reflect expansion changes to WinoExpander.
// 2. Automatically select first item on expansion, if none selected.
// 3. Unselect all items on collapse.
}
private static void OnIsThreadExpandedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs dp)
@@ -42,79 +41,32 @@ public partial class WinoThreadMailItemViewModelListViewItem : ListViewItem
// 2. Automatically select first item on expansion, if none selected.
// 3. Unselect all items on collapse.
var control = sender as WinoThreadMailItemViewModelListViewItem;
//var control = sender as WinoThreadMailItemViewModelListViewItem;
var innerControl = control?.GetWinoListViewControl();
var expander = control?.GetExpander();
//var innerControl = control?.GetWinoListViewControl();
//var expander = control?.GetExpander();
if (innerControl == null || control == null || expander == null) return;
//if (innerControl == null || control == null || expander == null) return;
// 2
if (control.IsThreadExpanded && innerControl.SelectedItems.Count == 0 && innerControl.Items.Count > 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()
+17 -15
View File
@@ -69,7 +69,10 @@
</DataTemplate>
<DataTemplate x:Key="ThreadMailItemTemplate" x:DataType="viewModelData:ThreadMailItemViewModel">
<controls:WinoExpander x:Name="ExpanderPart" IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
<controls:WinoExpander
x:Name="ExpanderPart"
Margin="0,2"
IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
<controls:WinoExpander.Header>
<controls:MailItemDisplayInformationControl
x:DefaultBindMode="OneWay"
@@ -95,11 +98,20 @@
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0"
ChoosingItemContainer="WinoListViewChoosingItemContainer"
IsItemClickEnabled="True"
IsThreadListView="True"
ItemClick="WinoListViewItemClicked"
ItemContainerStyle="{StaticResource DefaultMailListViewItemStyle}"
ItemTemplate="{StaticResource SingleMailItemTemplate}"
ItemsSource="{x:Bind ThreadEmails, Mode=OneTime}"
ProcessKeyboardAccelerators="WinoListViewProcessKeyboardAccelerators"
SelectionMode="Extended" />
SelectionMode="Extended">
<listview:WinoListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Margin="8,0,12,0" AreStickyGroupHeadersEnabled="True" />
</ItemsPanelTemplate>
</listview:WinoListView.ItemsPanel>
</listview:WinoListView>
</controls:WinoExpander.Content>
</controls:WinoExpander>
@@ -117,7 +129,7 @@
<DataTemplate x:Key="MailGroupHeaderDefaultTemplate" x:DataType="collections:IReadOnlyObservableGroup">
<Grid
Margin="4,2"
Margin="0,0,0,6"
AllowFocusOnInteraction="False"
Background="{ThemeResource MailListHeaderBackgroundColor}"
CornerRadius="6">
@@ -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">
<listview:WinoListView.ItemContainerTransitions>
<TransitionCollection>
@@ -400,7 +411,7 @@
</listview:WinoListView.ItemContainerTransitions>
<listview:WinoListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Margin="8,0,12,0" AreStickyGroupHeadersEnabled="True" />
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" GroupPadding="10,10,10,0" />
</ItemsPanelTemplate>
</listview:WinoListView.ItemsPanel>
<listview:WinoListView.Resources>
@@ -412,15 +423,6 @@
<GroupStyle HeaderTemplate="{StaticResource MailGroupHeaderDefaultTemplate}" HidesIfEmpty="True" />
</listview:WinoListView.GroupStyle>
</listview:WinoListView>
<!--<advanced:WinoItemsView
x:Name="MailListView"
Margin="4"
ItemTemplate="{StaticResource MailItemContainerSelector}"
ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}"
Layout="{StaticResource DefaultItemsViewLayout}"
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
ProcessKeyboardAccelerators="MailListView_ProcessKeyboardAccelerators"
SelectionMode="Extended" />-->
<!-- Try online search panel. -->
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}">
+77 -11
View File
@@ -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;
}
}
}