diff --git a/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs b/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs index f0487d6e77..8781821a69 100644 --- a/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs +++ b/NAPS2.Sdk/Platform/Windows/Win32MessagePump.cs @@ -5,6 +5,9 @@ namespace NAPS2.Platform.Windows; +/// +/// Allows creation of a Win32 event loop without any references to WinForms or WPF. +/// [System.Runtime.Versioning.SupportedOSPlatform("windows")] internal class Win32MessagePump : IInvoker, IDisposable { diff --git a/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs b/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs index ef3acd0e46..b5903e5cac 100644 --- a/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs +++ b/NAPS2.Sdk/Platform/Windows/Win32TwainHandleManager.cs @@ -4,6 +4,9 @@ namespace NAPS2.Platform.Windows; +/// +/// TwainHandleManager implementation that uses a Win32MessagePump to get window handles to hand off to TWAIN. +/// [System.Runtime.Versioning.SupportedOSPlatform("windows")] internal class Win32TwainHandleManager : TwainHandleManager { @@ -64,6 +67,8 @@ public override MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = defa return new Win32MessageLoopHook(_messagePump, GetDsmHandle(dialogParent, useNativeUi)); } + public override IInvoker Invoker => _messagePump; + public override void Dispose() { if (_disposed) return; diff --git a/NAPS2.Sdk/Scan/Internal/Twain/DefaultTwainHandleManager.cs b/NAPS2.Sdk/Scan/Internal/Twain/DefaultTwainHandleManager.cs new file mode 100644 index 0000000000..b0f39436b7 --- /dev/null +++ b/NAPS2.Sdk/Scan/Internal/Twain/DefaultTwainHandleManager.cs @@ -0,0 +1,65 @@ +#if !MAC +using System.Threading; +using NAPS2.Platform.Windows; +using NTwain; + +namespace NAPS2.Scan.Internal.Twain; + +/// +/// TwainHandleManager implementation that lazily starts a Win32MessagePump and delegates to a Win32TwainHandleManager. +/// +[System.Runtime.Versioning.SupportedOSPlatform("windows")] +internal class DefaultTwainHandleManager : TwainHandleManager +{ + private Win32MessagePump? _messagePump; + private Win32TwainHandleManager? _inner; + + private Win32TwainHandleManager CreateInner() + { + var mre = new ManualResetEvent(false); + var messageThread = new Thread(() => + { + _messagePump = Win32MessagePump.Create(); + mre.Set(); + _messagePump.RunMessageLoop(); + }); + messageThread.SetApartmentState(ApartmentState.STA); + messageThread.Start(); + mre.WaitOne(); + return new Win32TwainHandleManager(_messagePump!); + } + + public override IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi) + { + _inner ??= CreateInner(); + return _inner.GetDsmHandle(dialogParent, useNativeUi); + } + + public override IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi) + { + _inner ??= CreateInner(); + return _inner.GetEnableHandle(dialogParent, useNativeUi); + } + + public override MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = default, bool useNativeUi = false) + { + _inner ??= CreateInner(); + return _inner.CreateMessageLoopHook(dialogParent, useNativeUi); + } + + public override IInvoker Invoker + { + get + { + _inner ??= CreateInner(); + return _inner.Invoker; + } + } + + public override void Dispose() + { + _inner?.Dispose(); + _messagePump?.Dispose(); + } +} +#endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs b/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs index 46b2c7f2b6..3fcebd5a3c 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs @@ -61,7 +61,8 @@ private List InternalGetDeviceList(ScanOptions options) { PlatformInfo.Current.PreferNewDSM = options.TwainOptions.Dsm != TwainDsm.Old; var session = new TwainSession(TwainAppId); - session.Open(TwainHandleManager.Factory().CreateMessageLoopHook()); + using var handleManager = TwainHandleManager.Factory(); + session.Open(handleManager.CreateMessageLoopHook()); try { return session.GetSources().Select(ds => new ScanDevice(Driver.Twain, ds.Name, ds.Name)).ToList(); @@ -103,7 +104,8 @@ private ScanCaps InternalGetCaps(ScanOptions options) { PlatformInfo.Current.PreferNewDSM = options.TwainOptions.Dsm != TwainDsm.Old; var session = new TwainSession(TwainAppId); - session.Open(TwainHandleManager.Factory().CreateMessageLoopHook()); + using var handleManager = TwainHandleManager.Factory(); + session.Open(handleManager.CreateMessageLoopHook()); try { var ds = session.GetSources().FirstOrDefault(ds => ds.Name == options.Device!.ID); diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs index 33f362b50f..61836a5072 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainHandleManager.cs @@ -3,23 +3,31 @@ namespace NAPS2.Scan.Internal.Twain; -internal class TwainHandleManager : IDisposable +/// +/// Abstracts how HWND handles are obtained for use with TWAIN. +/// +internal abstract class TwainHandleManager : IDisposable { - public static Func Factory { get; set; } = () => new TwainHandleManager(); + public static Func Factory { get; set; } = () => + { +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) throw new NotSupportedException(); +#endif + return new DefaultTwainHandleManager(); + }; protected TwainHandleManager() { } - public virtual IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi) => dialogParent; + public abstract IntPtr GetDsmHandle(IntPtr dialogParent, bool useNativeUi); - public virtual IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi) => dialogParent; + public abstract IntPtr GetEnableHandle(IntPtr dialogParent, bool useNativeUi); - public virtual MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = default, bool useNativeUi = false) => - throw new NotSupportedException(); + public abstract MessageLoopHook CreateMessageLoopHook(IntPtr dialogParent = default, bool useNativeUi = false); - public virtual void Dispose() - { - } + public abstract IInvoker Invoker { get; } + + public abstract void Dispose(); } #endif \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs b/NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs index 61f0268ec5..be3a02a1ca 100644 --- a/NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs +++ b/NAPS2.Sdk/Scan/Internal/Twain/TwainScanRunner.cs @@ -52,8 +52,7 @@ public TwainScanRunner(ILogger logger, TWIdentity twainAppId, TwainDsm dsm, Scan public Task Run() { - // TODO: Work around needing Invoker (maybe pass in SyncContext or something?) and move it to NAPS2.Lib - Invoker.Current.InvokeDispatch(Init); + _handleManager.Invoker.InvokeDispatch(Init); return _tcs.Task; } @@ -99,8 +98,8 @@ private void Init() throw GetExceptionForStatus(_source.GetStatus()); } - _cancelToken.Register(() => Invoker.Current.Invoke(FinishWithCancellation)); - _sourceDisabledTcs.Task.ContinueWith(_ => Invoker.Current.Invoke(FinishWithCompletion)).AssertNoAwait(); + _cancelToken.Register(() => _handleManager.Invoker.Invoke(FinishWithCancellation)); + _sourceDisabledTcs.Task.ContinueWith(_ => _handleManager.Invoker.Invoke(FinishWithCompletion)).AssertNoAwait(); } catch (Exception ex) {