From de9e21f0d166f2ebd68cc8dc4fc947a818fff469 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sat, 9 Nov 2019 14:20:32 -0600 Subject: [PATCH] FilePickerVM Removed most logic from FilePicker.xaml in favor of a VM --- Wabbajack/View Models/CompilerVM.cs | 65 +++++--- Wabbajack/View Models/FilePickerVM.cs | 164 +++++++++++++++++++++ Wabbajack/View Models/InstallerVM.cs | 62 ++++---- Wabbajack/Views/CompilerView.xaml | 21 +-- Wabbajack/Views/FilePicker.xaml | 14 +- Wabbajack/Views/FilePicker.xaml.cs | 205 +------------------------- Wabbajack/Views/InstallationView.xaml | 16 +- Wabbajack/Wabbajack.csproj | 1 + 8 files changed, 257 insertions(+), 291 deletions(-) create mode 100644 Wabbajack/View Models/FilePickerVM.cs diff --git a/Wabbajack/View Models/CompilerVM.cs b/Wabbajack/View Models/CompilerVM.cs index 0eefd80e..960323f3 100644 --- a/Wabbajack/View Models/CompilerVM.cs +++ b/Wabbajack/View Models/CompilerVM.cs @@ -1,4 +1,5 @@ -using ReactiveUI; +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.IO; @@ -25,8 +26,7 @@ namespace Wabbajack [Reactive] public string ModListName { get; set; } - [Reactive] - public string Location { get; set; } + public FilePickerVM Location { get; } [Reactive] public bool UIReady { get; set; } = true; @@ -37,8 +37,7 @@ namespace Wabbajack [Reactive] public string Summary { get; set; } = "Description (700 characters max)"; - [Reactive] - public string ImagePath { get; set; } + public FilePickerVM ImagePath { get; } private readonly ObservableAsPropertyHelper _Image; public BitmapImage Image => _Image.Value; @@ -46,28 +45,50 @@ namespace Wabbajack [Reactive] public string Website { get; set; } - [Reactive] - public string ReadMeText { get; set; } + public FilePickerVM ReadMeText { get; } [Reactive] public string HTMLReport { get; set; } - [Reactive] - public string DownloadLocation { get; set; } + public FilePickerVM DownloadLocation { get; } public IReactiveCommand BeginCommand { get; } public CompilerVM(MainWindowVM mainWindowVM, string source) { this.MWVM = mainWindowVM; - this.Location = source; + this.Location = new FilePickerVM() + { + TargetPath = source, + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.File, + }; + this.DownloadLocation = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.Folder, + }; + this.ImagePath = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.File, + Filters = + { + new CommonFileDialogFilter("Banner image", "*.png") + } + }; + this.ReadMeText = new FilePickerVM() + { + PathType = FilePickerVM.PathTypeOptions.File, + DoExistsCheck = true, + }; this.BeginCommand = ReactiveCommand.CreateFromTask( execute: this.ExecuteBegin, canExecute: this.WhenAny(x => x.UIReady) .ObserveOnGuiThread()); - this._Image = this.WhenAny(x => x.ImagePath) + this._Image = this.WhenAny(x => x.ImagePath.TargetPath) .Select(path => { if (string.IsNullOrWhiteSpace(path)) return UIUtils.BitmapImageFromResource("Wabbajack.Resources.Banner_Dark.png"); @@ -86,16 +107,16 @@ namespace Wabbajack this.AuthorName = settings.Author; this.ModListName = settings.ModListName; this.Summary = settings.Description; - this.ReadMeText = settings.Readme; - this.ImagePath = settings.SplashScreen; + this.ReadMeText.TargetPath = settings.Readme; + this.ImagePath.TargetPath = settings.SplashScreen; this.Website = settings.Website; if (!string.IsNullOrWhiteSpace(settings.DownloadLocation)) { - this.DownloadLocation = settings.DownloadLocation; + this.DownloadLocation.TargetPath = settings.DownloadLocation; } if (!string.IsNullOrWhiteSpace(settings.Location)) { - this.Location = settings.Location; + this.Location.TargetPath = settings.Location; } this.MWVM.Settings.SaveSignal .Subscribe(_ => @@ -103,11 +124,11 @@ namespace Wabbajack settings.Author = this.AuthorName; settings.ModListName = this.ModListName; settings.Description = this.Summary; - settings.Readme = this.ReadMeText; - settings.SplashScreen = this.ImagePath; + settings.Readme = this.ReadMeText.TargetPath; + settings.SplashScreen = this.ImagePath.TargetPath; settings.Website = this.Website; - settings.Location = this.Location; - settings.DownloadLocation = this.DownloadLocation; + settings.Location = this.Location.TargetPath; + settings.DownloadLocation = this.DownloadLocation.TargetPath; }) .DisposeWith(this.CompositeDisposable); } @@ -125,7 +146,7 @@ namespace Wabbajack this.ModListName = this.MOProfile; var tmp_compiler = new Compiler(this.Mo2Folder); - this.DownloadLocation = tmp_compiler.MO2DownloadsFolder; + this.DownloadLocation.TargetPath = tmp_compiler.MO2DownloadsFolder; } private async Task ExecuteBegin() @@ -138,9 +159,9 @@ namespace Wabbajack ModListName = this.ModListName, ModListAuthor = this.AuthorName, ModListDescription = this.Summary, - ModListImage = this.ImagePath, + ModListImage = this.ImagePath.TargetPath, ModListWebsite = this.Website, - ModListReadme = this.ReadMeText, + ModListReadme = this.ReadMeText.TargetPath, }; await Task.Run(() => { diff --git a/Wabbajack/View Models/FilePickerVM.cs b/Wabbajack/View Models/FilePickerVM.cs new file mode 100644 index 00000000..2f9aeda7 --- /dev/null +++ b/Wabbajack/View Models/FilePickerVM.cs @@ -0,0 +1,164 @@ +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using Wabbajack.Lib; + +namespace Wabbajack +{ + public class FilePickerVM : ViewModel + { + public enum PathTypeOptions + { + Off, + Either, + File, + Folder + } + + public object Parent { get; } + + [Reactive] + public ICommand SetTargetPathCommand { get; set; } + + [Reactive] + public string TargetPath { get; set; } + + [Reactive] + public string PromptTitle { get; set; } + + [Reactive] + public PathTypeOptions PathType { get; set; } + + [Reactive] + public bool DoExistsCheck { get; set; } + + [Reactive] + public IObservable AdditionalError { get; set; } + + private readonly ObservableAsPropertyHelper _Exists; + public bool Exists => _Exists.Value; + + private readonly ObservableAsPropertyHelper _InError; + public bool InError => _InError.Value; + + private readonly ObservableAsPropertyHelper _ErrorTooltip; + public string ErrorTooltip => _ErrorTooltip.Value; + + public List Filters { get; } = new List(); + + public FilePickerVM(object parentVM = null) + { + this.Parent = parentVM; + this.SetTargetPathCommand = ConstructTypicalPickerCommand(); + + // Check that file exists + this._Exists = Observable.Interval(TimeSpan.FromSeconds(3)) + .FilterSwitch( + Observable.CombineLatest( + this.WhenAny(x => x.PathType), + this.WhenAny(x => x.DoExistsCheck), + resultSelector: (type, doExists) => type != PathTypeOptions.Off && doExists)) + .Unit() + // Also do it when fields change + .Merge(this.WhenAny(x => x.PathType).Unit()) + .Merge(this.WhenAny(x => x.DoExistsCheck).Unit()) + .CombineLatest( + this.WhenAny(x => x.DoExistsCheck), + this.WhenAny(x => x.PathType), + this.WhenAny(x => x.TargetPath) + .Throttle(TimeSpan.FromMilliseconds(200)), + resultSelector: (_, DoExists, Type, Path) => (DoExists, Type, Path)) + // Refresh exists + .Select(t => + { + if (!t.DoExists) return true; + switch (t.Type) + { + case PathTypeOptions.Either: + return File.Exists(t.Path) || Directory.Exists(t.Path); + case PathTypeOptions.File: + return File.Exists(t.Path); + case PathTypeOptions.Folder: + return Directory.Exists(t.Path); + default: + return true; + } + }) + .DistinctUntilChanged() + .ObserveOn(RxApp.MainThreadScheduler) + .ToProperty(this, nameof(this.Exists)); + + this._InError = Observable.CombineLatest( + this.WhenAny(x => x.Exists), + this.WhenAny(x => x.AdditionalError) + .Select(x => x ?? Observable.Return(ErrorResponse.Success)) + .Switch() + .Select(err => !err?.Succeeded ?? false), + resultSelector: (exist, err) => !exist || err) + .ToProperty(this, nameof(this.InError)); + + this._ErrorTooltip = Observable.CombineLatest( + this.WhenAny(x => x.Exists) + .Select(exists => exists ? default(string) : "Path does not exist"), + this.WhenAny(x => x.AdditionalError) + .Select(x => x ?? Observable.Return(ErrorResponse.Success)) + .Switch(), + resultSelector: (exists, err) => + { + if ((!err?.Succeeded ?? false) + && !string.IsNullOrWhiteSpace(err.Reason)) + { + return err.Reason; + } + return exists; + }) + .ToProperty(this, nameof(this.ErrorTooltip)); + } + + public ICommand ConstructTypicalPickerCommand() + { + return ReactiveCommand.Create( + execute: () => + { + string dirPath; + if (File.Exists(this.TargetPath)) + { + dirPath = System.IO.Path.GetDirectoryName(this.TargetPath); + } + else + { + dirPath = this.TargetPath; + } + var dlg = new CommonOpenFileDialog + { + Title = this.PromptTitle, + IsFolderPicker = this.PathType == PathTypeOptions.Folder, + InitialDirectory = this.TargetPath, + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = this.TargetPath, + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = false, + ShowPlacesList = true, + }; + foreach (var filter in this.Filters) + { + dlg.Filters.Add(filter); + } + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + this.TargetPath = dlg.FileName; + }); + } + } +} diff --git a/Wabbajack/View Models/InstallerVM.cs b/Wabbajack/View Models/InstallerVM.cs index e0f38c2d..45dc09ec 100644 --- a/Wabbajack/View Models/InstallerVM.cs +++ b/Wabbajack/View Models/InstallerVM.cs @@ -49,17 +49,9 @@ namespace Wabbajack [Reactive] public bool InstallingMode { get; set; } - [Reactive] - public string Location { get; set; } + public FilePickerVM Location { get; } - private readonly ObservableAsPropertyHelper _LocationError; - public IErrorResponse LocationError => _LocationError.Value; - - [Reactive] - public string DownloadLocation { get; set; } - - private readonly ObservableAsPropertyHelper _DownloadLocationError; - public IErrorResponse DownloadLocationError => _DownloadLocationError.Value; + public FilePickerVM DownloadLocation { get; } private readonly ObservableAsPropertyHelper _ProgressPercent; public float ProgressPercent => _ProgressPercent.Value; @@ -98,15 +90,32 @@ namespace Wabbajack this.MWVM = mainWindowVM; this.ModListPath = source; + this.Location = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select Installation Directory", + }; + this.Location.AdditionalError = this.WhenAny(x => x.Location.TargetPath) + .Select(x => Utils.IsDirectoryPathValid(x)); + this.DownloadLocation = new FilePickerVM() + { + DoExistsCheck = false, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select a location for MO2 downloads", + }; + this.DownloadLocation.AdditionalError = this.WhenAny(x => x.DownloadLocation.TargetPath) + .Select(x => Utils.IsDirectoryPathValid(x)); + // Load settings InstallationSettings settings = this.MWVM.Settings.InstallationSettings.TryCreate(source); - this.Location = settings.InstallationLocation; - this.DownloadLocation = settings.DownloadLocation; + this.Location.TargetPath = settings.InstallationLocation; + this.DownloadLocation.TargetPath = settings.DownloadLocation; this.MWVM.Settings.SaveSignal .Subscribe(_ => { - settings.InstallationLocation = this.Location; - settings.DownloadLocation = this.DownloadLocation; + settings.InstallationLocation = this.Location.TargetPath; + settings.DownloadLocation = this.DownloadLocation.TargetPath; }) .DisposeWith(this.CompositeDisposable); @@ -186,14 +195,6 @@ namespace Wabbajack resultSelector: (modList, mod, installing) => installing ? mod : modList) .ToProperty(this, nameof(this.Description)); - this._LocationError = this.WhenAny(x => x.Location) - .Select(x => Utils.IsDirectoryPathValid(x)) - .ToProperty(this, nameof(this.LocationError)); - - this._DownloadLocationError = this.WhenAny(x => x.DownloadLocation) - .Select(x => Utils.IsDirectoryPathValid(x)) - .ToProperty(this, nameof(this.DownloadLocationError)); - // Define commands this.ShowReportCommand = ReactiveCommand.Create(ShowReport); this.OpenReadmeCommand = ReactiveCommand.Create( @@ -205,13 +206,12 @@ namespace Wabbajack execute: this.ExecuteBegin, canExecute: Observable.CombineLatest( this.WhenAny(x => x.Installing), - this.WhenAny(x => x.LocationError), - this.WhenAny(x => x.DownloadLocationError), + this.WhenAny(x => x.Location.InError), + this.WhenAny(x => x.DownloadLocation.InError), resultSelector: (installing, loc, download) => { if (installing) return false; - return (loc?.Succeeded ?? false) - && (download?.Succeeded ?? false); + return !loc && !download; }) .ObserveOnGuiThread()); this.VisitWebsiteCommand = ReactiveCommand.Create( @@ -221,13 +221,13 @@ namespace Wabbajack .ObserveOnGuiThread()); // Have Installation location updates modify the downloads location if empty - this.WhenAny(x => x.Location) + this.WhenAny(x => x.Location.TargetPath) .Skip(1) // Don't do it initially .Subscribe(installPath => { - if (string.IsNullOrWhiteSpace(this.DownloadLocation)) + if (string.IsNullOrWhiteSpace(this.DownloadLocation.TargetPath)) { - this.DownloadLocation = Path.Combine(installPath, "downloads"); + this.DownloadLocation.TargetPath = Path.Combine(installPath, "downloads"); } }) .DisposeWith(this.CompositeDisposable); @@ -270,9 +270,9 @@ namespace Wabbajack { this.Installing = true; this.InstallingMode = true; - var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location) + var installer = new Installer(this.ModListPath, this.ModList.SourceModList, Location.TargetPath) { - DownloadFolder = DownloadLocation + DownloadFolder = DownloadLocation.TargetPath }; var th = new Thread(() => { diff --git a/Wabbajack/Views/CompilerView.xaml b/Wabbajack/Views/CompilerView.xaml index cef9d2bb..70cc47e9 100644 --- a/Wabbajack/Views/CompilerView.xaml +++ b/Wabbajack/Views/CompilerView.xaml @@ -62,11 +62,8 @@ Grid.Column="1" Width="534" HorizontalAlignment="Left" - DoExistsCheck="False" - Filter="Banner image|*.png" - IsEnabled="{Binding UIReady}" - PathType="File" - TargetPath="{Binding ImagePath}" /> + DataContext="{Binding ImagePath}" + IsEnabled="{Binding UIReady}" /> @@ -114,9 +111,7 @@ Text="Readme Path" ToolTip="Path to a readme file." /> @@ -167,11 +162,8 @@ + DataContext="{Binding Location}" />