Mac: Listview and thumbnail rendering mostly working

Still some minor issues around selection and multi-select, plus a few UI tweaks to go
This commit is contained in:
Ben Olden-Cooligan 2022-08-21 18:03:12 -07:00
parent 445c476689
commit b1e26712d0
14 changed files with 276 additions and 61 deletions

View File

@ -80,23 +80,23 @@ public class MacImageTransformer : AbstractImageTransformer<MacImage>
ImagePixelFormat.ARGB32 => ImagePixelFormat.ARGB32,
_ => throw new ArgumentException("Unsupported pixel format")
};
var newImage = (MacImage) ImageContext.Create(transform.Size, transform.Size, pixelFormat);
var (left, top, width, height) = transform.GetDrawRect(image.Width, image.Height);
var newImage = (MacImage) ImageContext.Create(width, height, pixelFormat);
newImage.SetResolution(
image.HorizontalResolution * image.Width / width,
image.VerticalResolution * image.Height / height);
using CGBitmapContext c = GetCgBitmapContext(newImage);
CGRect rect = new CGRect(left, top, width, height);
CGRect rect = new CGRect(0, 0, width, height);
c.DrawImage(rect, image._imageRep.AsCGImage(ref rect, null, null));
CGRect strokeRect = new CGRect(left + 0.5, top + 0.5, width - 1, height - 1);
#if MONOMAC
c.SetRGBStrokeColor(
#else
c.SetStrokeColor(
#endif
0.ToNFloat(), 0.ToNFloat(), 0.ToNFloat(), 255.ToNFloat());
c.StrokeRect(strokeRect);
// CGRect strokeRect = new CGRect(left + 0.5, top + 0.5, width - 1, height - 1);
// #if MONOMAC
// c.SetRGBStrokeColor(
// #else
// c.SetStrokeColor(
// #endif
// 0.ToNFloat(), 0.ToNFloat(), 0.ToNFloat(), 255.ToNFloat());
// c.StrokeRect(strokeRect);
return newImage;
}

View File

