1
1
Эх сурвалжийг харах

Implementation of crop, work in progress

Ruben 10 сар өмнө
parent
commit
28d1c3d424

+ 58 - 0
src/PicView.Avalonia/Crop/CropFunctions.cs

@@ -0,0 +1,58 @@
+using Avalonia;
+using Avalonia.Media.Imaging;
+using PicView.Avalonia.UI;
+using PicView.Avalonia.ViewModels;
+using PicView.Avalonia.Views.UC;
+using PicView.Core.Localization;
+
+namespace PicView.Avalonia.Crop;
+
+public static class CropFunctions
+{
+    public static bool IsCropping {get; private set;} 
+    
+    public static void Init(MainViewModel vm)
+    {
+        if (IsCropping)
+        {
+            return;
+        }
+        if (vm?.ImageSource is not Bitmap bitmap)
+        {
+            return;
+        }
+        var size = new Size(vm.ImageWidth, vm.ImageHeight);
+        var cropperViewModel = new ImageCropperViewModel(bitmap)
+        {
+            ImageWidth = size.Width,
+            ImageHeight = size.Height
+        };
+        var cropControl = new CropControl
+        {
+            DataContext = cropperViewModel,
+            Width = size.Width,
+            Height = size.Height,
+        };
+        UIHelper.GetMainView.MainGrid.Children.Add(cropControl);
+        
+        IsCropping = true;
+        vm.Title = TranslationHelper.Translation.CropMessage;
+        vm.TitleTooltip = TranslationHelper.Translation.CropMessage;
+    }
+    
+    public static void CloseCropControl(MainViewModel vm)
+    {
+        UIHelper.GetMainView.MainGrid.Children.Remove(UIHelper.GetMainView.MainGrid.Children.OfType<CropControl>().First());
+        IsCropping = false;
+        SetTitleHelper.RefreshTitle(vm);
+    }
+
+    public static bool DetermineIfShouldBeEnabled(MainViewModel vm)
+    {
+        if (vm?.ImageSource is not Bitmap bitmap)
+        {
+            return false;
+        }
+        return true;
+    }
+}

+ 2 - 1
src/PicView.Avalonia/Navigation/NavigationHelper.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Media.Imaging;
 using Avalonia.Threading;
 using ImageMagick;
+using PicView.Avalonia.Crop;
 using PicView.Avalonia.Gallery;
 using PicView.Avalonia.ImageHandling;
 using PicView.Avalonia.Input;
@@ -34,7 +35,7 @@ public static class NavigationHelper
     public static bool CanNavigate(MainViewModel vm)
     {
         return vm?.ImageIterator?.ImagePaths is not null &&
-               vm.ImageIterator.ImagePaths.Count > 0;
+               vm.ImageIterator.ImagePaths.Count > 0 && !CropFunctions.IsCropping;
         // TODO: should probably turn this into CanExecute observable for ReactiveUI
     }
     

+ 9 - 2
src/PicView.Avalonia/UI/FunctionsHelper.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using PicView.Avalonia.Clipboard;
 using PicView.Avalonia.ColorManagement;
+using PicView.Avalonia.Crop;
 using PicView.Avalonia.FileSystem;
 using PicView.Avalonia.Gallery;
 using PicView.Avalonia.ImageHandling;
@@ -421,6 +422,12 @@ public static class FunctionsHelper
             return;
         }
 
+        if (CropFunctions.IsCropping)
+        {
+            CropFunctions.CloseCropControl(Vm);
+            return;
+        }
+
         if (Navigation.Slideshow.IsRunning)
         {
             Navigation.Slideshow.StopSlideshow(Vm);
@@ -746,9 +753,9 @@ public static class FunctionsHelper
         return Task.CompletedTask;
     }
 
-    public static Task Crop()
+    public static async Task Crop()
     {
-        return Task.CompletedTask;
+        await Dispatcher.UIThread.InvokeAsync(() => CropFunctions.Init(Vm));
     }
 
     public static Task Flip()

+ 67 - 0
src/PicView.Avalonia/ViewModels/ImageCropperViewModel.cs

@@ -0,0 +1,67 @@
+using Avalonia;
+using Avalonia.Media.Imaging;
+using ReactiveUI;
+
+namespace PicView.Avalonia.ViewModels;
+
+public class ImageCropperViewModel(Bitmap bitmap) : ViewModelBase
+{
+    public Bitmap Bitmap
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    } = bitmap;
+
+    public double SelectionX
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+
+    public double SelectionY
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+
+    public double SelectionWidth
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    } = 100;
+
+    public double SelectionHeight
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    } = 100;
+    
+    public double ImageWidth
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    public double ImageHeight
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    
+    public double BottomOverlayHeight
+    {
+        get => ImageHeight - (SelectionY + SelectionHeight);
+    }
+
+    public double RightOverlayWidth
+    {
+        get => ImageWidth - (SelectionX + SelectionWidth);
+    }
+
+    // Call this method when the user completes the selection
+    public CroppedBitmap GetCroppedBitmap()
+    {
+        var sourceRect = new PixelRect((int)SelectionX, (int)SelectionY, (int)SelectionWidth, (int)SelectionHeight);
+        var croppedBitmap = new CroppedBitmap(Bitmap, sourceRect);
+        return croppedBitmap;
+    }
+}

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

