瀏覽代碼

Fixed an issue where the button fades out even though the cursor is still on it. Hover buttons refactor.

Ruben 5 月之前
父節點
當前提交
7f6b5f6344

+ 1 - 144
src/PicView.Avalonia/UI/HideInterfaceLogic.cs

@@ -1,6 +1,4 @@
-using Avalonia.Controls;
-using Avalonia.Threading;
-using PicView.Avalonia.Animations;
+using Avalonia.Threading;
 using PicView.Avalonia.Gallery;
 using PicView.Avalonia.Navigation;
 using PicView.Avalonia.ViewModels;
@@ -120,147 +118,6 @@ public static class HideInterfaceLogic
     
     #endregion
 
-    #region HoverButtons
-    
-    public static void AddHoverButtonEvents(Control parent,  MainViewModel vm)
-    {
-        parent.PointerEntered += async delegate
-        {
-            await DoHoverButtonAnimation(isShown:true, parent);
-        };
-        parent.PointerExited += async delegate
-        {
-            await DoHoverButtonAnimation(isShown: false, parent);
-        };
-    }
-    
-    public static void AddHoverButtonEvents(Control parent, Control childControl, MainViewModel vm)
-    {
-        childControl.PointerEntered += delegate
-        {
-            if (!Settings.UIProperties.ShowAltInterfaceButtons)
-            {
-                return;
-            }
-            
-            if (!NavigationManager.CanNavigate(vm))
-            {
-                Dispatcher.UIThread.Invoke(() =>
-                {
-                    parent.Opacity = 0;
-                    childControl.Opacity = 0;
-                });
-                return;
-            }
-
-            if (NavigationManager.GetCount <= 1)
-            {
-                Dispatcher.UIThread.Invoke(() =>
-                {
-                    parent.Opacity = 0;
-                    childControl.Opacity = 0;
-                });
-                return;
-            }
-
-            if (childControl.IsPointerOver)
-            {
-                Dispatcher.UIThread.Invoke(() =>
-                {
-                    parent.Opacity = 10;
-                    childControl.Opacity = 1;
-                });
-            }
-        };
-        parent.PointerEntered += async delegate
-        {
-            if (!Settings.UIProperties.ShowAltInterfaceButtons)
-            {
-                return;
-            }
-            
-            await DoHoverButtonAnimation(isShown:true, parent, childControl, vm);
-        };
-        parent.PointerExited += async delegate
-        {
-            await DoHoverButtonAnimation(isShown: false, parent, childControl, vm);
-        };
-        UIHelper.GetMainView.PointerExited += async delegate
-        {
-            var x = 0;
-            while (_isHoverButtonAnimationRunning)
-            {
-                await Task.Delay(10);
-                x++;
-                if (x > 20)
-                {
-                    await Dispatcher.UIThread.InvokeAsync(() =>
-                    {
-                        if (!childControl.IsPointerOver)
-                        {
-                            parent.Opacity = 0;
-                            childControl.Opacity = 0;
-                        }
-                    });
-                    break;
-                }
-            }
-
-            if (parent.Opacity > 0)
-            {
-                await DoHoverButtonAnimation(isShown: false, parent, childControl, vm);
-            }
-        };
-    }
-    
-    private static bool _isHoverButtonAnimationRunning;
-    
-    private static async Task DoHoverButtonAnimation(bool isShown, Control parent)
-    {
-        if (_isHoverButtonAnimationRunning || !Settings.UIProperties.ShowAltInterfaceButtons)
-        {
-            return;
-        }
-        
-        _isHoverButtonAnimationRunning = true;
-        var from = isShown ? 0d : 1d;
-        var to = isShown ? 1d : 0d;
-        var speed = isShown ? 0.3 : 0.45;
-        var anim = AnimationsHelper.OpacityAnimation(from, to, speed);
-        await anim.RunAsync(parent);
-        _isHoverButtonAnimationRunning = false;
-    }
-    private static async Task DoHoverButtonAnimation(bool isShown, Control parent, Control childControl, MainViewModel vm)
-    {
-        if (_isHoverButtonAnimationRunning || !Settings.UIProperties.ShowAltInterfaceButtons)
-        {
-            return;
-        }
-
-        if (!NavigationManager.CanNavigate(vm))
-        {
-            parent.Opacity = 0;
-            childControl.Opacity = 0;
-            return;
-        }
-
-        if (NavigationManager.GetCount <= 1)
-        {
-            parent.Opacity = 0;
-            childControl.Opacity = 0;
-            return;
-        }
-        _isHoverButtonAnimationRunning = true;
-        var from = isShown ? 0d : 1d;
-        var to = isShown ? 1d : 0d;
-        var speed = isShown ? 0.3 : 0.45;
-        var anim = AnimationsHelper.OpacityAnimation(from, to, speed);
-        await anim.RunAsync(childControl);
-        _isHoverButtonAnimationRunning = false;
-    }
-    
-    #endregion
-
     public static async Task ToggleBottomGalleryShownInHiddenUI(MainViewModel vm)
     {
         Settings.Gallery.ShowBottomGalleryInHiddenUI = !Settings.Gallery

+ 156 - 0
src/PicView.Avalonia/UI/HoverFadeButtonHandler.cs

@@ -0,0 +1,156 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Threading;
+using PicView.Avalonia.Animations;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.ViewModels;
+
+namespace PicView.Avalonia.UI;
+
+/// <summary>
+///     Handles fade-in and fade-out animation for a button (or button group) based on pointer proximity.
+/// </summary>
+public class HoverFadeButtonHandler
+{
+    private readonly Control? _childButton;
+    private readonly Control _mainButton;
+    private readonly MainViewModel _vm;
+    private CancellationTokenSource? _fadeCts;
+
+    /// <summary>
+    ///     Initializes the hover fade logic for a button or button group.
+    /// </summary>
+    /// <param name="mainButton">The main button or parent control.</param>
+    /// <param name="vm">The ViewModel for context (navigation, settings, etc).</param>
+    /// <param name="childButton">Optional child button (e.g., an icon inside the button).</param>
+    public HoverFadeButtonHandler(Control mainButton, MainViewModel vm, Control? childButton = null)
+    {
+        _mainButton = mainButton ?? throw new ArgumentNullException(nameof(mainButton));
+        _childButton = childButton;
+        _vm = vm ?? throw new ArgumentNullException(nameof(vm));
+
+        AttachEvents();
+    }
+
+    /// <summary>
+    ///     Duration of fade-in and fade-out in seconds.
+    /// </summary>
+    public double FadeInDuration { get; set; } = 0.3;
+
+    public double FadeOutDuration { get; set; } = 0.45;
+
+    private void AttachEvents()
+    {
+        _mainButton.PointerEntered += OnPointerEntered;
+        _mainButton.PointerExited += OnPointerExited;
+        if (_childButton != null)
+        {
+            _childButton.PointerEntered += OnPointerEntered;
+            _childButton.PointerExited += OnPointerExited;
+        }
+    }
+
+    private void OnPointerEntered(object? sender, PointerEventArgs e)
+    {
+        if (!ShouldShowButton())
+        {
+            SetOpacity(0);
+            return;
+        }
+
+        FadeTo(1, FadeInDuration);
+    }
+
+    private void OnPointerExited(object? sender, PointerEventArgs e)
+    {
+        // Delay fade-out to ensure pointer is truly outside both parent and child
+        Dispatcher.UIThread.Post(async () =>
+        {
+            await Task.Delay(30); // short delay to allow pointer transitions
+            if (!IsPointerOver())
+            {
+                FadeTo(0, FadeOutDuration);
+            }
+        });
+    }
+
+    private bool ShouldShowButton()
+    {
+        // You may want to extend this with more checks
+        if (!Settings.UIProperties.ShowAltInterfaceButtons)
+        {
+            return false;
+        }
+
+        if (_childButton != null && !NavigationManager.CanNavigate(_vm))
+        {
+            return false;
+        }
+
+        return _childButton == null || NavigationManager.GetCount > 1;
+    }
+
+    /// <summary>
+    ///     Checks if the pointer is over the main button or the child button.
+    /// </summary>
+    private bool IsPointerOver()
+    {
+        if (_mainButton.IsPointerOver)
+        {
+            return true;
+        }
+
+        return _childButton?.IsPointerOver == true;
+    }
+
+    /// <summary>
+    ///     Fades the button(s) from their current opacity to the target value.
+    /// </summary>
+    private void FadeTo(double targetOpacity, double durationSeconds)
+    {
+        _fadeCts?.Cancel();
+        var cts = new CancellationTokenSource();
+        _fadeCts = cts;
+
+        var controls = _childButton != null ? new[] { _mainButton, _childButton } : new[] { _mainButton };
+
+        foreach (var ctrl in controls)
+        {
+            // Run animation for each control
+            _ = AnimateOpacityAsync(ctrl, targetOpacity, durationSeconds, cts.Token);
+        }
+    }
+
+    private static async Task AnimateOpacityAsync(Control control, double targetOpacity, double durationSeconds,
+        CancellationToken token)
+    {
+        var from = control.Opacity;
+        if (Math.Abs(from - targetOpacity) < 0.01)
+        {
+            return;
+        }
+
+        var anim = AnimationsHelper.OpacityAnimation(from, targetOpacity, durationSeconds);
+        try
+        {
+            await anim.RunAsync(control, token);
+            // After fade out, ensure fully hidden (in case animation didn't complete)
+            if (Math.Abs(targetOpacity) < 0.01)
+            {
+                control.Opacity = 0;
+            }
+        }
+        catch (TaskCanceledException)
+        {
+        }
+    }
+
+    private void SetOpacity(double opacity)
+    {
+        _mainButton.Opacity = opacity;
+        if (_childButton != null)
+        {
+            _childButton.Opacity = opacity;
+        }
+    }
+}

+ 5 - 1
src/PicView.Avalonia/Views/MainView.axaml.cs

@@ -115,7 +115,11 @@ public partial class MainView : UserControl
                 await FunctionsMapper.ShowRecentHistoryFile();
             };
 
