| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- using Avalonia;
- using Avalonia.Animation;
- using Avalonia.Controls;
- using Avalonia.Controls.Primitives;
- using Avalonia.Input;
- using Avalonia.Interactivity;
- using Avalonia.Media;
- using Avalonia.Media.Imaging;
- using Avalonia.Threading;
- using PicView.Avalonia.Navigation;
- using PicView.Avalonia.UI;
- using PicView.Avalonia.ViewModels;
- using PicView.Avalonia.WindowBehavior;
- using PicView.Core.ImageDecoding;
- using PicView.Core.ImageTransformations;
- using Point = Avalonia.Point;
- namespace PicView.Avalonia.Views;
- public partial class ImageViewer : UserControl
- {
- private static ScaleTransform? _scaleTransform;
- private static TranslateTransform? _translateTransform;
- private static Point _start;
- private static Point _origin;
- private static Point _current;
- private bool _captured;
- private bool _isZoomed;
- public ImageViewer()
- {
- InitializeComponent();
- TriggerScalingModeUpdate(false);
- AddHandler(PointerWheelChangedEvent, PreviewOnPointerWheelChanged, RoutingStrategies.Tunnel);
- AddHandler(Gestures.PointerTouchPadGestureMagnifyEvent, TouchMagnifyEvent, RoutingStrategies.Bubble);
- Loaded += delegate
- {
- InitializeZoom();
- LostFocus += (_, _) =>
- {
- _captured = false;
- };
- };
- }
-
- public void TriggerScalingModeUpdate(bool invalidate)
- {
- var scalingMode = Settings.ImageScaling.IsScalingSetToNearestNeighbor
- ? BitmapInterpolationMode.LowQuality
- : BitmapInterpolationMode.HighQuality;
- RenderOptions.SetBitmapInterpolationMode(MainImage,scalingMode);
- if (invalidate)
- {
- MainImage.InvalidateVisual();
- }
- }
- private void TouchMagnifyEvent(object? sender, PointerDeltaEventArgs e)
- {
- ZoomTo(e.GetPosition(this), e.Delta.X > 0);
- }
- public async Task PreviewOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
- {
- e.Handled = true;
- await Main_OnPointerWheelChanged(e);
- }
-
- private async Task Main_OnPointerWheelChanged(PointerWheelEventArgs e)
- {
- if (DataContext is not MainViewModel mainViewModel)
- return;
- if (Settings.Zoom.IsUsingTouchPad)
- {
- // Use touch gestures for zooming
- 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)
- {
- await LoadNextPic();
- return;
- }
- if (ImageScrollViewer.VerticalScrollBarVisibility is ScrollBarVisibility.Visible or ScrollBarVisibility.Auto)
- {
- if (reverse)
- {
- ImageScrollViewer.LineDown();
- }
- else
- {
- ImageScrollViewer.LineUp();
- }
- }
- else
- {
- await LoadNextPic();
- }
- return;
- }
-
- }
- if (Settings.Zoom.CtrlZoom)
- {
- if (ctrl)
- {
- if (reverse)
- {
- ZoomOut(e);
- }
- else
- {
- ZoomIn(e);
- }
- }
- else
- {
- await ScrollOrNavigate();
- }
- }
- else
- {
- if (ctrl)
- {
- await ScrollOrNavigate();
- }
- else
- {
- if (reverse)
- {
- ZoomOut(e);
- }
- else
- {
- ZoomIn(e);
- }
- }
- }
- return;
- async Task ScrollOrNavigate()
- {
- if (!Settings.Zoom.ScrollEnabled || e.KeyModifiers == KeyModifiers.Shift)
- {
- await LoadNextPic();
- }
- else
- {
- if (ImageScrollViewer.VerticalScrollBarVisibility is ScrollBarVisibility.Visible or ScrollBarVisibility.Auto)
- {
- if (reverse)
- {
- ImageScrollViewer.LineDown();
- }
- else
- {
- ImageScrollViewer.LineUp();
- }
- }
- else
- {
- await LoadNextPic();
- }
- }
- }
- async Task LoadNextPic()
- {
- if (!NavigationHelper.CanNavigate(mainViewModel))
- {
- return;
- }
- bool next;
- if (reverse)
- {
- next = Settings.Zoom.HorizontalReverseScroll;
- }
- else
- {
- next = !Settings.Zoom.HorizontalReverseScroll;
- }
- await NavigationHelper.Navigate(next, mainViewModel).ConfigureAwait(false);
- }
- }
- #region Zoom
- private void InitializeZoom()
- {
- MainBorder.RenderTransform = new TransformGroup
- {
- Children =
- [
- new ScaleTransform(),
- new TranslateTransform()
- ]
- };
- _scaleTransform = (ScaleTransform)((TransformGroup)MainBorder.RenderTransform)
- .Children.First(tr => tr is ScaleTransform);
- _translateTransform = (TranslateTransform)((TransformGroup)MainBorder.RenderTransform)
- .Children.First(tr => tr is TranslateTransform);
- MainBorder.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
- MainImage.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
- }
- public void ZoomIn(PointerWheelEventArgs e)
- {
- ZoomTo(e.GetPosition(ImageScrollViewer), true);
- }
- public void ZoomOut(PointerWheelEventArgs e)
- {
- ZoomTo(e.GetPosition(ImageScrollViewer), false);
- }
- public void ZoomIn()
- {
- ZoomTo(_current, true);
- }
- public void ZoomOut()
- {
- ZoomTo(_current, false);
- }
- public void ZoomTo(Point point, bool isZoomIn)
- {
- if (_scaleTransform == null || _translateTransform == null)
- {
- return;
- }
- var currentZoom = _scaleTransform.ScaleX;
- var zoomSpeed = Settings.Zoom.ZoomSpeed;
-
- switch (currentZoom)
- {
- // Increase speed based on the current zoom level
- case > 15 when isZoomIn:
- return;
- case > 4:
- zoomSpeed += 1;
- break;
- case > 3.2:
- zoomSpeed += 0.8;
- break;
- case > 1.6:
- zoomSpeed += 0.5;
- break;
- }
- if (!isZoomIn)
- {
- zoomSpeed = -zoomSpeed;
- }
- currentZoom += zoomSpeed;
- currentZoom = Math.Max(0.09, currentZoom); // Fix for zooming out too much
- TriggerScalingModeUpdate(false);
- if (Settings.Zoom.AvoidZoomingOut && currentZoom < 1.0)
- {
- ResetZoom(true);
- }
- else
- {
- ZoomTo(point, currentZoom, true);
- }
- }
- public void ZoomTo(Point point, double zoomValue, bool enableAnimations)
- {
- if (_scaleTransform == null || _translateTransform == null)
- {
- return;
- }
- if (DataContext is not MainViewModel vm)
- return;
- if (enableAnimations)
- {
- _scaleTransform.Transitions ??=
- [
- new DoubleTransition { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.25) },
- new DoubleTransition { Property = ScaleTransform.ScaleYProperty, Duration = TimeSpan.FromSeconds(.25) }
- ];
- _translateTransform.Transitions ??=
- [
- new DoubleTransition { Property = TranslateTransform.XProperty, Duration = TimeSpan.FromSeconds(.25) },
- new DoubleTransition { Property = TranslateTransform.YProperty, Duration = TimeSpan.FromSeconds(.25) }
- ];
- }
- else
- {
- _scaleTransform.Transitions = null;
- _translateTransform.Transitions = null;
- }
- var absoluteX = point.X * _scaleTransform.ScaleX + _translateTransform.X;
- var absoluteY = point.Y * _scaleTransform.ScaleY + _translateTransform.Y;
- var newTranslateValueX = Math.Abs(zoomValue - 1) > .2 ? absoluteX - point.X * zoomValue : 0;
- var newTranslateValueY = Math.Abs(zoomValue - 1) > .2 ? absoluteY - point.Y * zoomValue : 0;
-
- _scaleTransform.ScaleX = zoomValue;
- _scaleTransform.ScaleY = zoomValue;
- _translateTransform.X = newTranslateValueX;
- _translateTransform.Y = newTranslateValueY;
- vm.ZoomValue = zoomValue;
- _isZoomed = zoomValue != 0;
- if (_isZoomed)
- {
- SetTitleHelper.SetTitle(vm);
- TooltipHelper.ShowTooltipMessage($"{Math.Floor(zoomValue * 100)}%", center: true);
- }
- }
- public void ResetZoom(bool enableAnimations)
- {
- if (_scaleTransform == null || _translateTransform == null)
- {
- return;
- }
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- if (enableAnimations)
- {
- _scaleTransform.Transitions ??=
- [
- new DoubleTransition { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.25) },
- new DoubleTransition { Property = ScaleTransform.ScaleYProperty, Duration = TimeSpan.FromSeconds(.25) }
- ];
- _translateTransform.Transitions ??=
- [
- new DoubleTransition { Property = TranslateTransform.XProperty, Duration = TimeSpan.FromSeconds(.25) },
- new DoubleTransition { Property = TranslateTransform.YProperty, Duration = TimeSpan.FromSeconds(.25) }
- ];
- }
- else
- {
- _scaleTransform.Transitions = null;
- _translateTransform.Transitions = null;
- }
- _scaleTransform.ScaleX = 1;
- _scaleTransform.ScaleY = 1;
- _translateTransform.X = 0;
- _translateTransform.Y = 0;
- }, DispatcherPriority.Send);
- if (DataContext is not MainViewModel vm)
- {
- return;
- }
- vm.ZoomValue = 1;
- vm.RotationAngle = 0;
- SetTitleHelper.SetTitle(vm);
- _isZoomed = false;
- }
- public void Reset()
- {
- if (Dispatcher.UIThread.CheckAccess())
- {
- DoReset();
- }
- else
- {
- Dispatcher.UIThread.InvokeAsync(DoReset);
- }
- return;
-
- void DoReset()
- {
- if (_isZoomed)
- {
- ResetZoom(false);
- }
- ImageLayoutTransformControl.LayoutTransform = null;
- MainImage.RenderTransform = null;
- if (DataContext is MainViewModel vm)
- {
- vm.RotationAngle = 0;
- }
- }
- }
-
- private void Capture(PointerEventArgs e)
- {
- if (_captured)
- {
- return;
- }
- if (_scaleTransform == null || _translateTransform == null)
- {
- return;
- }
- var mainView = UIHelper.GetMainView;
- var point = e.GetCurrentPoint(mainView);
- var x = point.Position.X;
- var y = point.Position.Y;
- _start = new Point(x, y);
- _origin = new Point(_translateTransform.X, _translateTransform.Y);
- _captured = true;
- }
-
- public void Pan(PointerEventArgs e)
- {
- if (!_captured || _scaleTransform == null || !_isZoomed)
- {
- return;
- }
- var dragMousePosition = _start - e.GetPosition(this);
-
- var newXproperty = _origin.X - dragMousePosition.X;
- var newYproperty = _origin.Y - dragMousePosition.Y;
- if (!Settings.WindowProperties.AutoFit || Settings.WindowProperties.Fullscreen)
- {
- // TODO: figure out how to pan when not auto fitting window while keeping it in bounds
- _translateTransform.Transitions = null;
- _translateTransform.X = newXproperty;
- _translateTransform.Y = newYproperty;
- e.Handled = true;
- return;
- }
-
- var actualScrollWidth = ImageScrollViewer.Bounds.Width;
- var actualBorderWidth = MainBorder.Bounds.Width;
- var actualScrollHeight = ImageScrollViewer.Bounds.Height;
- var actualBorderHeight = MainBorder.Bounds.Height;
- var isXOutOfBorder = actualScrollWidth < actualBorderWidth * _scaleTransform.ScaleX;
- var isYOutOfBorder = actualScrollHeight < actualBorderHeight * _scaleTransform.ScaleY;
- var maxX = actualScrollWidth - actualBorderWidth * _scaleTransform.ScaleX;
- var maxY = actualScrollHeight - actualBorderHeight * _scaleTransform.ScaleY;
-
- if (isXOutOfBorder && newXproperty < maxX || isXOutOfBorder == false && newXproperty > maxX)
- {
- newXproperty = maxX;
- }
- if (isXOutOfBorder && newYproperty < maxY || isXOutOfBorder == false && newYproperty > maxY)
- {
- newYproperty = maxY;
- }
- if (isXOutOfBorder && newXproperty > 0 || isXOutOfBorder == false && newXproperty < 0)
- {
- newXproperty = 0;
- }
- if (isYOutOfBorder && newYproperty > 0 || isYOutOfBorder == false && newYproperty < 0)
- {
- newYproperty = 0;
- }
- _translateTransform.Transitions = null;
- _translateTransform.X = newXproperty;
- _translateTransform.Y = newYproperty;
- e.Handled = true;
- }
- #endregion Zoom
- #region Rotation and Flip
- public void Rotate(bool clockWise)
- {
- if (DataContext is not MainViewModel vm)
- return;
- if (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);
- }
- var rotateTransform = new RotateTransform(vm.RotationAngle);
- if (Dispatcher.UIThread.CheckAccess())
- {
- ImageLayoutTransformControl.LayoutTransform = rotateTransform;
- }
- else
- {
- Dispatcher.UIThread.Invoke(() =>
- {
- ImageLayoutTransformControl.LayoutTransform = rotateTransform;
- });
- }
- WindowResizing.SetSize(vm);
- MainImage.InvalidateVisual();
- }
-
- public void Rotate(double angle)
- {
- Dispatcher.UIThread.Invoke(() =>
- {
- var rotateTransform = new RotateTransform(angle);
- ImageLayoutTransformControl.LayoutTransform = rotateTransform;
-
- WindowResizing.SetSize(DataContext as MainViewModel);
- MainImage.InvalidateVisual();
- });
- }
- public void Flip(bool animate)
- {
- if (DataContext is not MainViewModel vm)
- return;
- if (MainImage.Source is null)
- {
- return;
- }
- int prevScaleX;
- vm.ScaleX = vm.ScaleX == -1 ? 1 : -1;
- if (vm.ScaleX == 1)
- {
- prevScaleX = 1;
- vm.ScaleX = -1;
- vm.GetIsFlippedTranslation = vm.UnFlip;
- }
- else
- {
- prevScaleX = -1;
- vm.ScaleX = 1;
- vm.GetIsFlippedTranslation = vm.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.ScaleX;
- }
- else
- {
- var flipTransform = new ScaleTransform(vm.ScaleX, 1);
- ImageLayoutTransformControl.RenderTransform = flipTransform;
- }
- }
-
- public void SetTransform(int scaleX, int rotationAngle)
- {
- if (DataContext is not MainViewModel vm)
- return;
- vm.ScaleX = scaleX;
- vm.RotationAngle = rotationAngle;
- var flipTransform = new ScaleTransform(vm.ScaleX, 1);
- ImageLayoutTransformControl.RenderTransform = flipTransform;
-
- var rotateTransform = new RotateTransform(rotationAngle);
- ImageLayoutTransformControl.LayoutTransform = rotateTransform;
-
- if (_isZoomed)
- {
- ResetZoom(false);
- }
- }
- public void SetTransform(EXIFHelper.EXIFOrientation? orientation)
- {
- if (Dispatcher.UIThread.CheckAccess())
- {
- Set();
- }
- else
- {
- Dispatcher.UIThread.InvokeAsync(Set, DispatcherPriority.Send);
- }
- return;
- void Set()
- {
- if (Settings.Zoom.ScrollEnabled)
- {
- ImageScrollViewer.ScrollToHome();
- }
- switch (orientation)
- {
- case null:
- default:
- case EXIFHelper.EXIFOrientation.None:
- case EXIFHelper.EXIFOrientation.Horizontal:
- Reset();
- return;
- 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); // should be 270, but it's not working
- break;
- case EXIFHelper.EXIFOrientation.Rotate90Cw:
- SetTransform(1, 90);
- break;
- case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate90Cw:
- SetTransform(-1, 270); // should be 90, but it's not working
- break;
- case EXIFHelper.EXIFOrientation.Rotated270Cw:
- SetTransform(1, 270);
- break;
- }
- }
- }
- #endregion Rotation and Flip
- #region Events
- private void ImageScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
- {
- e.Handled = true;
- }
- private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
- {
- if (e.ClickCount == 2)
- {
- ResetZoom(true);
- }
- }
- private void MainImage_OnPointerPressed(object? sender, PointerPressedEventArgs e)
- {
- if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
- {
- return;
- }
- if (e.ClickCount == 2)
- {
- ResetZoom(true);
- }
- else
- {
- Pressed(e);
- }
- }
- private void MainImage_OnPointerMoved(object? sender, PointerEventArgs e)
- {
- _current = e.GetPosition(this);
- Pan(e);
- }
- private void Pressed(PointerEventArgs e)
- {
- if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
- {
- return;
- }
- Capture(e);
- }
- private void MainImage_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
- {
- _captured = false;
- }
- #endregion Events
-
-
- }
|