@ -1,11 +1,16 @@
using CoreAnimation;
using Eto.Drawing;
using Eto.Forms;
using Eto.Mac;
namespace NAPS2.EtoForms.Mac;
public class MacListView<T> : IListView<T> where T : notnull
public class MacListView<T> : NSCollectionViewDelegateFlowLayout, IListView<T> where T : notnull
{
private readonly NSCollectionView _view = new();
private readonly ListViewBehavior<T> _behavior;
private readonly NSCollectionView _view = new();
private readonly NSCollectionViewFlowLayout _layout;
private readonly DataSource<T> _dataSource;
private ListSelection<T> _selection = ListSelection.Empty<T>();
private bool _refreshing;
@ -13,12 +18,24 @@ public class MacListView<T> : IListView<T> where T : notnull
public MacListView(ListViewBehavior<T> behavior)
{
_behavior = behavior;
_layout = new NSCollectionViewFlowLayout
{
SectionInset = new NSEdgeInsets(20, 20, 20, 20),
MinimumInteritemSpacing = 15,
MinimumLineSpacing = 15
};
_dataSource = new DataSource<T>(this, _behavior);
_view.DataSource = _dataSource;
_view.Delegate = this;
_view.CollectionViewLayout = _layout;
_view.Selectable = true;
_view.AllowsMultipleSelection = true;
}
public int ImageSize
{
get => 0;
set { }
get => (int) _layout.ItemSize.Width;
set => _layout.ItemSize = new CGSize(value, value);
}
// TODO: Properties here vs on behavior?
@ -45,15 +62,32 @@ public class MacListView<T> : IListView<T> where T : notnull
public void SetItems(IEnumerable<T> items)
{
_dataSource.Items.Clear();
_dataSource.Items.AddRange(items);
_view.ReloadData();
}
// TODO: Do we need this method? Clean up the name/doc at least
public void RegenerateImages()
{
_view.ReloadData();
}
public void ApplyDiffs(ListViewDiffs<T> diffs)
{
foreach (var op in diffs.AppendOperations)
{
_dataSource.Items.Add(op.Item);
}
foreach (var op in diffs.ReplaceOperations)
{
_dataSource.Items[op.Index] = op.Item;
}
foreach (var op in diffs.TrimOperations)
{
_dataSource.Items.RemoveRange(_dataSource.Items.Count - op.Count, op.Count);
}
_view.ReloadData();
}
public ListSelection<T> Selection
@ -66,7 +100,113 @@ public class MacListView<T> : IListView<T> where T : notnull
return;
}
_selection = value ?? throw new ArgumentNullException(nameof(value));
UpdateViewSelection();
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
private void UpdateViewSelection()
{
if (!_refreshing)
{
_refreshing = true;
_view.SelectionIndexes =
NSIndexSet.FromArray(_selection.ToSelectedIndices(_dataSource.Items).ToArray());
_refreshing = false;
}
}
public override void ItemsSelected(NSCollectionView collectionView, NSSet indexPaths)
{
_selection = ListSelection.FromSelectedIndices(_dataSource.Items,
indexPaths.Cast<NSIndexPath>().Select(x => (int) x.Item));
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
public override void ItemsChanged(NSCollectionView collectionView, NSSet indexPaths, NSCollectionViewItemHighlightState highlightState)
{
UpdateViewSelection();
}
public override CGSize SizeForItem(NSCollectionView collectionView, NSCollectionViewLayout collectionViewLayout, NSIndexPath indexPath)
{
var item = _dataSource.Items[(int) indexPath.Item];
var size = _behavior.GetImage(item, ImageSize).Size;
var max = (double) Math.Max(size.Width, size.Height);
return new CGSize(size.Width * ImageSize / max, size.Height * ImageSize / max);
}
}
public class DataSource<T> : NSCollectionViewDataSource where T : notnull
{
private readonly IListView<T> _listView;
private readonly ListViewBehavior<T> _behavior;
public DataSource(IListView<T> listView, ListViewBehavior<T> behavior)
{
_listView = listView;
_behavior = behavior;
}
public List<T> Items { get; } = new();
public override nint GetNumberofItems(NSCollectionView collectionView, nint section)
{
return Items.Count;
}
public override NSCollectionViewItem GetItem(NSCollectionView collectionView, NSIndexPath indexPath)
{
var i = (int) indexPath.Item;
return new Cell
{
CellImage = _behavior.GetImage(Items[i], _listView.ImageSize)
};
}
}
public class Cell : NSCollectionViewItem
{
private bool _selected;
public Image CellImage { get; set; }
public override void LoadView()
{
var imageView = new NSImageView
{
Image = CellImage.ToNS()
};
Console.WriteLine("Setting up imageview layer " + CellImage.Width + " " + CellImage.Height);
imageView.WantsLayer = true;
imageView.CanDrawSubviewsIntoLayer = true;
imageView.Frame = new CGRect(0, 0, CellImage.Width, CellImage.Height);
var layer = new CALayer();
layer.Frame = new CGRect(0, 0, CellImage.Width, CellImage.Height);
layer.CornerRadius = 4;
layer.MasksToBounds = true;
layer.Contents = CellImage.ToCG();
layer.ZPosition = 1000;
imageView.Layer = layer;
View = imageView;
UpdateViewForSelectedState();
}
public override bool Selected
{
get => _selected;
set
{
_selected = value;
UpdateViewForSelectedState();
}
}
private void UpdateViewForSelectedState()
{
Console.WriteLine("UpdateViewForSelectedState " + Selected);
var layer = ((NSImageView) View).Layer;
layer.BorderWidth = Selected ? 4 : 1;
layer.BorderColor = Selected ? NSColor.SelectedContentBackground.ToCG() : NSColor.Black.ToCG();
}
}

View File

@ -1,3 +1,4 @@
using System.Threading;
using Eto.Drawing;
using Eto.Forms;
using Eto.Mac;
@ -29,10 +30,21 @@ public class MacDesktopForm : DesktopForm
{
}
protected override void SetContent(Control content)
{
var scrollView = new NSScrollView();
scrollView.DocumentView = content.ToNative();
Content = scrollView.ToEto();
}
protected override void OnLoad(EventArgs e)
{
// TODO: What's the best place to initialize this? It needs to happen from the UI event loop.
Invoker.Current = new SyncContextInvoker(SynchronizationContext.Current);
base.OnLoad(e);
ClientSize = new Size(1000, 600);
// TODO: Initialize everything that needs to be initialized where it's best
ResizeThumbnails(Config.ThumbnailSize());
}
protected override void CreateToolbarsAndMenus()
@ -126,13 +138,15 @@ public class MacDesktopForm : DesktopForm
window.ToolbarStyle = NSWindowToolbarStyle.Unified;
// TODO: Subtitle based on active profile?
window.Subtitle = "Not Another PDF Scanner 2";
// TODO: Do we want full size content?
window.StyleMask |= NSWindowStyle.FullSizeContentView;
window.StyleMask |= NSWindowStyle.UnifiedTitleAndToolbar;
}
private void ZoomUpdated(NSSlider sender)
{
Config.User.Set(c => c.ThumbnailSize, sender.IntValue);
var size = ThumbnailSizes.CurveToSize(sender.DoubleValue);
ResizeThumbnails(size);
}
public class ToolbarDelegate : NSToolbarDelegate
@ -179,9 +193,9 @@ public class MacDesktopForm : DesktopForm
{
View = new NSSlider
{
MinValue = ThumbnailSizes.MIN_SIZE,
MaxValue = ThumbnailSizes.MAX_SIZE,
IntValue = _form.Config.ThumbnailSize()
MinValue = 0,
MaxValue = 1,
DoubleValue = ThumbnailSizes.SizeToCurve(_form.Config.ThumbnailSize())
}.WithAction(_form.ZoomUpdated),
MaxSize = new CGSize(64, 999)
},