-            HideInterfaceLogic.AddHoverButtonEvents(AltButtonsPanel, vm);
+            // Setup hover fade buttons
+            _ = new HoverFadeButtonHandler(ClickArrowRight, vm, ClickArrowRight.PolyButton);
+            _ = new HoverFadeButtonHandler(ClickArrowLeft, vm, ClickArrowLeft.PolyButton);
+            _ = new HoverFadeButtonHandler(AltButtonsPanel, vm);
+            
             PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
     }

+ 0 - 2
src/PicView.Avalonia/Views/UC/Buttons/ClickArrowLeft.axaml.cs

@@ -1,5 +1,4 @@
 using Avalonia.Controls;
-using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 
 namespace PicView.Avalonia.Views.UC.Buttons;
@@ -14,7 +13,6 @@ public partial class ClickArrowLeft : UserControl
             {
                 return;
             }
-            HideInterfaceLogic.AddHoverButtonEvents(this, PolyButton, vm);
             PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
     }

+ 0 - 2
src/PicView.Avalonia/Views/UC/Buttons/ClickArrowRight.axaml.cs

@@ -1,5 +1,4 @@
 using Avalonia.Controls;
-using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 
 namespace PicView.Avalonia.Views.UC.Buttons;
@@ -14,7 +13,6 @@ public partial class ClickArrowRight : UserControl
             {
                 return;
             }
-            HideInterfaceLogic.AddHoverButtonEvents(this, PolyButton, vm);
             PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
     }

+ 1 - 1
src/PicView.Avalonia/Views/UC/Buttons/GalleryShortcut.axaml.cs

@@ -15,7 +15,7 @@ public partial class GalleryShortcut : UserControl
             {
                 return;
             }
-            HideInterfaceLogic.AddHoverButtonEvents(this, InnerButton, DataContext as MainViewModel);
+            _ = new HoverFadeButtonHandler(this, DataContext as MainViewModel, InnerButton);
             PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
     }