Browse Source

Refactor `ImageViewer` and `Zoom` logic: extract image transformation logic into `ImageTransformer`, simplify event handling, and centralize scaling and transformation methods.

Ruben 4 months ago
parent
commit
c425eb2d04

+ 135 - 128
src/PicView.Avalonia/CustomControls/IconButton.cs

@@ -14,126 +14,6 @@ namespace PicView.Avalonia.CustomControls;
 /// </summary>
 /// </summary>
 public class IconButton : Button
 public class IconButton : Button
 {
 {
-    #region Repeat
-
-     /// <summary>
-        /// Defines the <see cref="Interval"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> IntervalProperty =
-            AvaloniaProperty.Register<RepeatButton, int>(nameof(Interval), 100);
-
-        /// <summary>
-        /// Defines the <see cref="Delay"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> DelayProperty =
-            AvaloniaProperty.Register<RepeatButton, int>(nameof(Delay), 300);
-
-        private DispatcherTimer? _repeatTimer;
-
-        /// <summary>
-        /// Gets or sets the amount of time, in milliseconds, of repeating clicks.
-        /// </summary>
-        public int Interval
-        {
-            get => GetValue(IntervalProperty);
-            set => SetValue(IntervalProperty, value);
-        }
-
-        /// <summary>
-        /// Gets or sets the amount of time, in milliseconds, to wait before repeating begins.
-        /// </summary>
-        public int Delay
-        {
-            get => GetValue(DelayProperty);
-            set => SetValue(DelayProperty, value);
-        }
-        
-        public static readonly StyledProperty<bool> IsRepeatEnabledProperty =
-            AvaloniaProperty.Register<RepeatButton, bool>(nameof(IsRepeatEnabled), true);
-        
-        public bool IsRepeatEnabled
-        {
-            get => GetValue(IsRepeatEnabledProperty);
-            set => SetValue(IsRepeatEnabledProperty, value);
-        }
-
-        private void StartTimer()
-        {
-            if (!IsRepeatEnabled)
-            {
-                return;
-            }
-            if (_repeatTimer == null)
-            {
-                _repeatTimer = new DispatcherTimer();
-                _repeatTimer.Tick += RepeatTimerOnTick;
-            }
-
-            if (_repeatTimer.IsEnabled) return;
-
-            _repeatTimer.Interval = TimeSpan.FromMilliseconds(Delay);
-            _repeatTimer.Start();
-        }
-
-        private void RepeatTimerOnTick(object? sender, EventArgs e)
-        {
-            if (!IsRepeatEnabled)
-            {
-                return;
-            }
-            var interval = TimeSpan.FromMilliseconds(Interval);
-            if (_repeatTimer!.Interval != interval)
-            {
-                _repeatTimer.Interval = interval;
-            }
-            OnClick();
-        }
-
-        private void StopTimer()
-        {
-            _repeatTimer?.Stop();
-        }
-
-        protected override void OnKeyDown(KeyEventArgs e)
-        {
-            base.OnKeyDown(e);
-
-            if (e.Key == Key.Space)
-            {
-                StartTimer();
-            }
-        }
-
-        protected override void OnKeyUp(KeyEventArgs e)
-        {
-            base.OnKeyUp(e);
-
-            StopTimer();
-        }
-
-        protected override void OnPointerPressed(PointerPressedEventArgs e)
-        {
-            base.OnPointerPressed(e);
-
-            if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
-            {
-                StartTimer();
-            }
-        }
-
-        protected override void OnPointerReleased(PointerReleasedEventArgs e)
-        {
-            base.OnPointerReleased(e);
-
-            if (e.InitialPressMouseButton == MouseButton.Left)
-            {
-                StopTimer();
-            }
-        }
-    
-
-    #endregion
-    
     /// <summary>
     /// <summary>
     /// Defines the <see cref="Icon"/> property.
     /// Defines the <see cref="Icon"/> property.
     /// The icon is displayed as a <see cref="DrawingImage"/> with support for dynamic brush changes.
     /// The icon is displayed as a <see cref="DrawingImage"/> with support for dynamic brush changes.
@@ -180,9 +60,9 @@ public class IconButton : Button
     /// <summary>
     /// <summary>
     /// Gets or sets the <see cref="StreamGeometry"/> used as the icon's path data.
     /// Gets or sets the <see cref="StreamGeometry"/> used as the icon's path data.
     /// </summary>
     /// </summary>
-    public StreamGeometry Data
+    public StreamGeometry? Data
     {
     {
-        get => (StreamGeometry)GetValue(PathProperty);
+        get => (StreamGeometry)GetValue(PathProperty)!;
         set => SetValue(PathProperty, value);
         set => SetValue(PathProperty, value);
     }
     }
 
 
@@ -191,7 +71,7 @@ public class IconButton : Button
     /// </summary>
     /// </summary>
     public double IconWidth
     public double IconWidth
     {
     {
-        get => (double)GetValue(IconWidthProperty);
+        get => (double)GetValue(IconWidthProperty)!;
         set => SetValue(IconWidthProperty, value);
         set => SetValue(IconWidthProperty, value);
     }
     }
 
 
@@ -200,7 +80,7 @@ public class IconButton : Button
     /// </summary>
     /// </summary>
     public double IconHeight
     public double IconHeight
     {
     {
-        get => (double)GetValue(IconHeightProperty);
+        get => (double)GetValue(IconHeightProperty)!;
         set => SetValue(IconHeightProperty, value);
         set => SetValue(IconHeightProperty, value);
     }
     }
 
 
@@ -222,6 +102,7 @@ public class IconButton : Button
         {
         {
             Content = BuildIcon();
             Content = BuildIcon();
         }
         }
+
         if (change.Property == IsPressedProperty && change.GetNewValue<bool>() == false)
         if (change.Property == IsPressedProperty && change.GetNewValue<bool>() == false)
         {
         {
             StopTimer();
             StopTimer();
@@ -288,7 +169,7 @@ public class IconButton : Button
                     {
                     {
                         if (drawing is GeometryDrawing { Pen: Pen pen })
                         if (drawing is GeometryDrawing { Pen: Pen pen })
                         {
                         {
-                            pen.Brush = new SolidColorBrush((Color)(secondaryAccentColor));
+                            pen.Brush = new SolidColorBrush((Color)secondaryAccentColor);
                         }
                         }
                     }
                     }
                 });
                 });
