ImageViewer.axaml.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. using Avalonia;
  2. using Avalonia.Animation;
  3. using Avalonia.Controls;
  4. using Avalonia.Controls.Primitives;
  5. using Avalonia.Input;
  6. using Avalonia.Interactivity;
  7. using Avalonia.Media;
  8. using Avalonia.Media.Imaging;
  9. using Avalonia.Threading;
  10. using PicView.Avalonia.Navigation;
  11. using PicView.Avalonia.UI;
  12. using PicView.Avalonia.ViewModels;
  13. using PicView.Avalonia.WindowBehavior;
  14. using PicView.Core.ImageDecoding;
  15. using PicView.Core.ImageTransformations;
  16. using Point = Avalonia.Point;
  17. namespace PicView.Avalonia.Views;
  18. public partial class ImageViewer : UserControl
  19. {
  20. private static ScaleTransform? _scaleTransform;
  21. private static TranslateTransform? _translateTransform;
  22. private static Point _start;
  23. private static Point _origin;
  24. private static Point _current;
  25. private bool _captured;
  26. private bool _isZoomed;
  27. public ImageViewer()
  28. {
  29. InitializeComponent();
  30. TriggerScalingModeUpdate(false);
  31. AddHandler(PointerWheelChangedEvent, PreviewOnPointerWheelChanged, RoutingStrategies.Tunnel);
  32. AddHandler(Gestures.PointerTouchPadGestureMagnifyEvent, TouchMagnifyEvent, RoutingStrategies.Bubble);
  33. Loaded += delegate
  34. {
  35. InitializeZoom();
  36. LostFocus += (_, _) =>
  37. {
  38. _captured = false;
  39. };
  40. };
  41. }
  42. public void TriggerScalingModeUpdate(bool invalidate)
  43. {
  44. var scalingMode = Settings.ImageScaling.IsScalingSetToNearestNeighbor
  45. ? BitmapInterpolationMode.LowQuality
  46. : BitmapInterpolationMode.HighQuality;
  47. RenderOptions.SetBitmapInterpolationMode(MainImage,scalingMode);
  48. if (invalidate)
  49. {
  50. MainImage.InvalidateVisual();
  51. }
  52. }
  53. private void TouchMagnifyEvent(object? sender, PointerDeltaEventArgs e)
  54. {
  55. ZoomTo(e.GetPosition(this), e.Delta.X > 0);
  56. }
  57. public async Task PreviewOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
  58. {
  59. e.Handled = true;
  60. await Main_OnPointerWheelChanged(e);
  61. }
  62. private async Task Main_OnPointerWheelChanged(PointerWheelEventArgs e)
  63. {
  64. if (DataContext is not MainViewModel mainViewModel)
  65. return;
  66. if (Settings.Zoom.IsUsingTouchPad)
  67. {
  68. // Use touch gestures for zooming
  69. return;
  70. }
  71. var ctrl = e.KeyModifiers == KeyModifiers.Control;
  72. var shift = e.KeyModifiers == KeyModifiers.Shift;
  73. var reverse = e.Delta.Y < 0;
  74. if (Settings.Zoom.ScrollEnabled)
  75. {
  76. if (!shift)
  77. {
  78. if (ctrl && !Settings.Zoom.CtrlZoom)
  79. {
  80. await LoadNextPic();
  81. return;
  82. }
  83. if (ImageScrollViewer.VerticalScrollBarVisibility is ScrollBarVisibility.Visible or ScrollBarVisibility.Auto)
  84. {
  85. if (reverse)
  86. {
  87. ImageScrollViewer.LineDown();
  88. }
  89. else
  90. {
  91. ImageScrollViewer.LineUp();
  92. }
  93. }
  94. else
  95. {
  96. await LoadNextPic();
  97. }
  98. return;
  99. }
  100. }
  101. if (Settings.Zoom.CtrlZoom)
  102. {
  103. if (ctrl)
  104. {
  105. if (reverse)
  106. {
  107. ZoomOut(e);
  108. }
  109. else
  110. {
  111. ZoomIn(e);
  112. }
  113. }
  114. else
  115. {
  116. await ScrollOrNavigate();
  117. }
  118. }
  119. else
  120. {
  121. if (ctrl)
  122. {
  123. await ScrollOrNavigate();
  124. }
  125. else
  126. {
  127. if (reverse)
  128. {
  129. ZoomOut(e);
  130. }
  131. else
  132. {
  133. ZoomIn(e);
  134. }
  135. }
  136. }
  137. return;
  138. async Task ScrollOrNavigate()
  139. {
  140. if (!Settings.Zoom.ScrollEnabled || e.KeyModifiers == KeyModifiers.Shift)
  141. {
  142. await LoadNextPic();
  143. }
  144. else
  145. {
  146. if (ImageScrollViewer.VerticalScrollBarVisibility is ScrollBarVisibility.Visible or ScrollBarVisibility.Auto)
  147. {
  148. if (reverse)
  149. {
  150. ImageScrollViewer.LineDown();
  151. }
  152. else
  153. {
  154. ImageScrollViewer.LineUp();
  155. }
  156. }
  157. else
  158. {
  159. await LoadNextPic();
  160. }
  161. }
  162. }
  163. async Task LoadNextPic()
  164. {
  165. if (!NavigationHelper.CanNavigate(mainViewModel))
  166. {
  167. return;
  168. }
  169. bool next;
  170. if (reverse)
  171. {
  172. next = Settings.Zoom.HorizontalReverseScroll;
  173. }
  174. else
  175. {
  176. next = !Settings.Zoom.HorizontalReverseScroll;
  177. }
  178. await NavigationHelper.Navigate(next, mainViewModel).ConfigureAwait(false);
  179. }
  180. }
  181. #region Zoom
  182. private void InitializeZoom()
  183. {
  184. MainBorder.RenderTransform = new TransformGroup
  185. {
  186. Children =
  187. [
  188. new ScaleTransform(),
  189. new TranslateTransform()
  190. ]
  191. };
  192. _scaleTransform = (ScaleTransform)((TransformGroup)MainBorder.RenderTransform)
  193. .Children.First(tr => tr is ScaleTransform);
  194. _translateTransform = (TranslateTransform)((TransformGroup)MainBorder.RenderTransform)
  195. .Children.First(tr => tr is TranslateTransform);
  196. MainBorder.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
  197. MainImage.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Relative);
  198. }
  199. public void ZoomIn(PointerWheelEventArgs e)
  200. {
  201. ZoomTo(e.GetPosition(ImageScrollViewer), true);
  202. }
  203. public void ZoomOut(PointerWheelEventArgs e)
  204. {
  205. ZoomTo(e.GetPosition(ImageScrollViewer), false);
  206. }
  207. public void ZoomIn()
  208. {
  209. ZoomTo(_current, true);
  210. }
  211. public void ZoomOut()
  212. {
  213. ZoomTo(_current, false);
  214. }
  215. public void ZoomTo(Point point, bool isZoomIn)
  216. {
  217. if (_scaleTransform == null || _translateTransform == null)
  218. {
  219. return;
  220. }
  221. var currentZoom = _scaleTransform.ScaleX;
  222. var zoomSpeed = Settings.Zoom.ZoomSpeed;
  223. switch (currentZoom)
  224. {
  225. // Increase speed based on the current zoom level
  226. case > 15 when isZoomIn:
  227. return;
  228. case > 4:
  229. zoomSpeed += 1;
  230. break;
  231. case > 3.2:
  232. zoomSpeed += 0.8;
  233. break;
  234. case > 1.6:
  235. zoomSpeed += 0.5;
  236. break;
  237. }
  238. if (!isZoomIn)
  239. {
  240. zoomSpeed = -zoomSpeed;
  241. }
  242. currentZoom += zoomSpeed;
  243. currentZoom = Math.Max(0.09, currentZoom); // Fix for zooming out too much
  244. TriggerScalingModeUpdate(false);
  245. if (Settings.Zoom.AvoidZoomingOut && currentZoom < 1.0)
  246. {
  247. ResetZoom(true);
  248. }
  249. else
  250. {
  251. ZoomTo(point, currentZoom, true);
  252. }
  253. }
  254. public void ZoomTo(Point point, double zoomValue, bool enableAnimations)
  255. {
  256. if (_scaleTransform == null || _translateTransform == null)
  257. {
  258. return;
  259. }
  260. if (DataContext is not MainViewModel vm)
  261. return;
  262. if (enableAnimations)
  263. {
  264. _scaleTransform.Transitions ??=
  265. [
  266. new DoubleTransition { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.25) },
  267. new DoubleTransition { Property = ScaleTransform.ScaleYProperty, Duration = TimeSpan.FromSeconds(.25) }
  268. ];
  269. _translateTransform.Transitions ??=
  270. [
  271. new DoubleTransition { Property = TranslateTransform.XProperty, Duration = TimeSpan.FromSeconds(.25) },
  272. new DoubleTransition { Property = TranslateTransform.YProperty, Duration = TimeSpan.FromSeconds(.25) }
  273. ];
  274. }
  275. else
  276. {
  277. _scaleTransform.Transitions = null;
  278. _translateTransform.Transitions = null;
  279. }
  280. var absoluteX = point.X * _scaleTransform.ScaleX + _translateTransform.X;
  281. var absoluteY = point.Y * _scaleTransform.ScaleY + _translateTransform.Y;
  282. var newTranslateValueX = Math.Abs(zoomValue - 1) > .2 ? absoluteX - point.X * zoomValue : 0;
  283. var newTranslateValueY = Math.Abs(zoomValue - 1) > .2 ? absoluteY - point.Y * zoomValue : 0;
  284. _scaleTransform.ScaleX = zoomValue;
  285. _scaleTransform.ScaleY = zoomValue;
  286. _translateTransform.X = newTranslateValueX;
  287. _translateTransform.Y = newTranslateValueY;
  288. vm.ZoomValue = zoomValue;
  289. _isZoomed = zoomValue != 0;
  290. if (_isZoomed)
  291. {
  292. SetTitleHelper.SetTitle(vm);
  293. TooltipHelper.ShowTooltipMessage($"{Math.Floor(zoomValue * 100)}%", center: true);
  294. }
  295. }
  296. public void ResetZoom(bool enableAnimations)
  297. {
  298. if (_scaleTransform == null || _translateTransform == null)
  299. {
  300. return;
  301. }
  302. Dispatcher.UIThread.InvokeAsync(() =>
  303. {
  304. if (enableAnimations)
  305. {
  306. _scaleTransform.Transitions ??=
  307. [
  308. new DoubleTransition { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.25) },
  309. new DoubleTransition { Property = ScaleTransform.ScaleYProperty, Duration = TimeSpan.FromSeconds(.25) }
  310. ];
  311. _translateTransform.Transitions ??=
  312. [
  313. new DoubleTransition { Property = TranslateTransform.XProperty, Duration = TimeSpan.FromSeconds(.25) },
  314. new DoubleTransition { Property = TranslateTransform.YProperty, Duration = TimeSpan.FromSeconds(.25) }
  315. ];
  316. }
  317. else
  318. {
  319. _scaleTransform.Transitions = null;
  320. _translateTransform.Transitions = null;
  321. }
  322. _scaleTransform.ScaleX = 1;
  323. _scaleTransform.ScaleY = 1;
  324. _translateTransform.X = 0;
  325. _translateTransform.Y = 0;
  326. }, DispatcherPriority.Send);
  327. if (DataContext is not MainViewModel vm)
  328. {
  329. return;
  330. }
  331. vm.ZoomValue = 1;
  332. vm.RotationAngle = 0;
  333. SetTitleHelper.SetTitle(vm);
  334. _isZoomed = false;
  335. }
  336. public void Reset()
  337. {
  338. if (Dispatcher.UIThread.CheckAccess())
  339. {
  340. DoReset();
  341. }
  342. else
  343. {
  344. Dispatcher.UIThread.InvokeAsync(DoReset);
  345. }
  346. return;
  347. void DoReset()
  348. {
  349. if (_isZoomed)
  350. {
  351. ResetZoom(false);
  352. }
  353. ImageLayoutTransformControl.LayoutTransform = null;
  354. MainImage.RenderTransform = null;
  355. if (DataContext is MainViewModel vm)
  356. {
  357. vm.RotationAngle = 0;
  358. }
  359. }
  360. }
  361. private void Capture(PointerEventArgs e)
  362. {
  363. if (_captured)
  364. {
  365. return;
  366. }
  367. if (_scaleTransform == null || _translateTransform == null)
  368. {
  369. return;
  370. }
  371. var mainView = UIHelper.GetMainView;
  372. var point = e.GetCurrentPoint(mainView);
  373. var x = point.Position.X;
  374. var y = point.Position.Y;
  375. _start = new Point(x, y);
  376. _origin = new Point(_translateTransform.X, _translateTransform.Y);
  377. _captured = true;
  378. }
  379. public void Pan(PointerEventArgs e)
  380. {
  381. if (!_captured || _scaleTransform == null || !_isZoomed)
  382. {
  383. return;
  384. }
  385. var dragMousePosition = _start - e.GetPosition(this);
  386. var newXproperty = _origin.X - dragMousePosition.X;
  387. var newYproperty = _origin.Y - dragMousePosition.Y;
  388. if (!Settings.WindowProperties.AutoFit || Settings.WindowProperties.Fullscreen)
  389. {
  390. // TODO: figure out how to pan when not auto fitting window while keeping it in bounds
  391. _translateTransform.Transitions = null;
  392. _translateTransform.X = newXproperty;
  393. _translateTransform.Y = newYproperty;
  394. e.Handled = true;
  395. return;
  396. }
  397. var actualScrollWidth = ImageScrollViewer.Bounds.Width;
  398. var actualBorderWidth = MainBorder.Bounds.Width;
  399. var actualScrollHeight = ImageScrollViewer.Bounds.Height;
  400. var actualBorderHeight = MainBorder.Bounds.Height;
  401. var isXOutOfBorder = actualScrollWidth < actualBorderWidth * _scaleTransform.ScaleX;
  402. var isYOutOfBorder = actualScrollHeight < actualBorderHeight * _scaleTransform.ScaleY;
  403. var maxX = actualScrollWidth - actualBorderWidth * _scaleTransform.ScaleX;
  404. var maxY = actualScrollHeight - actualBorderHeight * _scaleTransform.ScaleY;
  405. if (isXOutOfBorder && newXproperty < maxX || isXOutOfBorder == false && newXproperty > maxX)
  406. {
  407. newXproperty = maxX;
  408. }
  409. if (isXOutOfBorder && newYproperty < maxY || isXOutOfBorder == false && newYproperty > maxY)
  410. {
  411. newYproperty = maxY;
  412. }
  413. if (isXOutOfBorder && newXproperty > 0 || isXOutOfBorder == false && newXproperty < 0)
  414. {
  415. newXproperty = 0;
  416. }
  417. if (isYOutOfBorder && newYproperty > 0 || isYOutOfBorder == false && newYproperty < 0)
  418. {
  419. newYproperty = 0;
  420. }
  421. _translateTransform.Transitions = null;
  422. _translateTransform.X = newXproperty;
  423. _translateTransform.Y = newYproperty;
  424. e.Handled = true;
  425. }
  426. #endregion Zoom
  427. #region Rotation and Flip
  428. public void Rotate(bool clockWise)
  429. {
  430. if (DataContext is not MainViewModel vm)
  431. return;
  432. if (MainImage.Source is null)
  433. {
  434. return;
  435. }
  436. if (RotationHelper.IsValidRotation(vm.RotationAngle))
  437. {
  438. var nextAngle = RotationHelper.Rotate(vm.RotationAngle, clockWise);
  439. vm.RotationAngle = nextAngle switch
  440. {
  441. 360 => 0,
  442. -90 => 270,
  443. _ => nextAngle
  444. };
  445. }
  446. else
  447. {
  448. vm.RotationAngle = RotationHelper.NextRotationAngle(vm.RotationAngle, true);
  449. }
  450. var rotateTransform = new RotateTransform(vm.RotationAngle);
  451. if (Dispatcher.UIThread.CheckAccess())
  452. {
  453. ImageLayoutTransformControl.LayoutTransform = rotateTransform;
  454. }
  455. else
  456. {
  457. Dispatcher.UIThread.Invoke(() =>
  458. {
  459. ImageLayoutTransformControl.LayoutTransform = rotateTransform;
  460. });
  461. }
  462. WindowResizing.SetSize(vm);
  463. MainImage.InvalidateVisual();
  464. }
  465. public void Rotate(double angle)
  466. {
  467. Dispatcher.UIThread.Invoke(() =>
  468. {
  469. var rotateTransform = new RotateTransform(angle);
  470. ImageLayoutTransformControl.LayoutTransform = rotateTransform;
  471. WindowResizing.SetSize(DataContext as MainViewModel);
  472. MainImage.InvalidateVisual();
  473. });
  474. }
  475. public void Flip(bool animate)
  476. {
  477. if (DataContext is not MainViewModel vm)
  478. return;
  479. if (MainImage.Source is null)
  480. {
  481. return;
  482. }
  483. int prevScaleX;
  484. vm.ScaleX = vm.ScaleX == -1 ? 1 : -1;
  485. if (vm.ScaleX == 1)
  486. {
  487. prevScaleX = 1;
  488. vm.ScaleX = -1;
  489. vm.GetIsFlippedTranslation = vm.UnFlip;
  490. }
  491. else
  492. {
  493. prevScaleX = -1;
  494. vm.ScaleX = 1;
  495. vm.GetIsFlippedTranslation = vm.Flip;
  496. }
  497. if (animate)
  498. {
  499. var flipTransform = new ScaleTransform(prevScaleX, 1)
  500. {
  501. Transitions =
  502. [
  503. new DoubleTransition { Property = ScaleTransform.ScaleXProperty, Duration = TimeSpan.FromSeconds(.2) },
  504. ]
  505. };
  506. ImageLayoutTransformControl.RenderTransform = flipTransform;
  507. flipTransform.ScaleX = vm.ScaleX;
  508. }
  509. else
  510. {
  511. var flipTransform = new ScaleTransform(vm.ScaleX, 1);
  512. ImageLayoutTransformControl.RenderTransform = flipTransform;
  513. }
  514. }
  515. public void SetTransform(int scaleX, int rotationAngle)
  516. {
  517. if (DataContext is not MainViewModel vm)
  518. return;
  519. vm.ScaleX = scaleX;
  520. vm.RotationAngle = rotationAngle;
  521. var flipTransform = new ScaleTransform(vm.ScaleX, 1);
  522. ImageLayoutTransformControl.RenderTransform = flipTransform;
  523. var rotateTransform = new RotateTransform(rotationAngle);
  524. ImageLayoutTransformControl.LayoutTransform = rotateTransform;
  525. if (_isZoomed)
  526. {
  527. ResetZoom(false);
  528. }
  529. }
  530. public void SetTransform(EXIFHelper.EXIFOrientation? orientation)
  531. {
  532. if (Dispatcher.UIThread.CheckAccess())
  533. {
  534. Set();
  535. }
  536. else
  537. {
  538. Dispatcher.UIThread.InvokeAsync(Set, DispatcherPriority.Send);
  539. }
  540. return;
  541. void Set()
  542. {
  543. if (Settings.Zoom.ScrollEnabled)
  544. {
  545. ImageScrollViewer.ScrollToHome();
  546. }
  547. switch (orientation)
  548. {
  549. case null:
  550. default:
  551. case EXIFHelper.EXIFOrientation.None:
  552. case EXIFHelper.EXIFOrientation.Horizontal:
  553. Reset();
  554. return;
  555. case EXIFHelper.EXIFOrientation.MirrorHorizontal:
  556. SetTransform(-1, 0);
  557. break;
  558. case EXIFHelper.EXIFOrientation.Rotate180:
  559. SetTransform(1, 180);
  560. break;
  561. case EXIFHelper.EXIFOrientation.MirrorVertical:
  562. SetTransform(-1, 180);
  563. break;
  564. case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate270Cw:
  565. SetTransform(-1, 90); // should be 270, but it's not working
  566. break;
  567. case EXIFHelper.EXIFOrientation.Rotate90Cw:
  568. SetTransform(1, 90);
  569. break;
  570. case EXIFHelper.EXIFOrientation.MirrorHorizontalRotate90Cw:
  571. SetTransform(-1, 270); // should be 90, but it's not working
  572. break;
  573. case EXIFHelper.EXIFOrientation.Rotated270Cw:
  574. SetTransform(1, 270);
  575. break;
  576. }
  577. }
  578. }
  579. #endregion Rotation and Flip
  580. #region Events
  581. private void ImageScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
  582. {
  583. e.Handled = true;
  584. }
  585. private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
  586. {
  587. if (e.ClickCount == 2)
  588. {
  589. ResetZoom(true);
  590. }
  591. }
  592. private void MainImage_OnPointerPressed(object? sender, PointerPressedEventArgs e)
  593. {
  594. if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
  595. {
  596. return;
  597. }
  598. if (e.ClickCount == 2)
  599. {
  600. ResetZoom(true);
  601. }
  602. else
  603. {
  604. Pressed(e);
  605. }
  606. }
  607. private void MainImage_OnPointerMoved(object? sender, PointerEventArgs e)
  608. {
  609. _current = e.GetPosition(this);
  610. Pan(e);
  611. }
  612. private void Pressed(PointerEventArgs e)
  613. {
  614. if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
  615. {
  616. return;
  617. }
  618. Capture(e);
  619. }
  620. private void MainImage_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
  621. {
  622. _captured = false;
  623. }
  624. #endregion Events
  625. }