View File

@ -70,6 +70,18 @@ public class WinFormsDesktopForm : DesktopForm
_toolbarFormatter.RelayoutToolbar(_toolStrip);
}
protected override void SetThumbnailSpacing(int thumbnailSize)
{
_listView.NativeControl.Padding = new Padding(0, 20, 0, 0);
const int MIN_PADDING = 6;
const int MAX_PADDING = 66;
// Linearly scale the padding with the thumbnail size
int padding = MIN_PADDING + (MAX_PADDING - MIN_PADDING) * (thumbnailSize - ThumbnailSizes.MIN_SIZE) /
(ThumbnailSizes.MAX_SIZE - ThumbnailSizes.MIN_SIZE);
int spacing = thumbnailSize + padding * 2;
WinFormsHacks.SetListSpacing(_listView.NativeControl, spacing, spacing);
}
protected override void CreateToolbarButton(Command command)
{
var item = new wf.ToolStripButton

View File

@ -32,6 +32,8 @@ public class WinFormsModule : NinjectModule
Bind<IDesktopScanController>().To<DesktopScanController>();
Bind<IDesktopSubFormController>().To<DesktopSubFormController>();
Bind<DesktopFormProvider>().ToSelf().InSingletonScope();
Bind<ImageContext>().To<GdiImageContext>();
Bind<GdiImageContext>().ToSelf();
Bind<DesktopForm>().To<WinFormsDesktopForm>();

View File

@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Threading;
using Eto.Forms;
using NAPS2.ImportExport.Images;
using NAPS2.WinForms;
@ -72,7 +73,7 @@ public abstract class DesktopForm : EtoFormBase
private readonly ListProvider<Command> _scanMenuCommands = new();
private readonly ListProvider<Command> _languageMenuCommands = new();
private IListView<UiImage> _listView;
protected IListView<UiImage> _listView;
private ImageListSyncer? _imageListSyncer;
// private LayoutManager _layoutManager;
@ -364,6 +365,10 @@ public abstract class DesktopForm : EtoFormBase
// imageList.ImagesUpdated += (_, _) => Invoker.Current.SafeInvoke(UpdateToolbar);
_profileManager.ProfilesUpdated += (_, _) => UpdateScanButton();
_desktopFormProvider.DesktopForm = this;
// TODO: Initialization needs work
_imageListSyncer = new ImageListSyncer(_imageList, _listView.ApplyDiffs, SynchronizationContext.Current!);
_desktopController.Initialize();
}
protected virtual void CreateToolbarsAndMenus()
@ -874,48 +879,35 @@ public abstract class DesktopForm : EtoFormBase
// int thumbnailSize = Config.ThumbnailSize();
// thumbnailSize =
// (int) ThumbnailSizes.StepNumberToSize(ThumbnailSizes.SizeToStepNumber(thumbnailSize) + step);
// thumbnailSize = ThumbnailSizes.Validate(thumbnailSize);
// Config.User.Set(c => c.ThumbnailSize, thumbnailSize);
// ResizeThumbnails(thumbnailSize);
// }
//
// private void ResizeThumbnails(int thumbnailSize)
// {
// if (!_imageList.Images.Any())
// {
// // Can't show visual feedback so don't do anything
// // TODO: This is wrong?
// return;
// }
// if (_listView.ImageSize == thumbnailSize)
// {
// // Same size so no resizing needed
// return;
// }
//
// // Adjust the visible thumbnail display with the new size
// _listView.ImageSize = thumbnailSize;
// _listView.RegenerateImages();
//
// SetThumbnailSpacing(thumbnailSize);
// UpdateToolbar(); // TODO: Do we need this?
//
// // Render high-quality thumbnails at the new size in a background task
// // The existing (poorly scaled) thumbnails are used in the meantime
// _thumbnailRenderQueue.SetThumbnailSize(thumbnailSize);
// }
//
// private void SetThumbnailSpacing(int thumbnailSize)
// {
// _listView.NativeControl.Padding = new Padding(0, 20, 0, 0);
// const int MIN_PADDING = 6;
// const int MAX_PADDING = 66;
// // Linearly scale the padding with the thumbnail size
// int padding = MIN_PADDING + (MAX_PADDING - MIN_PADDING) * (thumbnailSize - ThumbnailSizes.MIN_SIZE) /
// (ThumbnailSizes.MAX_SIZE - ThumbnailSizes.MIN_SIZE);
// int spacing = thumbnailSize + padding * 2;
// WinFormsHacks.SetListSpacing(_listView.NativeControl, spacing, spacing);
// }
protected void ResizeThumbnails(int thumbnailSize)
{
thumbnailSize = ThumbnailSizes.Validate(thumbnailSize);
Config.User.Set(c => c.ThumbnailSize, thumbnailSize);
if (_listView.ImageSize == thumbnailSize)
{
// Same size so no resizing needed
return;
}
// Adjust the visible thumbnail display with the new size
_listView.ImageSize = thumbnailSize;
_listView.RegenerateImages();
SetThumbnailSpacing(thumbnailSize);
UpdateToolbar(); // TODO: Do we need this?
// Render high-quality thumbnails at the new size in a background task
// The existing (poorly scaled) thumbnails are used in the meantime
_thumbnailRenderQueue.SetThumbnailSize(thumbnailSize);
}
protected virtual void SetThumbnailSpacing(int thumbnailSize)
{
}
//
// private void btnZoomOut_Click(object sender, EventArgs e) => StepThumbnailSize(-1);
// private void btnZoomIn_Click(object sender, EventArgs e) => StepThumbnailSize(1);

