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