Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions WindowTranslator.Abstractions/UserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public class TargetSettings
/// </summary>
public bool DisplayBusy { get; set; } = true;

/// <summary>
/// ホットキーが押されたときだけOCRと翻訳を行うか
/// </summary>
public bool IsOneShotMode { get; set; }

/// <summary>
/// マウスポインター判定の余白(WPF上のピクセル値)
/// </summary>
Expand Down
58 changes: 58 additions & 0 deletions WindowTranslator.Tests/OcrObservationSelectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Drawing;
using WindowTranslator.Modules.Ocr;

namespace WindowTranslator.Tests;

public class OcrObservationSelectorTests
{
[Fact]
public void OneShotReturnsCurrentObservationsWithoutCallingTracker()
{
TextRect[] observations = [new("current", 10, 20, 100, 30, 18, false)];
var tracker = new RecordingTracker
{
Result = [new("previous", 1, 2, 3, 4, 5, false)],
};

IReadOnlyList<TextRect> result = OcrObservationSelector.Select(
observations,
new Size(1920, 1080),
tracker,
true);

Assert.False(tracker.WasCalled);
Assert.Equal(observations, result);
}

[Fact]
public void ContinuousModeReturnsTrackerResult()
{
TextRect[] tracked = [new("tracked", 11, 22, 101, 31, 19, false)];
var tracker = new RecordingTracker { Result = tracked };

IReadOnlyList<TextRect> result = OcrObservationSelector.Select(
[new("current", 10, 20, 100, 30, 18, false)],
new Size(1920, 1080),
tracker,
false);

Assert.True(tracker.WasCalled);
Assert.Same(tracked, result);
}

private sealed class RecordingTracker : IOcrTextTracker
{
public bool WasCalled { get; private set; }
public required IReadOnlyList<TextRect> Result { get; init; }

public IReadOnlyList<TextRect> Update(IEnumerable<TextRect> observations, Size imageSize)
{
this.WasCalled = true;
return this.Result;
}

public void Reset()
{
}
}
}
5 changes: 3 additions & 2 deletions WindowTranslator.Tests/OcrTextTrackerAccuracyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,15 +1705,16 @@ public void DormantChildrenDoNotConsumeAnActiveTracksObservation()
}

[Fact]
public void RemovedFeaturesAreNotExposed()
public void RemovedBufferFeaturesAreNotExposedAndOneShotIsAvailable()
{
const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Static
| System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.NonPublic;
Type appResources = typeof(OcrTextTracker).Assembly.GetType("WindowTranslator.Properties.Resources", throwOnError: true)!;
Type abstractionResources = typeof(TextRect).Assembly.GetType("WindowTranslator.Properties.Resources", throwOnError: true)!;

Assert.Null(appResources.GetProperty("IsOneShotMode", flags));
Assert.NotNull(appResources.GetProperty("IsOneShotMode", flags));
Assert.NotNull(typeof(TargetSettings).GetProperty(nameof(TargetSettings.IsOneShotMode)));
Assert.Null(abstractionResources.GetProperty("Buffer", flags));
Assert.Null(abstractionResources.GetProperty("BufferSize", flags));
Assert.Null(abstractionResources.GetProperty("IsSuppressVibe", flags));
Expand Down
5 changes: 4 additions & 1 deletion WindowTranslator/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Windows;

[assembly: InternalsVisibleTo("WindowTranslator.Tests")]
Comment thread
Freeesia marked this conversation as resolved.
Outdated

[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
Expand All @@ -10,4 +13,4 @@
// app, or any theme specific resource dictionaries)
)]

