From 7f695a4a9ea14c64441c736a8dd6499655633f44 Mon Sep 17 00:00:00 2001 From: Justin Swanson Date: Sun, 8 Dec 2019 18:19:36 -0600 Subject: [PATCH] Install/Compile views display ConfirmationInterventions in CPU area --- .../StatusFeed/IUserIntervention.cs | 20 ------- .../Interventions/AUserIntervention.cs | 30 ++++++++++ .../Interventions/ConfirmationIntervention.cs | 41 ++++++++++++++ .../Interventions/IUserIntervention.cs | 33 +++++++++++ Wabbajack.Common/Wabbajack.Common.csproj | 4 +- Wabbajack.Common/WorkQueue.cs | 14 ++++- Wabbajack.Lib/ABatchProcessor.cs | 13 ++++- .../Downloaders/LoversLabDownloader.cs | 10 ++-- Wabbajack.Lib/MO2Installer.cs | 1 - .../NexusApi/RequestNexusAuthorization.cs | 9 ++- .../ConfirmUpdateOfExistingInstall.cs | 26 ++------- Wabbajack/Themes/Styles.xaml | 2 + .../View Models/Installers/InstallerVM.cs | 20 ++++++- Wabbajack/View Models/MainWindowVM.cs | 3 - .../View Models/UserInterventionHandlers.cs | 16 +----- Wabbajack/Views/Compilers/CompilerView.xaml | 24 +++++++- .../Views/Installers/InstallationView.xaml | 55 ++++++++++++++++++- .../ConfirmationInterventionView.xaml | 49 +++++++++++++++++ .../ConfirmationInterventionView.xaml.cs | 28 ++++++++++ Wabbajack/Wabbajack.csproj | 10 ++++ 20 files changed, 328 insertions(+), 80 deletions(-) delete mode 100644 Wabbajack.Common/StatusFeed/IUserIntervention.cs create mode 100644 Wabbajack.Common/StatusFeed/Interventions/AUserIntervention.cs create mode 100644 Wabbajack.Common/StatusFeed/Interventions/ConfirmationIntervention.cs create mode 100644 Wabbajack.Common/StatusFeed/Interventions/IUserIntervention.cs create mode 100644 Wabbajack/Views/Interventions/ConfirmationInterventionView.xaml create mode 100644 Wabbajack/Views/Interventions/ConfirmationInterventionView.xaml.cs diff --git a/Wabbajack.Common/StatusFeed/IUserIntervention.cs b/Wabbajack.Common/StatusFeed/IUserIntervention.cs deleted file mode 100644 index 6285fc70..00000000 --- a/Wabbajack.Common/StatusFeed/IUserIntervention.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Wabbajack.Common.StatusFeed -{ - /// - /// Defines a message that requires user interaction. The user must perform some action - /// or make a choice. - /// - public interface IUserIntervention : IStatusMessage - { - /// - /// The user didn't make a choice, so this action should be aborted - /// - void Cancel(); - } -} diff --git a/Wabbajack.Common/StatusFeed/Interventions/AUserIntervention.cs b/Wabbajack.Common/StatusFeed/Interventions/AUserIntervention.cs new file mode 100644 index 00000000..2c33a010 --- /dev/null +++ b/Wabbajack.Common/StatusFeed/Interventions/AUserIntervention.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using ReactiveUI; + +namespace Wabbajack.Common +{ + public abstract class AUserIntervention : ReactiveObject, IUserIntervention + { + public DateTime Timestamp { get; } = DateTime.Now; + public abstract string ShortDescription { get; } + public abstract string ExtendedDescription { get; } + + private bool _handled; + public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); } + + public int CpuID { get; } = WorkQueue.CpuId; + + public abstract void Cancel(); + public ICommand CancelCommand { get; } + + public AUserIntervention() + { + CancelCommand = ReactiveCommand.Create(() => Cancel()); + } + } +} diff --git a/Wabbajack.Common/StatusFeed/Interventions/ConfirmationIntervention.cs b/Wabbajack.Common/StatusFeed/Interventions/ConfirmationIntervention.cs new file mode 100644 index 00000000..de097c6e --- /dev/null +++ b/Wabbajack.Common/StatusFeed/Interventions/ConfirmationIntervention.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using ReactiveUI; + +namespace Wabbajack.Common +{ + public abstract class ConfirmationIntervention : AUserIntervention + { + public enum Choice + { + Continue, + Abort + } + + private TaskCompletionSource _source = new TaskCompletionSource(); + public Task Task => _source.Task; + + public ICommand ConfirmCommand { get; } + + public ConfirmationIntervention() + { + ConfirmCommand = ReactiveCommand.Create(() => Confirm()); + } + + public override void Cancel() + { + Handled = true; + _source.SetResult(Choice.Abort); + } + + public void Confirm() + { + Handled = true; + _source.SetResult(Choice.Continue); + } + } +} diff --git a/Wabbajack.Common/StatusFeed/Interventions/IUserIntervention.cs b/Wabbajack.Common/StatusFeed/Interventions/IUserIntervention.cs new file mode 100644 index 00000000..1f2ce864 --- /dev/null +++ b/Wabbajack.Common/StatusFeed/Interventions/IUserIntervention.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ReactiveUI; +using Wabbajack.Common.StatusFeed; + +namespace Wabbajack.Common +{ + /// + /// Defines a message that requires user interaction. The user must perform some action + /// or make a choice. + /// + public interface IUserIntervention : IStatusMessage, IReactiveObject + { + /// + /// The user didn't make a choice, so this action should be aborted + /// + void Cancel(); + + /// + /// Whether the interaction has been handled and no longer needs attention + /// Note: This needs to be Reactive so that users can monitor its status + /// + bool Handled { get; } + + /// + /// WorkQueue job ID that is blocking on this intervention + /// + int CpuID { get; } + } +} diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj index db46d3b1..1eb44526 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -114,6 +114,7 @@ + @@ -122,8 +123,9 @@ + - + diff --git a/Wabbajack.Common/WorkQueue.cs b/Wabbajack.Common/WorkQueue.cs index 97a13a14..15400642 100644 --- a/Wabbajack.Common/WorkQueue.cs +++ b/Wabbajack.Common/WorkQueue.cs @@ -14,7 +14,10 @@ namespace Wabbajack.Common internal BlockingCollection Queue = new BlockingCollection(new ConcurrentStack()); - [ThreadStatic] private static int CpuId; + public const int UnassignedCpuId = -1; + + [ThreadStatic] private static int _cpuId = UnassignedCpuId; + public static int CpuId => _cpuId; internal static bool WorkerThread => CurrentQueue != null; [ThreadStatic] internal static WorkQueue CurrentQueue; @@ -24,6 +27,11 @@ namespace Wabbajack.Common public static List Threads { get; private set; } + // This is currently a lie, as it wires to the Utils singleton stream This is still good to have, + // so that logic related to a single WorkQueue can subscribe to this dummy member so that If/when we + // implement log messages in a non-singleton fashion, they will already be wired up properly. + public IObservable LogMessages => Utils.LogMessages; + public WorkQueue(int threadCount = 0) { StartThreads(threadCount == 0 ? Environment.ProcessorCount : threadCount); @@ -48,7 +56,7 @@ namespace Wabbajack.Common private void ThreadBody(int idx) { - CpuId = idx; + _cpuId = idx; CurrentQueue = this; while (true) @@ -67,7 +75,7 @@ namespace Wabbajack.Common Progress = progress, ProgressPercent = progress / 100f, Msg = msg, - ID = CpuId, + ID = _cpuId, IsWorking = isWorking }); } diff --git a/Wabbajack.Lib/ABatchProcessor.cs b/Wabbajack.Lib/ABatchProcessor.cs index c05b9bde..38998a87 100644 --- a/Wabbajack.Lib/ABatchProcessor.cs +++ b/Wabbajack.Lib/ABatchProcessor.cs @@ -1,9 +1,11 @@ using System; using System.IO; +using System.Reactive.Disposables; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; using Wabbajack.Common; +using Wabbajack.Common.StatusFeed; using Wabbajack.VirtualFileSystem; namespace Wabbajack.Lib @@ -15,6 +17,7 @@ namespace Wabbajack.Lib public void Dispose() { Queue?.Shutdown(); + _subs.Dispose(); } public Context VFS { get; private set; } @@ -38,6 +41,9 @@ namespace Wabbajack.Lib private Subject _queueStatus { get; } = new Subject(); public IObservable QueueStatus => _queueStatus; + private Subject _logMessages { get; } = new Subject(); + public IObservable LogMessages => _logMessages; + private Subject _isRunning { get; } = new Subject(); public IObservable IsRunning => _isRunning; @@ -46,6 +52,8 @@ namespace Wabbajack.Lib private int _configured; private int _started; + private readonly CompositeDisposable _subs = new CompositeDisposable(); + protected void ConfigureProcessor(int steps, int threads = 0) { if (1 == Interlocked.CompareExchange(ref _configured, 1, 1)) @@ -54,7 +62,10 @@ namespace Wabbajack.Lib } Queue = new WorkQueue(threads); UpdateTracker = new StatusUpdateTracker(steps); - Queue.Status.Subscribe(_queueStatus); + Queue.Status.Subscribe(_queueStatus) + .DisposeWith(_subs); + Queue.LogMessages.Subscribe(_logMessages) + .DisposeWith(_subs); UpdateTracker.Progress.Subscribe(_percentCompleted); UpdateTracker.StepName.Subscribe(_textStatus); VFS = new Context(Queue) { UpdateTracker = UpdateTracker }; diff --git a/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs b/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs index eb93bc46..3fcb5cda 100644 --- a/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs +++ b/Wabbajack.Lib/Downloaders/LoversLabDownloader.cs @@ -9,11 +9,8 @@ using System.Threading; using System.Threading.Tasks; using System.Web; using Wabbajack.Common; -using Wabbajack.Common.StatusFeed; -using Wabbajack.Common.StatusFeed.Errors; using Wabbajack.Lib.LibCefHelpers; using Wabbajack.Lib.Validation; -using Wabbajack.Lib.WebAutomation; using Xilium.CefGlue.Common; using File = Alphaleonis.Win32.Filesystem.File; @@ -183,7 +180,7 @@ namespace Wabbajack.Lib.Downloaders } } - public class RequestLoversLabLogin : AStatusMessage, IUserIntervention + public class RequestLoversLabLogin : AUserIntervention { public override string ShortDescription => "Getting LoversLab information"; public override string ExtendedDescription { get; } @@ -193,10 +190,13 @@ namespace Wabbajack.Lib.Downloaders public void Resume(Helpers.Cookie[] cookies) { + Handled = true; _source.SetResult(cookies); } - public void Cancel() + + public override void Cancel() { + Handled = true; _source.SetCanceled(); } } diff --git a/Wabbajack.Lib/MO2Installer.cs b/Wabbajack.Lib/MO2Installer.cs index f55e4d84..0002e2f9 100644 --- a/Wabbajack.Lib/MO2Installer.cs +++ b/Wabbajack.Lib/MO2Installer.cs @@ -10,7 +10,6 @@ using Wabbajack.Common; using Wabbajack.Lib.CompilationSteps.CompilationErrors; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; -using Wabbajack.Lib.StatusMessages; using Wabbajack.Lib.Validation; using Directory = Alphaleonis.Win32.Filesystem.Directory; using File = Alphaleonis.Win32.Filesystem.File; diff --git a/Wabbajack.Lib/NexusApi/RequestNexusAuthorization.cs b/Wabbajack.Lib/NexusApi/RequestNexusAuthorization.cs index 473c8e7c..fb9cffae 100644 --- a/Wabbajack.Lib/NexusApi/RequestNexusAuthorization.cs +++ b/Wabbajack.Lib/NexusApi/RequestNexusAuthorization.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Wabbajack.Common.StatusFeed; +using Wabbajack.Common; namespace Wabbajack.Lib.NexusApi { - public class RequestNexusAuthorization : AStatusMessage, IUserIntervention + public class RequestNexusAuthorization : AUserIntervention { public override string ShortDescription => "Getting User's Nexus API Key"; public override string ExtendedDescription { get; } @@ -17,10 +17,13 @@ namespace Wabbajack.Lib.NexusApi public void Resume(string apikey) { + Handled = true; _source.SetResult(apikey); } - public void Cancel() + + public override void Cancel() { + Handled = true; _source.SetCanceled(); } } diff --git a/Wabbajack.Lib/StatusMessages/ConfirmUpdateOfExistingInstall.cs b/Wabbajack.Lib/StatusMessages/ConfirmUpdateOfExistingInstall.cs index 97b51326..c7250bec 100644 --- a/Wabbajack.Lib/StatusMessages/ConfirmUpdateOfExistingInstall.cs +++ b/Wabbajack.Lib/StatusMessages/ConfirmUpdateOfExistingInstall.cs @@ -3,24 +3,16 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Wabbajack.Common.StatusFeed; +using Wabbajack.Common; -namespace Wabbajack.Lib.StatusMessages +namespace Wabbajack.Lib { - public class ConfirmUpdateOfExistingInstall : AStatusMessage, IUserIntervention + public class ConfirmUpdateOfExistingInstall : ConfirmationIntervention { - public enum Choice - { - Continue, - Abort - } - public string OutputFolder { get; set; } public string ModListName { get; set; } - public override string ShortDescription { get; } = "Do you want to overwrite existing files?"; - private TaskCompletionSource _source = new TaskCompletionSource(); - public Task Task => _source.Task; + public override string ShortDescription { get; } = "Do you want to overwrite existing files?"; public override string ExtendedDescription { @@ -29,15 +21,5 @@ namespace Wabbajack.Lib.StatusMessages Any files that exist in {OutputFolder} will be changed to match the files found in the {ModListName} modlist. This means that save games will be removed, custom settings will be reverted. Are you sure you wish to continue?"; } - - public void Cancel() - { - _source.SetResult(Choice.Abort); - } - - public void Confirm() - { - _source.SetResult(Choice.Continue); - } } } diff --git a/Wabbajack/Themes/Styles.xaml b/Wabbajack/Themes/Styles.xaml index 5eb76514..a7a388b7 100644 --- a/Wabbajack/Themes/Styles.xaml +++ b/Wabbajack/Themes/Styles.xaml @@ -43,6 +43,7 @@ #03DAC6 #0e8f83 #095952 + #042421 #cef0ed #8cede5 #00ffe7 @@ -115,6 +116,7 @@ + diff --git a/Wabbajack/View Models/Installers/InstallerVM.cs b/Wabbajack/View Models/Installers/InstallerVM.cs index 42f771c3..3794d854 100644 --- a/Wabbajack/View Models/Installers/InstallerVM.cs +++ b/Wabbajack/View Models/Installers/InstallerVM.cs @@ -19,7 +19,6 @@ using DynamicData; using DynamicData.Binding; using Wabbajack.Common.StatusFeed; using System.Reactive; -using Wabbajack.Common.StatusFeed; namespace Wabbajack { @@ -79,6 +78,9 @@ namespace Wabbajack private readonly ObservableAsPropertyHelper _TargetManager; public ModManager? TargetManager => _TargetManager.Value; + private readonly ObservableAsPropertyHelper _ActiveGlobalUserIntervention; + public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value; + // Command properties public IReactiveCommand ShowReportCommand { get; } public IReactiveCommand OpenReadmeCommand { get; } @@ -293,6 +295,22 @@ namespace Wabbajack InstallingMode = true; }) .DisposeWith(CompositeDisposable); + + // Listen for user interventions, and compile a dynamic list of all unhandled ones + var activeInterventions = this.WhenAny(x => x.Installer.ActiveInstallation) + .SelectMany(c => c?.LogMessages ?? Observable.Empty()) + .WhereCastable() + .ToObservableChangeSet() + .AutoRefresh(i => i.Handled) + .Filter(i => !i.Handled) + .AsObservableList(); + + // Find the top intervention /w no CPU ID to be marked as "global" + _ActiveGlobalUserIntervention = activeInterventions.Connect() + .Filter(x => x.CpuID == WorkQueue.UnassignedCpuId) + .QueryWhenChanged(query => query.FirstOrDefault()) + .ObserveOnGuiThread() + .ToProperty(this, nameof(ActiveGlobalUserIntervention)); } private void ShowReport() diff --git a/Wabbajack/View Models/MainWindowVM.cs b/Wabbajack/View Models/MainWindowVM.cs index 3301181b..3746b3db 100644 --- a/Wabbajack/View Models/MainWindowVM.cs +++ b/Wabbajack/View Models/MainWindowVM.cs @@ -12,9 +12,6 @@ using System.Windows.Threading; using Wabbajack.Common; using Wabbajack.Common.StatusFeed; using Wabbajack.Lib; -using Wabbajack.Lib.Downloaders; -using Wabbajack.Lib.NexusApi; -using Wabbajack.Lib.StatusMessages; namespace Wabbajack { diff --git a/Wabbajack/View Models/UserInterventionHandlers.cs b/Wabbajack/View Models/UserInterventionHandlers.cs index 83f0a2a1..bcc5d941 100644 --- a/Wabbajack/View Models/UserInterventionHandlers.cs +++ b/Wabbajack/View Models/UserInterventionHandlers.cs @@ -8,10 +8,8 @@ using System.Windows; using System.Windows.Threading; using ReactiveUI; using Wabbajack.Common; -using Wabbajack.Common.StatusFeed; using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; -using Wabbajack.Lib.StatusMessages; namespace Wabbajack { @@ -54,22 +52,10 @@ namespace Wabbajack MainWindow.ActivePane = oldPane; } - public void Handle(ConfirmUpdateOfExistingInstall msg) - { - var result = MessageBox.Show(msg.ExtendedDescription, msg.ShortDescription, MessageBoxButton.OKCancel); - if (result == MessageBoxResult.OK) - msg.Confirm(); - else - msg.Cancel(); - } - public async Task Handle(IUserIntervention msg) { switch (msg) { - case ConfirmUpdateOfExistingInstall c: - Handle(c); - break; case RequestNexusAuthorization c: await WrapBrowserJob(msg, async (vm, cancel) => { @@ -84,6 +70,8 @@ namespace Wabbajack c.Resume(data); }); break; + case ConfirmationIntervention c: + break; default: throw new NotImplementedException($"No handler for {msg}"); } diff --git a/Wabbajack/Views/Compilers/CompilerView.xaml b/Wabbajack/Views/Compilers/CompilerView.xaml index 51dbf30d..5bff7e3a 100644 --- a/Wabbajack/Views/Compilers/CompilerView.xaml +++ b/Wabbajack/Views/Compilers/CompilerView.xaml @@ -2,6 +2,7 @@ x:Class="Wabbajack.CompilerView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:common="clr-namespace:Wabbajack.Common;assembly=Wabbajack.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:local="clr-namespace:Wabbajack" @@ -241,12 +242,29 @@ Margin="5" Visibility="{Binding Compiling, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}"> - + - + - + + + + + + + + + + diff --git a/Wabbajack/Views/Installers/InstallationView.xaml b/Wabbajack/Views/Installers/InstallationView.xaml index 0872b3c5..26edeeff 100644 --- a/Wabbajack/Views/Installers/InstallationView.xaml +++ b/Wabbajack/Views/Installers/InstallationView.xaml @@ -2,8 +2,10 @@ x:Class="Wabbajack.InstallationView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:common="clr-namespace:Wabbajack.Common;assembly=Wabbajack.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks" + xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib" xmlns:local="clr-namespace:Wabbajack" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance local:InstallerVM}" @@ -344,12 +346,59 @@ Margin="5,0,5,5" Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}"> - + - + - + + + + + + + + + + + + + diff --git a/Wabbajack/Views/Interventions/ConfirmationInterventionView.xaml b/Wabbajack/Views/Interventions/ConfirmationInterventionView.xaml new file mode 100644 index 00000000..328ec7e7 --- /dev/null +++ b/Wabbajack/Views/Interventions/ConfirmationInterventionView.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + +