diff --git a/Wino.Core/MenuItems/MenuItemCollection.cs b/Wino.Core/MenuItems/MenuItemCollection.cs index d6f50cb3..c96351fe 100644 --- a/Wino.Core/MenuItems/MenuItemCollection.cs +++ b/Wino.Core/MenuItems/MenuItemCollection.cs @@ -192,10 +192,12 @@ namespace Wino.Core.MenuItems item.IsExpanded = false; item.IsSelected = false; - Remove(item); + try + { + Remove(item); + } + catch (Exception) { } }); - - // RemoveRange(itemsToRemove); } } } diff --git a/Wino.Mail/App.xaml b/Wino.Mail/App.xaml index 91b2d0b2..29f4d1b3 100644 --- a/Wino.Mail/App.xaml +++ b/Wino.Mail/App.xaml @@ -21,7 +21,9 @@ + + diff --git a/Wino.Mail/Controls/Advanced/WinoListView.cs b/Wino.Mail/Controls/Advanced/WinoListView.cs index 764084e2..bd8e4d56 100644 --- a/Wino.Mail/Controls/Advanced/WinoListView.cs +++ b/Wino.Mail/Controls/Advanced/WinoListView.cs @@ -1,8 +1,8 @@ using System; using System.Linq; +using System.Threading.Tasks; using System.Windows.Input; using CommunityToolkit.Mvvm.Messaging; -using Microsoft.UI.Xaml.Controls; using MoreLinq; using Serilog; using Windows.UI.Xaml; @@ -48,8 +48,15 @@ namespace Wino.Controls.Advanced 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)); + 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() @@ -65,7 +72,6 @@ namespace Wino.Controls.Advanced DragItemsCompleted += ItemDragCompleted; DragItemsStarting += ItemDragStarting; SelectionChanged += SelectedItemsChanged; - ItemClick += MailItemClicked; ProcessKeyboardAccelerators += ProcessDelKey; } @@ -85,6 +91,22 @@ namespace Wino.Controls.Advanced 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; @@ -160,19 +182,6 @@ namespace Wino.Controls.Advanced } } - private void MailItemClicked(object sender, ItemClickEventArgs e) - { - if (e.ClickedItem is ThreadMailItemViewModel clickedThread) - { - clickedThread.IsThreadExpanded = !clickedThread.IsThreadExpanded; - - if (!clickedThread.IsThreadExpanded) - { - SelectedItems.Clear(); - } - } - } - public void ChangeSelectionMode(ListViewSelectionMode selectionMode) { SelectionMode = selectionMode; @@ -294,6 +303,10 @@ namespace Wino.Controls.Advanced removedMailItemViewModel.IsSelected = false; WeakReferenceMessenger.Default.Send(new MailItemSelectionRemovedEvent(removedMailItemViewModel)); } + else if (removedItem is ThreadMailItemViewModel removedThreadItemViewModel) + { + removedThreadItemViewModel.IsThreadExpanded = false; + } } } @@ -311,10 +324,19 @@ namespace Wino.Controls.Advanced } else if (addedItem is ThreadMailItemViewModel threadMailItemViewModel) { - threadMailItemViewModel.IsThreadExpanded = true; + if (IsThreadScrollingEnabled) + { + if (internalScrollviewer != null && ContainerFromItem(threadMailItemViewModel) is FrameworkElement threadFrameworkElement) + { + internalScrollviewer.ScrollToElement(threadFrameworkElement, true, true, bringToTopOrLeft: true); + } + } - // Don't select thread containers. - SelectedItems.Remove(addedItem); + // Try to select first item. + if (GetThreadInternalListView(threadMailItemViewModel) is WinoListView internalListView) + { + internalListView.SelectFirstItem(); + } } } } @@ -336,16 +358,23 @@ namespace Wino.Controls.Advanced }); } } - else - { - if (SelectionMode == ListViewSelectionMode.Extended && SelectedItems.Count == 1) - { - // Tell main list view to unselect all his items. + } - if (SelectedItems[0] is MailItemViewModel selectedMailItemViewModel) + 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) { - WeakReferenceMessenger.Default.Send(new ResetSingleMailItemSelectionEvent(selectedMailItemViewModel)); + firstItemContainer.IsSelected = true; } + + firstMailItemViewModel.IsSelected = true; } } } @@ -356,7 +385,7 @@ namespace Wino.Controls.Advanced if (itemContainer is ListViewItem listItem) { - var expander = listItem.GetChildByName("ThreadExpander"); + var expander = listItem.GetChildByName("ThreadExpander"); if (expander != null) return expander.Content as WinoListView; @@ -370,7 +399,6 @@ namespace Wino.Controls.Advanced DragItemsCompleted -= ItemDragCompleted; DragItemsStarting -= ItemDragStarting; SelectionChanged -= SelectedItemsChanged; - ItemClick -= MailItemClicked; ProcessKeyboardAccelerators -= ProcessDelKey; if (internalScrollviewer != null) diff --git a/Wino.Mail/Controls/ImagePreviewControl.cs b/Wino.Mail/Controls/ImagePreviewControl.cs index f8edd39f..344e1057 100644 --- a/Wino.Mail/Controls/ImagePreviewControl.cs +++ b/Wino.Mail/Controls/ImagePreviewControl.cs @@ -80,8 +80,6 @@ namespace Wino.Controls control.UpdateInformation(); } - - private async void UpdateInformation() { if (KnownHostImage == null || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress))) diff --git a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml index 889f633d..307b111a 100644 --- a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml +++ b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml @@ -2,15 +2,21 @@ x:Class="Wino.Controls.MailItemDisplayInformationControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals" xmlns:controls="using:Wino.Controls" - xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:domain="using:Wino.Core.Domain" + xmlns:enums="using:Wino.Core.Domain.Enums" + xmlns:helpers="using:Wino.Helpers" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + HorizontalContentAlignment="Stretch" + VerticalContentAlignment="Stretch" FocusVisualMargin="8" FocusVisualPrimaryBrush="{StaticResource SystemControlRevealFocusVisualBrush}" FocusVisualPrimaryThickness="2" FocusVisualSecondaryBrush="{StaticResource SystemControlFocusVisualSecondaryBrush}" FocusVisualSecondaryThickness="1" - xmlns:helpers="using:Wino.Helpers" PointerEntered="ControlPointerEntered" PointerExited="ControlPointerExited"> @@ -25,26 +31,24 @@ - - - - - + + Canvas.ZIndex="9999" + Fill="{ThemeResource SystemAccentColor}" + Visibility="{x:Bind IsCustomFocused, Mode=OneWay}" /> - + + + + + + + + + + - + @@ -243,7 +274,7 @@ - + @@ -255,7 +286,7 @@ - + @@ -277,6 +308,19 @@ + + + + + + + + + + + + + diff --git a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs index 4b080e1c..5e104b0b 100644 --- a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs +++ b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs @@ -1,9 +1,8 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using System.Windows.Input; -using Microsoft.UI.Xaml.Controls; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Input; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.MailItem; @@ -16,12 +15,14 @@ namespace Wino.Controls { public ImagePreviewControl GetImagePreviewControl() => ContactImage; + public bool IsRunningHoverAction { get; set; } + public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(MailListDisplayMode), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailListDisplayMode.Spacious)); public static readonly DependencyProperty ShowPreviewTextProperty = DependencyProperty.Register(nameof(ShowPreviewText), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); public static readonly DependencyProperty IsCustomFocusedProperty = DependencyProperty.Register(nameof(IsCustomFocused), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsAvatarVisibleProperty = DependencyProperty.Register(nameof(IsAvatarVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); public static readonly DependencyProperty IsSubjectVisibleProperty = DependencyProperty.Register(nameof(IsSubjectVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); - public static readonly DependencyProperty ConnectedExpanderProperty = DependencyProperty.Register(nameof(ConnectedExpander), typeof(Expander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); + public static readonly DependencyProperty ConnectedExpanderProperty = DependencyProperty.Register(nameof(ConnectedExpander), typeof(WinoExpander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); public static readonly DependencyProperty LeftHoverActionProperty = DependencyProperty.Register(nameof(LeftHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); public static readonly DependencyProperty CenterHoverActionProperty = DependencyProperty.Register(nameof(CenterHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); public static readonly DependencyProperty RightHoverActionProperty = DependencyProperty.Register(nameof(RightHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); @@ -29,6 +30,20 @@ namespace Wino.Controls public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); public static readonly DependencyProperty IsHoverActionsEnabledProperty = DependencyProperty.Register(nameof(IsHoverActionsEnabled), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); + public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); + public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); + + public bool IsThreadExpanded + { + get { return (bool)GetValue(IsThreadExpandedProperty); } + set { SetValue(IsThreadExpandedProperty, value); } + } + + public bool IsThreadExpanderVisible + { + get { return (bool)GetValue(IsThreadExpanderVisibleProperty); } + set { SetValue(IsThreadExpanderVisibleProperty, value); } + } public bool Prefer24HourTimeFormat { @@ -72,10 +87,9 @@ namespace Wino.Controls set { SetValue(RightHoverActionProperty, value); } } - - public Expander ConnectedExpander + public WinoExpander ConnectedExpander { - get { return (Expander)GetValue(ConnectedExpanderProperty); } + get { return (WinoExpander)GetValue(ConnectedExpanderProperty); } set { SetValue(ConnectedExpanderProperty, value); } } @@ -109,8 +123,6 @@ namespace Wino.Controls set { SetValue(DisplayModeProperty, value); } } - private bool tappedHandlingFlag = false; - public MailItemDisplayInformationControl() { this.InitializeComponent(); @@ -147,38 +159,22 @@ namespace Wino.Controls private void ExecuteHoverAction(MailOperation operation) { + IsRunningHoverAction = true; + MailOperationPreperationRequest package = null; if (MailItem is MailCopy mailCopy) package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true); else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel) package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true); + else if (MailItem is ThreadMailItem threadMailItem) + package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast().Select(a => a.MailCopy), toggleExecution: true); if (package == null) return; - tappedHandlingFlag = true; - HoverActionExecutedCommand?.Execute(package); } - private void ThreadHeaderTapped(object sender, TappedRoutedEventArgs e) - { - // Due to CanDrag=True, outer expander doesn't get the click event and it doesn't expand. We expand here manually. - // Also hover action button clicks will be delegated here after the execution runs. - // We should not expand the thread if the reason we are here is for hover actions. - - if (tappedHandlingFlag) - { - tappedHandlingFlag = false; - e.Handled = true; - return; - } - - if (ConnectedExpander == null) return; - - ConnectedExpander.IsExpanded = !ConnectedExpander.IsExpanded; - } - private void FirstActionClicked(object sender, RoutedEventArgs e) { ExecuteHoverAction(LeftHoverAction); diff --git a/Wino.Mail/Controls/WinoExpander.cs b/Wino.Mail/Controls/WinoExpander.cs index e407b6f2..c286ee0e 100644 --- a/Wino.Mail/Controls/WinoExpander.cs +++ b/Wino.Mail/Controls/WinoExpander.cs @@ -1,17 +1,129 @@ -using Microsoft.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; +using CommunityToolkit.Diagnostics; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Xaml.Markup; namespace Wino.Controls { - public class WinoExpander : Expander + [ContentProperty(Name = nameof(Content))] + public class WinoExpander : Control { + private const string PART_HeaderGrid = "HeaderGrid"; + private const string PART_ContentAreaWrapper = "ContentAreaWrapper"; + private const string PART_ContentArea = "ContentArea"; + + private ContentControl HeaderGrid; + private ContentControl ContentArea; + private Grid ContentAreaWrapper; + + public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(nameof(Header), typeof(UIElement), typeof(WinoExpander), new PropertyMetadata(null)); + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(UIElement), typeof(WinoExpander), new PropertyMetadata(null)); + public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(WinoExpander), new PropertyMetadata(false, new PropertyChangedCallback(OnIsExpandedChanged))); + public static readonly DependencyProperty TemplateSettingsProperty = DependencyProperty.Register(nameof(TemplateSettings), typeof(WinoExpanderTemplateSettings), typeof(WinoExpander), new PropertyMetadata(new WinoExpanderTemplateSettings())); + + public UIElement Content + { + get { return (UIElement)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + public WinoExpanderTemplateSettings TemplateSettings + { + get { return (WinoExpanderTemplateSettings)GetValue(TemplateSettingsProperty); } + set { SetValue(TemplateSettingsProperty, value); } + } + + public bool IsExpanded + { + get { return (bool)GetValue(IsExpandedProperty); } + set { SetValue(IsExpandedProperty, value); } + } + + public UIElement Header + { + get { return (UIElement)GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + protected override void OnApplyTemplate() { base.OnApplyTemplate(); - if (GetTemplateChild("ExpanderHeader") is ToggleButton toggleButton) + + HeaderGrid = GetTemplateChild(PART_HeaderGrid) as ContentControl; + ContentAreaWrapper = GetTemplateChild(PART_ContentAreaWrapper) as Grid; + ContentArea = GetTemplateChild(PART_ContentArea) as ContentControl; + + Guard.IsNotNull(HeaderGrid, nameof(HeaderGrid)); + Guard.IsNotNull(ContentAreaWrapper, nameof(ContentAreaWrapper)); + Guard.IsNotNull(ContentArea, nameof(ContentArea)); + + var clipComposition = ElementCompositionPreview.GetElementVisual(ContentAreaWrapper); + clipComposition.Clip = clipComposition.Compositor.CreateInsetClip(); + + ContentAreaWrapper.SizeChanged += ContentSizeChanged; + HeaderGrid.Tapped += HeaderTapped; + } + + private void ContentSizeChanged(object sender, SizeChangedEventArgs e) + { + TemplateSettings.ContentHeight = e.NewSize.Height; + TemplateSettings.NegativeContentHeight = -1 * (double)e.NewSize.Height; + } + + private void HeaderTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e) + { + // Tapped is delegated from executing hover action like flag or delete. + // No need to toggle the expander. + + if (Header is MailItemDisplayInformationControl itemDisplayInformationControl && + itemDisplayInformationControl.IsRunningHoverAction) { - toggleButton.Padding = new Windows.UI.Xaml.Thickness(0, 4, 0, 4); + itemDisplayInformationControl.IsRunningHoverAction = false; + return; } + + IsExpanded = !IsExpanded; + } + + private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) + { + if (obj is WinoExpander control) + control.UpdateVisualStates(); + } + + private void UpdateVisualStates() + { + VisualStateManager.GoToState(this, IsExpanded ? "Expanded" : "Collapsed", true); } } + + #region Settings + + public class WinoExpanderTemplateSettings : DependencyObject + { + public static readonly DependencyProperty HeaderHeightProperty = DependencyProperty.Register(nameof(HeaderHeight), typeof(double), typeof(WinoExpanderTemplateSettings), new PropertyMetadata(0.0)); + public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(WinoExpanderTemplateSettings), new PropertyMetadata(0.0)); + public static readonly DependencyProperty NegativeContentHeightProperty = DependencyProperty.Register(nameof(NegativeContentHeight), typeof(double), typeof(WinoExpanderTemplateSettings), new PropertyMetadata(0.0)); + + public double NegativeContentHeight + { + get { return (double)GetValue(NegativeContentHeightProperty); } + set { SetValue(NegativeContentHeightProperty, value); } + } + + public double HeaderHeight + { + get { return (double)GetValue(HeaderHeightProperty); } + set { SetValue(HeaderHeightProperty, value); } + } + + public double ContentHeight + { + get { return (double)GetValue(ContentHeightProperty); } + set { SetValue(ContentHeightProperty, value); } + } + } + + #endregion } diff --git a/Wino.Mail/Extensions/UtilExtensions.cs b/Wino.Mail/Extensions/UtilExtensions.cs index bfbea266..50367afc 100644 --- a/Wino.Mail/Extensions/UtilExtensions.cs +++ b/Wino.Mail/Extensions/UtilExtensions.cs @@ -93,7 +93,8 @@ namespace Wino.Extensions if (isVerticalScrolling) { - scrollViewer.ChangeView(null, position.Y, zoomFactor, !smoothScrolling); + // Accomodate for additional header. + scrollViewer.ChangeView(null, Math.Max(0, position.Y - 48), zoomFactor, !smoothScrolling); } else { diff --git a/Wino.Mail/Styles/WinoExpanderStyle.xaml b/Wino.Mail/Styles/WinoExpanderStyle.xaml new file mode 100644 index 00000000..0e455ff9 --- /dev/null +++ b/Wino.Mail/Styles/WinoExpanderStyle.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail/Styles/WinoExpanderStyle.xaml.cs b/Wino.Mail/Styles/WinoExpanderStyle.xaml.cs new file mode 100644 index 00000000..dd8a56fc --- /dev/null +++ b/Wino.Mail/Styles/WinoExpanderStyle.xaml.cs @@ -0,0 +1,12 @@ +using Windows.UI.Xaml; + +namespace Wino.Styles +{ + partial class WinoExpanderStyle : ResourceDictionary + { + public WinoExpanderStyle() + { + InitializeComponent(); + } + } +} diff --git a/Wino.Mail/Views/MailListPage.xaml b/Wino.Mail/Views/MailListPage.xaml index e7ec8e49..0af153da 100644 --- a/Wino.Mail/Views/MailListPage.xaml +++ b/Wino.Mail/Views/MailListPage.xaml @@ -169,18 +169,14 @@ - + - - + + - + @@ -261,7 +259,8 @@ CustomMessageDialogStyles.xaml + + WinoExpanderStyle.xaml + @@ -527,6 +530,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile