diff --git a/NAPS2.App.Mac/.gitignore b/NAPS2.App.Mac/.gitignore
new file mode 100644
index 000000000..a8a3b16a8
--- /dev/null
+++ b/NAPS2.App.Mac/.gitignore
@@ -0,0 +1,31 @@
+Thumbs.db
+*.obj
+*.exe
+*.pdb
+*.user
+*.aps
+*.pch
+*.vspscc
+*_i.c
+*_p.c
+*.ncb
+*.suo
+*.sln.docstates
+*.tlb
+*.tlh
+*.bak
+*.cache
+*.ilk
+*.log
+[Bb]in
+[Dd]ebug*/
+*.lib
+*.sbr
+obj/
+[Rr]elease*/
+_ReSharper*/
+[Tt]est[Rr]esult*
+*.vssscc
+$tf*/
+publish/
+bin/
\ No newline at end of file
diff --git a/NAPS2.App.Mac/Icon.icns b/NAPS2.App.Mac/Icon.icns
new file mode 100644
index 000000000..5e8d6ff8c
Binary files /dev/null and b/NAPS2.App.Mac/Icon.icns differ
diff --git a/NAPS2.App.Mac/Info.plist b/NAPS2.App.Mac/Info.plist
new file mode 100644
index 000000000..8ca4a3add
--- /dev/null
+++ b/NAPS2.App.Mac/Info.plist
@@ -0,0 +1,21 @@
+
+
+
+
+ CFBundleName
+ NAPS2
+ CFBundleIdentifier
+ com.naps2.desktop
+ CFBundleShortVersionString
+ 1.0
+ LSMinimumSystemVersion
+ 10.15
+ CFBundleDevelopmentRegion
+ en
+ NSHumanReadableCopyright
+
+
+ CFBundleIconFile
+ Icon.icns
+
+
diff --git a/NAPS2.App.Mac/NAPS2.App.Mac.csproj b/NAPS2.App.Mac/NAPS2.App.Mac.csproj
new file mode 100644
index 000000000..01aff6fdc
--- /dev/null
+++ b/NAPS2.App.Mac/NAPS2.App.Mac.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net6
+ Exe
+ NAPS2
+ NAPS2
+ 7.0.1
+ ../NAPS2.Lib/Icons/favicon.ico
+
+ true
+ true
+ true
+
+
+ osx-arm64
+
+ NAPS2 (Not Another PDF Scanner 2)
+ NAPS2 (Not Another PDF Scanner 2)
+ Copyright 2009, 2012-2022 NAPS2 Contributors
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+ appsettings.xml
+ appsettings.xml
+
+
+
\ No newline at end of file
diff --git a/NAPS2.App.Mac/Program.cs b/NAPS2.App.Mac/Program.cs
new file mode 100644
index 000000000..5ec76fe76
--- /dev/null
+++ b/NAPS2.App.Mac/Program.cs
@@ -0,0 +1,15 @@
+using NAPS2.EntryPoints;
+
+namespace NAPS2;
+
+static class Program
+{
+ ///
+ /// The NAPS2.app main method.
+ ///
+ static void Main(string[] args)
+ {
+ // Use reflection to avoid antivirus false positives (yes, really)
+ typeof(MacEntryPoint).GetMethod("Run").Invoke(null, new object[] { args });
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Images.Mac/MacImage.cs b/NAPS2.Images.Mac/MacImage.cs
index e6b338582..d051da1e5 100644
--- a/NAPS2.Images.Mac/MacImage.cs
+++ b/NAPS2.Images.Mac/MacImage.cs
@@ -4,19 +4,18 @@ namespace NAPS2.Images.Mac;
public class MacImage : IMemoryImage
{
- private readonly NSImage _image;
internal readonly NSBitmapImageRep _imageRep;
public MacImage(NSImage image)
{
- _image = image;
+ NsImage = image;
// TODO: Better error checking
lock (MacImageContext.ConstructorLock)
{
#if MONOMAC
- _imageRep = new NSBitmapImageRep(_image.Representations()[0].Handle, false);
+ _imageRep = new NSBitmapImageRep(Image.Representations()[0].Handle, false);
#else
- _imageRep = (NSBitmapImageRep) _image.Representations()[0];
+ _imageRep = (NSBitmapImageRep) NsImage.Representations()[0];
#endif
}
// TODO: Also verify color spaces.
@@ -45,23 +44,25 @@ public class MacImage : IMemoryImage
}
}
+ public NSImage NsImage { get; }
+
public void Dispose()
{
- _image.Dispose();
+ Image.Dispose();
// TODO: Does this need to dispose the imageRep?
}
public int Width => (int) _imageRep.PixelsWide;
public int Height => (int) _imageRep.PixelsHigh;
- public float HorizontalResolution => (float) _image.Size.Width.ToDouble() / Width * 72;
- public float VerticalResolution => (float) _image.Size.Height.ToDouble() / Height * 72;
+ public float HorizontalResolution => (float) Image.Size.Width.ToDouble() / Width * 72;
+ public float VerticalResolution => (float) Image.Size.Height.ToDouble() / Height * 72;
public void SetResolution(float xDpi, float yDpi)
{
// TODO: Image size or imagerep size?
if (xDpi > 0 && yDpi > 0)
{
- _image.Size = new CGSize(xDpi / 72 * Width, yDpi / 72 * Height);
+ Image.Size = new CGSize(xDpi / 72 * Width, yDpi / 72 * Height);
}
}
@@ -94,6 +95,8 @@ public class MacImage : IMemoryImage
public ImageFileFormat OriginalFileFormat { get; set; }
+ public NSImage Image => NsImage;
+
public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unspecified, int quality = -1)
{
if (imageFormat == ImageFileFormat.Unspecified)
@@ -156,9 +159,9 @@ public class MacImage : IMemoryImage
}
#if MONOMAC
- var nsImage = new NSImage(_image.Copy().Handle, true);
+ var nsImage = new NSImage(Image.Copy().Handle, true);
#else
- var nsImage = (NSImage) _image.Copy();
+ var nsImage = (NSImage) NsImage.Copy();
#endif
return new MacImage(nsImage)
{
diff --git a/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs b/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs
new file mode 100644
index 000000000..8df17e689
--- /dev/null
+++ b/NAPS2.Lib.Mac/EntryPoints/MacEntryPoint.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading.Tasks;
+using Eto;
+using Eto.Forms;
+using NAPS2.EtoForms.Ui;
+using NAPS2.Logging;
+using NAPS2.Modules;
+using NAPS2.Util;
+using NAPS2.WinForms;
+using Ninject;
+using UnhandledExceptionEventArgs = Eto.UnhandledExceptionEventArgs;
+
+namespace NAPS2.EntryPoints;
+
+///
+/// The entry point logic for NAPS2.exe, the NAPS2 GUI.
+///
+public static class MacEntryPoint
+{
+ public static void Run(string[] args)
+ {
+ // Initialize Ninject (the DI framework)
+ var kernel = new StandardKernel(new CommonModule(), new MacModule(), new RecoveryModule(), new ContextModule());
+
+ Paths.ClearTemp();
+
+ // Set up basic application configuration
+ kernel.Get().SetCulturesFromConfig();
+ TaskScheduler.UnobservedTaskException += UnhandledTaskException;
+
+ // Show the main form
+ var application = new Application(Platforms.Mac64);
+ application.UnhandledException += UnhandledException;
+ var formFactory = kernel.Get();
+ var desktop = formFactory.Create();
+ // Invoker.Current = new WinFormsInvoker(desktop.ToNative());
+
+ application.Run(desktop);
+ }
+
+ private static void UnhandledTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
+ {
+ Log.FatalException("An error occurred that caused the task to terminate.", e.Exception);
+ e.SetObserved();
+ }
+
+ private static void UnhandledException(object? sender, UnhandledExceptionEventArgs e)
+ {
+ Log.FatalException("An error occurred that caused the application to close.", e.ExceptionObject as Exception);
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs
new file mode 100644
index 000000000..652a597e6
--- /dev/null
+++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs
@@ -0,0 +1,38 @@
+using Eto.Drawing;
+using Eto.Forms;
+using Eto.Mac.Drawing;
+using NAPS2.Images;
+using NAPS2.Images.Mac;
+using sd = System.Drawing;
+
+namespace NAPS2.EtoForms.Mac;
+
+public class MacEtoPlatform : EtoPlatform
+{
+ private const int MIN_BUTTON_WIDTH = 75;
+ private const int MIN_BUTTON_HEIGHT = 32;
+ private const int IMAGE_PADDING = 5;
+
+ static MacEtoPlatform()
+ {
+ }
+
+ public override IListView CreateListView(ListViewBehavior behavior) =>
+ new MacListView(behavior);
+
+ public override void ConfigureImageButton(Button button)
+ {
+ }
+
+ public override Bitmap ToBitmap(IMemoryImage image)
+ {
+ var nsImage = ((MacImage) image).NsImage;
+ return new Bitmap(new BitmapHandler(nsImage));
+ }
+
+ public override IMemoryImage DrawHourglass(IMemoryImage image)
+ {
+ // TODO
+ return image;
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs
new file mode 100644
index 000000000..dd0955f5e
--- /dev/null
+++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacListView.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using Eto.Forms;
+using NAPS2.Images;
+using NAPS2.Util;
+
+namespace NAPS2.EtoForms.Mac;
+
+public class MacListView : IListView where T : notnull
+{
+ private readonly ListViewBehavior _behavior;
+
+ private ListSelection _selection = ListSelection.Empty();
+ private bool _refreshing;
+
+ public MacListView(ListViewBehavior behavior)
+ {
+ _behavior = behavior;
+ }
+
+ public int ImageSize
+ {
+ get => 0;
+ set { }
+ }
+
+ // TODO: Properties here vs on behavior?
+ public bool AllowDrag { get; set; }
+
+ public bool AllowDrop { get; set; }
+
+ private void OnDragEnter(object? sender, DragEventArgs e)
+ {
+ if (!AllowDrop)
+ {
+ return;
+ }
+ e.Effects = _behavior.GetDropEffect(e.Data);
+ }
+
+ public Control Control { get; set; }
+
+ public event EventHandler? SelectionChanged;
+
+ public event EventHandler? ItemClicked;
+
+ public event EventHandler? Drop;
+
+ public void SetItems(IEnumerable items)
+ {
+ }
+
+ // TODO: Do we need this method? Clean up the name/doc at least
+ public void RegenerateImages()
+ {
+ }
+
+ public void ApplyDiffs(ListViewDiffs diffs)
+ {
+ }
+
+ public ListSelection Selection
+ {
+ get => _selection;
+ set
+ {
+ if (_selection == value)
+ {
+ return;
+ }
+ _selection = value ?? throw new ArgumentNullException(nameof(value));
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs b/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs
new file mode 100644
index 000000000..57afb7aaf
--- /dev/null
+++ b/NAPS2.Lib.Mac/EtoForms/Ui/MacDesktopForm.cs
@@ -0,0 +1,43 @@
+using NAPS2.Config;
+using NAPS2.Images;
+using NAPS2.ImportExport.Images;
+using NAPS2.Util;
+using NAPS2.WinForms;
+
+namespace NAPS2.EtoForms.Ui;
+
+public class MacDesktopForm : DesktopForm
+{
+ public MacDesktopForm(
+ Naps2Config config,
+ // KeyboardShortcutManager ksm,
+ INotificationManager notify,
+ CultureHelper cultureHelper,
+ IProfileManager profileManager,
+ UiImageList imageList,
+ ImageTransfer imageTransfer,
+ ThumbnailRenderQueue thumbnailRenderQueue,
+ UiThumbnailProvider thumbnailProvider,
+ DesktopController desktopController,
+ IDesktopScanController desktopScanController,
+ ImageListActions imageListActions,
+ DesktopFormProvider desktopFormProvider,
+ IDesktopSubFormController desktopSubFormController)
+ : base(config, /*ksm,*/ notify, cultureHelper, profileManager,
+ imageList, imageTransfer, thumbnailRenderQueue, thumbnailProvider, desktopController, desktopScanController,
+ imageListActions, desktopFormProvider, desktopSubFormController)
+ {
+ }
+
+ protected override void CreateToolbarsAndMenus()
+ {
+ }
+
+ protected override void ConfigureToolbar()
+ {
+ }
+
+ protected override void AfterLayout()
+ {
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Lib.Mac/Modules/MacModule.cs b/NAPS2.Lib.Mac/Modules/MacModule.cs
new file mode 100644
index 000000000..12640f970
--- /dev/null
+++ b/NAPS2.Lib.Mac/Modules/MacModule.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using NAPS2.EtoForms;
+using NAPS2.EtoForms.Mac;
+using NAPS2.EtoForms.Ui;
+using NAPS2.Images;
+using NAPS2.ImportExport;
+using NAPS2.ImportExport.Email;
+using NAPS2.ImportExport.Pdf;
+using NAPS2.Logging;
+using NAPS2.Operation;
+using NAPS2.Scan;
+using NAPS2.Update;
+using NAPS2.Util;
+using NAPS2.WinForms;
+using Ninject;
+using Ninject.Modules;
+
+namespace NAPS2.Modules;
+
+public class MacModule : NinjectModule
+{
+ public override void Load()
+ {
+ // Bind().To();
+ Bind().To();
+ Bind().To();
+ Bind().To();
+ Bind().To().InSingletonScope();
+ Bind().To();
+ Bind().To().InSingletonScope();
+ Bind().ToMethod(ctx => ctx.Kernel.Get());
+ Bind().To();
+ Bind().To();
+ Bind().ToSelf().InSingletonScope();
+ Bind().To();
+ Bind().To();
+ Bind().To();
+ Bind().To();
+ Bind().ToSelf().InSingletonScope();
+
+ Bind().To();
+
+ EtoPlatform.Current = new MacEtoPlatform();
+ // Log.EventLogger = new WindowsEventLogger(Kernel!.Get());
+ }
+}
+
+public class StubDesktopSubFormController : IDesktopSubFormController
+{
+ public void ShowCropForm()
+ {
+ }
+
+ public void ShowBrightnessContrastForm()
+ {
+ }
+
+ public void ShowHueSaturationForm()
+ {
+ }
+
+ public void ShowBlackWhiteForm()
+ {
+ }
+
+ public void ShowSharpenForm()
+ {
+ }
+
+ public void ShowRotateForm()
+ {
+ }
+
+ public void ShowProfilesForm()
+ {
+ }
+
+ public void ShowOcrForm()
+ {
+ }
+
+ public void ShowBatchScanForm()
+ {
+ }
+
+ public void ShowViewerForm()
+ {
+ }
+
+ public void ShowPdfSettingsForm()
+ {
+ }
+
+ public void ShowImageSettingsForm()
+ {
+ }
+
+ public void ShowEmailSettingsForm()
+ {
+ }
+
+ public void ShowAboutForm()
+ {
+ }
+
+ public void ShowSettingsForm()
+ {
+ }
+}
+
+public class StubDesktopScanController : IDesktopScanController
+{
+ public Task ScanWithDevice(string deviceID)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task ScanDefault()
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task ScanWithNewProfile()
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task ScanWithProfile(ScanProfile profile)
+ {
+ return Task.CompletedTask;
+ }
+}
+
+public class StubExportHelper : IWinFormsExportHelper
+{
+ public Task SavePDF(IList images, ISaveNotify notify)
+ {
+ return Task.FromResult(false);
+ }
+
+ public Task ExportPDF(string filename, IList images, bool email, EmailMessage emailMessage)
+ {
+ return Task.FromResult(false);
+ }
+
+ public Task SaveImages(IList images, ISaveNotify notify)
+ {
+ return Task.FromResult(false);
+ }
+
+ public Task EmailPDF(IList images)
+ {
+ return Task.FromResult(false);
+ }
+}
+
+public class StubDevicePrompt : IDevicePrompt
+{
+ public ScanDevice? PromptForDevice(List deviceList, IntPtr dialogParent)
+ {
+ return null;
+ }
+}
+
+public class StubScannedImagePrinter : IScannedImagePrinter
+{
+ public Task PromptToPrint(IList images, IList selectedImages)
+ {
+ return Task.FromResult(false);
+ }
+}
+
+public class StubNotificationManager : INotificationManager
+{
+ public void PdfSaved(string path)
+ {
+ }
+
+ public void ImagesSaved(int imageCount, string path)
+ {
+ }
+
+ public void DonatePrompt()
+ {
+ }
+
+ public void OperationProgress(OperationProgress opModalProgress, IOperation op)
+ {
+ }
+
+ public void UpdateAvailable(IUpdateChecker updateChecker, UpdateInfo update)
+ {
+ }
+
+ public void Rebuild()
+ {
+ }
+}
+
+public class StubPdfPasswordProvider : IPdfPasswordProvider
+{
+ public bool ProvidePassword(string fileName, int attemptCount, out string password)
+ {
+ password = null!;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj b/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj
index 2b24dd582..18e6dc055 100644
--- a/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj
+++ b/NAPS2.Lib.Mac/NAPS2.Lib.Mac.csproj
@@ -1,7 +1,8 @@
- net6;net6-macos10.15
+
+ net6
enable
true
NAPS2
@@ -15,7 +16,8 @@
-
+
+
diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
index b2f5cdb13..585ec353d 100644
--- a/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
+++ b/NAPS2.Lib/EtoForms/Ui/DesktopForm.cs
@@ -433,17 +433,22 @@ public abstract class DesktopForm : EtoFormBase
{
}
- protected abstract void ConfigureToolbar();
+ protected virtual void ConfigureToolbar()
+ {
+ }
- protected abstract void CreateToolbarButton(Command command);
+ protected virtual void CreateToolbarButton(Command command) => throw new InvalidOperationException();
- protected abstract void CreateToolbarButtonWithMenu(Command command, MenuProvider menu);
+ protected virtual void CreateToolbarButtonWithMenu(Command command, MenuProvider menu) =>
+ throw new InvalidOperationException();
- protected abstract void CreateToolbarMenu(Command command, MenuProvider menu);
+ protected virtual void CreateToolbarMenu(Command command, MenuProvider menu) =>
+ throw new InvalidOperationException();
- protected abstract void CreateToolbarStackedButtons(Command command1, Command command2);
+ protected virtual void CreateToolbarStackedButtons(Command command1, Command command2) =>
+ throw new InvalidOperationException();
- protected abstract void CreateToolbarSeparator();
+ protected virtual void CreateToolbarSeparator() => throw new InvalidOperationException();
protected virtual void SetContent(Control content)
{
diff --git a/NAPS2.sln b/NAPS2.sln
index aadfa7a1f..614eef03b 100644
--- a/NAPS2.sln
+++ b/NAPS2.sln
@@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Client", "NAPS2.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.Escl.Tests", "NAPS2.Escl.Tests\NAPS2.Escl.Tests.csproj", "{ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NAPS2.App.Mac", "NAPS2.App.Mac\NAPS2.App.Mac.csproj", "{A280B315-3670-484D-B7A1-294E3DE56E7E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -338,6 +340,18 @@ Global
{ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Standalone|Any CPU.Build.0 = Debug|Any CPU
{ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Tools|Any CPU.ActiveCfg = Debug|Any CPU
{ECD69481-CD4D-4EEC-A3BA-612DB29F13B3}.Tools|Any CPU.Build.0 = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.DebugLang|Any CPU.ActiveCfg = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.DebugLang|Any CPU.Build.0 = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.InstallerEXE|Any CPU.ActiveCfg = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.InstallerEXE|Any CPU.Build.0 = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.InstallerMSI|Any CPU.ActiveCfg = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.InstallerMSI|Any CPU.Build.0 = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.Standalone|Any CPU.ActiveCfg = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.Standalone|Any CPU.Build.0 = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.Tools|Any CPU.ActiveCfg = Debug|Any CPU
+ {A280B315-3670-484D-B7A1-294E3DE56E7E}.Tools|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE