Refactor ImageFormBase with a UnaryImageFormBase subclass

This commit is contained in:
Ben Olden-Cooligan 2024-03-08 18:08:54 -08:00
parent f37d2fd348
commit 3bcd97a97a
10 changed files with 173 additions and 150 deletions

View File

@ -3,7 +3,7 @@ using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public class BlackWhiteForm : ImageFormBase
public class BlackWhiteForm : UnaryImageFormBase
{
private readonly SliderWithTextBox _thresholdSlider = new();

View File

@ -3,7 +3,7 @@ using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public class BrightContForm : ImageFormBase
public class BrightContForm : UnaryImageFormBase
{
private readonly SliderWithTextBox _brightnessSlider = new();
private readonly SliderWithTextBox _contrastSlider = new();

View File

@ -23,8 +23,6 @@ public class CombineForm : ImageFormBase
_scanningContext = scanningContext;
Icon = new Icon(1f, Icons.combine.ToEtoImage());
Title = UiStrings.Combine;
CanApplyToAllSelected = false;
ShowRevertButton = false;
}
private UiImage Image1 { get; set; } = null!;
@ -93,10 +91,10 @@ public class CombineForm : ImageFormBase
// Otherwise, we look for the previous image in the list, which should be considered the first image, and then
// this image is the second.
var nextImage = SelectedImages?.ElementAtOrDefault(1) ??
_imageList.Images.ElementAtOrDefault(_imageList.Images.IndexOf(Image) + 1);
ImageList.Images.ElementAtOrDefault(ImageList.Images.IndexOf(Image) + 1);
Image1 = nextImage != null
? Image
: _imageList.Images.ElementAtOrDefault(_imageList.Images.IndexOf(Image) - 1) ??
: ImageList.Images.ElementAtOrDefault(ImageList.Images.IndexOf(Image) - 1) ??
throw new InvalidOperationException("No image to combine with");
Image2 = nextImage ?? Image;
@ -152,13 +150,13 @@ public class CombineForm : ImageFormBase
using var combinedImage = CombineImages(renderedImage1, renderedImage2);
// TODO: Use working images for thumbnail?
var thumbnail = combinedImage.Clone().PerformTransform(new ThumbnailTransform(_thumbnailController.RenderSize));
var thumbnail = combinedImage.Clone().PerformTransform(new ThumbnailTransform(ThumbnailController.RenderSize));
var ppd = new PostProcessingData { Thumbnail = thumbnail, ThumbnailTransformState = TransformState.Empty };
var processedImage = _scanningContext.CreateProcessedImage(combinedImage).WithPostProcessingData(ppd, true);
_imageList.Mutate(new ListMutation<UiImage>.InsertAfter(new UiImage(processedImage), Image));
ImageList.Mutate(new ListMutation<UiImage>.InsertAfter(new UiImage(processedImage), Image));
// TODO: Maybe have a checkbox to keep the original images?
_imageList.Mutate(new ListMutation<UiImage>.DeleteSelected(), ListSelection.Of(Image1, Image2));
ImageList.Mutate(new ListMutation<UiImage>.DeleteSelected(), ListSelection.Of(Image1, Image2));
}
protected override void OnClosed(EventArgs e)

View File

