diff --git a/Photos/Animations/CardHoverEffectBehavior.cs b/Photos/Animations/CardHoverEffectBehavior.cs index 8519212..44252dc 100644 --- a/Photos/Animations/CardHoverEffectBehavior.cs +++ b/Photos/Animations/CardHoverEffectBehavior.cs @@ -29,6 +29,12 @@ public static class CardHoverEffectBehavior typeof(CardHoverEffectBehavior), 0.99); + public static readonly AttachedProperty SuppressPointerHoverProperty = + AvaloniaProperty.RegisterAttached( + "SuppressPointerHover", + typeof(CardHoverEffectBehavior), + false); + static CardHoverEffectBehavior() { IsEnabledProperty.Changed.AddClassHandler(OnIsEnabledChanged); @@ -43,10 +49,64 @@ public static class CardHoverEffectBehavior public static double GetScaleFactor(AvaloniaObject obj) => obj.GetValue(ScaleFactorProperty); public static void SetScaleFactor(AvaloniaObject obj, double value) => obj.SetValue(ScaleFactorProperty, value); + public static bool GetSuppressPointerHover(AvaloniaObject obj) => obj.GetValue(SuppressPointerHoverProperty); + + public static void SetSuppressPointerHover(AvaloniaObject obj, bool value) => obj.SetValue(SuppressPointerHoverProperty, value); + + public static void ApplyHover(Control element) + { + element.ZIndex = 10; + + var visual = ElementComposition.GetElementVisual(element); + var compositor = visual?.Compositor; + + if (visual is null || compositor is null) return; + + var scaleFactor = (float)GetScaleFactor(element); + + var width = (float)element.Bounds.Width; + var height = (float)element.Bounds.Height; + + if (width <= 0 || height <= 0) return; + + visual.CenterPoint = new Vector3(width / 2f, height / 2f, 0f); + + var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.InsertKeyFrame(1.0f, new Vector3(scaleFactor, scaleFactor, 1f)); + scaleAnimation.Duration = _animationDuration; + + var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation(); + orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity); + orientationAnimation.Duration = _animationDuration; + + visual.StartAnimation("Scale", scaleAnimation); + visual.StartAnimation(nameof(visual.Orientation), orientationAnimation); + } + + public static void ResetHover(Control element) + { + element.ZIndex = 0; + + var visual = ElementComposition.GetElementVisual(element); + var compositor = visual?.Compositor; + + if (visual is null || compositor is null) return; + + var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.InsertKeyFrame(1.0f, new Vector3(1f, 1f, 1f)); + scaleAnimation.Duration = _animationDuration; + + var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation(); + orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity); + orientationAnimation.Duration = _animationDuration; + + visual.StartAnimation("Scale", scaleAnimation); + visual.StartAnimation(nameof(visual.Orientation), orientationAnimation); + } + private static void OnIsEnabledChanged(Control c, AvaloniaPropertyChangedEventArgs e) { - if (e.NewValue is not bool enabled) - return; + if (e.NewValue is not bool enabled) return; if (enabled) { @@ -64,55 +124,30 @@ public static class CardHoverEffectBehavior private static void Element_PointerEntered(object? sender, PointerEventArgs e) { - if (sender is not Control element) - return; + if (sender is not Control element || GetSuppressPointerHover(element)) return; + ApplyHover(element); + } - element.ZIndex = 10; - var visual = ElementComposition.GetElementVisual(element); - var compositor = visual?.Compositor; - - if (visual is null || compositor is null) - return; - - var scaleFactor = (float)GetScaleFactor(element); - - var width = (float)element.Bounds.Width; - var height = (float)element.Bounds.Height; - - if (width <= 0 || height <= 0) - return; - - visual.CenterPoint = new Vector3(width / 2f, height / 2f, 0f); - - var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); - scaleAnimation.InsertKeyFrame(1.0f, new Vector3(scaleFactor, scaleFactor, 1f)); - scaleAnimation.Duration = _animationDuration; - - var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation(); - orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity); - orientationAnimation.Duration = _animationDuration; - - visual.StartAnimation("Scale", scaleAnimation); - visual.StartAnimation(nameof(visual.Orientation), orientationAnimation); + private static void Element_PointerExited(object? sender, PointerEventArgs e) + { + if (sender is not Control element || GetSuppressPointerHover(element)) return; + ResetHover(element); } private static void Element_PointerMoved(object? sender, PointerEventArgs e) { - if (sender is not Control element) - return; + if (sender is not Control element || GetSuppressPointerHover(element)) return; var visual = ElementComposition.GetElementVisual(element); - if (visual is null) - return; + if (visual is null) return; var maxRotationAngle = (float)GetMaxRotationAngle(element); var width = element.Bounds.Width; var height = element.Bounds.Height; - if (width <= 0 || height <= 0) - return; + if (width <= 0 || height <= 0) return; var position = e.GetPosition(element); var centerX = width / 2.0; @@ -132,28 +167,4 @@ public static class CardHoverEffectBehavior visual.Orientation = qy * qx; } - - private static void Element_PointerExited(object? sender, PointerEventArgs e) - { - if (sender is not Control element) - return; - - element.ZIndex = 0; - var visual = ElementComposition.GetElementVisual(element); - var compositor = visual?.Compositor; - - if (visual is null || compositor is null) - return; - - var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); - scaleAnimation.InsertKeyFrame(1.0f, new Vector3(1f, 1f, 1f)); - scaleAnimation.Duration = _animationDuration; - - var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation(); - orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity); - orientationAnimation.Duration = _animationDuration; - - visual.StartAnimation("Scale", scaleAnimation); - visual.StartAnimation(nameof(visual.Orientation), orientationAnimation); - } } \ No newline at end of file diff --git a/Photos/ViewModels/MainWindowViewModel.cs b/Photos/ViewModels/MainWindowViewModel.cs index 13c7d53..0cd0a27 100644 --- a/Photos/ViewModels/MainWindowViewModel.cs +++ b/Photos/ViewModels/MainWindowViewModel.cs @@ -101,7 +101,6 @@ public partial class MainWindowViewModel : ViewModelBase public async Task TryFocusImage(int index) { if (index < 0 || index >= Photos.Count ) return; - // if(SelectedPhoto != null) ResetSelectedPhoto(); SelectedPhoto = Photos[index]; await SelectedPhoto.LoadFullImage(); } diff --git a/Photos/Views/MainWindow.axaml b/Photos/Views/MainWindow.axaml index d58a077..ce6e5e7 100644 --- a/Photos/Views/MainWindow.axaml +++ b/Photos/Views/MainWindow.axaml @@ -64,6 +64,8 @@ Background="Transparent" Foreground="#EDEDED" Margin="5,0,0,0" + IsTabStop="False" + Focusable="False" > - - + + @@ -114,11 +116,13 @@ Grid.RowSpan="2" IsVisible="{Binding PhotosLoaded}"> - + IsTabStop="True" + > + anim:CardHoverEffectBehavior.ScaleFactor="1.08" + > diff --git a/Photos/Views/MainWindow.axaml.cs b/Photos/Views/MainWindow.axaml.cs index e76f1a5..e35166c 100644 --- a/Photos/Views/MainWindow.axaml.cs +++ b/Photos/Views/MainWindow.axaml.cs @@ -1,7 +1,11 @@ +using System; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.VisualTree; using Avalonia.Xaml.Interactions.DragAndDrop; +using Photos.Animations; using Photos.Models; using Photos.ViewModels; @@ -9,8 +13,17 @@ namespace Photos.Views; public partial class MainWindow : Window { + private enum NavigationInputMode + { + Mouse, + Keyboard + } + + private NavigationInputMode _navigationInputMode = NavigationInputMode.Mouse; + private IFolderPickerService _folderPickerService; private int _currentImageIndex = 0; + private Control? _currentHoveredCard; public MainWindow() { @@ -24,10 +37,25 @@ public partial class MainWindow : Window (DataContext as MainWindowViewModel)?.WindowDropHandler(e); } - private void OnKeyDownHandler(object sender, KeyEventArgs e) + private async void OnKeyDownHandler(object sender, KeyEventArgs e) { if (DataContext is not MainWindowViewModel vm) return; + // input state changer + switch (e.Key) + { + case Key.Enter: + case Key.Escape: + case Key.Left: + case Key.Right: + case Key.Up: + case Key.Down: + _navigationInputMode = NavigationInputMode.Keyboard; + UpdatePointerHoverSuppression(); + UpdateKeyboardHover(); + break; + } + async Task HandleEnter() { if (vm.IsPhotoFocused()) vm.UnfocusPhoto(); @@ -45,28 +73,98 @@ public partial class MainWindow : Window // TODO: move basic selection } + int CalculateCurrentIndexHop() + { + // Values of UniformGridLayout + var minItemWidth = 220; + var minColumnSpacing = 18; + + var repeaterWidth = Repeater.Bounds.Width; + + var abstractedColumnCount = Math.Floor(repeaterWidth / (minItemWidth + minColumnSpacing)); + return Math.Max(1, (int)abstractedColumnCount); + } + + void HandleArrowKeyDown() + { + var nextIdx = CalculateCurrentIndexHop(); + if (_currentImageIndex + nextIdx >= vm.Photos.Count) _currentImageIndex = vm.Photos.Count - 1; + else _currentImageIndex += nextIdx; + } + + void HandleArrowKeyUp() + { + var nextIdx = CalculateCurrentIndexHop(); + if (_currentImageIndex - nextIdx < 0) _currentImageIndex = 0; + else _currentImageIndex -= nextIdx; + } + + Control? FindCardControlForCurrentIndex() + { + if (_currentImageIndex < 0 || _currentImageIndex >= vm.Photos.Count) + return null; + + var currentPhoto = vm.Photos[_currentImageIndex]; + + return Repeater.GetVisualDescendants() + .OfType() + .FirstOrDefault(c => + ReferenceEquals(c.DataContext, currentPhoto) && + CardHoverEffectBehavior.GetIsEnabled(c)); + } + + void UpdateKeyboardHover() + { + if (DataContext is not MainWindowViewModel vm) + return; + + if (_currentHoveredCard is not null) + { + CardHoverEffectBehavior.ResetHover(_currentHoveredCard); + _currentHoveredCard = null; + } + + var newCard = FindCardControlForCurrentIndex(); + if (newCard is null) return; + + CardHoverEffectBehavior.ApplyHover(newCard); + _currentHoveredCard = newCard; + _currentHoveredCard.BringIntoView(); + } + switch (e.Key) { - case Key.Enter: - HandleEnter().GetAwaiter(); + case Key.Enter: case Key.Space: + if (!vm.PhotosLoaded) await vm.OpenDirectory(); + else await HandleEnter(); break; case Key.Escape: HandleEscape(); break; case Key.Down: + HandleArrowKeyDown(); + UpdateKeyboardHover(); + await HandleArrowKey(); + break; + case Key.Up: + HandleArrowKeyUp(); + UpdateKeyboardHover(); + await HandleArrowKey(); break; case Key.Left: if (_currentImageIndex > 0) { _currentImageIndex--; - HandleArrowKey().GetAwaiter(); + UpdateKeyboardHover(); + await HandleArrowKey(); } break; case Key.Right: if (_currentImageIndex < vm.Photos.Count - 1) { _currentImageIndex++; - HandleArrowKey().GetAwaiter(); + UpdateKeyboardHover(); + await HandleArrowKey(); } break; } @@ -74,9 +172,34 @@ public partial class MainWindow : Window private void PointerEnteredPhotoHandler(object sender, PointerEventArgs e) { - if (sender is not Image i || DataContext is not MainWindowViewModel vm) return; - var idx = vm.GetImageIndex(i); - if (idx == -1) return; + if (sender is not Image image || DataContext is not MainWindowViewModel vm) return; + + var photo = image.DataContext as Photo; + if (photo is null) return; + + var idx = vm.Photos.IndexOf(photo); + if (idx < 0) return; + + _navigationInputMode = NavigationInputMode.Mouse; + UpdatePointerHoverSuppression(); _currentImageIndex = idx; + + if (_currentHoveredCard is null) return; + + CardHoverEffectBehavior.ResetHover(_currentHoveredCard); + _currentHoveredCard = null; + } + + private void UpdatePointerHoverSuppression() + { + foreach (var control in Repeater.GetVisualDescendants().OfType()) + { + if (CardHoverEffectBehavior.GetIsEnabled(control)) + { + CardHoverEffectBehavior.SetSuppressPointerHover( + control, + _navigationInputMode == NavigationInputMode.Keyboard); + } + } } } \ No newline at end of file