@@ -299,8 +180,9 @@ public class IconButton : Button
             {
             {
                 if (Settings.Theme.GlassTheme)
                 if (Settings.Theme.GlassTheme)
                 {
                 {
-return;
+                    return;
                 }
                 }
+
                 Dispatcher.UIThread.InvokeAsync(() =>
                 Dispatcher.UIThread.InvokeAsync(() =>
                 {
                 {
                     if (!Application.Current.TryGetResource("MainTextColor", Application.Current.RequestedThemeVariant,
                     if (!Application.Current.TryGetResource("MainTextColor", Application.Current.RequestedThemeVariant,
@@ -315,7 +197,7 @@ return;
                     {
                     {
                         if (drawing is GeometryDrawing { Pen: Pen pen })
                         if (drawing is GeometryDrawing { Pen: Pen pen })
                         {
                         {
-                            pen.Brush = new SolidColorBrush((Color)(mainTextColor));
+                            pen.Brush = new SolidColorBrush((Color)mainTextColor);
                         }
                         }
                     }
                     }
                 });
                 });
@@ -340,4 +222,129 @@ return;
 
 
         return pathIcon;
         return pathIcon;
     }
     }
-}
+
+    #region Repeat
+
+    /// <summary>
+    /// Defines the <see cref="Interval"/> property.
+    /// </summary>
+    public static readonly StyledProperty<int> IntervalProperty =
+        AvaloniaProperty.Register<RepeatButton, int>(nameof(Interval), 100);
+
+    /// <summary>
+    /// Defines the <see cref="Delay"/> property.
+    /// </summary>
+    public static readonly StyledProperty<int> DelayProperty =
+        AvaloniaProperty.Register<RepeatButton, int>(nameof(Delay), 300);
+
+    private DispatcherTimer? _repeatTimer;
+
+    /// <summary>
+    /// Gets or sets the amount of time, in milliseconds, of repeating clicks.
+    /// </summary>
+    public int Interval
+    {
+        get => GetValue(IntervalProperty);
+        set => SetValue(IntervalProperty, value);
+    }
+
+    /// <summary>
+    /// Gets or sets the amount of time, in milliseconds, to wait before repeating begins.
+    /// </summary>
+    public int Delay
+    {
+        get => GetValue(DelayProperty);
+        set => SetValue(DelayProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> IsRepeatEnabledProperty =
+        AvaloniaProperty.Register<RepeatButton, bool>(nameof(IsRepeatEnabled), true);
+
+    public bool IsRepeatEnabled
+    {
+        get => GetValue(IsRepeatEnabledProperty);
+        set => SetValue(IsRepeatEnabledProperty, value);
+    }
+
+    private void StartTimer()
+    {
+        if (!IsRepeatEnabled)
+        {
+            return;
+        }
+
+        if (_repeatTimer == null)
+        {
+            _repeatTimer = new DispatcherTimer();
+            _repeatTimer.Tick += RepeatTimerOnTick;
+        }
+
+        if (_repeatTimer.IsEnabled)
+        {
+            return;
+        }
+
+        _repeatTimer.Interval = TimeSpan.FromMilliseconds(Delay);
+        _repeatTimer.Start();
+    }
+
+    private void RepeatTimerOnTick(object? sender, EventArgs e)
+    {
+        if (!IsRepeatEnabled)
+        {
+            return;
+        }
+
+        var interval = TimeSpan.FromMilliseconds(Interval);
+        if (_repeatTimer!.Interval != interval)
+        {
+            _repeatTimer.Interval = interval;
+        }
+
+        OnClick();
+    }
+
+    private void StopTimer()
+    {
+        _repeatTimer?.Stop();
+    }
+
+    protected override void OnKeyDown(KeyEventArgs e)
+    {
+        base.OnKeyDown(e);
+
+        if (e.Key == Key.Space)
+        {
+            StartTimer();
+        }
+    }
+
+    protected override void OnKeyUp(KeyEventArgs e)
+    {
+        base.OnKeyUp(e);
+
+        StopTimer();
+    }
+
+    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    {
+        base.OnPointerPressed(e);
+
+        if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        {
+            StartTimer();
+        }
+    }
+
+    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    {
+        base.OnPointerReleased(e);
+
+        if (e.InitialPressMouseButton == MouseButton.Left)
+        {
+            StopTimer();
+        }
+    }
+
+    #endregion
+}

+ 21 - 0
src/PicView.Avalonia/ImageTransformations/ImageControlHelper.cs

@@ -0,0 +1,21 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+
+namespace PicView.Avalonia.ImageTransformations;
+
+public static class ImageControlHelper
+{
+    public static void TriggerScalingModeUpdate(Control image, bool invalidate)
+    {
+        var scalingMode = Settings.ImageScaling.IsScalingSetToNearestNeighbor
+            ? BitmapInterpolationMode.LowQuality
+            : BitmapInterpolationMode.HighQuality;
+
+        RenderOptions.SetBitmapInterpolationMode(image, scalingMode);
+        if (invalidate)
+        {
+            image.InvalidateVisual();
+        }
+    }
+}

+ 176 - 0
src/PicView.Avalonia/ImageTransformations/ImageTransformer.cs

@@ -0,0 +1,176 @@
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+using ImageMagick;
+using PicView.Avalonia.CustomControls;
+using PicView.Avalonia.ViewModels;
+using PicView.Avalonia.WindowBehavior;
+using PicView.Core.ImageDecoding;
+using PicView.Core.ImageTransformations;
+
+namespace PicView.Avalonia.ImageTransformations;
+
+public class ImageTransformer(
+    LayoutTransformControl imageLayoutTransformControl,
+    PicBox mainImage,
+    Func<object?> getDataContext,
+    Action resetZoom)
+{
+    public void Rotate(bool clockWise)
+    {
+        if (getDataContext() is not MainViewModel vm || mainImage.Source is null)
+        {
+            return;
+        }
+
+        if (RotationHelper.IsValidRotation(vm.RotationAngle))
+        {
+            var nextAngle = RotationHelper.Rotate(vm.RotationAngle, clockWise);
+            vm.RotationAngle = nextAngle switch
+            {
+                360 => 0,
+                -90 => 270,
+                _ => nextAngle
+            };
+        }
+        else
+        {
+            vm.RotationAngle = RotationHelper.NextRotationAngle(vm.RotationAngle, true);
+        }
+
+        SetImageLayoutTransform(new RotateTransform(vm.RotationAngle));
+        WindowResizing.SetSize(vm);
+        mainImage.InvalidateVisual();
+    }
+
+    public void Rotate(double angle)
+    {
+        SetImageLayoutTransform(new RotateTransform(angle));
+        WindowResizing.SetSize(getDataContext() as MainViewModel);
+        mainImage.InvalidateVisual();
+    }
+
+    private void SetImageLayoutTransform(RotateTransform rotateTransform)
+    {
+        if (Dispatcher.UIThread.CheckAccess())
+        {
+            imageLayoutTransformControl.LayoutTransform = rotateTransform;
+        }
+        else
+        {
+            Dispatcher.UIThread.Invoke(() =>
+                imageLayoutTransformControl.LayoutTransform = rotateTransform);
+        }
+    }
+
+    public void Flip(bool animate)
+    {
+        if (getDataContext() is not MainViewModel vm || mainImage.Source is null)
+        {
+            return;
+        }
+
+        var prevScaleX = vm.PicViewer.ScaleX;
+        vm.PicViewer.ScaleX = vm.PicViewer.ScaleX == -1 ? 1 : -1;
+        vm.Translation.IsFlipped = vm.PicViewer.ScaleX == 1 ? vm.Translation.UnFlip : vm.Translation.Flip;
+
+        if (animate)
+        {
+            var flipTransform = new ScaleTransform(prevScaleX, 1)
+            {
+                Transitions =
+                [
+                    new DoubleTransition
+                        { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.2) }
+                ]
+            };
+            imageLayoutTransformControl.RenderTransform = flipTransform;
+            flipTransform.ScaleX = vm.PicViewer.ScaleX;
+        }
+        else
+        {
+            imageLayoutTransformControl.RenderTransform = new ScaleTransform(vm.PicViewer.ScaleX, 1);
+        }
+    }
+
+    public void SetTransform(int scaleX, int rotationAngle)
+    {
+        if (getDataContext() is not MainViewModel vm)
+        {
+            return;
+        }
+
+        vm.PicViewer.ScaleX = scaleX;
+        vm.RotationAngle = rotationAngle;
+        imageLayoutTransformControl.RenderTransform = new ScaleTransform(vm.PicViewer.ScaleX, 1);
+        imageLayoutTransformControl.LayoutTransform = new RotateTransform(rotationAngle);
+
+        resetZoom?.Invoke();
+    }
+
+    public void SetTransform(EXIFHelper.EXIFOrientation? orientation, MagickFormat? format, bool reset = true)
+    {
+        if (Dispatcher.UIThread.CheckAccess())
+        {
+            ApplyOrientationTransform(orientation, format, reset);
+        }
+        else
+        {
+            Dispatcher.UIThread.InvokeAsync(() =>
+                ApplyOrientationTransform(orientation, format, reset), DispatcherPriority.Send);
+        }
+    }
+
+    private void ApplyOrientationTransform(EXIFHelper.EXIFOrientation? orientation, MagickFormat? format, bool reset)
+    {
+        if (Settings.Zoom.ScrollEnabled && imageLayoutTransformControl.Parent is ScrollViewer scrollViewer)
+        {
+            scrollViewer.ScrollToHome();
+        }
+
+        if (format is MagickFormat.Heic or MagickFormat.Heif)
+        {
+            if (reset)
+            {
+                SetTransform(1, 0);
+            }
+
+            return;
+        }
+
+        switch (orientation)
+        {
+            case null:
+            case EXIFHelper.EXIFOrientation.None:
+            case EXIFHelper.EXIFOrientation.Horizontal:
+                if (reset)
+                {
+                    SetTransform(1, 0);
+                }
+
+                break;
+            case EXIFHelper.EXIFOrientation.MirrorHorizontal:
+                SetTransform(-1, 0);
+                break;
+            case EXIFHelper.EXIFOrientation.Rotate180:
+                SetTransform(1, 180);
+                break;
+            case EXIFHelper.EXIFOrientation.MirrorVertical:
+                SetTransform(-1, 180);
+                break;
+            case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate270Cw:
+                SetTransform(-1, 90);
+                break;
+            case EXIFHelper.EXIFOrientation.Rotate90Cw:
+                SetTransform(1, 90);
+                break;
+            case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate90Cw:
+                SetTransform(-1, 270);
+                break;
+            case EXIFHelper.EXIFOrientation.Rotated270Cw:
+                SetTransform(1, 270);
+                break;
+        }
+    }
+}

+ 71 - 30
src/PicView.Avalonia/ImageTransformations/Zoom.cs

@@ -14,23 +14,28 @@ namespace PicView.Avalonia.ImageTransformations;
 public class Zoom
 public class Zoom
 {
 {
     private bool _captured;
     private bool _captured;
+    
     private Point _origin;
     private Point _origin;
-
-    private ScaleTransform? _scaleTransform;
-
     private Point _start;
     private Point _start;
-    private TranslateTransform? _translateTransform;
     
     
-    public bool IsZoomed { get; private set; }
+    private TranslateTransform? _translateTransform;
+    private ScaleTransform? _scaleTransform;
 
 
+    /// <summary>
+    /// Provides zoom functionality for UI elements, supporting both zoom-in and zoom-out operations.
+    /// Manages transformations such as scaling and translating for visual adjustments.
+    /// </summary>
     public Zoom(Border border)
     public Zoom(Border border)
     {
     {
         InitializeZoom(border);
         InitializeZoom(border);
     }
     }
 
 
-    /// <summary>
-    /// Initialize the necessary transforms for zooming
-    /// </summary>
+    /// Indicates whether the current zoom level is applied or not.
+    /// This property will return true if the zoom level is active and differs from the default state (non-zoomed).
+    /// When the zoom level is reset to default, the property will return false.
+    public bool IsZoomed { get; private set; }
+
+
     private void InitializeZoom(Border border)
     private void InitializeZoom(Border border)
     {
     {
         border.RenderTransform = new TransformGroup
         border.RenderTransform = new TransformGroup
@@ -49,30 +54,43 @@ public class Zoom
             .Children.First(tr => tr is TranslateTransform);
             .Children.First(tr => tr is TranslateTransform);
 
 
         border.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
         border.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
-        border.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
     }
     }
 
 
-    public void ZoomIn(PointerWheelEventArgs e, Control parent, Control content, MainViewModel? vm = null)
-    {
-        ZoomTo(e, true, parent, content, vm);
-    }
+    /// <summary>
+    /// Zooms into the content within the specified parent control, scaling the view while retaining proper alignment.
+    /// </summary>
+    /// <param name="e">The pointer wheel event arguments containing the zoom gesture data.</param>
+    /// <param name="parent">The parent control acting as the container for the content being zoomed.</param>
+    /// <param name="content">The content control subjected to the zoom operation.</param>
+    /// <param name="vm">The main view model providing application state and behavior context.</param>
+    public void ZoomIn(PointerWheelEventArgs e, Control parent, Control content, MainViewModel vm) =>
+        HandlePointerWheelZoom(e, true, parent, content, vm);
 
 
-    public void ZoomOut(PointerWheelEventArgs e, Control parent, Control content, MainViewModel? vm = null)
-    {
-        ZoomTo(e, false, parent, content, vm);
-    }
+    /// <summary>
+    /// Zooms out the content within the specified parent control, scaling the view while retaining proper alignment.
+    /// </summary>
+    /// <param name="e">The pointer wheel event arguments containing the zoom gesture data.</param>
+    /// <param name="parent">The parent control acting as the container for the content being zoomed.</param>
+    /// <param name="content">The content control subjected to the zoom operation.</param>
+    /// <param name="vm">The main view model providing application state and behavior context.</param>
+    public void ZoomOut(PointerWheelEventArgs e, Control parent, Control content, MainViewModel vm) =>
+        HandlePointerWheelZoom(e, false, parent, content, vm);
 
 
-    public void ZoomIn(MainViewModel? vm = null)
-    {
+    /// <summary>
+    /// Zooms into the image using the current starting point and updates the application state.
+    /// </summary>
+    /// <param name="vm">The view model containing the application state and transformation details.</param>
+    public void ZoomIn(MainViewModel vm) =>
         ZoomTo(_start, true, vm);
         ZoomTo(_start, true, vm);
-    }
 
 
-    public void ZoomOut(MainViewModel? vm = null)
-    {
+    /// <summary>
+    /// Zooms out the image using the current starting point and updates the application state.
+    /// </summary>
+    /// <param name="vm">The view model containing the application state and transformation details.</param>
+    public void ZoomOut(MainViewModel vm) =>
         ZoomTo(_start, false, vm);
         ZoomTo(_start, false, vm);
-    }
 
 
-    private Point GetRelativePosition(Control parent, Control content)
+    private static Point GetRelativePosition(Control parent, Control content)
     {
     {
         // Get center of the ImageViewer control
         // Get center of the ImageViewer control
         var centerX = parent.Bounds.Width / 2;
         var centerX = parent.Bounds.Width / 2;
@@ -83,14 +101,20 @@ public class Zoom
                ?? new Point(content.Bounds.Width / 2, content.Bounds.Height / 2);
                ?? new Point(content.Bounds.Width / 2, content.Bounds.Height / 2);
     }
     }
 
 
-    public void ZoomTo(PointerWheelEventArgs e, bool isZoomIn, Control parent, Control content,
-        MainViewModel? vm = null)
+    private void HandlePointerWheelZoom(PointerWheelEventArgs e, bool isZoomIn, Control parent, Control content,
+        MainViewModel vm)
     {
     {
         var relativePosition = !content.IsPointerOver ? GetRelativePosition(parent, content) : e.GetPosition(content);
         var relativePosition = !content.IsPointerOver ? GetRelativePosition(parent, content) : e.GetPosition(content);
         ZoomTo(relativePosition, isZoomIn, vm);
         ZoomTo(relativePosition, isZoomIn, vm);
     }
     }
 
 
-    public void ZoomTo(Point point, bool isZoomIn, MainViewModel? vm = null)
+    /// <summary>
+    /// Adjust the zoom level at a specified point, either zooming in or out, based on the provided parameters.
+    /// </summary>
+    /// <param name="point">The reference point where the zooming action will be centered.</param>
+    /// <param name="isZoomIn">Determines whether to zoom in (true) or zoom out (false).</param>
+    /// <param name="vm">The main view model containing the application's state and settings.</param>
+    public void ZoomTo(Point point, bool isZoomIn, MainViewModel vm)
     {
     {
         if (_scaleTransform == null || _translateTransform == null)
         if (_scaleTransform == null || _translateTransform == null)
         {
         {
@@ -138,12 +162,12 @@ public class Zoom
             }
             }
             else
             else
             {
             {
-                ZoomTo(point, currentZoom, true, vm);
+                SetZoomAtPoint(point, currentZoom, true, vm);
             }
             }
         }
         }
     }
     }
 
 
-    public void ZoomTo(Point point, double zoomValue, bool enableAnimations, MainViewModel? vm = null)
+    private void SetZoomAtPoint(Point point, double zoomValue, bool enableAnimations, MainViewModel vm)
     {
     {
         if (_scaleTransform == null || _translateTransform == null)
         if (_scaleTransform == null || _translateTransform == null)
         {
         {
@@ -196,7 +220,12 @@ public class Zoom
         _ = TooltipHelper.ShowTooltipMessageAsync($"{Math.Floor(zoomValue * 100)}%", true, TimeSpan.FromSeconds(1));
         _ = TooltipHelper.ShowTooltipMessageAsync($"{Math.Floor(zoomValue * 100)}%", true, TimeSpan.FromSeconds(1));
     }
     }
 
 
-    public void ResetZoom(bool enableAnimations, MainViewModel? vm = null)
+    /// <summary>
+    /// Resets the zoom to its default state.
+    /// </summary>
+    /// <param name="enableAnimations">Specifies whether animations should be applied during the reset.</param>
+    /// <param name="vm">The view model associated with the main application, used for managing zoom state and title updates.</param>
+    public void ResetZoom(bool enableAnimations, MainViewModel vm)
     {
     {
         if (_scaleTransform == null || _translateTransform == null)
         if (_scaleTransform == null || _translateTransform == null)
         {
         {
@@ -246,6 +275,10 @@ public class Zoom
         TitleManager.SetTitle(vm);
         TitleManager.SetTitle(vm);
     }
     }
 
 
+    /// <summary>
+    /// Captures the current pointer position and initializes the origin point for zooming transformations.
+    /// </summary>
+    /// <param name="e">The pointer event arguments providing data about the pointer position and device state.</param>
     public void Capture(PointerEventArgs e)
     public void Capture(PointerEventArgs e)
     {
     {
         if (_captured)
         if (_captured)
@@ -268,6 +301,11 @@ public class Zoom
         _captured = true;
         _captured = true;
     }
     }
 
 
+    /// <summary>
+    /// Handles panning of the zoomed image by adjusting translation transforms based on pointer movement.
+    /// </summary>
+    /// <param name="e">Pointer event arguments containing details about the pointer's position and state.</param>
+    /// <param name="imageViewer">The image viewer instance on which the panning operation is performed.</param>
     public void Pan(PointerEventArgs e, ImageViewer imageViewer)
     public void Pan(PointerEventArgs e, ImageViewer imageViewer)
     {
     {
         if (!_captured || _scaleTransform == null || !IsZoomed)
         if (!_captured || _scaleTransform == null || !IsZoomed)
@@ -355,6 +393,9 @@ public class Zoom
         e.Handled = true;
         e.Handled = true;
     }
     }
 
 
+    /// <summary>
+    /// Releases any current state of capturing associated with zoom or panning functionality.
+    /// </summary>
     public void Release()
     public void Release()
     {
     {
         _captured = false;
         _captured = false;

+ 158 - 0
src/PicView.Avalonia/Input/MouseShortcuts.cs

@@ -1,13 +1,171 @@
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input;
+using PicView.Avalonia.CustomControls;
 using PicView.Avalonia.Functions;
 using PicView.Avalonia.Functions;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.ViewModels;
 
 
 namespace PicView.Avalonia.Input;
 namespace PicView.Avalonia.Input;
 
 
 public static class MouseShortcuts
 public static class MouseShortcuts
 {
 {
+    private static AutoScrollViewer? _imageScrollViewer;
+    private static Func<object?>? _getDataContext;
+    private static Func<PointerWheelEventArgs, Task>? _zoomIn;
+    private static Func<PointerWheelEventArgs, Task>? _zoomOut;
+
+    public static void InitializeMouseShortcuts(
+        AutoScrollViewer imageScrollViewer,
+        Func<object?> getDataContext,
+        Func<PointerWheelEventArgs, Task> zoomIn,
+        Func<PointerWheelEventArgs, Task> zoomOut)
+    {
+        _imageScrollViewer = imageScrollViewer;
+        _getDataContext = getDataContext;
+        _zoomIn = zoomIn;
+        _zoomOut = zoomOut;
+    }
+
+    public static async Task HandlePointerWheelChanged(PointerWheelEventArgs e)
+    {
+        if (_getDataContext() is not MainViewModel mainViewModel)
+        {
+            return;
+        }
+        
+        e.Handled = true;
+
+        var ctrl = e.KeyModifiers == KeyModifiers.Control;
+        var shift = e.KeyModifiers == KeyModifiers.Shift;
+        var reverse = e.Delta.Y < 0;
+
+        if (Settings.Zoom.ScrollEnabled)
+        {
+            if (!shift)
+            {
+                if (ctrl && !Settings.Zoom.CtrlZoom)
+                {
+                    if (IsTouchPadOrTouch(e))
+                    {
+                        return;
+                    }
+
+                    await LoadNextPicAsync(reverse, mainViewModel);
+                    return;
+                }
+
+                if (IsVerticalScrollBarVisible())
+                {
+                    ScrollVertically(reverse);
+                }
+                else
+                {
+                    await LoadNextPicAsync(reverse, mainViewModel);
+                }
+
+                return;
+            }
+        }
+
+        if (Settings.Zoom.CtrlZoom)
+        {
+            if (ctrl)
+            {
+                if (IsTouchPadOrTouch(e))
+                {
+                    return;
+                }
+
+                if (reverse)
+                {
+                    await _zoomOut(e);
+                }
+                else
+                {
+                    await _zoomIn(e);
+                }
+            }
+            else
+            {
+                await ScrollOrNavigateAsync(e, reverse, mainViewModel);
+            }
+        }
+        else
+        {
+            if (ctrl)
+            {
+                await ScrollOrNavigateAsync(e, reverse, mainViewModel);
+            }
+            else
+            {
+                if (reverse)
+                {
+                    await _zoomOut(e);
+                }
+                else
+                {
+                    await _zoomIn(e);
+                }
+            }
+        }
+    }
+
+    private static bool IsTouchPadOrTouch(PointerEventArgs e)
+        => Settings.Zoom.IsUsingTouchPad || e.Pointer.Type == PointerType.Touch;
+
+    private static bool IsVerticalScrollBarVisible()
+        => _imageScrollViewer.VerticalScrollBarVisibility is ScrollBarVisibility.Visible or ScrollBarVisibility.Auto;
+
+    private static void ScrollVertically(bool reverse)
+    {
+        if (reverse)
+        {
+            _imageScrollViewer.LineDown();
+        }
+        else
+        {
+            _imageScrollViewer.LineUp();
+        }
+    }
+
+    private static async Task ScrollOrNavigateAsync(PointerWheelEventArgs e, bool reverse, MainViewModel mainViewModel)
+    {
+        if (!Settings.Zoom.ScrollEnabled || e.KeyModifiers == KeyModifiers.Shift)
+        {
+            if (IsTouchPadOrTouch(e))
+            {
+                return;
+            }
+
+            await LoadNextPicAsync(reverse, mainViewModel);
+        }
+        else
+        {
+            if (IsVerticalScrollBarVisible())
+            {
+                ScrollVertically(reverse);
+            }
+            else
+            {
+                await LoadNextPicAsync(reverse, mainViewModel);
+            }
+        }
+    }
+
+    private static async Task LoadNextPicAsync(bool reverse, MainViewModel mainViewModel)
+    {
+        if (Settings.Zoom.IsUsingTouchPad)
+        {
+            return;
+        }
+
+        var next = reverse ? Settings.Zoom.HorizontalReverseScroll : !Settings.Zoom.HorizontalReverseScroll;
+        await NavigationManager.Navigate(next, mainViewModel).ConfigureAwait(false);
+    }
+    
     public static async Task MainWindow_PointerPressed(PointerPressedEventArgs e)
     public static async Task MainWindow_PointerPressed(PointerPressedEventArgs e)
     {
     {
         if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
         if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)

+ 83 - 0
src/PicView.Avalonia/SettingsManagement/SettingsUpdater.cs

@@ -11,10 +11,93 @@ using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Gallery;
 using PicView.Core.Gallery;
 using PicView.Core.Localization;
 using PicView.Core.Localization;
+using PicView.Core.Sizing;
 
 
 namespace PicView.Avalonia.SettingsManagement;
 namespace PicView.Avalonia.SettingsManagement;
 public static class SettingsUpdater
 public static class SettingsUpdater
 {
 {
+    public static void ValidateGallerySettings(MainViewModel vm, bool settingsExists)
+    {
+        vm.GetFullGalleryItemHeight = Settings.Gallery.ExpandedGalleryItemSize;
+        vm.GetBottomGalleryItemHeight = Settings.Gallery.BottomGalleryItemSize;
+        if (!settingsExists)
+        {
+            vm.GetBottomGalleryItemHeight = GalleryDefaults.DefaultBottomGalleryHeight;
+            vm.GetFullGalleryItemHeight = GalleryDefaults.DefaultFullGalleryHeight;
+        }
+
+        // Set default gallery sizes if they are out of range or upgrading from an old version
+        if (vm.GetBottomGalleryItemHeight < vm.MinBottomGalleryItemHeight ||
+            vm.GetBottomGalleryItemHeight > vm.MaxBottomGalleryItemHeight)
+        {
+            vm.GetBottomGalleryItemHeight = GalleryDefaults.DefaultBottomGalleryHeight;
+        }
+
+        if (vm.GetFullGalleryItemHeight < vm.MinFullGalleryItemHeight ||
+            vm.GetFullGalleryItemHeight > vm.MaxFullGalleryItemHeight)
+        {
+            vm.GetFullGalleryItemHeight = GalleryDefaults.DefaultFullGalleryHeight;
+        }
+
+        if (settingsExists)
+        {
+            return;
+        }
+
+        if (string.IsNullOrWhiteSpace(Settings.Gallery.BottomGalleryStretchMode))
+        {
+            Settings.Gallery.BottomGalleryStretchMode = "UniformToFill";
+        }
+
+        if (string.IsNullOrWhiteSpace(Settings.Gallery.FullGalleryStretchMode))
+        {
+            Settings.Gallery.FullGalleryStretchMode = "UniformToFill";
+        }
+    }
+
+    public static void InitializeSettings(MainViewModel vm)
+    {    
+        // Set corner radius on macOS
+        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+        {
+            vm.BottomCornerRadius = new CornerRadius(0, 0, 8, 8);
+        }
+        
+        vm.TitlebarHeight = Settings.WindowProperties.Fullscreen
+            || !Settings.UIProperties.ShowInterface
+            ? 0
+            : SizeDefaults.MainTitlebarHeight;
+        vm.BottombarHeight = Settings.WindowProperties.Fullscreen
+                             || !Settings.UIProperties.ShowInterface
+            ? 0
+            : SizeDefaults.BottombarHeight;
+        vm.GetNavSpeed = Settings.UIProperties.NavSpeed;
+        vm.GetSlideshowSpeed = Settings.UIProperties.SlideShowTimer;
+        vm.GetZoomSpeed = Settings.Zoom.ZoomSpeed;
+        vm.PicViewer.IsShowingSideBySide = Settings.ImageScaling.ShowImageSideBySide;
+        vm.IsBottomGalleryShown = Settings.Gallery.IsBottomGalleryShown;
+        vm.IsBottomGalleryShownInHiddenUI = Settings.Gallery.ShowBottomGalleryInHiddenUI;
+        vm.IsAvoidingZoomingOut  = Settings.Zoom.AvoidZoomingOut;
+        vm.IsUIShown  = Settings.UIProperties.ShowInterface;
+        vm.IsTopToolbarShown  = Settings.UIProperties.ShowInterface;
+        vm.IsBottomToolbarShown   = Settings.UIProperties.ShowBottomNavBar &&
+                                    Settings.UIProperties.ShowInterface;
+        vm.IsShowingTaskbarProgress  = Settings.UIProperties.IsTaskbarProgressEnabled;
+        vm.IsFullscreen  = Settings.WindowProperties.Fullscreen;
+        vm.IsTopMost  = Settings.WindowProperties.TopMost;
+        vm.IsIncludingSubdirectories = Settings.Sorting.IncludeSubDirectories;
+        vm.IsStretched = Settings.ImageScaling.StretchImage;
+        vm.IsLooping  = Settings.UIProperties.Looping;
+        vm.IsAutoFit  = Settings.WindowProperties.AutoFit;
+        vm.IsStayingCentered  = Settings.WindowProperties.KeepCentered;
+        vm.IsOpeningInSameWindow  = Settings.UIProperties.OpenInSameWindow;
+        vm.IsShowingConfirmationOnEsc  = Settings.UIProperties.ShowConfirmationOnEsc;   
+        vm.IsUsingTouchpad  = Settings.Zoom.IsUsingTouchPad;
+        vm.IsAscending  = Settings.Sorting.Ascending;
+        vm.BackgroundChoice = Settings.UIProperties.BgColorChoice;
+        vm.IsConstrainingBackgroundColor = Settings.UIProperties.IsConstrainBackgroundColorEnabled;
+    }
+    
     public static async Task ResetSettings(MainViewModel vm)
     public static async Task ResetSettings(MainViewModel vm)
     {
     {
         vm.IsLoading = true;
         vm.IsLoading = true;

+ 3 - 87
src/PicView.Avalonia/StartUp/StartUpHelper.cs

@@ -16,9 +16,7 @@ using PicView.Avalonia.Views;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.FileAssociations;
 using PicView.Core.FileAssociations;
 using PicView.Core.FileHistory;
 using PicView.Core.FileHistory;
-using PicView.Core.Gallery;
 using PicView.Core.ProcessHandling;
 using PicView.Core.ProcessHandling;
-using PicView.Core.Sizing;
 using PicView.Core.ViewModels;
 using PicView.Core.ViewModels;
 
 
 namespace PicView.Avalonia.StartUp;
 namespace PicView.Avalonia.StartUp;
@@ -70,7 +68,7 @@ public static class StartUpHelper
             }
             }
         }
         }
         
         
-        InitializeSettings(vm);
+        SettingsUpdater.InitializeSettings(vm);
         
         
         HandleWindowScalingMode(vm, window);
         HandleWindowScalingMode(vm, window);
         
         
@@ -83,7 +81,7 @@ public static class StartUpHelper
     public static void StartWithoutArguments(MainViewModel vm, bool settingsExists, IClassicDesktopStyleApplicationLifetime desktop,
     public static void StartWithoutArguments(MainViewModel vm, bool settingsExists, IClassicDesktopStyleApplicationLifetime desktop,
         Window window, string? arg = null)
         Window window, string? arg = null)
     {
     {
-        InitializeSettings(vm);
+        SettingsUpdater.InitializeSettings(vm);
         
         
         HandleWindowScalingMode(vm, window);
         HandleWindowScalingMode(vm, window);
         
         
@@ -131,7 +129,7 @@ public static class StartUpHelper
         
         
         UIHelper.SetControls(desktop);
         UIHelper.SetControls(desktop);
 
 
-        ValidateGallerySettings(vm, settingsExists);
+        SettingsUpdater.ValidateGallerySettings(vm, settingsExists);
         
         
         // Need to delay setting fullscreen or maximized until after the window is shown to select the correct monitor
         // Need to delay setting fullscreen or maximized until after the window is shown to select the correct monitor
         if (Settings.WindowProperties.Maximized && !Settings.WindowProperties.Fullscreen)
         if (Settings.WindowProperties.Maximized && !Settings.WindowProperties.Fullscreen)
@@ -305,88 +303,6 @@ public static class StartUpHelper
         }
         }
     }
     }
 
 
-    private static void ValidateGallerySettings(MainViewModel vm, bool settingsExists)
-    {
-        vm.GetFullGalleryItemHeight = Settings.Gallery.ExpandedGalleryItemSize;
-        vm.GetBottomGalleryItemHeight = Settings.Gallery.BottomGalleryItemSize;
-        if (!settingsExists)
-        {
-            vm.GetBottomGalleryItemHeight = GalleryDefaults.DefaultBottomGalleryHeight;
-            vm.GetFullGalleryItemHeight = GalleryDefaults.DefaultFullGalleryHeight;
-        }
-
-        // Set default gallery sizes if they are out of range or upgrading from an old version
-        if (vm.GetBottomGalleryItemHeight < vm.MinBottomGalleryItemHeight ||
-            vm.GetBottomGalleryItemHeight > vm.MaxBottomGalleryItemHeight)
-        {
-            vm.GetBottomGalleryItemHeight = GalleryDefaults.DefaultBottomGalleryHeight;
-        }
-
-        if (vm.GetFullGalleryItemHeight < vm.MinFullGalleryItemHeight ||
-            vm.GetFullGalleryItemHeight > vm.MaxFullGalleryItemHeight)
-        {
-            vm.GetFullGalleryItemHeight = GalleryDefaults.DefaultFullGalleryHeight;
-        }
-
-        if (settingsExists)
-        {
-            return;
-        }
-
-        if (string.IsNullOrWhiteSpace(Settings.Gallery.BottomGalleryStretchMode))
-        {
-            Settings.Gallery.BottomGalleryStretchMode = "UniformToFill";
-        }
-
-        if (string.IsNullOrWhiteSpace(Settings.Gallery.FullGalleryStretchMode))
-        {
-            Settings.Gallery.FullGalleryStretchMode = "UniformToFill";
-        }
-    }
-
-    private static void InitializeSettings(MainViewModel vm)
-    {    
-        // Set corner radius on macOS
-        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-        {
-            vm.BottomCornerRadius = new CornerRadius(0, 0, 8, 8);
-        }
-        
-        vm.TitlebarHeight = Settings.WindowProperties.Fullscreen
-            || !Settings.UIProperties.ShowInterface
-            ? 0
-            : SizeDefaults.MainTitlebarHeight;
-        vm.BottombarHeight = Settings.WindowProperties.Fullscreen
-                             || !Settings.UIProperties.ShowInterface
-            ? 0
-            : SizeDefaults.BottombarHeight;
-        vm.GetNavSpeed = Settings.UIProperties.NavSpeed;
-        vm.GetSlideshowSpeed = Settings.UIProperties.SlideShowTimer;
-        vm.GetZoomSpeed = Settings.Zoom.ZoomSpeed;
-        vm.PicViewer.IsShowingSideBySide = Settings.ImageScaling.ShowImageSideBySide;
-        vm.IsBottomGalleryShown = Settings.Gallery.IsBottomGalleryShown;
-        vm.IsBottomGalleryShownInHiddenUI = Settings.Gallery.ShowBottomGalleryInHiddenUI;
-        vm.IsAvoidingZoomingOut  = Settings.Zoom.AvoidZoomingOut;
-        vm.IsUIShown  = Settings.UIProperties.ShowInterface;
-        vm.IsTopToolbarShown  = Settings.UIProperties.ShowInterface;
-        vm.IsBottomToolbarShown   = Settings.UIProperties.ShowBottomNavBar &&
-                                    Settings.UIProperties.ShowInterface;
-        vm.IsShowingTaskbarProgress  = Settings.UIProperties.IsTaskbarProgressEnabled;
-        vm.IsFullscreen  = Settings.WindowProperties.Fullscreen;
-        vm.IsTopMost  = Settings.WindowProperties.TopMost;
-        vm.IsIncludingSubdirectories = Settings.Sorting.IncludeSubDirectories;
-        vm.IsStretched = Settings.ImageScaling.StretchImage;
-        vm.IsLooping  = Settings.UIProperties.Looping;
-        vm.IsAutoFit  = Settings.WindowProperties.AutoFit;
-        vm.IsStayingCentered  = Settings.WindowProperties.KeepCentered;
-        vm.IsOpeningInSameWindow  = Settings.UIProperties.OpenInSameWindow;
-        vm.IsShowingConfirmationOnEsc  = Settings.UIProperties.ShowConfirmationOnEsc;   
-        vm.IsUsingTouchpad  = Settings.Zoom.IsUsingTouchPad;
-        vm.IsAscending  = Settings.Sorting.Ascending;
-        vm.BackgroundChoice = Settings.UIProperties.BgColorChoice;
-        vm.IsConstrainingBackgroundColor = Settings.UIProperties.IsConstrainBackgroundColorEnabled;
-    }
-
     private static void SetWindowEventHandlers(Window w)
     private static void SetWindowEventHandlers(Window w)
     {
     {
         w.KeyDown += async (_, e) => await MainKeyboardShortcuts.MainWindow_KeysDownAsync(e).ConfigureAwait(false);
         w.KeyDown += async (_, e) => await MainKeyboardShortcuts.MainWindow_KeysDownAsync(e).ConfigureAwait(false);

+ 39 - 323
src/PicView.Avalonia/Views/ImageViewer.axaml.cs

@@ -1,209 +1,79 @@
-using Avalonia.Animation;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
-using Avalonia.Media;
-using Avalonia.Media.Imaging;
 using Avalonia.Threading;
 using Avalonia.Threading;
 using ImageMagick;
 using ImageMagick;
 using PicView.Avalonia.ImageTransformations;
 using PicView.Avalonia.ImageTransformations;
-using PicView.Avalonia.Navigation;
+using PicView.Avalonia.Input;
 using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.ViewModels;
-using PicView.Avalonia.WindowBehavior;
 using PicView.Core.ImageDecoding;
 using PicView.Core.ImageDecoding;
-using PicView.Core.ImageTransformations;
 
 
 namespace PicView.Avalonia.Views;
 namespace PicView.Avalonia.Views;
 
 
 public partial class ImageViewer : UserControl
 public partial class ImageViewer : UserControl
 {
 {
+    private ImageTransformer? _imageTransformer;
     private Zoom? _zoom;
     private Zoom? _zoom;
 
 
     public ImageViewer()
     public ImageViewer()
     {
     {
         InitializeComponent();
         InitializeComponent();
-        TriggerScalingModeUpdate(false);
+        ImageControlHelper.TriggerScalingModeUpdate(MainImage, false);
         AddHandler(PointerWheelChangedEvent, PreviewOnPointerWheelChanged, RoutingStrategies.Tunnel);
         AddHandler(PointerWheelChangedEvent, PreviewOnPointerWheelChanged, RoutingStrategies.Tunnel);
         AddHandler(Gestures.PointerTouchPadGestureMagnifyEvent, TouchMagnifyEvent, RoutingStrategies.Bubble);
         AddHandler(Gestures.PointerTouchPadGestureMagnifyEvent, TouchMagnifyEvent, RoutingStrategies.Bubble);
-
-        Loaded += OnLoaded;
     }
     }
 
 
-    private void OnLoaded(object? sender, EventArgs e)
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
     {
     {
+        base.OnAttachedToVisualTree(e);
         InitializeZoom();
         InitializeZoom();
+        InitializeImageTransformer();
+        InitializeMouseInputHelper();
         LostFocus += OnLostFocus;
         LostFocus += OnLostFocus;
     }
     }
 
 
-    private void OnLostFocus(object? sender, EventArgs e)
-    {
-        _zoom?.Release();
-    }
-
-    public void TriggerScalingModeUpdate(bool invalidate)
-    {
-        var scalingMode = Settings.ImageScaling.IsScalingSetToNearestNeighbor
-            ? BitmapInterpolationMode.LowQuality
-            : BitmapInterpolationMode.HighQuality;
+    private void OnLostFocus(object? sender, EventArgs e) => _zoom?.Release();
 
 
-        RenderOptions.SetBitmapInterpolationMode(MainImage, scalingMode);
-        if (invalidate)
-        {
-            MainImage.InvalidateVisual();
-        }
-    }
+    public void TriggerScalingModeUpdate(bool invalidate) =>
+        ImageControlHelper.TriggerScalingModeUpdate(MainImage, invalidate);
 
 
-    private void TouchMagnifyEvent(object? sender, PointerDeltaEventArgs e)
-    {
+    private void TouchMagnifyEvent(object? sender, PointerDeltaEventArgs e) =>
         _zoom?.ZoomTo(e.GetPosition(this), e.Delta.X > 0, DataContext as MainViewModel);
         _zoom?.ZoomTo(e.GetPosition(this), e.Delta.X > 0, DataContext as MainViewModel);
-    }
-
-    public async Task PreviewOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
-    {
-        e.Handled = true;
-        await HandlePointerWheelChanged(e);
-    }
-
-    private async Task HandlePointerWheelChanged(PointerWheelEventArgs e)
-    {
-        if (DataContext is not MainViewModel mainViewModel)
-        {
-            return;
-        }
-
-        var ctrl = e.KeyModifiers == KeyModifiers.Control;
-        var shift = e.KeyModifiers == KeyModifiers.Shift;
-        var reverse = e.Delta.Y < 0;
 
 
-        if (Settings.Zoom.ScrollEnabled)
-        {
-            if (!shift)
-            {
-                if (ctrl && !Settings.Zoom.CtrlZoom)
-                {
-                    if (IsTouchPadOrTouch(e))
-                    {
-                        return;
-                    }
+    public static async Task PreviewOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) =>
+        await MouseShortcuts.HandlePointerWheelChanged(e);
 
 
-                    await LoadNextPicAsync(reverse, mainViewModel);
-                    return;
-                }
+    #region Initialization
 
 
-                if (IsVerticalScrollBarVisible())
-                {
-                    ScrollVertically(reverse);
-                }
-                else
-                {
-                    await LoadNextPicAsync(reverse, mainViewModel);
-                }
-
-                return;
-            }
-        }
-
-        if (Settings.Zoom.CtrlZoom)
-        {
-            if (ctrl)
-            {
-                if (IsTouchPadOrTouch(e))
-                {
-                    return;
-                }
+    private void InitializeZoom() => _zoom = new Zoom(MainBorder);
 
 
-                if (reverse)
-                {
-                    ZoomOut(e);
-                }
-                else
-                {
-                    ZoomIn(e);
-                }
-            }
-            else
-            {
-                await ScrollOrNavigateAsync(e, reverse, mainViewModel);
-            }
-        }
-        else
-        {
-            if (ctrl)
-            {
-                await ScrollOrNavigateAsync(e, reverse, mainViewModel);
-            }
-            else
+    private void InitializeImageTransformer()
+    {
+        _imageTransformer = new ImageTransformer(
+            ImageLayoutTransformControl,
+            MainImage,
+            () => DataContext,
+            () =>
             {
             {
-                if (reverse)
+                if (_zoom?.IsZoomed == true)
                 {
                 {
-                    ZoomOut(e);
+                    ResetZoom(false);
                 }
                 }
-                else
-                {
-                    ZoomIn(e);
-                }
-            }
-        }
-    }
-
-    private static bool IsTouchPadOrTouch(PointerEventArgs e)
-        => Settings.Zoom.IsUsingTouchPad || e.Pointer.Type == PointerType.Touch;
-
-    private bool IsVerticalScrollBarVisible()
-        => ImageScrollViewer.VerticalScrollBarVisibility is ScrollBarVisibility.Visible or ScrollBarVisibility.Auto;
-
-    private void ScrollVertically(bool reverse)
-    {
-        if (reverse)
-        {
-            ImageScrollViewer.LineDown();
-        }
-        else
-        {
-            ImageScrollViewer.LineUp();
-        }
+            });
     }
     }
 
 
-    private async Task ScrollOrNavigateAsync(PointerWheelEventArgs e, bool reverse, MainViewModel mainViewModel)
-    {
-        if (!Settings.Zoom.ScrollEnabled || e.KeyModifiers == KeyModifiers.Shift)
-        {
-            if (IsTouchPadOrTouch(e))
-            {
-                return;
-            }
-
-            await LoadNextPicAsync(reverse, mainViewModel);
-        }
-        else
-        {
-            if (IsVerticalScrollBarVisible())
-            {
-                ScrollVertically(reverse);
-            }
-            else
-            {
-                await LoadNextPicAsync(reverse, mainViewModel);
-            }
-        }
-    }
-
-    private static async Task LoadNextPicAsync(bool reverse, MainViewModel mainViewModel)
-    {
-        if (Settings.Zoom.IsUsingTouchPad)
-        {
-            return;
-        }
+    private void InitializeMouseInputHelper() =>
+        MouseShortcuts.InitializeMouseShortcuts(
+            ImageScrollViewer,
+            () => DataContext, 
+            async e => { await Dispatcher.UIThread.InvokeAsync(() => { ZoomIn(e); }); },
+            async e => { await Dispatcher.UIThread.InvokeAsync(() => { ZoomOut(e); }); });
 
 
-        var next = reverse ? Settings.Zoom.HorizontalReverseScroll : !Settings.Zoom.HorizontalReverseScroll;
-        await NavigationManager.Navigate(next, mainViewModel).ConfigureAwait(false);
-    }
+    #endregion
 
 
     #region Zoom
     #region Zoom
 
 
-    private void InitializeZoom() => _zoom = new Zoom(MainBorder);
-
     public void ZoomIn(PointerWheelEventArgs e) =>
     public void ZoomIn(PointerWheelEventArgs e) =>
         _zoom?.ZoomIn(e, this, MainImage, DataContext as MainViewModel);
         _zoom?.ZoomIn(e, this, MainImage, DataContext as MainViewModel);
 
 
@@ -219,170 +89,16 @@ public partial class ImageViewer : UserControl
 
 
     #endregion
     #endregion
 
 
-    #region Rotation and Flip
-
-    public void Rotate(bool clockWise)
-    {
-        if (DataContext is not MainViewModel vm || MainImage.Source is null)
-        {
-            return;
-        }
-
-        if (RotationHelper.IsValidRotation(vm.RotationAngle))
-        {
-            var nextAngle = RotationHelper.Rotate(vm.RotationAngle, clockWise);
-            vm.RotationAngle = nextAngle switch
-            {
-                360 => 0,
-                -90 => 270,
-                _ => nextAngle
-            };
-        }
-        else
-        {
-            vm.RotationAngle = RotationHelper.NextRotationAngle(vm.RotationAngle, true);
-        }
-
-        SetImageLayoutTransform(new RotateTransform(vm.RotationAngle));
-        WindowResizing.SetSize(vm);
-        MainImage.InvalidateVisual();
-    }
-
-    public void Rotate(double angle)
-    {
-        SetImageLayoutTransform(new RotateTransform(angle));
-        WindowResizing.SetSize(DataContext as MainViewModel);
-        MainImage.InvalidateVisual();
-    }
+    #region Image Transformation
 
 
-    private void SetImageLayoutTransform(RotateTransform rotateTransform)
-    {
-        if (Dispatcher.UIThread.CheckAccess())
-        {
-            ImageLayoutTransformControl.LayoutTransform = rotateTransform;
-        }
-        else
-        {
-            Dispatcher.UIThread.Invoke(() =>
-                ImageLayoutTransformControl.LayoutTransform = rotateTransform);
-        }
-    }
+    public void Rotate(bool clockWise) => _imageTransformer?.Rotate(clockWise);
 
 
-    public void Flip(bool animate)
-    {
-        if (DataContext is not MainViewModel vm || MainImage.Source is null)
-        {
-            return;
-        }
+    public void Rotate(double angle) => _imageTransformer?.Rotate(angle);
 
 
-        var prevScaleX = vm.PicViewer.ScaleX;
-        vm.PicViewer.ScaleX = vm.PicViewer.ScaleX == -1 ? 1 : -1;
-        vm.Translation.IsFlipped = vm.PicViewer.ScaleX == 1 ? vm.Translation.UnFlip : vm.Translation.Flip;
+    public void Flip(bool animate) => _imageTransformer?.Flip(animate);
 
 
-        if (animate)
-        {
-            var flipTransform = new ScaleTransform(prevScaleX, 1)
-            {
-                Transitions =
-                [
-                    new DoubleTransition
-                        { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.2) }
-                ]
-            };
-            ImageLayoutTransformControl.RenderTransform = flipTransform;
-            flipTransform.ScaleX = vm.PicViewer.ScaleX;
-        }
-        else
-        {
-            ImageLayoutTransformControl.RenderTransform = new ScaleTransform(vm.PicViewer.ScaleX, 1);
-        }
-    }
-
-    public void SetTransform(int scaleX, int rotationAngle)
-    {
-        if (DataContext is not MainViewModel vm)
-        {
-            return;
-        }
-
-        vm.PicViewer.ScaleX = scaleX;
-        vm.RotationAngle = rotationAngle;
-        ImageLayoutTransformControl.RenderTransform = new ScaleTransform(vm.PicViewer.ScaleX, 1);
-        ImageLayoutTransformControl.LayoutTransform = new RotateTransform(rotationAngle);
-
-        if (_zoom is not null)
-        {
-            if (_zoom.IsZoomed)
-            {
-                ResetZoom(false);
-            }
-        }
-    }
-
-    public void SetTransform(EXIFHelper.EXIFOrientation? orientation, MagickFormat? format, bool reset = true)
-    {
-        if (Dispatcher.UIThread.CheckAccess())
-        {
-            ApplyOrientationTransform(orientation, format, reset);
-        }
-        else
-        {
-            Dispatcher.UIThread.InvokeAsync(() =>
-                ApplyOrientationTransform(orientation, format, reset), DispatcherPriority.Send);
-        }
-    }
-
-    private void ApplyOrientationTransform(EXIFHelper.EXIFOrientation? orientation, MagickFormat? format, bool reset)
-    {
-        if (Settings.Zoom.ScrollEnabled)
-        {
-            ImageScrollViewer.ScrollToHome();
-        }
-
-        if (format is MagickFormat.Heic or MagickFormat.Heif)
-        {
-            if (reset)
-            {
-                SetTransform(1, 0);
-            }
-
-            return;
-        }
-
-        switch (orientation)
-        {
-            case null:
-            case EXIFHelper.EXIFOrientation.None:
-            case EXIFHelper.EXIFOrientation.Horizontal:
-                if (reset)
-                {
-                    SetTransform(1, 0);
-                }
-
-                break;
-            case EXIFHelper.EXIFOrientation.MirrorHorizontal:
-                SetTransform(-1, 0);
-                break;
-            case EXIFHelper.EXIFOrientation.Rotate180:
-                SetTransform(1, 180);
-                break;
-            case EXIFHelper.EXIFOrientation.MirrorVertical:
-                SetTransform(-1, 180);
-                break;
-            case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate270Cw:
-                SetTransform(-1, 90);
-                break;
-            case EXIFHelper.EXIFOrientation.Rotate90Cw:
-                SetTransform(1, 90);
-                break;
-            case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate90Cw:
-                SetTransform(-1, 270);
-                break;
-            case EXIFHelper.EXIFOrientation.Rotated270Cw:
-                SetTransform(1, 270);
-                break;
-        }
-    }
+    public void SetTransform(EXIFHelper.EXIFOrientation? orientation, MagickFormat? format, bool reset = true) =>
+        _imageTransformer?.SetTransform(orientation, format, reset);
 
 
     #endregion
     #endregion
 
 

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

@@ -119,7 +119,7 @@ public partial class MainView : UserControl
             _ = new HoverFadeButtonHandler(ClickArrowLeft, vm, ClickArrowLeft.PolyButton);
             _ = new HoverFadeButtonHandler(ClickArrowLeft, vm, ClickArrowLeft.PolyButton);
             _ = new HoverFadeButtonHandler(AltButtonsPanel, vm);
             _ = new HoverFadeButtonHandler(AltButtonsPanel, vm);
             
             
-            PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
+            PointerWheelChanged += async (_, e) => await ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
         };
     }
     }
 
 

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

@@ -13,7 +13,7 @@ public partial class ClickArrowLeft : UserControl
             {
             {
                 return;
                 return;
             }
             }
-            PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
+            PointerWheelChanged += async (_, e) => await ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
         };
     }
     }
 }
 }

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

@@ -13,7 +13,7 @@ public partial class ClickArrowRight : UserControl
             {
             {
                 return;
                 return;
             }
             }
-            PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
+            PointerWheelChanged += async (_, e) => await ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
         };
     }
     }
 }
 }

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

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