@@ -46,6 +46,7 @@ public partial class MainView : UserControl
             }
             HideInterfaceLogic.AddHoverButtonEvents(AltButtonsPanel, vm);
             PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
+            
         };
     }
 

+ 35 - 0
src/PicView.Avalonia/Views/UC/CropControl.axaml

@@ -0,0 +1,35 @@
+<UserControl
+    d:DesignHeight="450"
+    d:DesignWidth="800"
+    mc:Ignorable="d"
+    x:Class="PicView.Avalonia.Views.UC.CropControl"
+    x:DataType="viewModels:ImageCropperViewModel"
+    xmlns="https://github.com/avaloniaui"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:viewModels="clr-namespace:PicView.Avalonia.ViewModels"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Panel>
+        <!--  Image control to display the bitmap  -->
+        <Image Source="{Binding Bitmap}" x:Name="ImageControl" />
+
+
+
+        <Canvas x:Name="RootCanvas">
+            <!--  Main Rectangle  -->
+            <Border
+                Background="Transparent"
+                BorderBrush="{DynamicResource MainBorderColor}"
+                BorderThickness="1"
+                Height="{CompiledBinding SelectionHeight}"
+                Width="{CompiledBinding SelectionWidth}"
+                x:Name="MainRectangle" />
+
+            <!--  Surrounding rectangles  -->
+            <Rectangle Fill="#80000000" x:Name="TopRectangle" />
+            <Rectangle Fill="#80000000" x:Name="BottomRectangle" />
+            <Rectangle Fill="#80000000" x:Name="LeftRectangle" />
+            <Rectangle Fill="#80000000" x:Name="RightRectangle" />
+        </Canvas>
+    </Panel>
+</UserControl>

+ 192 - 0
src/PicView.Avalonia/Views/UC/CropControl.axaml.cs

@@ -0,0 +1,192 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using PicView.Avalonia.ViewModels;
+
+namespace PicView.Avalonia.Views.UC;
+
+public partial class CropControl : UserControl
+{
+    private Point _dragStart;
+    private bool _isDragging;
+    private Rect _originalRect;
+
+    public CropControl()
+    {
+        InitializeComponent();
+        Loaded += delegate
+        {
+            InitializeLayout();
+            MainRectangle.PointerPressed += OnPointerPressed;
+            MainRectangle.PointerReleased += OnPointerReleased;
+            MainRectangle.PointerMoved += OnPointerMoved;
+        };
+    }
+
+    private void InitializeLayout()
+    {
+        if (DataContext is not ImageCropperViewModel vm)
+        {
+            return;
+        }
+
+        // Ensure image dimensions are valid before proceeding
+        if (vm.ImageWidth <= 0 || vm.ImageHeight <= 0)
+        {
+            return;
+        }
+
+        // Set initial width and height for the crop rectangle
+        vm.SelectionWidth = 200;
+        vm.SelectionHeight = 200;
+
+        // Calculate centered position
+        vm.SelectionX = (vm.ImageWidth - vm.SelectionWidth) / 2;
+        vm.SelectionY = (vm.ImageHeight - vm.SelectionHeight) / 2;
+
+        // Apply the calculated position to the MainRectangle
+        Canvas.SetLeft(MainRectangle, vm.SelectionX);
+        Canvas.SetTop(MainRectangle, vm.SelectionY);
+
+        try
+        {
+            UpdateSurroundingRectangles();
+        }
+        catch (Exception e)
+        {
+            #if DEBUG
+            Console.WriteLine(e);
+            #endif
+        }
+    }
+
+    private void UpdateSurroundingRectangles()
+    {
+        if (DataContext is not ImageCropperViewModel vm)
+        {
+            return;
+        }
+
+        // Converting to int fixes black border
+        var left = Convert.ToInt32(Canvas.GetLeft(MainRectangle));
+        var top = Convert.ToInt32(Canvas.GetTop(MainRectangle));
+        var right = Convert.ToInt32(left + vm.SelectionWidth);
+        var bottom= Convert.ToInt32(top + vm.SelectionHeight);
+
+        // Ensure the left and top values are not negative
+        left = left < 0 ? 0 : left;
+        top = top < 0 ? 0 : top;
+
+        // Calculate the positions and sizes for the surrounding rectangles
+        // Top Rectangle (above MainRectangle)
+        TopRectangle.Width = vm.ImageWidth;
+        TopRectangle.Height = top;
+        Canvas.SetTop(TopRectangle, 0);
+
+        // Bottom Rectangle (below MainRectangle)
+        BottomRectangle.Width = vm.ImageWidth;
+        BottomRectangle.Height = vm.ImageHeight - bottom;
+        Canvas.SetTop(BottomRectangle, bottom);
+
+        // Left Rectangle (left of MainRectangle)
+        LeftRectangle.Width = left;
+        LeftRectangle.Height = vm.SelectionHeight;
+        Canvas.SetLeft(LeftRectangle, 0);
+        Canvas.SetTop(LeftRectangle, top);
+
+        // Right Rectangle (right of MainRectangle)
+        RightRectangle.Width = vm.ImageWidth - right;
+        RightRectangle.Height = vm.SelectionHeight;
+        Canvas.SetLeft(RightRectangle, right);
+        Canvas.SetTop(RightRectangle, top);
+    }
+
+    private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+    {
+        if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        {
+            return;
+        }
+
+        if (DataContext is not ImageCropperViewModel vm)
+        {
+            return;
+        }
+
+        _dragStart = e.GetPosition(RootCanvas); // Make sure to get position relative to RootCanvas
+
+        // Get current left and top values; ensure they are initialized
+        var currentLeft = Canvas.GetLeft(MainRectangle);
+        var currentTop = Canvas.GetTop(MainRectangle);
+
+        // Set default values if NaN
+        if (double.IsNaN(currentLeft))
+        {
+            currentLeft = 0;
+        }
+
+        if (double.IsNaN(currentTop))
+        {
+            currentTop = 0;
+        }
+
+        _originalRect = new Rect(currentLeft, currentTop, vm.SelectionWidth, vm.SelectionHeight);
+        _isDragging = true;
+    }
+
+    private void OnPointerMoved(object? sender, PointerEventArgs e)
+    {
+        if (DataContext is not ImageCropperViewModel vm)
+        {
+            return;
+        }
+
+        if (!_isDragging)
+        {
+            return;
+        }
+
+        var currentPos = e.GetPosition(RootCanvas); // Ensure it's relative to RootCanvas
+        var delta = currentPos - _dragStart;
+
+        // Calculate new left and top positions, ensure _originalRect is valid
+        var newLeft = _originalRect.X + delta.X;
+        var newTop = _originalRect.Y + delta.Y;
+
+        // Clamp the newLeft and newTop values to keep the rectangle within bounds
+        newLeft = Math.Max(0, Math.Min(vm.ImageWidth - vm.SelectionWidth, newLeft));
+        newTop = Math.Max(0, Math.Min(vm.ImageHeight - vm.SelectionHeight, newTop));
+
+        // Only proceed if new positions are valid (i.e., not NaN)
+        if (double.IsNaN(newLeft) || double.IsNaN(newTop))
+        {
+            return;
+        }
+
+        // Update the main rectangle's position
+        Canvas.SetLeft(MainRectangle, newLeft);
+        Canvas.SetTop(MainRectangle, newTop);
+
+        // Update view model values
+        vm.SelectionX = newLeft;
+        vm.SelectionY = newTop;
+
+        // Update the surrounding rectangles to fill the space
+        try
+        {
+            UpdateSurroundingRectangles();
+        }
+        catch (Exception exception)
+        {
+#if DEBUG
+            Console.WriteLine(exception);
+#endif
+        }
+    }
+
+
+    private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+    {
+        _isDragging = false;
+    }
+}

