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)
{