@ -3,7 +3,7 @@ using Eto.Forms;
namespace NAPS2.EtoForms.Ui;
public class CropForm : ImageFormBase
public class CropForm : UnaryImageFormBase
{
private static CropTransform? _lastTransform;
@ -49,8 +49,9 @@ public class CropForm : ImageFormBase
private int HandleLength =>
(int) Math.Max(Math.Round(Math.Min(_overlayH, _overlayW) * HANDLE_LENGTH_RATIO), HANDLE_MIN_LENGTH);
protected override void InitTransform()
protected override void OnPreLoad(EventArgs e)
{
base.OnPreLoad(e);
if (_lastTransform != null && _lastTransform.OriginalWidth == ImageWidth &&
_lastTransform.OriginalHeight == ImageHeight)
{
@ -65,12 +66,13 @@ public class CropForm : ImageFormBase
}
}
protected override void TransformSaved()
protected override void Apply()
{
base.Apply();
_lastTransform = (CropTransform) Transforms.Single();
}
protected override void ResetTransform()
protected override void Revert()
{
_cropT = _cropL = _cropB = _cropR = _realT = _realL = _realB = _realR = 0;
Overlay.Invalidate();

View File

@ -3,7 +3,7 @@ using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public class HueSatForm : ImageFormBase
public class HueSatForm : UnaryImageFormBase
{
private readonly SliderWithTextBox _hueSlider = new();
private readonly SliderWithTextBox _saturationSlider = new();

View File

@ -1,49 +1,49 @@
using Eto.Drawing;
using Eto.Forms;
using NAPS2.EtoForms.Layout;
using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public abstract class ImageFormBase : EtoDialogBase
{
protected readonly UiImageList _imageList;
protected readonly ThumbnailController _thumbnailController;
private readonly ImageView _imageView = new();
private readonly CheckBox _applyToSelected = new();
private readonly Button _revert = C.Button(UiStrings.Revert);
private readonly RefreshThrottle _renderThrottle;
// Image bounds in the coordinate space of the overlay control
protected float _overlayT, _overlayL, _overlayR, _overlayB, _overlayW, _overlayH;
public ImageFormBase(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController) :
protected ImageFormBase(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController) :
base(config)
{
_imageList = imageList;
_thumbnailController = thumbnailController;
_revert.Click += Revert;
ImageList = imageList;
ThumbnailController = thumbnailController;
_renderThrottle = new RefreshThrottle(RenderImage);
Overlay.Paint += PaintOverlay;
Overlay.SizeChanged += (_, _) => UpdateImageCoords();
FormStateController.DefaultExtraLayoutSize = new Size(400, 400);
}
public UiImage Image { get; set; } = null!;
public List<UiImage>? SelectedImages { get; set; }
protected UiImageList ImageList { get; }
protected ThumbnailController ThumbnailController { get; }
protected int ImageHeight { get; set; }
protected int ImageWidth { get; set; }
protected IMemoryImage? DisplayImage { get; set; }
protected Drawable Overlay { get; } = new();
protected int OverlayBorderSize { get; set; }
protected override void BuildLayout()
{
foreach (var slider in Sliders)
{
slider.ValueChanged += UpdatePreviewBox;
}
LayoutController.Content = L.Column(
Overlay.Scale(),
CreateControls(),
SelectedImages is { Count: > 1 } && CanApplyToAllSelected ? _applyToSelected : C.None(),
L.Row(
ShowRevertButton ? _revert : C.None(),
CreateExtraButtons(),
C.Filler(),
L.OkCancel(
C.OkButton(this, beforeClose: Apply),
@ -52,15 +52,26 @@ public abstract class ImageFormBase : EtoDialogBase
);
}
protected int ImageHeight { get; set; }
protected int ImageWidth { get; set; }
protected override void OnPreLoad(EventArgs e)
{
base.OnPreLoad(e);
InitDisplayImage();
ImageWidth = DisplayImage!.Width;
ImageHeight = DisplayImage.Height;
}
protected IMemoryImage? WorkingImage { get; set; }
protected IMemoryImage? DisplayImage { get; set; }
protected Drawable Overlay { get; } = new();
protected int OverlayBorderSize { get; set; }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
UpdatePreviewBox();
}
protected SliderWithTextBox[] Sliders { get; set; } = Array.Empty<SliderWithTextBox>();
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
DisplayImage?.Dispose();
_imageView.Image?.Dispose();
}
protected override void OnSizeChanged(EventArgs e)
{
@ -124,79 +135,20 @@ public abstract class ImageFormBase : EtoDialogBase
});
}
protected virtual LayoutElement CreateControls()
protected abstract LayoutElement CreateControls();
protected virtual LayoutElement CreateExtraButtons() => C.None();
protected abstract IMemoryImage RenderPreview();
protected abstract void InitDisplayImage();
protected abstract void Apply();
protected void UpdatePreviewBox()
{
return L.Column(Sliders.Select(x => (LayoutElement) x).ToArray());
}
public UiImage Image { get; set; } = null!;
public List<UiImage>? SelectedImages { get; set; }
protected bool CanScaleWorkingImage { get; set; } = true;
protected bool CanApplyToAllSelected { get; set; } = true;
protected bool ShowRevertButton { get; set; } = true;
protected virtual List<Transform> Transforms => throw new NotImplementedException();
private bool TransformMultiple => SelectedImages != null && _applyToSelected.IsChecked();
private List<UiImage> ImagesToTransform => TransformMultiple ? SelectedImages! : [Image];
protected virtual IMemoryImage RenderPreview()
{
var result = WorkingImage!.Clone();
return result.PerformAllTransforms(Transforms);
}
protected virtual void InitTransform()
{
}
protected virtual void ResetTransform()
{
foreach (var slider in Sliders)
{
slider.IntValue = 0;
}
}
protected virtual void TransformSaved()
{
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
_applyToSelected.Text = string.Format(UiStrings.ApplyToSelected, SelectedImages?.Count);
InitDisplayImage();
ImageWidth = DisplayImage!.Width;
ImageHeight = DisplayImage.Height;
InitTransform();
UpdatePreviewBox();
}
protected virtual void InitDisplayImage()
{
using var imageToRender = Image.GetClonedImage();
WorkingImage = imageToRender.Render();
if (CanScaleWorkingImage)
{
// Scale down the image to the screen size for better efficiency without losing much fidelity
var workingArea = GetScreenWorkingArea();
var widthRatio = WorkingImage.Width / workingArea.Width;
var heightRatio = WorkingImage.Height / workingArea.Height;
if (widthRatio > 1 || heightRatio > 1)
{
WorkingImage = WorkingImage.PerformTransform(new ScaleTransform(1 / Math.Max(widthRatio, heightRatio)));
}
}
DisplayImage = WorkingImage.Clone();
Overlay.Invalidate();
_renderThrottle.RunAction();
}
protected RectangleF GetScreenWorkingArea()
@ -217,41 +169,4 @@ public abstract class ImageFormBase : EtoDialogBase
// Assume 1080p screen by default
return new RectangleF(0, 0, 1920, 1080);
}
protected void UpdatePreviewBox()
{
Overlay.Invalidate();
_renderThrottle.RunAction();
}
protected virtual void Apply()
{
IMemoryImage? firstImageThumb = null;
if (WorkingImage != null)
{
// Optimize thumbnail rendering for the first (or only) image since we already have it loaded into memory
var transformed = WorkingImage.Clone().PerformAllTransforms(Transforms);
firstImageThumb =
transformed.PerformTransform(new ThumbnailTransform(_thumbnailController.RenderSize));
}
var mutation = new ImageListMutation.AddTransforms(
Transforms.ToList(),
new Dictionary<UiImage, IMemoryImage?> { [Image] = firstImageThumb });
_imageList.Mutate(mutation, ListSelection.From(ImagesToTransform));
TransformSaved();
}
private void Revert(object? sender, EventArgs e)
{
ResetTransform();
UpdatePreviewBox();
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
WorkingImage?.Dispose();
DisplayImage?.Dispose();
_imageView.Image?.Dispose();
}
}

