/// <summary> /// コンストラクタ。 /// </summary> /// <param name="canModify"> /// 再生や音声保存に関わる設定値の変更可否状態値。 /// </param> /// <param name="config">設定値。</param> /// <param name="appConfig">アプリ設定値。</param> /// <param name="uiConfig">UI設定値。</param> /// <param name="lastStatus">直近のアプリ状態値の設定先。</param> public TalkTextReplaceConfigViewModel( IReadOnlyReactiveProperty <bool> canModify, IReadOnlyReactiveProperty <TalkTextReplaceConfig> config, IReadOnlyReactiveProperty <AppConfig> appConfig, IReadOnlyReactiveProperty <UIConfig> uiConfig, IReactiveProperty <IAppStatus> lastStatus) : base(canModify, config) { ValidateArgumentNull(appConfig, nameof(appConfig)); ValidateArgumentNull(uiConfig, nameof(uiConfig)); ValidateArgumentNull(lastStatus, nameof(lastStatus)); this.AppConfig = appConfig; this.LastStatus = lastStatus; // 選択中タブインデックス this.SelectedTabIndex = this.MakeInnerPropertyOf(uiConfig, c => c.TalkTextReplaceConfigTabIndex); // 内包 ViewModel のセットアップ this.SetupViewModels(); // ファイル作成設定有効化コマンド表示状態 this.IsFileMakingCommandVisible = new[] { appConfig .ObserveInnerProperty(c => c.IsTextFileForceMaking) .DistinctUntilChanged(), appConfig .ObserveInnerProperty(c => c.IsExoFileMaking) .DistinctUntilChanged(), } .CombineLatestValuesAreAllFalse() .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); this.IsFileMakingCommandInvisible = this.IsFileMakingCommandVisible .Inverse() .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // ファイル作成設定有効化コマンド this.FileMakingCommand = this.MakeCommand <string>( this.ExecuteFileMakingCommand, this.CanModify, this.IsFileMakingCommandVisible); }
/// <summary> /// UI設定周りのセットアップを行う。 /// </summary> /// <param name="uiConfig">UI設定値。</param> private void SetupUIConfig(IReadOnlyReactiveProperty <UIConfig> uiConfig) { // 設定変更時に選択中キャラ別スタイル反映 Observable .CombineLatest( this.VisibleCharaStyles, uiConfig .ObserveInnerProperty(c => c.ExoCharaVoiceroidId) .DistinctUntilChanged(), (vcs, id) => vcs.FirstOrDefault(s => s.VoiceroidId == id) ?? vcs.First()) .DistinctUntilChanged() .Subscribe(s => this.SelectedCharaStyle.Value = s) .AddTo(this.CompositeDisposable); // 選択中キャラ別スタイル変更時処理 this.SelectedCharaStyle .Where(s => s != null) .Subscribe(s => uiConfig.Value.ExoCharaVoiceroidId = s.VoiceroidId) .AddTo(this.CompositeDisposable); this.SelectedCharaStyle .Where(s => s == null) .ObserveOnUIDispatcher() .Subscribe( _ => this.SelectedCharaStyle.Value = this.BaseConfig.Value.CharaStyles.First( s => s.VoiceroidId == uiConfig.Value.ExoCharaVoiceroidId)) .AddTo(this.CompositeDisposable); }
/// <summary> /// コンストラクタ。 /// </summary> /// <param name="canModify"> /// 再生や音声保存に関わる設定値の変更可否状態値。 /// </param> /// <param name="config">設定値。</param> /// <param name="appConfig">アプリ設定値。</param> /// <param name="uiConfig">UI設定値。</param> /// <param name="lastStatus">直近のアプリ状態値の設定先。</param> /// <param name="openFileDialogService">ファイル選択ダイアログサービス。</param> public ExoConfigViewModel( IReadOnlyReactiveProperty <bool> canModify, IReadOnlyReactiveProperty <ExoConfig> config, IReadOnlyReactiveProperty <AppConfig> appConfig, IReadOnlyReactiveProperty <UIConfig> uiConfig, IReactiveProperty <IAppStatus> lastStatus, IOpenFileDialogService openFileDialogService) : base(canModify, config) { ValidateArgumentNull(appConfig, nameof(appConfig)); ValidateArgumentNull(uiConfig, nameof(uiConfig)); ValidateArgumentNull(lastStatus, nameof(lastStatus)); ValidateArgumentNull(openFileDialogService, nameof(openFileDialogService)); this.AppConfig = appConfig; this.LastStatus = lastStatus; // 選択中タブインデックス this.SelectedTabIndex = this.MakeInnerPropertyOf(uiConfig, c => c.ExoConfigTabIndex); // 共通設定 this.Common = this.MakeConfigProperty(c => c.Common); // キャラ別スタイル設定コレクション var charaStyles = this.MakeReadOnlyConfigProperty( c => c.CharaStyles, notifyOnSameValue: true); // 表示状態のキャラ別スタイル設定コレクション this.VisibleCharaStyles = Observable .CombineLatest( appConfig.ObserveInnerProperty(c => c.VoiceroidVisibilities), charaStyles.Select(s => s.Count()).DistinctUntilChanged(), (vv, _) => vv.SelectVisibleOf(charaStyles.Value)) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // キャラ別スタイル設定選択コマンド実行可能状態 // 表示状態のキャラ別スタイル設定が2つ以上あれば選択可能 this.IsSelectCharaStyleCommandExecutable = this.VisibleCharaStyles .Select(vcs => vcs.Count >= 2) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // キャラ別スタイル設定選択コマンドのチップテキスト this.SelectCharaStyleCommandTip = this.VisibleCharaStyles .Select(_ => this.MakeSelectCharaStyleCommandTip()) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // 最適表示列数 // 6キャラ単位で列数を増やす this.VisibleCharaStylesColumnCount = this.VisibleCharaStyles .Select(vp => Math.Min(Math.Max(1, (vp.Count + 5) / 6), 3)) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // 選択中キャラ別スタイル this.SelectedCharaStyle = new ReactiveProperty <ExoCharaStyle>(this.VisibleCharaStyles.Value.First()) .AddTo(this.CompositeDisposable); // UI設定周りのセットアップ this.SetupUIConfig(uiConfig); // 選択中キャラ別スタイル ViewModel 作成 this.SelectedCharaStyleViewModel = new ExoCharaStyleViewModel( this.CanModify, this.SelectedCharaStyle, uiConfig, this.LastStatus, openFileDialogService) .AddTo(this.CompositeDisposable); // ファイル作成設定有効化コマンド表示状態 this.IsFileMakingCommandInvisible = this.MakeInnerReadOnlyPropertyOf(this.AppConfig, c => c.IsExoFileMaking); this.IsFileMakingCommandVisible = this.IsFileMakingCommandInvisible .Inverse() .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // ファイル作成設定有効化コマンド this.FileMakingCommand = this.MakeCommand( this.ExecuteFileMakingCommand, this.CanModify, this.IsFileMakingCommandVisible); // キャラ別スタイル設定選択コマンドコレクション(要素数 10 固定) this.SelectCharaStyleCommands = new ReadOnlyCollection <ICommand>( Enumerable.Range(0, 10) .Select( index => this.MakeCommand( () => this.ExecuteSelectCharaStyleCommand(index), this.IsSelectCharaStyleCommandExecutable, this.VisibleCharaStyles .Select(vcs => index < vcs.Count) .DistinctUntilChanged())) .ToArray()); // 前方/後方キャラ別スタイル設定選択コマンド this.SelectPreviousCharaStyleCommand = this.MakeCommand( this.ExecuteSelectPreviousCharaStyleCommand, this.IsSelectCharaStyleCommandExecutable); this.SelectNextCharaStyleCommand = this.MakeCommand( this.ExecuteSelectNextCharaStyleCommand, this.IsSelectCharaStyleCommandExecutable); }
/// <summary> /// UI設定周りのセットアップを行う。 /// </summary> /// <param name="uiConfig">UI設定値。</param> /// <param name="processes">VOICEROIDプロセスコレクション。</param> private void SetupUIConfig( IReadOnlyReactiveProperty <UIConfig> uiConfig, IReadOnlyCollection <IProcess> processes) { // 設定変更時に選択中プロセス反映 Observable .CombineLatest( this.VisibleProcesses, uiConfig .ObserveInnerProperty(c => c.VoiceroidId) .DistinctUntilChanged(), (vp, id) => vp.FirstOrDefault(p => p.Id == id) ?? vp.First()) .DistinctUntilChanged() .Subscribe(p => this.SelectedProcess.Value = p) .AddTo(this.CompositeDisposable); // 選択中プロセス変更時処理 this.SelectedProcess .Where(p => p != null) .Subscribe(p => uiConfig.Value.VoiceroidId = p.Id) .AddTo(this.CompositeDisposable); this.SelectedProcess .Where(p => p == null) .ObserveOnUIDispatcher() .Subscribe( _ => this.SelectedProcess.Value = processes.First(p => p.Id == uiConfig.Value.VoiceroidId)) .AddTo(this.CompositeDisposable); // 実行ファイルパス反映用デリゲート Action <VoiceroidId, string> pathSetter = (id, path) => { // パスが有効な場合のみ反映する if (!string.IsNullOrEmpty(path) && File.Exists(path)) { uiConfig.Value.VoiceroidExecutablePathes[id].Path = path; } }; // UI設定変更時に実行ファイルパスを反映する uiConfig .Subscribe( c => { foreach (var process in processes) { pathSetter(process.Id, process.ExecutablePath); } }) .AddTo(this.CompositeDisposable); // VOICEROIDプロセスの実行ファイルパスが判明したらUI設定に反映する foreach (var process in processes) { var id = process.Id; // 現在値を設定 pathSetter(id, process.ExecutablePath); // 変更時に反映する process .ObserveProperty(p => p.ExecutablePath) .Subscribe(path => pathSetter(id, path)) .AddTo(this.CompositeDisposable); } }
/// <summary> /// コンストラクタ。 /// </summary> /// <param name="processes">VOICEROIDプロセスコレクション。</param> /// <param name="canUseConfig">各設定値を利用可能な状態であるか否か。</param> /// <param name="talkTextReplaceConfig">トークテキスト置換設定値。</param> /// <param name="exoConfig">AviUtl拡張編集ファイル用設定値。</param> /// <param name="appConfig">アプリ設定値。</param> /// <param name="uiConfig">UI設定値。</param> /// <param name="lastStatus">直近のアプリ状態値の設定先。</param> /// <param name="canModifyNotifier"> /// 設定変更可能な状態であるか否かの設定先。 /// </param> /// <param name="windowActivateService">ウィンドウアクティブ化サービス。</param> /// <param name="voiceroidActionService"> /// VOICEROIDプロセスアクションサービス。 /// </param> /// <param name="aviUtlFileDropService"> /// AviUtl拡張編集ファイルドロップサービス。 /// </param> public VoiceroidViewModel( IReadOnlyCollection <IProcess> processes, IReadOnlyReactiveProperty <bool> canUseConfig, IReadOnlyReactiveProperty <TalkTextReplaceConfig> talkTextReplaceConfig, IReadOnlyReactiveProperty <ExoConfig> exoConfig, IReadOnlyReactiveProperty <AppConfig> appConfig, IReadOnlyReactiveProperty <UIConfig> uiConfig, IReactiveProperty <IAppStatus> lastStatus, IReactiveProperty <bool> canModifyNotifier, IWindowActivateService windowActivateService, IVoiceroidActionService voiceroidActionService, IAviUtlFileDropService aviUtlFileDropService) { ValidateArgumentNull(processes, nameof(processes)); ValidateArgumentNull(canUseConfig, nameof(canUseConfig)); ValidateArgumentNull(talkTextReplaceConfig, nameof(talkTextReplaceConfig)); ValidateArgumentNull(exoConfig, nameof(exoConfig)); ValidateArgumentNull(appConfig, nameof(appConfig)); ValidateArgumentNull(uiConfig, nameof(uiConfig)); ValidateArgumentNull(lastStatus, nameof(lastStatus)); ValidateArgumentNull(canModifyNotifier, nameof(canModifyNotifier)); ValidateArgumentNull(windowActivateService, nameof(windowActivateService)); ValidateArgumentNull(voiceroidActionService, nameof(voiceroidActionService)); ValidateArgumentNull(aviUtlFileDropService, nameof(aviUtlFileDropService)); this.LastStatus = lastStatus; this.WindowActivateService = windowActivateService; this.VoiceroidActionService = voiceroidActionService; this.IsTextClearing = this.MakeInnerPropertyOf(appConfig, c => c.IsTextClearing); this.VoiceroidExecutablePathes = this.MakeInnerPropertyOf( uiConfig, c => c.VoiceroidExecutablePathes, notifyOnSameValue: true); // 表示状態のVOICEROIDプロセスコレクション this.VisibleProcesses = appConfig .ObserveInnerProperty(c => c.VoiceroidVisibilities) .Select(vv => vv.SelectVisibleOf(processes)) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // VOICEROID選択コマンド実行可能状態 // 表示状態のVOICEROIDプロセスが2つ以上あれば選択可能 this.IsSelectVoiceroidCommandExecutable = this.VisibleProcesses .Select(vp => vp.Count >= 2) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // VOICEROID選択コマンドのチップテキスト this.SelectVoiceroidCommandTip = this.VisibleProcesses .Select(_ => this.MakeSelectVoiceroidCommandTip()) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // 最適表示列数 // 6プロセス単位で列数を増やす this.VisibleProcessesColumnCount = this.VisibleProcesses .Select(vp => Math.Min(Math.Max(1, (vp.Count + 5) / 6), 3)) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // 選択中VOICEROIDプロセス this.SelectedProcess = new ReactiveProperty <IProcess>(this.VisibleProcesses.Value.First()) .AddTo(this.CompositeDisposable); // UI設定周りのセットアップ this.SetupUIConfig(uiConfig, processes); // 選択プロセス状態 this.IsProcessStartup = this .ObserveSelectedProcessProperty(p => p.IsStartup) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); this.IsProcessRunning = this .ObserveSelectedProcessProperty(p => p.IsRunning) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); this.IsProcessPlaying = this .ObserveSelectedProcessProperty(p => p.IsPlaying) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); this.IsProcessExecutable = Observable .CombineLatest( this.SelectedProcess, uiConfig.ObserveInnerProperty(c => c.VoiceroidExecutablePathes), (p, pathes) => { var path = (p == null) ? null : pathes[p.Id]?.Path; return(!string.IsNullOrEmpty(path) && File.Exists(path)); }) .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); var processSaving = this .ObserveSelectedProcessProperty(p => p.IsSaving) .DistinctUntilChanged(); var processDialogShowing = this .ObserveSelectedProcessProperty(p => p.IsDialogShowing) .DistinctUntilChanged(); // トークテキスト this.TalkText = new ReactiveProperty <string>("").AddTo(this.CompositeDisposable); this.TalkTextLengthLimit = new ReactiveProperty <int>(TextComponent.TextLengthLimit) .AddTo(this.CompositeDisposable); this.IsTalkTextTabAccepted = this.MakeInnerPropertyOf(appConfig, c => c.IsTabAccepted); // アイドル状態設定先 var idle = new ReactiveProperty <bool>(true).AddTo(this.CompositeDisposable); this.IsIdle = idle; // 音声保存コマンド処理中フラグ設定先 var saveCommandBusy = new ReactiveProperty <bool>(false).AddTo(this.CompositeDisposable); // トークテキストファイルドロップコマンド処理中フラグ設定先 var dropTalkTextFileCommandBusy = new ReactiveProperty <bool>(false).AddTo(this.CompositeDisposable); // 本体側のテキストを使う設定 this.UseTargetText = this.MakeInnerPropertyOf( appConfig, c => c.UseTargetText, new[] { canUseConfig, this.IsIdle } .CombineLatestValuesAreAllTrue() .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable)); // - 本体側のテキストを使う設定が有効 // または // - 音声保存処理中かつ保存成功時クリア設定が有効 // ならばトークテキスト編集不可 this.IsTalkTextEditable = new[] { this.UseTargetText, new[] { saveCommandBusy, this.IsTextClearing } .CombineLatestValuesAreAllTrue() .DistinctUntilChanged(), } .CombineLatest(flags => flags.Any(f => f)) .Inverse() .ToReadOnlyReactiveProperty() .AddTo(this.CompositeDisposable); // VOICEROID選択コマンドコレクション(要素数 10 固定) this.SelectVoiceroidCommands = new ReadOnlyCollection <ICommand>( Enumerable.Range(0, 10) .Select( index => this.MakeCommand( () => this.ExecuteSelectVoiceroidCommand(index), this.IsSelectVoiceroidCommandExecutable, this.VisibleProcesses .Select(vp => index < vp.Count) .DistinctUntilChanged())) .ToArray()); // 前方/後方VOICEROID選択コマンド this.SelectPreviousVoiceroidCommand = this.MakeCommand( this.ExecuteSelectPreviousVoiceroidCommand, this.IsSelectVoiceroidCommandExecutable); this.SelectNextVoiceroidCommand = this.MakeCommand( this.ExecuteSelectNextVoiceroidCommand, this.IsSelectVoiceroidCommandExecutable); // 起動/終了コマンド this.RunExitCommand = this.MakeAsyncCommand( this.ExecuteRunExitCommand, canUseConfig, this.IsIdle, this.IsProcessStartup.Inverse(), processSaving.Inverse(), processDialogShowing.Inverse()); // 再生/停止コマンド var playStopCommandHolder = new AsyncPlayStopCommandHolder( new[] { canUseConfig, this.IsIdle, this.IsProcessRunning, processSaving.Inverse(), processDialogShowing.Inverse(), dropTalkTextFileCommandBusy.Inverse(), } .CombineLatestValuesAreAllTrue() .DistinctUntilChanged() .ObserveOnUIDispatcher(), () => this.SelectedProcess.Value, () => talkTextReplaceConfig.Value.VoiceReplaceItems, () => this.TalkText.Value, () => appConfig.Value.UseTargetText); this.PlayStopCommand = playStopCommandHolder.Command; playStopCommandHolder.Result .Where(r => r != null) .Subscribe(async r => await this.OnPlayStopCommandExecuted(r)) .AddTo(this.CompositeDisposable); // 保存コマンド var saveCommandHolder = new AsyncSaveCommandHolder( new[] { canUseConfig, this.IsIdle, this.IsProcessRunning, processSaving.Inverse(), processDialogShowing.Inverse(), new[] { this.TalkText .Select(t => !string.IsNullOrWhiteSpace(t)) .DistinctUntilChanged(), this.UseTargetText, // 敢えて空白文を保存したいことはまず無いと思われるので、 // 誤クリック抑止の意味も込めて空白文は送信不可とする。 // ただし本体側の文章を使う場合は空白文でも保存可能とする。 // ↓のコメントを外すと(可能ならば)空白文送信保存可能になる。 //this // .ObserveSelectedProcessProperty(p => p.CanSaveBlankText) // .DistinctUntilChanged(), } .CombineLatest(flags => flags.Any(f => f)) .DistinctUntilChanged(), dropTalkTextFileCommandBusy.Inverse(), } .CombineLatestValuesAreAllTrue() .DistinctUntilChanged() .ObserveOnUIDispatcher(), () => this.SelectedProcess.Value, () => talkTextReplaceConfig.Value, () => exoConfig.Value, () => appConfig.Value, () => this.TalkText.Value, aviUtlFileDropService); this.SaveCommand = saveCommandHolder.Command; saveCommandHolder.IsBusy .Subscribe(f => saveCommandBusy.Value = f) .AddTo(this.CompositeDisposable); saveCommandHolder.Result .Where(r => r != null) .Subscribe(async r => await this.OnSaveCommandExecuted(r)) .AddTo(this.CompositeDisposable); // 再生も音声保存もしていない時をアイドル状態とみなす new[] { playStopCommandHolder.IsBusy, saveCommandHolder.IsBusy, } .CombineLatestValuesAreAllFalse() .DistinctUntilChanged() .Subscribe( f => { idle.Value = f; // アイドル状態なら設定変更可能とする canModifyNotifier.Value = f; }) .AddTo(this.CompositeDisposable); // トークテキスト用ファイルドラッグオーバーコマンド this.DragOverTalkTextFileCommand = this.MakeCommand <DragEventArgs>( this.ExecuteDragOverTalkTextFileCommand, this.IsTalkTextEditable); // トークテキスト用ファイルドロップコマンド var dropTalkTextFileCommandHolder = new AsyncCommandHolder <DragEventArgs>( this.IsTalkTextEditable, this.ExecuteDropTalkTextFileCommand); this.DropTalkTextFileCommand = dropTalkTextFileCommandHolder.Command; dropTalkTextFileCommandHolder.IsBusy .Subscribe(f => dropTalkTextFileCommandBusy.Value = f) .AddTo(this.CompositeDisposable); }