+ 3 - 1
src/PicView.Avalonia/Views/UC/Menus/ImageMenu.axaml

@@ -232,10 +232,12 @@
                     Canvas.Left="160"
                     Canvas.Top="53"
                     Classes="ButtonBorder altHover"
+                    Command="{CompiledBinding CropCommand}"
                     Height="46"
                     IsEnabled="False"
                     ToolTip.Tip="{CompiledBinding Crop,
-                                                  Mode=OneWay}">
+                                                  Mode=OneWay}"
+                    x:Name="CropButton">
                     <StackPanel Orientation="Horizontal">
                         <Path
                             Data="{StaticResource CropGeometry}"

+ 12 - 0
src/PicView.Avalonia/Views/UC/Menus/ImageMenu.axaml.cs

@@ -1,6 +1,7 @@
 using System.Reactive.Linq;
 using Avalonia.Input;
 using Avalonia.Media;
+using PicView.Avalonia.Crop;
 using PicView.Avalonia.CustomControls;
 using PicView.Avalonia.Navigation;
 using PicView.Avalonia.ViewModels;
@@ -29,9 +30,20 @@ public partial class ImageMenu  : AnimatedMenu
             GoToPicBox.KeyDown += async (_, e) => await GoToPicBox_OnKeyDown(e);
             this.WhenAnyValue(x => x.IsVisible)
                 .Where(isVisible => !isVisible).Subscribe(_ => SlideShowButton.Flyout.Hide());
+            this.WhenAnyValue(x => x.IsOpen).Subscribe(_ => DetermineIfCropShouldBeEnabled());
         };
     }
 
+    private void DetermineIfCropShouldBeEnabled()
+    {
+        if (DataContext is not MainViewModel vm)
+        {
+            return;
+        }
+
+        CropButton.IsEnabled = CropFunctions.DetermineIfShouldBeEnabled(vm);
+    }
+
     private async Task GoToPicBox_OnKeyDown(KeyEventArgs e)
     {
         if (e.Key == Key.Enter)

+ 0 - 1
src/PicView.Avalonia/WindowBehavior/WindowResizing.cs

@@ -239,7 +239,6 @@ public static class WindowResizing
         else
         {
             vm.GalleryWidth = double.NaN;
-            ;
         }
     }