View File

@ -4,7 +4,7 @@ using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public class RotateForm : ImageFormBase
public class RotateForm : UnaryImageFormBase
{
private const int MIN_LINE_DISTANCE = 50;

View File

@ -3,7 +3,7 @@ using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public class SharpenForm : ImageFormBase
public class SharpenForm : UnaryImageFormBase
{
private readonly SliderWithTextBox _sharpenSlider = new();

View File

@ -2,7 +2,7 @@ using Eto.Drawing;
namespace NAPS2.EtoForms.Ui;
public class SplitForm : ImageFormBase
public class SplitForm : UnaryImageFormBase
{
public SplitForm(Naps2Config config, UiImageList imageList, ThumbnailController thumbnailController) :
base(config, imageList, thumbnailController)

View File

@ -0,0 +1,108 @@
using Eto.Forms;
using NAPS2.EtoForms.Layout;
using NAPS2.EtoForms.Widgets;
namespace NAPS2.EtoForms.Ui;
public abstract class UnaryImageFormBase(
Naps2Config config,
UiImageList imageList,
ThumbnailController thumbnailController)
: ImageFormBase(config, imageList, thumbnailController)
{
private readonly CheckBox _applyToSelected = new();
private readonly Button _revert = C.Button(UiStrings.Revert);
protected IMemoryImage? WorkingImage { get; set; }
protected SliderWithTextBox[] Sliders { get; set; } = Array.Empty<SliderWithTextBox>();
protected bool CanScaleWorkingImage { get; set; } = true;
protected abstract List<Transform> Transforms { get; }
protected override void OnPreLoad(EventArgs e)
{
_applyToSelected.Text = string.Format(UiStrings.ApplyToSelected, SelectedImages?.Count);
_revert.Click += (_, _) =>
{
Revert();
UpdatePreviewBox();
};
foreach (var slider in Sliders)
{
slider.ValueChanged += UpdatePreviewBox;
}
base.OnPreLoad(e);
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
WorkingImage?.Dispose();
}
protected override LayoutElement CreateControls()
{
return L.Column([
..Sliders.Select(x => (LayoutElement) x),
SelectedImages is { Count: > 1 } ? _applyToSelected : C.None()
]);
}
protected override LayoutElement CreateExtraButtons() => _revert;
private List<UiImage> ImagesToTransform => SelectedImages != null && _applyToSelected.IsChecked()
? SelectedImages!
: [Image];
protected override IMemoryImage RenderPreview()
{
var result = WorkingImage!.Clone();
return result.PerformAllTransforms(Transforms);
}
protected override void InitDisplayImage()
{
using var imageToRender = Image.GetClonedImage();
WorkingImage = imageToRender.Render();
if (CanScaleWorkingImage)
{
// Scale down the image to the screen size for better efficiency without losing much fidelity
var workingArea = GetScreenWorkingArea();
var widthRatio = WorkingImage.Width / workingArea.Width;
var heightRatio = WorkingImage.Height / workingArea.Height;
if (widthRatio > 1 || heightRatio > 1)
{
WorkingImage = WorkingImage.PerformTransform(new ScaleTransform(1 / Math.Max(widthRatio, heightRatio)));
}
}
DisplayImage = WorkingImage.Clone();
}
protected override void Apply()
{
IMemoryImage? firstImageThumb = null;
if (WorkingImage != null)
{
// Optimize thumbnail rendering for the first (or only) image since we already have it loaded into memory
var transformed = WorkingImage.Clone().PerformAllTransforms(Transforms);
firstImageThumb =
transformed.PerformTransform(new ThumbnailTransform(ThumbnailController.RenderSize));
}
var mutation = new ImageListMutation.AddTransforms(
Transforms.ToList(),
new Dictionary<UiImage, IMemoryImage?> { [Image] = firstImageThumb });
ImageList.Mutate(mutation, ListSelection.From(ImagesToTransform));
}
protected virtual void Revert()
{
foreach (var slider in Sliders)
{
slider.IntValue = 0;
}
}
}