Files
Wino-Mail/Wino.Mail/Controls/Advanced/WinoListView.cs
Aleh Khantsevich 2ec05ea7cc UWP .NET9 (#555)
* Ground work for NET9 UWP switch.

* Add launch settings for Wino.Mail

* Added new test WAP project

* fix platforms in slnx solution

* ManagePackageVersionsCentrally set default

* Fixing assets and couple issues with the new packaging project.

* Add back markdown

* Fix nuget warnings

* FIx error in WAP about build tools

* Add build.props with default language preview

* Some AOT compilation progress.

* More AOT stuff.

* Remove deprecated protocol auth activation handler.

* Fix remaining protocol handler for google auth.

* Even more AOT

* More more AOT fixes

* Fix a few more AOT warnings

* Fix signature editor AOT

* Fix composer and renderer AOT JSON

* Outlook Sync AOT

* Fixing bundle generation and package signing.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-14 01:43:52 +01:00

420 lines
15 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using Serilog;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
namespace Wino.Controls.Advanced
{
/// <summary>
/// Custom ListView control that handles multiple selection with Extended/Multiple selection mode
/// and supports threads.
/// </summary>
public partial class WinoListView : ListView, IDisposable
{
private ILogger logger = Log.ForContext<WinoListView>();
private const string PART_ScrollViewer = "ScrollViewer";
private ScrollViewer internalScrollviewer;
/// <summary>
/// Gets or sets whether this ListView belongs to thread items.
/// This is important for detecting selected items etc.
/// </summary>
public bool IsThreadListView
{
get { return (bool)GetValue(IsThreadListViewProperty); }
set { SetValue(IsThreadListViewProperty, value); }
}
public ICommand ItemDeletedCommand
{
get { return (ICommand)GetValue(ItemDeletedCommandProperty); }
set { SetValue(ItemDeletedCommandProperty, value); }
}
public ICommand LoadMoreCommand
{
get { return (ICommand)GetValue(LoadMoreCommandProperty); }
set { SetValue(LoadMoreCommandProperty, value); }
}
public bool IsThreadScrollingEnabled
{
get { return (bool)GetValue(IsThreadScrollingEnabledProperty); }
set { SetValue(IsThreadScrollingEnabledProperty, value); }
}
public static readonly DependencyProperty IsThreadScrollingEnabledProperty = DependencyProperty.Register(nameof(IsThreadScrollingEnabled), typeof(bool), typeof(WinoListView), new PropertyMetadata(false));
public static readonly DependencyProperty LoadMoreCommandProperty = DependencyProperty.Register(nameof(LoadMoreCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null));
public static readonly DependencyProperty IsThreadListViewProperty = DependencyProperty.Register(nameof(IsThreadListView), typeof(bool), typeof(WinoListView), new PropertyMetadata(false, new PropertyChangedCallback(OnIsThreadViewChanged)));
public static readonly DependencyProperty ItemDeletedCommandProperty = DependencyProperty.Register(nameof(ItemDeletedCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null));
public WinoListView()
{
CanDragItems = true;
IsItemClickEnabled = true;
IsMultiSelectCheckBoxEnabled = true;
IsRightTapEnabled = true;
SelectionMode = ListViewSelectionMode.Extended;
ShowsScrollingPlaceholders = false;
SingleSelectionFollowsFocus = true;
DragItemsCompleted += ItemDragCompleted;
DragItemsStarting += ItemDragStarting;
SelectionChanged += SelectedItemsChanged;
ProcessKeyboardAccelerators += ProcessDelKey;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
internalScrollviewer = GetTemplateChild(PART_ScrollViewer) as ScrollViewer;
if (internalScrollviewer == null)
{
logger.Warning("WinoListView does not have an internal ScrollViewer. Infinite scrolling behavior might be effected.");
return;
}
internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged;
internalScrollviewer.ViewChanged += InternalScrollVeiwerViewChanged;
}
private static void OnIsThreadViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is WinoListView winoListView)
{
winoListView.AdjustThreadViewContainerVisuals();
}
}
private void AdjustThreadViewContainerVisuals()
{
if (IsThreadListView)
{
ItemContainerTransitions.Clear();
}
}
private double lastestRaisedOffset = 0;
private int lastItemSize = 0;
// TODO: This is buggy. Does not work all the time. Debug.
private void InternalScrollVeiwerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (internalScrollviewer == null) return;
// No need to raise init request if there are no items in the list.
if (Items.Count == 0) return;
// If the scrolling is finished, check the current viewport height.
if (e.IsIntermediate)
{
var currentOffset = internalScrollviewer.VerticalOffset;
var maxOffset = internalScrollviewer.ScrollableHeight;
if (currentOffset + 10 >= maxOffset && lastestRaisedOffset != maxOffset && Items.Count != lastItemSize)
{
// We must load more.
lastestRaisedOffset = maxOffset;
lastItemSize = Items.Count;
LoadMoreCommand?.Execute(null);
}
}
}
private void ProcessDelKey(UIElement sender, Windows.UI.Xaml.Input.ProcessKeyboardAcceleratorEventArgs args)
{
if (args.Key == Windows.System.VirtualKey.Delete)
{
args.Handled = true;
ItemDeletedCommand?.Execute(MailOperation.SoftDelete);
}
}
private void ItemDragCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
if (args.Items.Any(a => a is MailItemViewModel))
{
args.Items.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = false);
}
}
private void ItemDragStarting(object sender, DragItemsStartingEventArgs args)
{
// Dragging multiple mails from different accounts/folders are supported with the condition below:
// All mails belongs to the drag will be matched on the dropped folder's account.
// Meaning that if users drag 1 mail from Account A/Inbox and 1 mail from Account B/Inbox,
// and drop to Account A/Inbox, the mail from Account B/Inbox will NOT be moved.
if (IsThreadListView)
{
var allItems = args.Items.Cast<MailItemViewModel>();
// Highlight all items
allItems.ForEach(a => a.IsCustomFocused = true);
// Set native drag arg properties.
var dragPackage = new MailDragPackage(allItems.Cast<IMailItem>());
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
}
else
{
var dragPackage = new MailDragPackage(args.Items.Cast<IMailItem>());
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
}
}
public void ChangeSelectionMode(ListViewSelectionMode selectionMode)
{
SelectionMode = selectionMode;
if (!IsThreadListView)
{
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
var threadListView = GetThreadInternalListView(c);
if (threadListView != null)
{
threadListView.SelectionMode = selectionMode;
}
});
}
}
/// <summary>
/// Finds the container for given mail item and adds it to selected items.
/// </summary>
/// <param name="mailItemViewModel">Mail to be added to selected items.</param>
/// <returns>Whether selection was successful or not.</returns>
public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel)
{
var itemContainer = ContainerFromItem(mailItemViewModel);
// This item might be in thread container.
if (itemContainer == null)
{
bool found = false;
Items.OfType<ThreadMailItemViewModel>().ForEach(c =>
{
if (!found)
{
var threadListView = GetThreadInternalListView(c);
if (threadListView != null)
found = threadListView.SelectMailItemContainer(mailItemViewModel);
}
});
return found;
}
SelectedItems.Add(mailItemViewModel);
return true;
}
/// <summary>
/// Recursively clears all selections except the given mail.
/// </summary>
/// <param name="exceptViewModel">Exceptional mail item to be not unselected.</param>
/// <param name="preserveThreadExpanding">Whether expansion states of thread containers should stay as it is or not.</param>
public void ClearSelections(MailItemViewModel exceptViewModel = null, bool preserveThreadExpanding = false)
{
SelectedItems.Clear();
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
var threadListView = GetThreadInternalListView(c);
if (threadListView == null)
return;
if (exceptViewModel != null)
{
if (!threadListView.SelectedItems.Contains(exceptViewModel))
{
if (!preserveThreadExpanding)
{
c.IsThreadExpanded = false;
}
threadListView.SelectedItems.Clear();
}
}
else
{
if (!preserveThreadExpanding)
{
c.IsThreadExpanded = false;
}
threadListView.SelectedItems.Clear();
}
});
}
/// <summary>
/// Recursively selects all mails, including thread items.
/// </summary>
public void SelectAllWino()
{
SelectAll();
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
c.IsThreadExpanded = true;
var threadListView = GetThreadInternalListView(c);
threadListView?.SelectAll();
});
}
// SelectedItems changed.
private void SelectedItemsChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems != null)
{
foreach (var removedItem in e.RemovedItems)
{
if (removedItem is MailItemViewModel removedMailItemViewModel)
{
// Mail item un-selected.
removedMailItemViewModel.IsSelected = false;
WeakReferenceMessenger.Default.Send(new MailItemSelectionRemovedEvent(removedMailItemViewModel));
}
else if (removedItem is ThreadMailItemViewModel removedThreadItemViewModel)
{
removedThreadItemViewModel.IsThreadExpanded = false;
}
}
}
if (e.AddedItems != null)
{
foreach (var addedItem in e.AddedItems)
{
if (addedItem is MailItemViewModel addedMailItemViewModel)
{
// Mail item selected.
addedMailItemViewModel.IsSelected = true;
WeakReferenceMessenger.Default.Send(new MailItemSelectedEvent(addedMailItemViewModel));
}
else if (addedItem is ThreadMailItemViewModel threadMailItemViewModel)
{
if (IsThreadScrollingEnabled)
{
if (internalScrollviewer != null && ContainerFromItem(threadMailItemViewModel) is FrameworkElement threadFrameworkElement)
{
internalScrollviewer.ScrollToElement(threadFrameworkElement, true, true, bringToTopOrLeft: true);
}
}
// Try to select first item.
if (GetThreadInternalListView(threadMailItemViewModel) is WinoListView internalListView)
{
internalListView.SelectFirstItem();
}
}
}
}
if (!IsThreadListView)
{
if (SelectionMode == ListViewSelectionMode.Extended && SelectedItems.Count == 1)
{
// Only 1 single item is selected in extended mode for main list view.
// We should un-select all thread items.
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
// c.IsThreadExpanded = false;
var threadListView = GetThreadInternalListView(c);
threadListView?.SelectedItems.Clear();
});
}
}
}
public async void SelectFirstItem()
{
if (Items.Count > 0)
{
if (Items[0] is MailItemViewModel firstMailItemViewModel)
{
// Make sure the invisible container is realized.
await Task.Delay(250);
if (ContainerFromItem(firstMailItemViewModel) is ListViewItem firstItemContainer)
{
firstItemContainer.IsSelected = true;
}
firstMailItemViewModel.IsSelected = true;
}
}
}
private WinoListView GetThreadInternalListView(ThreadMailItemViewModel threadMailItemViewModel)
{
var itemContainer = ContainerFromItem(threadMailItemViewModel);
if (itemContainer is ListViewItem listItem)
{
var expander = listItem.GetChildByName<WinoExpander>("ThreadExpander");
if (expander != null)
return expander.Content as WinoListView;
}
return null;
}
public void Dispose()
{
DragItemsCompleted -= ItemDragCompleted;
DragItemsStarting -= ItemDragStarting;
SelectionChanged -= SelectedItemsChanged;
ProcessKeyboardAccelerators -= ProcessDelKey;
if (internalScrollviewer != null)
{
internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged;
}
foreach (var item in Items)
{
if (item is ThreadMailItemViewModel threadMailItemViewModel)
{
var threadListView = GetThreadInternalListView(threadMailItemViewModel);
threadListView?.Dispose();
}
}
}
}
}