[assembly: SupportedOSPlatform("windows10.0.19041")]
[assembly: SupportedOSPlatform("windows10.0.19041")]
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ private static string GetInfo(Exception ex, UserSettings? settings, string targe
sb.AppendLine(CultureInfo.CurrentCulture, $"OverlayShortcut: {value.OverlayShortcut ?? "N/A"}");
sb.AppendLine(CultureInfo.CurrentCulture, $"OverlayOpacity: {value.OverlayOpacity}");
sb.AppendLine(CultureInfo.CurrentCulture, $"DisplayBusy: {value.DisplayBusy}");
sb.AppendLine(CultureInfo.CurrentCulture, $"IsOneShotMode: {value.IsOneShotMode}");

if (value.SelectedPlugins?.Count > 0)
{
Expand Down
31 changes: 30 additions & 1 deletion WindowTranslator/Modules/Main/CaptureMainWindow.xaml.cs
Comment thread
Freeesia marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Options;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using WindowTranslator.Extensions;
using WindowTranslator.Stores;
using static Windows.Win32.PInvoke;

Expand All @@ -16,18 +20,30 @@ public partial class CaptureMainWindow
{
private readonly IProcessInfoStore processInfo;
private readonly DispatcherTimer timer = new();
private readonly bool isOneShotMode;
private readonly HOT_KEY_MODIFIERS shortcutModifiers;
private readonly int shortcutKey;
private IntPtr windowHandle;

public CaptureMainWindow(IProcessInfoStore processInfo)
public CaptureMainWindow(IProcessInfoStore processInfo, IOptionsSnapshot<TargetSettings> targetSettings)
{
InitializeComponent();
this.processInfo = processInfo;
this.isOneShotMode = targetSettings.Value.IsOneShotMode;
(this.shortcutModifiers, this.shortcutKey) = targetSettings.Value.OverlayShortcut.ToHotKey();
this.timer.Interval = TimeSpan.FromMilliseconds(10);
this.timer.Tick += (s, e) => CheckTargetWindow();
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.timer.Start();
if (this.isOneShotMode)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ホットキーの登録はOneShotかどうかに関わらず必要では?本当にオーバーレイと実装揃ってる?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ご指摘のとおり、Capture表示のホットキー登録はOneShot固有の変更として入れるべきではありませんでした。今回の変更範囲とは関係ないため、CaptureMainWindowの差分をすべて戻し、OneShotはOverlay表示の既存ホットキー経路だけで起動する形に修正しました。

{
this.windowHandle = new WindowInteropHelper(this).Handle;
RegisterHotKey(new(this.windowHandle), 0, this.shortcutModifiers, (uint)this.shortcutKey);
HwndSource.FromHwnd(this.windowHandle).AddHook(WndProc);
}
StrongReferenceMessenger.Default.Register<CaptureMainWindow, CloseMessage>(this, CloseIfViewModel);
}

Expand All @@ -45,9 +61,22 @@ protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
this.timer.Stop();
if (this.isOneShotMode)
{
UnregisterHotKey(new(this.windowHandle), 0);
}
StrongReferenceMessenger.Default.Unregister<CloseMessage>(this);
}

private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
{
if (msg == WM_HOTKEY && this.DataContext is CaptureMainViewModel viewModel)
{
viewModel.RequestOneShot();
}
return 0;
}

private static void CloseIfViewModel(CaptureMainWindow w, CloseMessage m)
{
if (w.DataContext == m.ViewModel)
Expand Down
67 changes: 61 additions & 6 deletions WindowTranslator/Modules/Main/MainViewModelBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public abstract partial class MainViewModelBase : IDisposable
private readonly double fontScale;
private readonly double overlayOpacity;
private readonly double mousePointerHitTestPadding;
private readonly bool isOneShotMode;
private TextRect[]? lastRequested;

[ObservableProperty]
Expand All @@ -64,6 +65,7 @@ public abstract partial class MainViewModelBase : IDisposable
public ObservableCollection<TextRect> OcrTexts { get; } = [];
public string Font { get; }
public double MousePointerHitTestPadding => this.mousePointerHitTestPadding;
public bool IsOneShotMode => this.isOneShotMode;

public MainViewModelBase(
IPresentationService presentationService,
Expand All @@ -85,6 +87,7 @@ public MainViewModelBase(
this.fontScale = options.Value.FontScale;
this.overlayOpacity = options.Value.OverlayOpacity;
this.mousePointerHitTestPadding = options.Value.MousePointerHitTestPadding;
this.isOneShotMode = options.Value.IsOneShotMode;
this.DisplayBusy = options.Value.DisplayBusy;
this.capture = capture ?? throw new ArgumentNullException(nameof(capture));
this.capture.Captured += Capture_CapturedAsync;
Expand All @@ -96,13 +99,24 @@ public MainViewModelBase(
this.filters = filters.ToArray();
this.logger = logger;
this.capture.StartCapture(processInfoStore.MainWindowHandle);
this.timer = new(_ => Application.Current.Dispatcher.Invoke(() => CreateTextOverlayAsync().Forget()), null, 0, 500);
this.timer = new(
Comment thread
Freeesia marked this conversation as resolved.
Outdated
_ => Application.Current.Dispatcher.Invoke(() => CreateTextOverlayAsync().Forget()),
null,
this.isOneShotMode ? Timeout.Infinite : 0,
500);
var transAsm = this.translator.GetType().Assembly;
this.title = $"{this.name} - {this.translator.Name} ({transAsm.GetName().Version})";
}

partial void OnOverlayVisibleChanged(bool value)
{
// OneShotではホットキー押下時点の最新フレームが必要なため、表示状態にかかわらず
// キャプチャーを継続する。表示の切り替えも前回の結果を破棄する契機にはしない。
if (this.isOneShotMode)
Comment thread
Freeesia marked this conversation as resolved.
{
return;
}

if (value)
{
this.OcrTexts.Clear();
Expand All @@ -128,12 +142,30 @@ private async Task Capture_CapturedAsync(object? sender, CapturedEventArgs args)
var sbmp = Interlocked.Exchange(ref this.capturedBmp, newBmp);
this.Width = newBmp.PixelWidth;
this.Height = newBmp.PixelHeight;
CreateTextOverlayAsync().Forget();
if (!this.isOneShotMode)
{
CreateTextOverlayAsync().Forget();
}
sbmp?.Dispose();
}

private async Task CreateTextOverlayAsync()
public void RequestOneShot()
{
if (!this.isOneShotMode)
{
return;
}

this.logger.LogDebug("OneShot OCR requested");
CreateTextOverlayAsync(true).Forget();
}

private async Task CreateTextOverlayAsync(bool oneShotRequested = false)
{
if (this.isOneShotMode && !oneShotRequested)
{
return;
}
if (!await this.analyzing.WaitAsync(0))
{
return;
Expand Down Expand Up @@ -163,8 +195,12 @@ private async Task CreateTextOverlayAsync()
{
try
{
texts = await this.ocr.RecognizeAsync(sbmp);
texts = this.ocrTextTracker.Update(texts, new(sbmp.PixelWidth, sbmp.PixelHeight));
var observations = await this.ocr.RecognizeAsync(sbmp);
texts = OcrObservationSelector.Select(
observations,
new(sbmp.PixelWidth, sbmp.PixelHeight),
this.ocrTextTracker,
this.isOneShotMode);
}
catch (ObjectDisposedException)
{
Expand Down Expand Up @@ -212,7 +248,15 @@ private async Task CreateTextOverlayAsync()
using var t = this.logger.LogDebugTime("PreTranslate");
texts = await tmp.ToArrayAsync();
}
TranslateAsync(texts).Forget();
if (this.isOneShotMode)
{
// 周期処理がないため、この1回の処理内で翻訳完了まで待って表示へ反映する。
await TranslateAsync(texts);
Comment thread
Freeesia marked this conversation as resolved.
Outdated
}
else
{
TranslateAsync(texts).Forget();
}
texts = texts.Select(t => t switch
{
{ TranslatedText: null } when this.cache.Contains(t.SourceText) => t with { TranslatedText = this.cache.Get(t.SourceText) },
Expand All @@ -232,6 +276,17 @@ private async Task CreateTextOverlayAsync()
texts = texts.Select(t => t with { Background = Color.FromArgb((int)(255 * this.overlayOpacity), t.Background) }).ToArray();
}

if (this.isOneShotMode)
{
// 前回の矩形との同一性や位置関係を一切引き継がない。
this.OcrTexts.Clear();
foreach (var text in texts)
{
this.OcrTexts.Add(text);
Comment thread
Freeesia marked this conversation as resolved.
Outdated
}
return;
}

var hash = texts.ToHashSet();
foreach (var text in this.OcrTexts.Where(t => !hash.Contains(t)).ToArray())
{
Expand Down
4 changes: 4 additions & 0 deletions WindowTranslator/Modules/Main/OverlayMainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool hand
{
return 0;
}
if (this.DataContext is OverlayMainViewModel viewModel)
{
viewModel.RequestOneShot();
}
if (this.overlaySwitch == OverlaySwitch.Hold)
{
HoldHideOverlay();
Expand Down
16 changes: 16 additions & 0 deletions WindowTranslator/Modules/Ocr/OcrObservationSelector.cs
Comment thread
Freeesia marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Drawing;

namespace WindowTranslator.Modules.Ocr;

internal static class OcrObservationSelector
{
public static IReadOnlyList<TextRect> Select(
IEnumerable<TextRect> observations,
Size imageSize,
IOcrTextTracker tracker,
bool isOneShotMode)
{
var current = observations.ToArray();
return isOneShotMode ? current : tracker.Update(current, imageSize);
}
}
8 changes: 7 additions & 1 deletion WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ public async Task SaveAsync(object window)
},
PluginParams = t.Params.ToDictionary(p => p.GetType().Name),
DisplayBusy = t.DisplayBusy,
IsOneShotMode = t.IsOneShotMode,
OverlayOpacity = t.OverlayOpacity,
MousePointerHitTestPadding = t.MousePointerHitTestPadding,
}),
Expand Down Expand Up @@ -467,10 +468,15 @@ public partial class TargetSettingsViewModel(
[ObservableProperty]
private bool displayBusy = settings.DisplayBusy;

[property: Category("SettingsViewModel|Misc")]
[property: SortIndex(9)]
[ObservableProperty]
private bool isOneShotMode = settings.IsOneShotMode;

[property: Category("SettingsViewModel|Misc")]
[property: LocalizedDescription(typeof(Resources), $"{nameof(MousePointerHitTestPadding)}_Desc")]
[property: Slidable(0, 100, 1, 10, true, 1)]
[property: SortIndex(9)]
[property: SortIndex(10)]
[ObservableProperty]
private double mousePointerHitTestPadding = settings.MousePointerHitTestPadding;

Expand Down
5 changes: 5 additions & 0 deletions WindowTranslator/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions WindowTranslator/Properties/Resources.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@
<data name="DisplayBusy" xml:space="preserve">
<value>Show busy icon</value>
</data>
<data name="IsOneShotMode" xml:space="preserve">
<value>Run OCR and translation only when the hotkey is pressed</value>
</data>
<data name="IsOverlayPointSwap" xml:space="preserve">
<value>Display overlay translation only for text at mouse pointer position</value>
</data>
Expand Down
Loading
Loading