View File

@ -74,7 +74,7 @@ public class ThumbnailRenderQueue : IDisposable
{
// TODO: Make this run as async?
// TODO: Verify WorkerFactory is not null? Or handle this better for tests?
bool useWorker = PlatformCompat.Runtime.UseWorker;
bool useWorker = PlatformCompat.System.RenderInWorker;
var worker = useWorker ? _scanningContext.WorkerFactory?.Create() : null;
var fallback = new ExpFallback(100, 60 * 1000);
while (true)
@ -174,7 +174,7 @@ public class ThumbnailRenderQueue : IDisposable
foreach (var img in listCopy)
{
var thumb = img.GetThumbnailClone();
if (thumb == null || thumb.Width != thumbnailSize || thumb.Height != thumbnailSize)
if (thumb == null || thumb.Width != thumbnailSize && thumb.Height != thumbnailSize)
{
return img;
}

View File

@ -31,6 +31,9 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>NAPS2.Lib.WinForms</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>NAPS2.Lib.Mac</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">

View File

@ -9,7 +9,7 @@ public static class ThumbnailSizes
public static int Validate(int inputSize)
{
return inputSize.Clamp(MIN_SIZE, MAX_SIZE);
}
}
public static double StepNumberToSize(double stepNumber)
{
@ -45,4 +45,18 @@ public static class ThumbnailSizes
}
return (size - 832) / 96 + 16;
}
public static int CurveToSize(double value)
{
value = value.Clamp(0, 1);
var curved = (Math.Exp(value) - 1) / (Math.E - 1);
return (int) Math.Round(MIN_SIZE + curved * (MAX_SIZE - MIN_SIZE));
}
public static double SizeToCurve(int size)
{
size = Validate(size);
var curved = (size - MIN_SIZE) / (double) (MAX_SIZE - MIN_SIZE);
return Math.Log(curved * (Math.E - 1) + 1);
}
}

View File

@ -14,6 +14,8 @@ public interface ISystemCompat
bool UseSystemTesseract { get; }
bool RenderInWorker { get; }
string? TesseractExecutablePath { get; }
string PdfiumLibraryPath { get; }

View File

@ -19,6 +19,8 @@ public class LinuxSystemCompat : ISystemCompat
public bool UseSystemTesseract => true;
public bool RenderInWorker => false;
public string? TesseractExecutablePath => null;
public string PdfiumLibraryPath => "_linux/libpdfium.so";

View File

@ -20,6 +20,8 @@ public class MacSystemCompat : ISystemCompat
public bool UseSystemTesseract => true;
public bool RenderInWorker => false;
public string? TesseractExecutablePath => null;
public string PdfiumLibraryPath =>

View File

@ -17,6 +17,8 @@ public abstract class WindowsSystemCompat : ISystemCompat
public bool UseSystemTesseract => false;
public bool RenderInWorker => true;
public abstract string? TesseractExecutablePath { get; }
public abstract string PdfiumLibraryPath { get; }

View File

@ -0,0 +1,30 @@
using System.Threading;
namespace NAPS2.Threading;
public class SyncContextInvoker : IInvoker
{
private readonly SynchronizationContext _current;
public SyncContextInvoker(SynchronizationContext current)
{
_current = current;
}
public void Invoke(Action action)
{
_current.Send(_ => action(), null);
}
public void SafeInvoke(Action action)
{
_current.Send(_ => action(), null);
}
public T InvokeGet<T>(Func<T> func)
{
T value = default!;
_current.Send(_ => value = func(), null);
return value;
}
}