/// <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);
        }
Exemple #4
0
        /// <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);
            }
        }
Exemple #5
0
        /// <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);
        }