Skip to content

Commit 68e1f21

Browse files
authored
Merge pull request #517 from Freeesia/copilot/validate-settings-on-translation-start
翻訳開始時に設定の検証を行う
2 parents 8cc787c + 775dd94 commit 68e1f21

11 files changed

Lines changed: 246 additions & 41 deletions

File tree

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
<PackageVersion Include="Wacton.Unicolour" Version="6.3.0" />
4747
<PackageVersion Include="Weikio.PluginFramework.AspNetCore" Version="1.5.1" />
4848
<PackageVersion Include="WixSharp_wix4" Version="2.8.0" />
49-
<PackageVersion Include="WPF-UI" Version="4.0.3" />
50-
<PackageVersion Include="WPF-UI.Tray" Version="4.0.3" />
49+
<PackageVersion Include="WPF-UI" Version="4.1.0" />
50+
<PackageVersion Include="WPF-UI.Tray" Version="4.1.0" />
5151
<PackageVersion Include="WpfAnalyzers" Version="4.1.1" />
5252
<PackageVersion Include="XamlAnimatedGif" Version="2.3.1" />
5353
<PackageVersion Include="xunit" Version="2.9.3" />

WindowTranslator.Abstractions/Properties/Resources.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,16 @@
174174
<data name="IsSuppressVibe" xml:space="preserve">
175175
<value>振動の抑制</value>
176176
</data>
177+
<data name="TranslateLanguage" xml:space="preserve">
178+
<value>言語設定</value>
179+
</data>
180+
<data name="SameSourceTargetLanguage" xml:space="preserve">
181+
<value>翻訳元言語と翻訳先言語が同一です。異なる言語を指定してください。</value>
182+
</data>
183+
<data name="TranslateModule" xml:space="preserve">
184+
<value>翻訳モジュール</value>
185+
</data>
186+
<data name="CacheModule" xml:space="preserve">
187+
<value>キャッシュモジュール</value>
188+
</data>
177189
</root>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using WindowTranslator.Modules;
2+
using WindowTranslator.Properties;
3+
4+
namespace WindowTranslator.Extensions;
5+
6+
/// <summary>
7+
/// 翻訳対象設定バリデーターの拡張メソッド
8+
/// </summary>
9+
public static class TargetSettingsValidatorExtensions
10+
{
11+
/// <summary>
12+
/// 設定の検証を行う
13+
/// </summary>
14+
/// <param name="validators">バリデーターのコレクション</param>
15+
/// <param name="settings">検証する設定</param>
16+
/// <returns>検証結果のリスト(空の場合は全て有効)</returns>
17+
public static async Task<IReadOnlyList<ValidateResult>> ValidateAsync(
18+
this IEnumerable<ITargetSettingsValidator> validators,
19+
TargetSettings settings)
20+
{
21+
var results = new List<ValidateResult>();
22+
23+
// 翻訳元言語と翻訳先言語が同じでないかチェック
24+
if (settings.Language.Source == settings.Language.Target)
25+
{
26+
results.Add(ValidateResult.Invalid(Resources.TranslateLanguage, Resources.SameSourceTargetLanguage));
27+
}
28+
29+
// 翻訳モジュールが選択されているかチェック
30+
if (!settings.SelectedPlugins.TryGetValue(nameof(ITranslateModule), out var translateModule) || string.IsNullOrEmpty(translateModule))
31+
{
32+
results.Add(ValidateResult.Invalid(Resources.TranslateModule, """
33+
翻訳モジュールが選択されていません。
34+
「対象ごとの設定」→「全体設定」タブの「翻訳モジュール」を設定してください。
35+
"""));
36+
}
37+
38+
// キャッシュモジュールが選択されているかチェック
39+
if (!settings.SelectedPlugins.TryGetValue(nameof(ICacheModule), out var cacheModule) || string.IsNullOrEmpty(cacheModule))
40+
{
41+
results.Add(ValidateResult.Invalid(Resources.CacheModule, """
42+
キャッシュモジュールが選択されていません。
43+
「対象ごとの設定」→「全体設定」タブの「キャッシュモジュール」を設定してください。
44+
"""));
45+
}
46+
47+
// 各バリデーターによる検証
48+
foreach (var validator in validators)
49+
{
50+
var result = await validator.Validate(settings);
51+
if (!result.IsValid)
52+
{
53+
results.Add(result);
54+
}
55+
}
56+
57+
return results;
58+
}
59+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Wpf.Ui.Controls;
2+
3+
namespace WindowTranslator;
4+
5+
partial interface IPresentationService
6+
{
7+
Task<MessageBoxResult> ShowMessageAsync(MessageContext context, CancellationToken cancellationToken = default);
8+
}
9+
10+
partial class PresentationService
11+
{
12+
public Task<MessageBoxResult> ShowMessageAsync(MessageContext context, CancellationToken cancellationToken = default)
13+
{
14+
var box = new MessageBox()
15+
{
16+
Title = context.Title,
17+
Content = context.Message,
18+
};
19+
if (!string.IsNullOrEmpty(context.CloseButtonText))
20+
{
21+
box.CloseButtonText = context.CloseButtonText;
22+
}
23+
if (!string.IsNullOrEmpty(context.PrimaryButtonText))
24+
{
25+
box.PrimaryButtonText = context.PrimaryButtonText;
26+
}
27+
if (!string.IsNullOrEmpty(context.SecondaryButtonText))
28+
{
29+
box.SecondaryButtonText = context.SecondaryButtonText;
30+
}
31+
return box.ShowDialogAsync(true, cancellationToken);
32+
}
33+
}
34+
35+
public record MessageContext(string Title, string Message)
36+
{
37+
public string? CloseButtonText { get; init; }
38+
39+
public string? PrimaryButtonText { get; init; }
40+
41+
public string? SecondaryButtonText { get; init; }
42+
}

WindowTranslator/Modules/Main/MainWindowModule.cs

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,94 @@
11
using Kamishibai;
22
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
34
using Microsoft.Extensions.Options;
45
using Microsoft.VisualStudio.Threading;
56
using System.Collections.ObjectModel;
7+
using WindowTranslator.Extensions;
8+
using WindowTranslator.Properties;
69
using WindowTranslator.Stores;
10+
using Wpf.Ui.Extensions;
711

812
namespace WindowTranslator.Modules.Main;
9-
public sealed class MainWindowModule(App app, IServiceProvider provider) : IMainWindowModule, IDisposable
13+
14+
public sealed class MainWindowModule(App app, IServiceProvider provider, ILogger<MainWindowModule> logger) : IMainWindowModule, IDisposable
1015
{
1116
private readonly App app = app;
1217
private readonly IServiceProvider provider = provider;
18+
private readonly ILogger<MainWindowModule> logger = logger;
1319
private readonly AsyncSemaphore asyncLock = new(1);
1420

1521
public ObservableCollection<WindowInfo> OpenedWindows { get; } = new();
1622

1723
public Task OpenTargetAsync(IntPtr targetWindowHandle, string name)
1824
=> this.app.Dispatcher.Invoke(() => OpenTargetWindowCoreAsync(targetWindowHandle, name));
1925

26+
private async ValueTask<TargetSettings?> GetSettingsAsync(string name)
27+
{
28+
using var scope = provider.CreateScope();
29+
var presentationService = scope.ServiceProvider.GetRequiredService<IPresentationService>();
30+
var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<UserSettings>>();
31+
// 対象の設定を取得
32+
if (options.Value.Targets.TryGetValue(name, out var settings))
33+
{
34+
// 設定を検証
35+
var validators = scope.ServiceProvider.GetRequiredService<IEnumerable<ITargetSettingsValidator>>();
36+
var validationResults = await validators.ValidateAsync(settings);
37+
if (validationResults.IsEmpty())
38+
{
39+
this.logger.LogInformation($"Settings for target '{name}' are valid.");
40+
return settings;
41+
}
42+
43+
// 検証エラーがある場合、エラーダイアログを表示
44+
var result = await presentationService.ShowMessageAsync(new(string.Format(Resources.InvalidSettings, name), string.Join("\n\n", validationResults.Select(r => $"### {r.Title}\n{r.Message}")))
45+
{
46+
PrimaryButtonText = Resources.Settings,
47+
SecondaryButtonText = Resources.RunAsIs,
48+
CloseButtonText = Resources.Cancel,
49+
});
50+
51+
switch (result)
52+
{
53+
case Wpf.Ui.Controls.MessageBoxResult.Primary:
54+
this.logger.LogInformation($"User chose to open settings for target '{name}'.");
55+
break;
56+
case Wpf.Ui.Controls.MessageBoxResult.Secondary:
57+
this.logger.LogWarning($"Settings for target '{name}' are invalid, but user chose to run as is.");
58+
return settings;
59+
default:
60+
this.logger.LogWarning($"Settings for target '{name}' are invalid, and user cancelled the operation.");
61+
return null;
62+
}
63+
}
64+
65+
// 設定が存在しない、または検証エラーがある場合、設定ダイアログを開く
66+
if (!await presentationService.OpenAllSettingsDialogAsync(name, false))
67+
{
68+
return null;
69+
}
70+
71+
return scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<TargetSettings>>().Get(name);
72+
}
73+
2074
private async Task OpenTargetWindowCoreAsync(IntPtr targetWindowHandle, string name)
2175
{
2276
using var l = await this.asyncLock.EnterAsync();
77+
var settings = await GetSettingsAsync(name);
78+
if (settings is null)
79+
{
80+
return;
81+
}
82+
2383
var scope = provider.CreateScope();
2484
try
2585
{
26-
27-
var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<UserSettings>>();
86+
var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<CommonSettings>>();
2887
var presentationService = scope.ServiceProvider.GetRequiredService<IPresentationService>();
2988
var processInfo = scope.ServiceProvider.GetRequiredService<IProcessInfoStoreInternal>();
3089
processInfo.SetTargetProcess(targetWindowHandle, name);
3190

32-
if (!options.Value.Targets.ContainsKey(name) && !await presentationService.OpenAllSettingsDialogAsync(name))
33-
{
34-
return;
35-
}
36-
37-
var window = options.Value.Common.ViewMode switch
91+
var window = options.Value.ViewMode switch
3892
{
3993
ViewMode.Capture => await presentationService.OpenCaptureMainWindowAsync(),
4094
ViewMode.Overlay => await presentationService.OpenOverlayMainWindowAsync(),

WindowTranslator/Modules/Settings/AllSettingsViewModel.cs

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Kamishibai;
1111
using Microsoft.Extensions.Configuration;
1212
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Logging;
1314
using Microsoft.Extensions.Options;
1415
using Microsoft.Win32;
1516
using PropertyTools.DataAnnotations;
@@ -46,6 +47,7 @@ sealed partial class AllSettingsViewModel : ObservableObject, IDisposable
4647
private readonly IAutoTargetStore autoTargetStore;
4748
private readonly IEnumerable<ITargetSettingsValidator> validators;
4849
private readonly IMainWindowModule mainWindowModule;
50+
private readonly ILogger<AllSettingsViewModel> logger;
4951
private readonly IConfigurationRoot? rootConfig;
5052
private readonly string target;
5153
[ObservableProperty]
@@ -109,7 +111,9 @@ public AllSettingsViewModel(
109111
[Inject] IConfiguration config,
110112
[Inject] IEnumerable<ITargetSettingsValidator> validators,
111113
[Inject] IMainWindowModule mainWindowModule,
112-
string target)
114+
[Inject] ILogger<AllSettingsViewModel> logger,
115+
string target,
116+
bool? applyMode = null)
113117
{
114118
var items = provider.GetPlugins();
115119
var ocrModules = items.Where(p => typeof(IOcrModule).IsAssignableFrom(p.Type)).Select(Convert).ToArray();
@@ -128,7 +132,7 @@ public AllSettingsViewModel(
128132
.DefaultIfEmpty(new KeyValuePair<string, TargetSettings>(string.Empty, new()))
129133
.Select(t => new TargetSettingsViewModel(t.Key, sp, t.Value, ocrModules, translateModules, cacheModules))];
130134

131-
this.ApplyMode = !string.IsNullOrEmpty(target) && options.Value.Targets.ContainsKey(target);
135+
this.ApplyMode = applyMode ?? !string.IsNullOrEmpty(target) && options.Value.Targets.ContainsKey(target);
132136
if (this.Targets.FirstOrDefault(t => t.Name == target) is not { } selected)
133137
{
134138
selected = new(target, sp, options.Value.Targets.TryGetValue(string.Empty, out var d) ? d : new(), ocrModules, translateModules, cacheModules);
@@ -146,6 +150,7 @@ public AllSettingsViewModel(
146150
this.autoTargetStore = autoTargetStore;
147151
this.validators = validators;
148152
this.mainWindowModule = mainWindowModule;
153+
this.logger = logger;
149154
this.target = target;
150155
this.rootConfig = config as IConfigurationRoot;
151156
this.updateChecker.UpdateAvailable += UpdateChecker_UpdateAvailable;
@@ -257,33 +262,10 @@ public async Task SaveAsync(object window)
257262
var results = new Dictionary<string, IReadOnlyList<ValidateResult>>();
258263
foreach (var (name, target) in string.IsNullOrEmpty(this.target) ? settings.Targets.ToArray() : [new KeyValuePair<string, TargetSettings>(this.target, settings.Targets[this.target])])
259264
{
260-
var r = new List<ValidateResult>();
261-
if (target.Language.Source == target.Language.Target)
265+
var validationResults = await this.validators.ValidateAsync(target);
266+
if (validationResults.Any())
262267
{
263-
r.Add(ValidateResult.Invalid(Resources.TranslateLanguage, Resources.SameSourceTargetLanguage));
264-
}
265-
266-
if (!target.SelectedPlugins.TryGetValue(nameof(ITranslateModule), out var t) || string.IsNullOrEmpty(t))
267-
{
268-
r.Add(ValidateResult.Invalid(Resources.TranslateModule, """
269-
翻訳モジュールが選択されていません。
270-
「対象ごとの設定」→「全体設定」タブの「翻訳モジュール」を設定してください。
271-
"""));
272-
}
273-
if (!target.SelectedPlugins.TryGetValue(nameof(ICacheModule), out var c) || string.IsNullOrEmpty(c))
274-
{
275-
r.Add(ValidateResult.Invalid(Resources.CacheModule, """
276-
キャッシュモジュールが選択されていません。
277-
「対象ごとの設定」→「全体設定」タブの「キャッシュモジュール」を設定してください。
278-
"""));
279-
}
280-
foreach (var validator in this.validators)
281-
{
282-
r.Add(await validator.Validate(target));
283-
}
284-
if (r.Any(r => !r.IsValid))
285-
{
286-
results.Add(name, r.Where(r => !r.IsValid).ToArray());
268+
results.Add(name, validationResults);
287269
}
288270
}
289271
if (results.Any())
@@ -299,6 +281,7 @@ public async Task SaveAsync(object window)
299281
{
300282
return;
301283
}
284+
this.logger.LogWarning("Settings are invalid, but user chose to save them anyway.");
302285
}
303286

304287
// 値の保存

WindowTranslator/Modules/Startup/StartupViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public async Task OpenSettingsDialogAsync(string? target)
132132
{
133133
using var scope = this.serviceProvider.CreateScope();
134134
var ps = scope.ServiceProvider.GetRequiredService<IPresentationService>();
135-
await ps.OpenAllSettingsDialogAsync(target ?? string.Empty, Application.Current.MainWindow, new() { WindowStartupLocation = Kamishibai.WindowStartupLocation.CenterOwner });
135+
await ps.OpenAllSettingsDialogAsync(target ?? string.Empty, null, Application.Current.MainWindow, new() { WindowStartupLocation = Kamishibai.WindowStartupLocation.CenterOwner });
136136
}
137137

138138
[RelayCommand]

WindowTranslator/Program.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
builder.Services.Configure<CommonSettings>(builder.Configuration.GetSection(nameof(UserSettings.Common)));
157157
builder.Services.AddTransient(typeof(IConfigureNamedOptions<>), typeof(ConfigurePluginParam<>));
158158
builder.Services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigurePluginParam<>));
159+
builder.Services.AddTransient<IConfigureNamedOptions<TargetSettings>, ConfigureTargetSettings>();
159160
builder.Services.AddTransient<IConfigureOptions<TargetSettings>, ConfigureTargetSettings>();
160161
builder.Services.AddTransient<IConfigureOptions<LanguageOptions>, ConfigureLanguageOptions>();
161162
builder.Services.AddSingleton(_ => (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a"))!)!);
@@ -259,7 +260,7 @@ private static IConfigurationSection GetTargetSection(IConfigurationSection sect
259260
}
260261
}
261262

262-
class ConfigureTargetSettings(IConfiguration configuration, IProcessInfoStore store) : IConfigureOptions<TargetSettings>
263+
class ConfigureTargetSettings(IConfiguration configuration, IProcessInfoStore store) : IConfigureOptions<TargetSettings>, IConfigureNamedOptions<TargetSettings>
263264
{
264265
private readonly IConfiguration configuration = configuration.GetSection(nameof(UserSettings.Targets));
265266
private readonly IProcessInfoStore store = store;
@@ -273,6 +274,17 @@ public void Configure(TargetSettings options)
273274
}
274275
section.Bind(options);
275276
}
277+
278+
public void Configure(string? name, TargetSettings options)
279+
{
280+
name = (string.IsNullOrEmpty(name) ? this.store.Name : name) ?? string.Empty;
281+
var section = this.configuration.GetSection(name);
282+
if (!section.Exists())
283+
{
284+
section = this.configuration.GetSection(Options.DefaultName);
285+
}
286+
section.Bind(options);
287+
}
276288
}
277289

278290

0 commit comments

Comments
 (0)