mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #381 from Noggog/cores-usage-settings
Cores Usage Settings
This commit is contained in:
commit
d80292d3e8
@ -90,6 +90,11 @@ namespace Wabbajack.Common
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static void Error(string errMessage)
|
||||
{
|
||||
Log(errMessage);
|
||||
}
|
||||
|
||||
public static void Error(Exception ex, string extraMessage = null)
|
||||
{
|
||||
Log(new GenericException(ex, extraMessage));
|
||||
@ -932,7 +937,7 @@ namespace Wabbajack.Common
|
||||
{
|
||||
var startTime = DateTime.Now;
|
||||
var seconds = 2;
|
||||
var results = await Enumerable.Range(0, queue.ThreadCount)
|
||||
var results = await Enumerable.Range(0, queue.DesiredNumWorkers)
|
||||
.PMap(queue, idx =>
|
||||
{
|
||||
var random = new Random();
|
||||
|
@ -2,18 +2,21 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DynamicData;
|
||||
using Wabbajack.Common.StatusFeed;
|
||||
|
||||
[assembly: InternalsVisibleTo("Wabbajack.Test")]
|
||||
namespace Wabbajack.Common
|
||||
{
|
||||
public class WorkQueue : IDisposable
|
||||
{
|
||||
internal BlockingCollection<Func<Task>>
|
||||
Queue = new BlockingCollection<Func<Task>>(new ConcurrentStack<Func<Task>>());
|
||||
internal BlockingCollection<Func<Task>> Queue = new BlockingCollection<Func<Task>>(new ConcurrentStack<Func<Task>>());
|
||||
|
||||
public const int UnassignedCpuId = 0;
|
||||
|
||||
@ -27,40 +30,86 @@ namespace Wabbajack.Common
|
||||
private readonly Subject<CPUStatus> _Status = new Subject<CPUStatus>();
|
||||
public IObservable<CPUStatus> Status => _Status;
|
||||
|
||||
public List<Thread> Threads { get; private set; }
|
||||
private int _nextCpuID = 1; // Start at 1, as 0 is "Unassigned"
|
||||
internal Dictionary<int, Task> _tasks = new Dictionary<int, Task>();
|
||||
public int DesiredNumWorkers { get; private set; } = 0;
|
||||
|
||||
private CancellationTokenSource _cancel = new CancellationTokenSource();
|
||||
private CancellationTokenSource _shutdown = new CancellationTokenSource();
|
||||
|
||||
private CompositeDisposable _disposables = new CompositeDisposable();
|
||||
|
||||
// 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<IStatusMessage> LogMessages => Utils.LogMessages;
|
||||
|
||||
public WorkQueue(int threadCount = 0)
|
||||
private AsyncLock _lock = new AsyncLock();
|
||||
|
||||
private readonly BehaviorSubject<(int DesiredCPUs, int CurrentCPUs)> _cpuCountSubj = new BehaviorSubject<(int DesiredCPUs, int CurrentCPUs)>((0, 0));
|
||||
public IObservable<(int CurrentCPUs, int DesiredCPUs)> CurrentCpuCount => _cpuCountSubj;
|
||||
|
||||
private readonly Subject<IObservable<int>> _activeNumThreadsObservable = new Subject<IObservable<int>>();
|
||||
|
||||
internal const int PollMS = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a WorkQueue with the given number of threads
|
||||
/// </summary>
|
||||
/// <param name="threadCount">Number of threads for the WorkQueue to have. Null represents default, which is the Processor count of the machine.</param>
|
||||
public WorkQueue(int? threadCount = null)
|
||||
: this(Observable.Return(threadCount ?? Environment.ProcessorCount))
|
||||
{
|
||||
StartThreads(threadCount == 0 ? Environment.ProcessorCount : threadCount);
|
||||
}
|
||||
|
||||
private void StartThreads(int threadCount)
|
||||
/// <summary>
|
||||
/// Creates a WorkQueue whos number of threads is determined by the given observable
|
||||
/// </summary>
|
||||
/// <param name="numThreads">Driving observable that determines how many threads should be actively pulling jobs from the queue</param>
|
||||
public WorkQueue(IObservable<int> numThreads)
|
||||
{
|
||||
ThreadCount = threadCount;
|
||||
Threads = Enumerable.Range(1, threadCount)
|
||||
.Select(idx =>
|
||||
// Hook onto the number of active threads subject, and subscribe to it for changes
|
||||
_activeNumThreadsObservable
|
||||
// Select the latest driving observable
|
||||
.Select(x => x ?? Observable.Return(Environment.ProcessorCount))
|
||||
.Switch()
|
||||
.DistinctUntilChanged()
|
||||
// Add new threads if it increases
|
||||
.SelectTask(AddNewThreadsIfNeeded)
|
||||
.Subscribe()
|
||||
.DisposeWith(_disposables);
|
||||
// Set the incoming driving observable to be active
|
||||
SetActiveThreadsObservable(numThreads);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the driving observable that determines how many threads should be actively pulling jobs from the queue
|
||||
/// </summary>
|
||||
/// <param name="numThreads">Driving observable that determines how many threads should be actively pulling jobs from the queue</param>
|
||||
public void SetActiveThreadsObservable(IObservable<int> numThreads)
|
||||
{
|
||||
_activeNumThreadsObservable.OnNext(numThreads);
|
||||
}
|
||||
|
||||
private async Task AddNewThreadsIfNeeded(int desired)
|
||||
{
|
||||
using (await _lock.Wait())
|
||||
{
|
||||
DesiredNumWorkers = desired;
|
||||
while (DesiredNumWorkers > _tasks.Count)
|
||||
{
|
||||
var thread = new Thread(() => ThreadBody(idx).Wait());
|
||||
thread.Priority = ThreadPriority.BelowNormal;
|
||||
thread.IsBackground = true;
|
||||
thread.Name = string.Format("Wabbajack_Worker_{0}", idx);
|
||||
thread.Start();
|
||||
return thread;
|
||||
}).ToList();
|
||||
var cpuID = _nextCpuID++;
|
||||
_tasks[cpuID] = Task.Run(async () =>
|
||||
{
|
||||
await ThreadBody(cpuID);
|
||||
});
|
||||
}
|
||||
_cpuCountSubj.OnNext((_tasks.Count, DesiredNumWorkers));
|
||||
}
|
||||
}
|
||||
|
||||
public int ThreadCount { get; private set; }
|
||||
|
||||
private async Task ThreadBody(int idx)
|
||||
private async Task ThreadBody(int cpuID)
|
||||
{
|
||||
_cpuId.Value = idx;
|
||||
_cpuId.Value = cpuID;
|
||||
AsyncLocalCurrentQueue.Value = this;
|
||||
|
||||
try
|
||||
@ -68,23 +117,52 @@ namespace Wabbajack.Common
|
||||
while (true)
|
||||
{
|
||||
Report("Waiting", 0, false);
|
||||
if (_cancel.IsCancellationRequested) return;
|
||||
if (_shutdown.IsCancellationRequested) return;
|
||||
|
||||
|
||||
Func<Task> f;
|
||||
bool got;
|
||||
try
|
||||
{
|
||||
f = Queue.Take(_cancel.Token);
|
||||
got = Queue.TryTake(out f, PollMS, _shutdown.Token);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
await f();
|
||||
if (got)
|
||||
{
|
||||
await f();
|
||||
}
|
||||
|
||||
// Check if we're currently trimming threads
|
||||
if (DesiredNumWorkers >= _tasks.Count) continue;
|
||||
|
||||
// Noticed that we may need to shut down, lock and check again
|
||||
using (await _lock.Wait())
|
||||
{
|
||||
// Check if another thread shut down before this one and got us back to the desired amount already
|
||||
if (DesiredNumWorkers >= _tasks.Count) continue;
|
||||
|
||||
// Shutdown
|
||||
if (!_tasks.Remove(cpuID))
|
||||
{
|
||||
Utils.Error($"Could not remove thread from workpool with CPU ID {cpuID}");
|
||||
}
|
||||
Report("Shutting down", 0, false);
|
||||
_cpuCountSubj.OnNext((_tasks.Count, DesiredNumWorkers));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.Error(ex, "Error in WorkQueue thread.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Report(string msg, int progress, bool isWorking = true)
|
||||
@ -107,7 +185,8 @@ namespace Wabbajack.Common
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancel.Cancel();
|
||||
_shutdown.Cancel();
|
||||
_disposables.Dispose();
|
||||
Queue?.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -12,7 +13,7 @@ namespace Wabbajack.Lib
|
||||
{
|
||||
public abstract class ABatchProcessor : IBatchProcessor
|
||||
{
|
||||
public WorkQueue Queue { get; private set; }
|
||||
public WorkQueue Queue { get; } = new WorkQueue();
|
||||
|
||||
public Context VFS { get; private set; }
|
||||
|
||||
@ -47,13 +48,18 @@ namespace Wabbajack.Lib
|
||||
|
||||
private readonly CompositeDisposable _subs = new CompositeDisposable();
|
||||
|
||||
protected void ConfigureProcessor(int steps, int threads = 0)
|
||||
// WorkQueue settings
|
||||
public BehaviorSubject<bool> ManualCoreLimit = new BehaviorSubject<bool>(true);
|
||||
public BehaviorSubject<byte> MaxCores = new BehaviorSubject<byte>(byte.MaxValue);
|
||||
public BehaviorSubject<double> TargetUsagePercent = new BehaviorSubject<double>(1.0d);
|
||||
|
||||
protected void ConfigureProcessor(int steps, IObservable<int> numThreads = null)
|
||||
{
|
||||
if (1 == Interlocked.CompareExchange(ref _configured, 1, 1))
|
||||
{
|
||||
throw new InvalidDataException("Can't configure a processor twice");
|
||||
}
|
||||
Queue = new WorkQueue(threads);
|
||||
Queue.SetActiveThreadsObservable(numThreads);
|
||||
UpdateTracker = new StatusUpdateTracker(steps);
|
||||
Queue.Status.Subscribe(_queueStatus)
|
||||
.DisposeWith(_subs);
|
||||
@ -64,6 +70,32 @@ namespace Wabbajack.Lib
|
||||
VFS = new Context(Queue) { UpdateTracker = UpdateTracker };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recommended maximum number of threads that should be used for the current machine.
|
||||
/// This will either run a heavy processing job to do the measurement in the current folder, or refer to caches.
|
||||
/// </summary>
|
||||
/// <returns>Recommended maximum number of threads to use</returns>
|
||||
public async Task<int> RecommendQueueSize()
|
||||
{
|
||||
const ulong GB = (1024 * 1024 * 1024);
|
||||
// Most of the heavy lifting is done on the scratch disk, so we'll use the value from that disk
|
||||
var memory = Utils.GetMemoryStatus();
|
||||
// Assume roughly 2GB of ram needed to extract each 7zip archive, and then leave 2GB for the OS
|
||||
var based_on_memory = (memory.ullTotalPhys - (2 * GB)) / (2 * GB);
|
||||
var scratch_size = await RecommendQueueSize(Directory.GetCurrentDirectory());
|
||||
var result = Math.Min((int)based_on_memory, (int)scratch_size);
|
||||
Utils.Log($"Recommending a queue size of {result} based on disk performance, number of cores, and {((long)memory.ullTotalPhys).ToFileSizeString()} of system RAM");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recommended maximum number of threads that should be used for the current machine.
|
||||
/// This will either run a heavy processing job to do the measurement in the specified folder, or refer to caches.
|
||||
///
|
||||
/// If the folder does not exist, it will be created, and not cleaned up afterwards.
|
||||
/// </summary>
|
||||
/// <param name="folder"></param>
|
||||
/// <returns>Recommended maximum number of threads to use</returns>
|
||||
public static async Task<int> RecommendQueueSize(string folder)
|
||||
{
|
||||
if (!Directory.Exists(folder))
|
||||
@ -81,6 +113,49 @@ namespace Wabbajack.Lib
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an observable of the number of threads to be used
|
||||
///
|
||||
/// Takes in a recommended amount (based off measuring the machine capabilities), and combines that with user preferences stored in subjects.
|
||||
///
|
||||
/// As user preferences change, the number of threads gets recalculated in the resulting observable
|
||||
/// </summary>
|
||||
/// <param name="recommendedCount">Maximum recommended number of threads</param>
|
||||
/// <returns>Observable of number of threads to use based off recommendations and user preferences</returns>
|
||||
public IObservable<int> ConstructDynamicNumThreads(int recommendedCount)
|
||||
{
|
||||
return Observable.CombineLatest(
|
||||
ManualCoreLimit,
|
||||
MaxCores,
|
||||
TargetUsagePercent,
|
||||
(manual, max, target) => CalculateThreadsToUse(recommendedCount, manual, max, target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the number of threads to use, based off recommended values and user preferences
|
||||
/// </summary>
|
||||
public static int CalculateThreadsToUse(
|
||||
int recommendedCount,
|
||||
bool manual,
|
||||
byte manualMax,
|
||||
double targetUsage)
|
||||
{
|
||||
if (manual)
|
||||
{
|
||||
if (recommendedCount > manualMax)
|
||||
{
|
||||
Utils.Log($"Only using {manualMax} due to user preferences.");
|
||||
}
|
||||
return Math.Max(1, Math.Min(manualMax, recommendedCount));
|
||||
}
|
||||
else if (targetUsage < 1.0d && targetUsage >= 0d)
|
||||
{
|
||||
var ret = (int)Math.Ceiling(recommendedCount * targetUsage);
|
||||
return Math.Max(1, ret);
|
||||
}
|
||||
return recommendedCount;
|
||||
}
|
||||
|
||||
protected abstract Task<bool> _Begin(CancellationToken cancel);
|
||||
public Task<bool> Begin()
|
||||
{
|
||||
@ -109,5 +184,7 @@ namespace Wabbajack.Lib
|
||||
Queue?.Dispose();
|
||||
_isRunning.OnNext(false);
|
||||
}
|
||||
|
||||
public void Add(IDisposable disposable) => _subs.Add(disposable);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
@ -327,19 +327,6 @@ namespace Wabbajack.Lib
|
||||
*/
|
||||
}
|
||||
|
||||
public async Task<int> RecommendQueueSize()
|
||||
{
|
||||
const ulong GB = (1024 * 1024 * 1024);
|
||||
// Most of the heavy lifting is done on the scratch disk, so we'll use the value from that disk
|
||||
var memory = Utils.GetMemoryStatus();
|
||||
// Assume roughly 2GB of ram needed to extract each 7zip archive, and then leave 2GB for the OS
|
||||
var based_on_memory = (memory.ullTotalPhys - (2 * GB)) / (2 * GB);
|
||||
var scratch_size = await RecommendQueueSize(Directory.GetCurrentDirectory());
|
||||
var result = Math.Min((int)based_on_memory, (int)scratch_size);
|
||||
Utils.Log($"Recommending a queue size of {result} based on disk performance, number of cores, and {((long)memory.ullTotalPhys).ToFileSizeString()} of system RAM");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The user may already have some files in the OutputFolder. If so we can go through these and
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -48,11 +49,11 @@ namespace Wabbajack.Lib.Downloaders
|
||||
canExecute: IsLoggedIn.ObserveOnGuiThread());
|
||||
}
|
||||
|
||||
public ICommand TriggerLogin { get; }
|
||||
public ICommand ClearLogin { get; }
|
||||
public ReactiveCommand<Unit, Unit> TriggerLogin { get; }
|
||||
public ReactiveCommand<Unit, Unit> ClearLogin { get; }
|
||||
public IObservable<bool> IsLoggedIn => Utils.HaveEncryptedJsonObservable(_encryptedKeyName);
|
||||
public abstract string SiteName { get; }
|
||||
public virtual string MetaInfo { get; }
|
||||
public virtual IObservable<string> MetaInfo { get; }
|
||||
public abstract Uri SiteURL { get; }
|
||||
public virtual Uri IconUri { get; }
|
||||
|
||||
|
@ -2,19 +2,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders
|
||||
{
|
||||
public interface INeedsLogin
|
||||
{
|
||||
ICommand TriggerLogin { get; }
|
||||
ICommand ClearLogin { get; }
|
||||
ReactiveCommand<Unit, Unit> TriggerLogin { get; }
|
||||
ReactiveCommand<Unit, Unit> ClearLogin { get; }
|
||||
IObservable<bool> IsLoggedIn { get; }
|
||||
string SiteName { get; }
|
||||
string MetaInfo { get; }
|
||||
IObservable<string> MetaInfo { get; }
|
||||
Uri SiteURL { get; }
|
||||
Uri IconUri { get; }
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -24,14 +25,14 @@ namespace Wabbajack.Lib.Downloaders
|
||||
|
||||
public string SiteName => "Nexus Mods";
|
||||
|
||||
public string MetaInfo => "";
|
||||
public IObservable<string> MetaInfo => Observable.Return("");
|
||||
|
||||
public Uri SiteURL => new Uri("https://www.nexusmods.com");
|
||||
|
||||
public Uri IconUri => new Uri("https://www.nexusmods.com/favicon.ico");
|
||||
|
||||
public ICommand TriggerLogin { get; }
|
||||
public ICommand ClearLogin { get; }
|
||||
public ReactiveCommand<Unit, Unit> TriggerLogin { get; }
|
||||
public ReactiveCommand<Unit, Unit> ClearLogin { get; }
|
||||
|
||||
public NexusDownloader()
|
||||
{
|
||||
|
@ -82,7 +82,7 @@ namespace Wabbajack.Lib
|
||||
protected override async Task<bool> _Begin(CancellationToken cancel)
|
||||
{
|
||||
if (cancel.IsCancellationRequested) return false;
|
||||
ConfigureProcessor(19);
|
||||
ConfigureProcessor(19, ConstructDynamicNumThreads(await RecommendQueueSize()));
|
||||
UpdateTracker.Reset();
|
||||
UpdateTracker.NextStep("Gathering information");
|
||||
Info("Looking for other profiles");
|
||||
|
@ -45,7 +45,7 @@ namespace Wabbajack.Lib
|
||||
if (cancel.IsCancellationRequested) return false;
|
||||
var metric = Metrics.Send("begin_install", ModList.Name);
|
||||
|
||||
ConfigureProcessor(19, await RecommendQueueSize());
|
||||
ConfigureProcessor(19, ConstructDynamicNumThreads(await RecommendQueueSize()));
|
||||
var game = ModList.GameType.MetaData();
|
||||
|
||||
if (GameFolder == null)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using ReactiveUI;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Disposables;
|
||||
@ -9,6 +10,7 @@ namespace Wabbajack.Lib
|
||||
public class ViewModel : ReactiveObject, IDisposable
|
||||
{
|
||||
private readonly Lazy<CompositeDisposable> _compositeDisposable = new Lazy<CompositeDisposable>();
|
||||
[JsonIgnore]
|
||||
public CompositeDisposable CompositeDisposable => _compositeDisposable.Value;
|
||||
|
||||
public virtual void Dispose()
|
||||
|
@ -85,7 +85,7 @@ namespace Wabbajack.Lib
|
||||
|
||||
Info($"Starting Vortex compilation for {GameName} at {GamePath} with staging folder at {StagingFolder} and downloads folder at {DownloadsFolder}.");
|
||||
|
||||
ConfigureProcessor(12);
|
||||
ConfigureProcessor(12, ConstructDynamicNumThreads(await RecommendQueueSize()));
|
||||
UpdateTracker.Reset();
|
||||
|
||||
if (cancel.IsCancellationRequested) return false;
|
||||
|
@ -51,7 +51,7 @@ namespace Wabbajack.Lib
|
||||
}
|
||||
|
||||
if (cancel.IsCancellationRequested) return false;
|
||||
ConfigureProcessor(10, await RecommendQueueSize());
|
||||
ConfigureProcessor(10, ConstructDynamicNumThreads(await RecommendQueueSize()));
|
||||
Directory.CreateDirectory(DownloadFolder);
|
||||
|
||||
if (cancel.IsCancellationRequested) return false;
|
||||
|
@ -4,6 +4,10 @@
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<NoWarn>1701;1702; CS1998</NoWarn>
|
||||
<WarningsAsErrors>NU1605, CS4014</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CefSharp.Common">
|
||||
<Version>75.1.143</Version>
|
||||
|
96
Wabbajack.Test/ABatchProcessorTests.cs
Normal file
96
Wabbajack.Test/ABatchProcessorTests.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Wabbajack.Lib;
|
||||
|
||||
namespace Wabbajack.Test
|
||||
{
|
||||
[TestClass]
|
||||
public class ABatchProcessorTests
|
||||
{
|
||||
#region CalculateThreadsToUse
|
||||
[TestMethod]
|
||||
public void Manual_OverRecommended()
|
||||
{
|
||||
Assert.AreEqual(8, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: true,
|
||||
manualMax: byte.MaxValue,
|
||||
targetUsage: 1.0d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Manual_NeedsTrimming()
|
||||
{
|
||||
Assert.AreEqual(5, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: true,
|
||||
manualMax: 5,
|
||||
targetUsage: 1.0d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Manual_Zero()
|
||||
{
|
||||
Assert.AreEqual(1, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: true,
|
||||
manualMax: 0,
|
||||
targetUsage: 1.0d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Auto_Full()
|
||||
{
|
||||
Assert.AreEqual(8, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: false,
|
||||
manualMax: byte.MaxValue,
|
||||
targetUsage: 1.0d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Auto_Half()
|
||||
{
|
||||
Assert.AreEqual(4, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: false,
|
||||
manualMax: byte.MaxValue,
|
||||
targetUsage: 0.5d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Auto_Zero()
|
||||
{
|
||||
Assert.AreEqual(1, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: false,
|
||||
manualMax: byte.MaxValue,
|
||||
targetUsage: 0d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Auto_OverAllowed()
|
||||
{
|
||||
Assert.AreEqual(8, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: false,
|
||||
manualMax: byte.MaxValue,
|
||||
targetUsage: 2d));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Auto_UnderAllowed()
|
||||
{
|
||||
Assert.AreEqual(8, ABatchProcessor.CalculateThreadsToUse(
|
||||
recommendedCount: 8,
|
||||
manual: false,
|
||||
manualMax: byte.MaxValue,
|
||||
targetUsage: -2d));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -118,6 +118,7 @@
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ABatchProcessorTests.cs" />
|
||||
<Compile Include="ACompilerTest.cs" />
|
||||
<Compile Include="AsyncLockTests.cs" />
|
||||
<Compile Include="AVortexCompilerTest.cs" />
|
||||
@ -143,6 +144,7 @@
|
||||
<Compile Include="UtilsTests.cs" />
|
||||
<Compile Include="VortexTests.cs" />
|
||||
<Compile Include="WebAutomationTests.cs" />
|
||||
<Compile Include="WorkQueueTests.cs" />
|
||||
<Compile Include="zEditIntegrationTests.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
273
Wabbajack.Test/WorkQueueTests.cs
Normal file
273
Wabbajack.Test/WorkQueueTests.cs
Normal file
@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Wabbajack.Common;
|
||||
|
||||
namespace Wabbajack.Test
|
||||
{
|
||||
[TestClass]
|
||||
public class WorkQueueTests
|
||||
{
|
||||
#region DynamicNumThreads
|
||||
const int Large = 8;
|
||||
const int Medium = 6;
|
||||
const int Small = 4;
|
||||
public int PollMS => WorkQueue.PollMS * 5;
|
||||
|
||||
[TestMethod]
|
||||
public void DynamicNumThreads_Typical()
|
||||
{
|
||||
using (var queue = new WorkQueue())
|
||||
{
|
||||
Assert.AreEqual(Environment.ProcessorCount, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Environment.ProcessorCount, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DynamicNumThreads_Increased()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Small);
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Small, queue._tasks.Count);
|
||||
subj.OnNext(Large);
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DynamicNumThreads_EmptyObs()
|
||||
{
|
||||
using (var queue = new WorkQueue(Observable.Empty<int>()))
|
||||
{
|
||||
Assert.AreEqual(0, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(0, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DynamicNumThreads_Decreased()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Large);
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
subj.OnNext(Small);
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
// Tasks don't go down immediately
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
// After things re-poll, they should be cleaned
|
||||
await Task.Delay(PollMS * 2);
|
||||
Assert.AreEqual(Small, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DynamicNumThreads_IncreasedWhileWorking()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Small);
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Small, queue._tasks.Count);
|
||||
Enumerable.Range(0, Small).Do(_ => queue.QueueTask(() => tcs.Task));
|
||||
subj.OnNext(Large);
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
Task.Run(() => tcs.SetResult(true)).FireAndForget();
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
await Task.Delay(PollMS * 2);
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DynamicNumThreads_DecreasedWhileWorking()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Large);
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
Enumerable.Range(0, Large).Do(_ => queue.QueueTask(() => tcs.Task));
|
||||
subj.OnNext(Small);
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
// After things re-poll, they should still be working at max
|
||||
await Task.Delay(PollMS * 2);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
// Complete, repoll, and check again
|
||||
Task.Run(() => tcs.SetResult(true)).FireAndForget();
|
||||
await Task.Delay(PollMS * 2);
|
||||
Assert.AreEqual(Small, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DynamicNumThreads_IncreasedThenDecreased()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Small);
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Small, queue._tasks.Count);
|
||||
subj.OnNext(Large);
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
subj.OnNext(Small);
|
||||
// Still large number of threads, as not immediate
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
// After things re-poll, they should still be working at max
|
||||
await Task.Delay(PollMS * 2);
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Small, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DynamicNumThreads_DecreasedThenIncreased()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Large);
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
subj.OnNext(Small);
|
||||
Assert.AreEqual(Small, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
subj.OnNext(Large);
|
||||
// New threads allocated immediately
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
// After things re-poll, still here
|
||||
await Task.Delay(PollMS * 2);
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Known Deadlock Scenario
|
||||
/// <summary>
|
||||
/// Known "deadlock" scenario related to WorkQueue.
|
||||
///
|
||||
/// When a task is completed via a TaskCompletionSource, the current thread is "in charge" of running the continuation code that
|
||||
/// completing that task kicked off. The problem with this when related to WorkQueue is that it's an infinite while loop of continuation.
|
||||
///
|
||||
/// The solution to this is just make sure that any work done relating to WorkQueue be done within its own Task.Run() call, so that if it that thread
|
||||
/// "takes over" a workqueue loop, it doesn't matter as it was a threadpool thread anyway.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Deadlock()
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Large);
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Enumerable.Range(0, Large).Do(_ => queue.QueueTask(() => tcs.Task));
|
||||
// This call deadlocks, as the continuations is a WorkQueue while loop
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
});
|
||||
var completed = Task.WhenAny(Task.Delay(3000), task);
|
||||
Assert.ReferenceEquals(completed, task);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Known Parallel Work Collapse Pitfall
|
||||
/// <summary>
|
||||
/// Putting a single TCS completion source onto the WorkQueue will result in parallization collapse, where
|
||||
/// all work is being done by one actual thread. Similar to the deadlock scenario, this is just slightly different.
|
||||
///
|
||||
/// Since all worker tasks in charge of pulling off the queue were working on a single job driven by a single TCS,
|
||||
/// when that TCS completes, the one thread that completed it is in charge of all the continuation. All the continuation
|
||||
/// tasks happen to be all Tasks in charge of pulling off the queue. This results in one actual thread essentially calling a
|
||||
/// Task.WhenAll() on all of our queue.Take tasks. This means only one thread is now ping-ponging around doing the work, rather
|
||||
/// than our desired number of threads working in parallel.
|
||||
///
|
||||
/// This will happen even if the WorkQueue is backed by Threads, rather than Task.Run() calls. It's just the nature of how async
|
||||
/// continuation is wired to work.
|
||||
///
|
||||
/// Other notes:
|
||||
/// This seems to fail when run in the normal pipeline of unit tests. I think the timing gets interrupted by other tests?
|
||||
/// Disabled the test from being run automatically for now
|
||||
///
|
||||
/// TLDR: Don't put the same work completion source to be done on the queue multiple times.
|
||||
/// </summary>
|
||||
public async Task ThreadCoalescenceExample()
|
||||
{
|
||||
var subj = new BehaviorSubject<int>(Large);
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
object lockObj = new object();
|
||||
using (var queue = new WorkQueue(subj))
|
||||
{
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
|
||||
bool[] workStartedArray = new bool[Large];
|
||||
async Task Job(int num, bool[] b)
|
||||
{
|
||||
// Mark work started as soon as job started
|
||||
lock (lockObj)
|
||||
{
|
||||
b[num] = true;
|
||||
}
|
||||
// Do lots of hard work for 1 second
|
||||
Thread.Sleep(5000);
|
||||
};
|
||||
|
||||
// Do hard work in parallel
|
||||
Enumerable.Range(0, Large).Do(i => queue.QueueTask(() => Job(i, workStartedArray)));
|
||||
// Wait some time, so all jobs should be started
|
||||
await Task.Delay(2500);
|
||||
// Show that all jobs are started
|
||||
lock (lockObj)
|
||||
{
|
||||
Assert.AreEqual(Large, workStartedArray.Where(i => i).Count());
|
||||
}
|
||||
|
||||
await Task.Delay(15000);
|
||||
|
||||
// Start lots of jobs, all pinning from the same TCS
|
||||
Enumerable.Range(0, Large).Do(_ => queue.QueueTask(() => tcs.Task));
|
||||
// All 8 worker tasks are completed by the same TCS, but continued by the single Task
|
||||
// that kicked it off and is in charge of the continuation tasks.
|
||||
// Parallel worker Tasks have now coalesced into a single thread
|
||||
Task.Run(() => tcs.SetResult(true)).FireAndForget();
|
||||
Assert.AreEqual(Large, queue.DesiredNumWorkers);
|
||||
Assert.AreEqual(Large, queue._tasks.Count);
|
||||
|
||||
await Task.Delay(10000);
|
||||
|
||||
// Do a test to prove work isn't being done in parallel anymore
|
||||
var secondWorkStartedArray = new bool[Large];
|
||||
Enumerable.Range(0, Large).Do(i => queue.QueueTask(() => Job(i, secondWorkStartedArray)));
|
||||
// Wait some time, so all jobs should be started
|
||||
await Task.Delay(2500);
|
||||
// Show that only one job was started/worked on (by our one coalesced worker thread)
|
||||
lock (lockObj)
|
||||
{
|
||||
Assert.AreEqual(1, secondWorkStartedArray.Where(i => i).Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
35
Wabbajack/Converters/CommandConverter.cs
Normal file
35
Wabbajack/Converters/CommandConverter.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class CommandConverter : IBindingTypeConverter
|
||||
{
|
||||
public int GetAffinityForObjects(Type fromType, Type toType)
|
||||
{
|
||||
if (toType != typeof(ICommand)) return 0;
|
||||
if (fromType == typeof(ICommand)
|
||||
|| fromType == typeof(IReactiveCommand))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool TryConvert(object from, Type toType, object conversionHint, out object result)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
result = default(ICommand);
|
||||
return true;
|
||||
}
|
||||
result = from as ICommand;
|
||||
return result != null;
|
||||
}
|
||||
}
|
||||
}
|
25
Wabbajack/Converters/ConverterRegistration.cs
Normal file
25
Wabbajack/Converters/ConverterRegistration.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using Splat;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public static class ConverterRegistration
|
||||
{
|
||||
public static void Register()
|
||||
{
|
||||
Locator.CurrentMutable.RegisterConstant(
|
||||
new CommandConverter(),
|
||||
typeof(IBindingTypeConverter)
|
||||
);
|
||||
Locator.CurrentMutable.RegisterConstant(
|
||||
new IntDownCastConverter(),
|
||||
typeof(IBindingTypeConverter)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
35
Wabbajack/Converters/IntDownCastConverter.cs
Normal file
35
Wabbajack/Converters/IntDownCastConverter.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class IntDownCastConverter : IBindingTypeConverter
|
||||
{
|
||||
public int GetAffinityForObjects(Type fromType, Type toType)
|
||||
{
|
||||
if (toType == typeof(int) || fromType == typeof(int?)) return 1;
|
||||
if (fromType == typeof(ICommand)
|
||||
|| fromType == typeof(IReactiveCommand))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool TryConvert(object from, Type toType, object conversionHint, out object result)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
result = default(ICommand);
|
||||
return true;
|
||||
}
|
||||
result = from as ICommand;
|
||||
return result != null;
|
||||
}
|
||||
}
|
||||
}
|
75
Wabbajack/Extensions/IViewForExt.cs
Normal file
75
Wabbajack/Extensions/IViewForExt.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public static class IViewForExt
|
||||
{
|
||||
public static IReactiveBinding<TView, TViewModel, TProp> OneWayBindStrict<TViewModel, TView, TProp>(
|
||||
this TView view,
|
||||
TViewModel viewModel,
|
||||
Expression<Func<TViewModel, TProp>> vmProperty,
|
||||
Expression<Func<TView, TProp>> viewProperty)
|
||||
where TViewModel : class
|
||||
where TView : class, IViewFor
|
||||
{
|
||||
return view.OneWayBind(
|
||||
viewModel: viewModel,
|
||||
vmProperty: vmProperty,
|
||||
viewProperty: viewProperty);
|
||||
}
|
||||
|
||||
public static IReactiveBinding<TView, TViewModel, TOut> OneWayBindStrict<TViewModel, TView, TProp, TOut>(
|
||||
this TView view,
|
||||
TViewModel viewModel,
|
||||
Expression<Func<TViewModel, TProp>> vmProperty,
|
||||
Expression<Func<TView, TOut>> viewProperty,
|
||||
Func<TProp, TOut> selector)
|
||||
where TViewModel : class
|
||||
where TView : class, IViewFor
|
||||
{
|
||||
return view.OneWayBind(
|
||||
viewModel: viewModel,
|
||||
vmProperty: vmProperty,
|
||||
viewProperty: viewProperty,
|
||||
selector: selector);
|
||||
}
|
||||
|
||||
public static IReactiveBinding<TView, TViewModel, (object view, bool isViewModel)> BindStrict<TViewModel, TView, TProp>(
|
||||
this TView view,
|
||||
TViewModel viewModel,
|
||||
Expression<Func<TViewModel, TProp>> vmProperty,
|
||||
Expression<Func<TView, TProp>> viewProperty)
|
||||
where TViewModel : class
|
||||
where TView : class, IViewFor
|
||||
{
|
||||
return view.Bind(
|
||||
viewModel: viewModel,
|
||||
vmProperty: vmProperty,
|
||||
viewProperty: viewProperty);
|
||||
}
|
||||
|
||||
public static IReactiveBinding<TView, TViewModel, (object view, bool isViewModel)> BindStrict<TViewModel, TView, TVMProp, TVProp>(
|
||||
this TView view,
|
||||
TViewModel viewModel,
|
||||
Expression<Func<TViewModel, TVMProp>> vmProperty,
|
||||
Expression<Func<TView, TVProp>> viewProperty,
|
||||
Func<TVMProp, TVProp> vmToViewConverter,
|
||||
Func<TVProp, TVMProp> viewToVmConverter)
|
||||
where TViewModel : class
|
||||
where TView : class, IViewFor
|
||||
{
|
||||
return view.Bind(
|
||||
viewModel: viewModel,
|
||||
vmProperty: vmProperty,
|
||||
viewProperty: viewProperty,
|
||||
vmToViewConverter: vmToViewConverter,
|
||||
viewToVmConverter: viewToVmConverter);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Subjects;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
@ -10,7 +12,7 @@ using Wabbajack.Lib;
|
||||
namespace Wabbajack
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptOut)]
|
||||
public class MainSettings : ViewModel
|
||||
public class MainSettings
|
||||
{
|
||||
private static string _filename = "settings.json";
|
||||
|
||||
@ -20,6 +22,7 @@ namespace Wabbajack
|
||||
public double Width { get; set; }
|
||||
public InstallerSettings Installer { get; set; } = new InstallerSettings();
|
||||
public CompilerSettings Compiler { get; set; } = new CompilerSettings();
|
||||
public PerformanceSettings Performance { get; set; } = new PerformanceSettings();
|
||||
|
||||
private Subject<Unit> _saveSignal = new Subject<Unit>();
|
||||
[JsonIgnore]
|
||||
@ -70,6 +73,32 @@ namespace Wabbajack
|
||||
public VortexCompilationSettings VortexCompilation { get; } = new VortexCompilationSettings();
|
||||
}
|
||||
|
||||
[JsonObject(MemberSerialization.OptOut)]
|
||||
public class PerformanceSettings : ViewModel
|
||||
{
|
||||
private bool _Manual = false;
|
||||
public bool Manual { get => _Manual; set => this.RaiseAndSetIfChanged(ref _Manual, value); }
|
||||
|
||||
private byte _MaxCores = byte.MaxValue;
|
||||
public byte MaxCores { get => _MaxCores; set => this.RaiseAndSetIfChanged(ref _MaxCores, value); }
|
||||
|
||||
private double _TargetUsage = 1.0d;
|
||||
public double TargetUsage { get => _TargetUsage; set => this.RaiseAndSetIfChanged(ref _TargetUsage, value); }
|
||||
|
||||
public void AttachToBatchProcessor(ABatchProcessor processor)
|
||||
{
|
||||
processor.Add(
|
||||
this.WhenAny(x => x.Manual)
|
||||
.Subscribe(processor.ManualCoreLimit));
|
||||
processor.Add(
|
||||
this.WhenAny(x => x.MaxCores)
|
||||
.Subscribe(processor.MaxCores));
|
||||
processor.Add(
|
||||
this.WhenAny(x => x.TargetUsage)
|
||||
.Subscribe(processor.TargetUsagePercent));
|
||||
}
|
||||
}
|
||||
|
||||
public class CompilationModlistSettings
|
||||
{
|
||||
public string ModListName { get; set; }
|
||||
|
@ -7,6 +7,7 @@
|
||||
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:options="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<!-- Converters -->
|
||||
@ -52,6 +53,7 @@
|
||||
<Color x:Key="IntenseSecondary">#00ffe7</Color>
|
||||
<Color x:Key="Complementary">#C7FC86</Color>
|
||||
<Color x:Key="DarkComplementary">#8eb55e</Color>
|
||||
<Color x:Key="ComplementaryBackground">#4b6130</Color>
|
||||
<Color x:Key="IntenseComplementary">#abf74d</Color>
|
||||
<Color x:Key="Analogous1">#868CFC</Color>
|
||||
<Color x:Key="Analogous2">#F686FC</Color>
|
||||
@ -129,13 +131,12 @@
|
||||
<SolidColorBrush x:Key="ComplementaryBrush" Color="{StaticResource Complementary}" />
|
||||
<SolidColorBrush x:Key="DarkComplementaryBrush" Color="{StaticResource DarkComplementary}" />
|
||||
<SolidColorBrush x:Key="IntenseComplementaryBrush" Color="{StaticResource IntenseComplementary}" />
|
||||
<SolidColorBrush x:Key="ComplementaryBackgroundBrush" Color="{StaticResource ComplementaryBackground}" />
|
||||
<SolidColorBrush x:Key="Analogous1Brush" Color="{StaticResource Analogous1}" />
|
||||
<SolidColorBrush x:Key="Analogous2Brush" Color="{StaticResource Analogous2}" />
|
||||
<SolidColorBrush x:Key="Triadic1Brush" Color="{StaticResource Triadic1}" />
|
||||
<SolidColorBrush x:Key="Triadic2Brush" Color="{StaticResource Triadic2}" />
|
||||
|
||||
<SolidColorBrush x:Key="TabControlNormalBorderBrush" Color="{StaticResource WindowBackgroundColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="TabItemHotBackground" Color="{StaticResource HotColor}" />
|
||||
<SolidColorBrush x:Key="TabItemSelectedBackground" Color="{StaticResource LightBackgroundColor}" />
|
||||
<SolidColorBrush x:Key="TabItemHotBorderBrush" Color="{StaticResource HotColor}" />
|
||||
@ -251,307 +252,14 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
|
||||
</Style>
|
||||
|
||||
|
||||
<!-- TabControl -->
|
||||
<Style TargetType="{x:Type TabControl}">
|
||||
<Style.Resources>
|
||||
<Style x:Key="BottomStyle" TargetType="{x:Type FrameworkElement}">
|
||||
<Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="1" ScaleY="-1" />
|
||||
<SkewTransform AngleX="0" AngleY="0" />
|
||||
<RotateTransform Angle="0" />
|
||||
<TranslateTransform />
|
||||
</TransformGroup>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="RightStyle" TargetType="{x:Type FrameworkElement}">
|
||||
<Setter Property="LayoutTransform">
|
||||
<Setter.Value>
|
||||
<RotateTransform Angle="90" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="RoundedCornerLeftStyle" TargetType="{x:Type FrameworkElement}">
|
||||
<Setter Property="LayoutTransform">
|
||||
<Setter.Value>
|
||||
<RotateTransform Angle="-90" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleY="-1" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="LeftStyle" TargetType="{x:Type FrameworkElement}">
|
||||
<Setter Property="LayoutTransform">
|
||||
<Setter.Value>
|
||||
<RotateTransform Angle="-90" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DefaultStyle" TargetType="{x:Type FrameworkElement}">
|
||||
<Setter Property="LayoutTransform">
|
||||
<Setter.Value>
|
||||
<RotateTransform Angle="0" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TabItemFocusVisual">
|
||||
<Setter Property="Control.Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<Rectangle
|
||||
Margin="3,3,3,1"
|
||||
SnapsToDevicePixels="true"
|
||||
Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"
|
||||
StrokeDashArray="1 2"
|
||||
StrokeThickness="1" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type TabItem}">
|
||||
<Setter Property="FocusVisualStyle" Value="{StaticResource TabItemFocusVisual}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
|
||||
<Setter Property="Padding" Value="6,1,6,1" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TabControlNormalBorderBrush}" />
|
||||
<Setter Property="Background" Value="{StaticResource ButtonNormalBackground}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type TabItem}">
|
||||
<Grid x:Name="Container">
|
||||
<Grid
|
||||
x:Name="Grid"
|
||||
Margin="1,0,-12,0"
|
||||
SnapsToDevicePixels="true">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Grid.Column="0"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="3,0,0,0">
|
||||
<ContentPresenter
|
||||
x:Name="Content"
|
||||
Margin="6,1,6,1"
|
||||
HorizontalAlignment="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
|
||||
VerticalAlignment="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
|
||||
ContentSource="Header"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
<Path
|
||||
x:Name="RoundedCorner"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Width="24"
|
||||
Data="{StaticResource TabItemRoundedCorner}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="true">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource TabItemHotBackground}" />
|
||||
<Setter TargetName="RoundedCorner" Property="Fill" Value="{StaticResource TabItemHotBackground}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="true">
|
||||
<Setter Property="Panel.ZIndex" Value="1" />
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource TabItemSelectedBackground}" />
|
||||
<Setter TargetName="RoundedCorner" Property="Fill" Value="{StaticResource TabItemSelectedBackground}" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="false" />
|
||||
<Condition Property="IsMouseOver" Value="true" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource TabItemHotBorderBrush}" />
|
||||
<Setter TargetName="RoundedCorner" Property="Fill" Value="{StaticResource TabItemHotBorderBrush}" />
|
||||
</MultiTrigger>
|
||||
<Trigger Property="TabStripPlacement" Value="Bottom">
|
||||
<!--<Setter Property="BorderThickness" TargetName="Bd" Value="1,0,1,1"/>-->
|
||||
<Setter TargetName="RoundedCorner" Property="Style" Value="{StaticResource BottomStyle}" />
|
||||
<Setter TargetName="Bd" Property="Style" Value="{StaticResource BottomStyle}" />
|
||||
<Setter TargetName="Content" Property="Style" Value="{StaticResource BottomStyle}" />
|
||||
</Trigger>
|
||||
<Trigger Property="TabStripPlacement" Value="Left">
|
||||
<!--<Setter Property="BorderThickness" TargetName="Bd" Value="1,1,0,1"/>-->
|
||||
<Setter TargetName="Bd" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="Bd" Property="Grid.Column" Value="0" />
|
||||
<Setter TargetName="RoundedCorner" Property="Grid.Row" Value="1" />
|
||||
<Setter TargetName="RoundedCorner" Property="Grid.Column" Value="0" />
|
||||
<Setter TargetName="RoundedCorner" Property="Style" Value="{StaticResource RoundedCornerLeftStyle}" />
|
||||
<Setter TargetName="Bd" Property="Style" Value="{StaticResource LeftStyle}" />
|
||||
<Setter TargetName="Content" Property="Style" Value="{StaticResource DefaultStyle}" />
|
||||
<Setter TargetName="Grid" Property="Margin" Value="0,1,0,-16" />
|
||||
<Setter TargetName="Bd" Property="CornerRadius" Value="0 3 0 0" />
|
||||
<Setter TargetName="RoundedCorner" Property="Margin" Value="0,4,0,0" />
|
||||
</Trigger>
|
||||
<Trigger Property="TabStripPlacement" Value="Right">
|
||||
<!--<Setter Property="BorderThickness" TargetName="Bd" Value="0,1,1,1"/>-->
|
||||
<Setter TargetName="Bd" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="Bd" Property="Grid.Column" Value="1" />
|
||||
<Setter TargetName="RoundedCorner" Property="Grid.Row" Value="1" />
|
||||
<Setter TargetName="RoundedCorner" Property="Grid.Column" Value="1" />
|
||||
<Setter TargetName="RoundedCorner" Property="Style" Value="{StaticResource RightStyle}" />
|
||||
<Setter TargetName="Bd" Property="Style" Value="{StaticResource RightStyle}" />
|
||||
<Setter TargetName="Content" Property="Style" Value="{StaticResource DefaultStyle}" />
|
||||
<Setter TargetName="Grid" Property="Margin" Value="0,1,0,-12" />
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="true" />
|
||||
<Condition Property="TabStripPlacement" Value="Top" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Margin" Value="0,0,0,-1" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="true" />
|
||||
<Condition Property="TabStripPlacement" Value="Bottom" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Margin" Value="0,-1,0,0" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="true" />
|
||||
<Condition Property="TabStripPlacement" Value="Left" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Margin" Value="0,0,-1,0" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="true" />
|
||||
<Condition Property="TabStripPlacement" Value="Right" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Margin" Value="-1,0,0,0" />
|
||||
</MultiTrigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource TabItemDisabledBackground}" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource TabItemDisabledBorderBrush}" />
|
||||
<Setter TargetName="RoundedCorner" Property="Fill" Value="{StaticResource TabItemDisabledBackground}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Style.Resources>
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<!--<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>-->
|
||||
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
|
||||
<Setter Property="Padding" Value="4" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TabControlNormalBorderBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource TabItemSelectedBackground}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type TabControl}">
|
||||
<Grid
|
||||
ClipToBounds="true"
|
||||
KeyboardNavigation.TabNavigation="Local"
|
||||
SnapsToDevicePixels="true">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="ColumnDefinition0" />
|
||||
<ColumnDefinition x:Name="ColumnDefinition1" Width="0" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition x:Name="RowDefinition0" Height="Auto" />
|
||||
<RowDefinition x:Name="RowDefinition1" Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TabPanel
|
||||
x:Name="HeaderPanel"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Panel.ZIndex="1"
|
||||
IsItemsHost="true"
|
||||
KeyboardNavigation.TabIndex="1" />
|
||||
<Border
|
||||
x:Name="ContentPanel"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
KeyboardNavigation.DirectionalNavigation="Contained"
|
||||
KeyboardNavigation.TabIndex="2"
|
||||
KeyboardNavigation.TabNavigation="Local">
|
||||
<ContentPresenter
|
||||
x:Name="PART_SelectedContentHost"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
ContentSource="SelectedContent"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="TabStripPlacement" Value="Bottom">
|
||||
<Setter TargetName="HeaderPanel" Property="Grid.Row" Value="1" />
|
||||
<Setter TargetName="ContentPanel" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="RowDefinition0" Property="Height" Value="*" />
|
||||
<Setter TargetName="RowDefinition1" Property="Height" Value="Auto" />
|
||||
<!--<Setter Property="Margin" TargetName="HeaderPanel" Value="2,0,2,2"/>-->
|
||||
</Trigger>
|
||||
<Trigger Property="TabStripPlacement" Value="Left">
|
||||
<Setter TargetName="HeaderPanel" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="ContentPanel" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="HeaderPanel" Property="Grid.Column" Value="0" />
|
||||
<Setter TargetName="ContentPanel" Property="Grid.Column" Value="1" />
|
||||
<Setter TargetName="ColumnDefinition0" Property="Width" Value="Auto" />
|
||||
<Setter TargetName="ColumnDefinition1" Property="Width" Value="*" />
|
||||
<Setter TargetName="RowDefinition0" Property="Height" Value="*" />
|
||||
<Setter TargetName="RowDefinition1" Property="Height" Value="0" />
|
||||
<!--<Setter Property="Margin" TargetName="HeaderPanel" Value="2,2,0,2"/>-->
|
||||
</Trigger>
|
||||
<Trigger Property="TabStripPlacement" Value="Right">
|
||||
<Setter TargetName="HeaderPanel" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="ContentPanel" Property="Grid.Row" Value="0" />
|
||||
<Setter TargetName="HeaderPanel" Property="Grid.Column" Value="1" />
|
||||
<Setter TargetName="ContentPanel" Property="Grid.Column" Value="0" />
|
||||
<Setter TargetName="ColumnDefinition0" Property="Width" Value="*" />
|
||||
<Setter TargetName="ColumnDefinition1" Property="Width" Value="Auto" />
|
||||
<Setter TargetName="RowDefinition0" Property="Height" Value="*" />
|
||||
<Setter TargetName="RowDefinition1" Property="Height" Value="0" />
|
||||
<!--<Setter Property="Margin" TargetName="HeaderPanel" Value="0,2,2,2"/>-->
|
||||
<Setter TargetName="HeaderPanel" Property="Grid.Column" Value="1" />
|
||||
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style TargetType="{x:Type ItemsControl}">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
</Style>
|
||||
|
||||
<!-- ScrollBar -->
|
||||
<Style TargetType="{x:Type ScrollViewer}">
|
||||
<Setter Property="Background" Value="{DynamicResource ScrollViewerBackground}" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ScrollViewer}">
|
||||
@ -578,7 +286,8 @@
|
||||
CanHorizontallyScroll="False"
|
||||
CanVerticallyScroll="False"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Focusable="False" />
|
||||
<ScrollBar
|
||||
x:Name="PART_VerticalScrollBar"
|
||||
Grid.Row="0"
|
||||
@ -1456,8 +1165,54 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
|
||||
<Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}" />
|
||||
<Setter Property="Background" Value="{StaticResource DarkBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource ButtonBorder}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Padding" Value="1" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Button}">
|
||||
<Grid>
|
||||
<Border
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3">
|
||||
<ContentPresenter
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="true">
|
||||
<Setter Property="Background" Value="{StaticResource DarkHoverBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource DarkComplementaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource ComplementaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="true">
|
||||
<Setter Property="Background" Value="{StaticResource PressedButtonBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource ComplementaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource IntenseComplementaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{StaticResource DisabledButtonForeground}" />
|
||||
<Setter Property="Background" Value="{StaticResource DisabledButtonBackground}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style x:Key="LargeButtonStyle" TargetType="{x:Type Button}">
|
||||
<Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}" />
|
||||
<Setter Property="Background" Value="{StaticResource DarkBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource ButtonBorder}" />
|
||||
@ -2195,7 +1950,10 @@
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
SnapsToDevicePixels="true">
|
||||
<ScrollViewer Padding="{TemplateBinding Padding}" Focusable="false">
|
||||
<ScrollViewer
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
Focusable="false">
|
||||
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
@ -3750,4 +3508,10 @@
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
</Style>
|
||||
<Style BasedOn="{StaticResource MainFilePickerStyle}" TargetType="{x:Type local:FilePicker}" />
|
||||
|
||||
<Style BasedOn="{StaticResource MetroTabControl}" TargetType="TabControl" />
|
||||
<Style BasedOn="{StaticResource MetroTabItem}" TargetType="{x:Type TabItem}" />
|
||||
|
||||
<sys:Double x:Key="TabItemFontSize">18</sys:Double>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@ -15,14 +16,14 @@ namespace Wabbajack
|
||||
public interface IBackNavigatingVM : IReactiveObject
|
||||
{
|
||||
ViewModel NavigateBackTarget { get; set; }
|
||||
IReactiveCommand BackCommand { get; }
|
||||
ReactiveCommand<Unit, Unit> BackCommand { get; }
|
||||
}
|
||||
|
||||
public class BackNavigatingVM : ViewModel, IBackNavigatingVM
|
||||
{
|
||||
[Reactive]
|
||||
public ViewModel NavigateBackTarget { get; set; }
|
||||
public IReactiveCommand BackCommand { get; protected set; }
|
||||
public ReactiveCommand<Unit, Unit> BackCommand { get; protected set; }
|
||||
|
||||
public BackNavigatingVM(MainWindowVM mainWindowVM)
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ using Wabbajack.UI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class CompilerVM : ViewModel, IBackNavigatingVM
|
||||
public class CompilerVM : ViewModel, IBackNavigatingVM, ICpuStatusVM
|
||||
{
|
||||
public MainWindowVM MWVM { get; }
|
||||
|
||||
@ -48,7 +48,7 @@ namespace Wabbajack
|
||||
|
||||
public ObservableCollectionExtended<IStatusMessage> Log => MWVM.Log;
|
||||
|
||||
public IReactiveCommand BackCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> BackCommand { get; }
|
||||
public IReactiveCommand GoToModlistCommand { get; }
|
||||
public IReactiveCommand CloseWhenCompleteCommand { get; }
|
||||
public IReactiveCommand BeginCommand { get; }
|
||||
@ -67,6 +67,9 @@ namespace Wabbajack
|
||||
private readonly ObservableAsPropertyHelper<string> _progressTitle;
|
||||
public string ProgressTitle => _progressTitle.Value;
|
||||
|
||||
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
|
||||
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
|
||||
|
||||
public CompilerVM(MainWindowVM mainWindowVM)
|
||||
{
|
||||
MWVM = mainWindowVM;
|
||||
@ -256,6 +259,11 @@ namespace Wabbajack
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(ProgressTitle));
|
||||
|
||||
_CurrentCpuCount = this.WhenAny(x => x.Compiler.ActiveCompilation.Queue.CurrentCpuCount)
|
||||
.Switch()
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(CurrentCpuCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +180,7 @@ namespace Wabbajack
|
||||
|
||||
try
|
||||
{
|
||||
ActiveCompilation = new MO2Compiler(
|
||||
using (ActiveCompilation = new MO2Compiler(
|
||||
mo2Folder: Mo2Folder,
|
||||
mo2Profile: MOProfile,
|
||||
outputFile: outputFile)
|
||||
@ -192,13 +192,16 @@ namespace Wabbajack
|
||||
ModListWebsite = ModlistSettings.Website,
|
||||
ModListReadme = ModlistSettings.ReadmeIsWebsite ? ModlistSettings.ReadmeWebsite : ModlistSettings.ReadmeFilePath.TargetPath,
|
||||
ReadmeIsWebsite = ModlistSettings.ReadmeIsWebsite,
|
||||
};
|
||||
await ActiveCompilation.Begin();
|
||||
})
|
||||
{
|
||||
Parent.MWVM.Settings.Performance.AttachToBatchProcessor(ActiveCompilation);
|
||||
|
||||
await ActiveCompilation.Begin();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
StatusTracker = null;
|
||||
ActiveCompilation.Dispose();
|
||||
ActiveCompilation = null;
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ namespace Wabbajack
|
||||
}
|
||||
try
|
||||
{
|
||||
ActiveCompilation = new VortexCompiler(
|
||||
using (ActiveCompilation = new VortexCompiler(
|
||||
game: SelectedGame.Game,
|
||||
gamePath: GameLocation.TargetPath,
|
||||
vortexFolder: VortexCompiler.TypicalVortexFolder(),
|
||||
@ -204,8 +204,11 @@ namespace Wabbajack
|
||||
ModListWebsite = ModlistSettings.Website,
|
||||
ModListReadme = ModlistSettings.ReadmeIsWebsite ? ModlistSettings.ReadmeWebsite : ModlistSettings.ReadmeFilePath.TargetPath,
|
||||
ReadmeIsWebsite = ModlistSettings.ReadmeIsWebsite,
|
||||
};
|
||||
await ActiveCompilation.Begin();
|
||||
})
|
||||
{
|
||||
Parent.MWVM.Settings.Performance.AttachToBatchProcessor(ActiveCompilation);
|
||||
await ActiveCompilation.Begin();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ using Wabbajack.UI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class InstallerVM : ViewModel, IBackNavigatingVM
|
||||
public class InstallerVM : ViewModel, IBackNavigatingVM, ICpuStatusVM
|
||||
{
|
||||
public SlideShow Slideshow { get; }
|
||||
|
||||
@ -87,11 +87,14 @@ namespace Wabbajack
|
||||
private readonly ObservableAsPropertyHelper<IUserIntervention> _ActiveGlobalUserIntervention;
|
||||
public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value;
|
||||
|
||||
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
|
||||
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
|
||||
|
||||
// Command properties
|
||||
public IReactiveCommand ShowReportCommand { get; }
|
||||
public IReactiveCommand OpenReadmeCommand { get; }
|
||||
public IReactiveCommand VisitWebsiteCommand { get; }
|
||||
public IReactiveCommand BackCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> BackCommand { get; }
|
||||
public IReactiveCommand CloseWhenCompleteCommand { get; }
|
||||
public IReactiveCommand GoToInstallCommand { get; }
|
||||
public IReactiveCommand BeginCommand { get; }
|
||||
@ -373,6 +376,11 @@ namespace Wabbajack
|
||||
{
|
||||
Installer.AfterInstallNavigation();
|
||||
});
|
||||
|
||||
_CurrentCpuCount = this.WhenAny(x => x.Installer.ActiveInstallation.Queue.CurrentCpuCount)
|
||||
.Switch()
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(CurrentCpuCount));
|
||||
}
|
||||
|
||||
private void ShowReport()
|
||||
|
@ -146,27 +146,30 @@ namespace Wabbajack
|
||||
|
||||
public async Task Install()
|
||||
{
|
||||
var installer = new MO2Installer(
|
||||
using (var installer = new MO2Installer(
|
||||
archive: Parent.ModListLocation.TargetPath,
|
||||
modList: Parent.ModList.SourceModList,
|
||||
outputFolder: Location.TargetPath,
|
||||
downloadFolder: DownloadLocation.TargetPath,
|
||||
parameters: SystemParametersConstructor.Create());
|
||||
|
||||
await Task.Run(async () =>
|
||||
parameters: SystemParametersConstructor.Create()))
|
||||
{
|
||||
try
|
||||
Parent.MWVM.Settings.Performance.AttachToBatchProcessor(installer);
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var workTask = installer.Begin();
|
||||
ActiveInstallation = installer;
|
||||
await workTask;
|
||||
return ErrorResponse.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveInstallation = null;
|
||||
}
|
||||
});
|
||||
try
|
||||
{
|
||||
var workTask = installer.Begin();
|
||||
ActiveInstallation = installer;
|
||||
await workTask;
|
||||
return ErrorResponse.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveInstallation = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,26 +63,29 @@ namespace Wabbajack
|
||||
|
||||
var download = VortexCompiler.RetrieveDownloadLocation(TargetGame);
|
||||
var staging = VortexCompiler.RetrieveStagingLocation(TargetGame);
|
||||
installer = new VortexInstaller(
|
||||
using (installer = new VortexInstaller(
|
||||
archive: Parent.ModListLocation.TargetPath,
|
||||
modList: Parent.ModList.SourceModList,
|
||||
outputFolder: staging,
|
||||
downloadFolder: download,
|
||||
parameters: SystemParametersConstructor.Create());
|
||||
|
||||
await Task.Run(async () =>
|
||||
parameters: SystemParametersConstructor.Create()))
|
||||
{
|
||||
try
|
||||
Parent.MWVM.Settings.Performance.AttachToBatchProcessor(installer);
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var workTask = installer.Begin();
|
||||
ActiveInstallation = installer;
|
||||
await workTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveInstallation = null;
|
||||
}
|
||||
});
|
||||
try
|
||||
{
|
||||
var workTask = installer.Begin();
|
||||
ActiveInstallation = installer;
|
||||
await workTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveInstallation = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
Wabbajack/View Models/Interfaces/ICpuStatusVM.cs
Normal file
17
Wabbajack/View Models/Interfaces/ICpuStatusVM.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public interface ICpuStatusVM : IReactiveObject
|
||||
{
|
||||
ObservableCollectionExtended<CPUDisplayVM> StatusList { get; }
|
||||
MainWindowVM MWVM { get; }
|
||||
(int CurrentCPUs, int DesiredCPUs) CurrentCpuCount { get; }
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ namespace Wabbajack
|
||||
|
||||
public MainWindowVM(MainWindow mainWindow, MainSettings settings)
|
||||
{
|
||||
ConverterRegistration.Register();
|
||||
MainWindow = mainWindow;
|
||||
Settings = settings;
|
||||
Installer = new Lazy<InstallerVM>(() => new InstallerVM(this));
|
||||
|
@ -1,21 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Downloaders;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class LoginManagerVM : BackNavigatingVM
|
||||
{
|
||||
public List<INeedsLogin> Downloaders { get; }
|
||||
public List<LoginTargetVM> Downloaders { get; }
|
||||
|
||||
public LoginManagerVM(SettingsVM settingsVM)
|
||||
: base(settingsVM.MWVM)
|
||||
{
|
||||
Downloaders = DownloadDispatcher.Downloaders.OfType<INeedsLogin>().ToList();
|
||||
Downloaders = DownloadDispatcher.Downloaders
|
||||
.OfType<INeedsLogin>()
|
||||
.Select(x => new LoginTargetVM(x))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class LoginTargetVM : ViewModel
|
||||
{
|
||||
private readonly ObservableAsPropertyHelper<string> _MetaInfo;
|
||||
public string MetaInfo => _MetaInfo.Value;
|
||||
|
||||
public INeedsLogin Login { get; }
|
||||
|
||||
public LoginTargetVM(INeedsLogin login)
|
||||
{
|
||||
Login = login;
|
||||
_MetaInfo = (login.MetaInfo ?? Observable.Return(""))
|
||||
.ObserveOnGuiThread()
|
||||
.ToProperty(this, nameof(MetaInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,13 +11,15 @@ namespace Wabbajack
|
||||
public class SettingsVM : BackNavigatingVM
|
||||
{
|
||||
public MainWindowVM MWVM { get; }
|
||||
public LoginManagerVM LoginManagerVM { get; }
|
||||
public LoginManagerVM Login { get; }
|
||||
public PerformanceSettings Performance { get; }
|
||||
|
||||
public SettingsVM(MainWindowVM mainWindowVM)
|
||||
: base(mainWindowVM)
|
||||
{
|
||||
MWVM = mainWindowVM;
|
||||
LoginManagerVM = new LoginManagerVM(this);
|
||||
Login = new LoginManagerVM(this);
|
||||
Performance = mainWindowVM.Settings.Performance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
@ -25,7 +26,7 @@ namespace Wabbajack
|
||||
public ViewModel NavigateBackTarget { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public IReactiveCommand BackCommand { get; set; }
|
||||
public ReactiveCommand<Unit, Unit> BackCommand { get; set; }
|
||||
|
||||
private WebBrowserVM(string url = "http://www.wabbajack.org")
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
<UserControl
|
||||
<local:UserControlRx
|
||||
x:Class="Wabbajack.CpuView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
@ -8,42 +8,85 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="local:ICpuStatusVM"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid x:Name="ControlGrid" Background="Transparent">
|
||||
<Rectangle Fill="{StaticResource HeatedBorderBrush}" Opacity="{Binding ProgressPercent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
|
||||
<ListBox
|
||||
HorizontalContentAlignment="Stretch"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="1"
|
||||
ItemsSource="{Binding StatusList}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Background="{StaticResource WindowBackgroundBrush}">
|
||||
<mahapps:MetroProgressBar
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{StaticResource DarkPrimaryVariantBrush}"
|
||||
Maximum="1"
|
||||
Opacity="{Binding ProgressPercent, Mode=OneWay}"
|
||||
Value="{Binding ProgressPercent, Mode=OneWay}" />
|
||||
<Grid Height="1" VerticalAlignment="Bottom">
|
||||
<Border BorderBrush="Transparent" BorderThickness="1" />
|
||||
<Grid Margin="1" Background="{StaticResource DarkBackgroundBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox
|
||||
x:Name="CpuListControl"
|
||||
Grid.Row="0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Background="{StaticResource WindowBackgroundBrush}">
|
||||
<mahapps:MetroProgressBar
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{StaticResource DarkSecondaryBrush}"
|
||||
Foreground="{StaticResource DarkPrimaryVariantBrush}"
|
||||
Maximum="1"
|
||||
Opacity="{Binding ProgressPercent, Mode=OneWay}"
|
||||
Value="{Binding ProgressPercent, Mode=OneWay}" />
|
||||
<Grid Height="1" VerticalAlignment="Bottom">
|
||||
<mahapps:MetroProgressBar
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{StaticResource DarkSecondaryBrush}"
|
||||
Maximum="1"
|
||||
Value="{Binding ProgressPercent, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Margin="0,0,0,2"
|
||||
Text="{Binding Msg, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
ToolTip="{Binding Msg, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Margin="0,0,0,2"
|
||||
Text="{Binding Msg, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
ToolTip="{Binding Msg, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<Border
|
||||
x:Name="SettingsBar"
|
||||
Grid.Row="1"
|
||||
Background="{StaticResource BackgroundBrush}"
|
||||
BorderBrush="{StaticResource ButtonNormalBorder}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Margin="5,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Percent Usage" />
|
||||
<Slider
|
||||
x:Name="TargetPercentageSlider"
|
||||
Grid.Column="1"
|
||||
Margin="2,0,4,0"
|
||||
Maximum="1"
|
||||
Minimum="0.1"
|
||||
Orientation="Horizontal" />
|
||||
<TextBlock
|
||||
x:Name="PercentageText"
|
||||
Grid.Column="2"
|
||||
Margin="2,0,4,0" />
|
||||
<TextBlock Grid.Column="3" Text="|" />
|
||||
<TextBlock
|
||||
x:Name="CpuCountText"
|
||||
Grid.Column="4"
|
||||
Margin="2,0,6,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</local:UserControlRx>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@ -12,13 +13,18 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Wabbajack.Lib;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for CpuView.xaml
|
||||
/// </summary>
|
||||
public partial class CpuView : UserControl
|
||||
public partial class CpuView : UserControlRx<ICpuStatusVM>
|
||||
{
|
||||
public double ProgressPercent
|
||||
{
|
||||
@ -28,9 +34,51 @@ namespace Wabbajack
|
||||
public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(CpuView),
|
||||
new FrameworkPropertyMetadata(default(double)));
|
||||
|
||||
public MainSettings SettingsHook
|
||||
{
|
||||
get => (MainSettings)GetValue(SettingsHookProperty);
|
||||
set => SetValue(SettingsHookProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty SettingsHookProperty = DependencyProperty.Register(nameof(SettingsHook), typeof(MainSettings), typeof(CpuView),
|
||||
new FrameworkPropertyMetadata(default(SettingsVM)));
|
||||
|
||||
private bool _ShowingSettings;
|
||||
public bool ShowingSettings { get => _ShowingSettings; set => this.RaiseAndSetIfChanged(ref _ShowingSettings, value); }
|
||||
|
||||
public CpuView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(disposable =>
|
||||
{
|
||||
Observable.CombineLatest(
|
||||
this.WhenAny(x => x.ControlGrid.IsMouseOver),
|
||||
this.WhenAny(x => x.SettingsHook.Performance.Manual)
|
||||
.StartWith(true),
|
||||
resultSelector: (over, manual) => over && !manual)
|
||||
.Subscribe(showing =>
|
||||
{
|
||||
SettingsBar.Visibility = showing ? Visibility.Visible : Visibility.Collapsed;
|
||||
})
|
||||
.DisposeWith(disposable);
|
||||
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.StatusList, x => x.CpuListControl.ItemsSource)
|
||||
.DisposeWith(disposable);
|
||||
|
||||
this.BindStrict(this.ViewModel, x => x.MWVM.Settings.Performance.TargetUsage, x => x.TargetPercentageSlider.Value)
|
||||
.DisposeWith(disposable);
|
||||
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.MWVM.Settings.Performance.TargetUsage, x => x.PercentageText.Text, x => $"{x.ToString("f2")}%")
|
||||
.DisposeWith(disposable);
|
||||
|
||||
this.WhenAny(x => x.ViewModel.CurrentCpuCount)
|
||||
.DistinctUntilChanged()
|
||||
.ObserveOnGuiThread()
|
||||
.Subscribe(x =>
|
||||
{
|
||||
this.CpuCountText.Text = $"{x.CurrentCPUs} / {x.DesiredCPUs}";
|
||||
})
|
||||
.DisposeWith(disposable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -307,6 +307,8 @@
|
||||
<local:CpuView
|
||||
Grid.Column="2"
|
||||
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
|
||||
SettingsHook="{Binding MWVM.Settings}"
|
||||
ViewModel="{Binding}"
|
||||
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
|
||||
<local:AttentionBorder Grid.Column="2" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
|
||||
<local:AttentionBorder.DisplayContent>
|
||||
|
@ -257,6 +257,7 @@
|
||||
Margin="30,5"
|
||||
Command="{Binding OpenReadmeCommand}"
|
||||
FontSize="20"
|
||||
Style="{StaticResource LargeButtonStyle}"
|
||||
ToolTip="Open the readme for the modlist">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -281,6 +282,7 @@
|
||||
Margin="30,5"
|
||||
Command="{Binding VisitWebsiteCommand}"
|
||||
FontSize="20"
|
||||
Style="{StaticResource LargeButtonStyle}"
|
||||
ToolTip="Open the webpage for the modlist">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -305,6 +307,7 @@
|
||||
Margin="30,5"
|
||||
Command="{Binding ShowReportCommand}"
|
||||
FontSize="20"
|
||||
Style="{StaticResource LargeButtonStyle}"
|
||||
ToolTip="Open an explicit listing of all actions this modlist will take">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -419,6 +422,8 @@
|
||||
<local:CpuView
|
||||
Grid.Column="2"
|
||||
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
|
||||
ViewModel="{Binding}"
|
||||
SettingsHook="{Binding MWVM.Settings}"
|
||||
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
|
||||
<local:AttentionBorder Grid.Column="2" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
|
||||
<local:AttentionBorder.DisplayContent>
|
||||
|
@ -28,9 +28,6 @@
|
||||
<DataTemplate DataType="{x:Type local:InstallerVM}">
|
||||
<local:InstallationView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:LoginManagerVM}">
|
||||
<local:LoginManagerView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ModeSelectionVM}">
|
||||
<local:ModeSelectionView />
|
||||
</DataTemplate>
|
||||
@ -41,7 +38,7 @@
|
||||
<local:WebBrowserView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:SettingsVM}">
|
||||
<local:SettingsView />
|
||||
<local:SettingsView ViewModel="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
|
39
Wabbajack/Views/Settings/LoginItemView.xaml
Normal file
39
Wabbajack/Views/Settings/LoginItemView.xaml
Normal file
@ -0,0 +1,39 @@
|
||||
<rxui:ReactiveUserControl
|
||||
x:Class="Wabbajack.LoginItemView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:rxui="http://reactiveui.net"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="local:LoginTargetVM"
|
||||
mc:Ignorable="d">
|
||||
<Grid Height="30" Margin="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="150" />
|
||||
<ColumnDefinition Width="100" />
|
||||
<ColumnDefinition Width="100" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
x:Name="SiteNameText"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center" />
|
||||
<Button
|
||||
x:Name="LoginButton"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Login" />
|
||||
<Button
|
||||
x:Name="LogoutButton"
|
||||
Grid.Column="2"
|
||||
Margin="5"
|
||||
Content="Logout" />
|
||||
<TextBlock
|
||||
x:Name="MetaText"
|
||||
Grid.Column="3"
|
||||
FontSize="14" />
|
||||
</Grid>
|
||||
</rxui:ReactiveUserControl>
|
56
Wabbajack/Views/Settings/LoginItemView.xaml.cs
Normal file
56
Wabbajack/Views/Settings/LoginItemView.xaml.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for LoginItemView.xaml
|
||||
/// </summary>
|
||||
public partial class LoginItemView : ReactiveUserControl<LoginTargetVM>
|
||||
{
|
||||
public LoginItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(disposable =>
|
||||
{
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Login.SiteName, x => x.SiteNameText.Text)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Login.TriggerLogin, x => x.LoginButton.Command)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Login.ClearLogin, x => x.LogoutButton.Command)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.MetaInfo, x => x.MetaText.Text)
|
||||
.DisposeWith(disposable);
|
||||
|
||||
// Modify label state
|
||||
this.WhenAny(x => x.ViewModel.Login.TriggerLogin.CanExecute)
|
||||
.Switch()
|
||||
.Subscribe(x =>
|
||||
{
|
||||
this.LoginButton.Content = x ? "Login" : "Logged In";
|
||||
});
|
||||
this.WhenAny(x => x.ViewModel.Login.ClearLogin.CanExecute)
|
||||
.Switch()
|
||||
.Subscribe(x =>
|
||||
{
|
||||
this.LogoutButton.Content = x ? "Logout" : "Logged Out";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="Wabbajack.LoginManagerView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<!-- Do it this way so we can access the browser directly from the VM -->
|
||||
<ListView Background="{StaticResource WindowBackgroundBrush}" ItemsSource="{Binding Downloaders}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Height="30" Margin="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="48" />
|
||||
<ColumnDefinition Width="200" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="100" />
|
||||
<ColumnDefinition Width="100" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" Source="{Binding IconUrl, Mode=OneTime}" />
|
||||
<Label
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Content="{Binding SiteName, Mode=OneTime}"
|
||||
FontSize="14" />
|
||||
<Label
|
||||
Grid.Column="2"
|
||||
Width="400"
|
||||
Content="{Binding MetaInfo, Mode=OneWay}"
|
||||
FontSize="14" />
|
||||
<Button
|
||||
Grid.Column="3"
|
||||
Margin="5"
|
||||
Command="{Binding TriggerLogin}"
|
||||
Content="Login" />
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Margin="5"
|
||||
Command="{Binding ClearLogin}"
|
||||
Content="Logout" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
</UserControl>
|
54
Wabbajack/Views/Settings/LoginSettingsView.xaml
Normal file
54
Wabbajack/Views/Settings/LoginSettingsView.xaml
Normal file
@ -0,0 +1,54 @@
|
||||
<rxui:ReactiveUserControl
|
||||
x:Class="Wabbajack.LoginSettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:rxui="http://reactiveui.net"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="local:LoginManagerVM"
|
||||
mc:Ignorable="d">
|
||||
<Border
|
||||
x:Name="LoginView"
|
||||
Margin="5"
|
||||
Background="{StaticResource BackgroundBrush}"
|
||||
BorderBrush="{StaticResource ButtonNormalBorder}"
|
||||
BorderThickness="1">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.Resources>
|
||||
<Style BasedOn="{StaticResource MainButtonStyle}" TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{StaticResource SecondaryBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource DarkSecondaryBrush}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<TextBlock
|
||||
Margin="5,0"
|
||||
FontFamily="Lucida Sans"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Text="Logins" />
|
||||
<ItemsControl
|
||||
x:Name="DownloadersList"
|
||||
Grid.Row="1"
|
||||
Margin="5"
|
||||
Background="{StaticResource BackgroundBrush}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<local:LoginItemView ViewModel="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
</rxui:ReactiveUserControl>
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@ -12,17 +13,23 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for LoginManager.xaml
|
||||
/// Interaction logic for LoginSettingsView.xaml
|
||||
/// </summary>
|
||||
public partial class LoginManagerView : UserControl
|
||||
public partial class LoginSettingsView : ReactiveUserControl<LoginManagerVM>
|
||||
{
|
||||
public LoginManagerView()
|
||||
public LoginSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(disposable =>
|
||||
{
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Downloaders, x => x.DownloadersList.ItemsSource)
|
||||
.DisposeWith(disposable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
121
Wabbajack/Views/Settings/PerformanceSettingsView.xaml
Normal file
121
Wabbajack/Views/Settings/PerformanceSettingsView.xaml
Normal file
@ -0,0 +1,121 @@
|
||||
<rxui:ReactiveUserControl
|
||||
x:Class="Wabbajack.PerformanceSettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:rxui="http://reactiveui.net"
|
||||
xmlns:xwpf="http://schemas.xceed.com/wpf/xaml/toolkit"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="local:PerformanceSettings"
|
||||
mc:Ignorable="d">
|
||||
<Border
|
||||
x:Name="PerformanceView"
|
||||
MinWidth="280"
|
||||
Margin="5"
|
||||
Background="{StaticResource BackgroundBrush}"
|
||||
BorderBrush="{StaticResource ButtonNormalBorder}"
|
||||
BorderThickness="1">
|
||||
<Grid Margin="15,10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="25" />
|
||||
<RowDefinition Height="25" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
FontFamily="Lucida Sans"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Text="Performance" />
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Height="25"
|
||||
Margin="0,0,0,10">
|
||||
<Grid.Resources>
|
||||
<Style BasedOn="{StaticResource MainButtonStyle}" TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{StaticResource SecondaryBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource DarkSecondaryBrush}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Name="ManualButton"
|
||||
Grid.Column="0"
|
||||
Content="Manual"
|
||||
ToolTip="Control the number of cores by setting the max limit manually" />
|
||||
<Button
|
||||
x:Name="AutoButton"
|
||||
Grid.Column="2"
|
||||
Content="Auto"
|
||||
ToolTip="Control the number of cores by scaling it to a percentage of what WJ would use at full speed" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
x:Name="MaxCoresLabel"
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Max Cores" />
|
||||
<xwpf:IntegerUpDown
|
||||
x:Name="MaxCoresSpinner"
|
||||
Grid.Row="3"
|
||||
Grid.Column="2"
|
||||
MinWidth="75"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="White"
|
||||
Maximum="255"
|
||||
Minimum="1" />
|
||||
<TextBlock
|
||||
x:Name="TargetUsageLabel"
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Target Percent Usage" />
|
||||
<xwpf:DoubleUpDown
|
||||
x:Name="TargetUsageSpinner"
|
||||
Grid.Row="3"
|
||||
Grid.Column="2"
|
||||
MinWidth="75"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="White"
|
||||
FormatString="F2"
|
||||
Increment="0.1"
|
||||
Maximum="1"
|
||||
Minimum="0.1" />
|
||||
<Slider
|
||||
x:Name="TargetUsageSlider"
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="0.1"
|
||||
Maximum="1"
|
||||
Minimum="0.1"
|
||||
TickFrequency="0.05" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</rxui:ReactiveUserControl>
|
73
Wabbajack/Views/Settings/PerformanceSettingsView.xaml.cs
Normal file
73
Wabbajack/Views/Settings/PerformanceSettingsView.xaml.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for PerformanceSettingsView.xaml
|
||||
/// </summary>
|
||||
public partial class PerformanceSettingsView : ReactiveUserControl<PerformanceSettings>
|
||||
{
|
||||
public PerformanceSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.AutoButton.Command = ReactiveCommand.Create(
|
||||
execute: () => this.ViewModel.Manual = false,
|
||||
canExecute: this.WhenAny(x => x.ViewModel.Manual)
|
||||
.ObserveOnGuiThread());
|
||||
this.ManualButton.Command = ReactiveCommand.Create(
|
||||
execute: () => this.ViewModel.Manual = true,
|
||||
canExecute: this.WhenAny(x => x.ViewModel.Manual)
|
||||
.Select(x => !x)
|
||||
.ObserveOnGuiThread());
|
||||
|
||||
this.WhenActivated(disposable =>
|
||||
{
|
||||
// Bind mode buttons
|
||||
|
||||
// Modify visibility of controls based on if auto
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Manual, x => x.MaxCoresLabel.Visibility,
|
||||
b => b ? Visibility.Visible : Visibility.Collapsed)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Manual, x => x.MaxCoresSpinner.Visibility,
|
||||
b => b ? Visibility.Visible : Visibility.Collapsed)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Manual, x => x.TargetUsageLabel.Visibility,
|
||||
b => b ? Visibility.Collapsed : Visibility.Visible)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Manual, x => x.TargetUsageSpinner.Visibility,
|
||||
b => b ? Visibility.Collapsed : Visibility.Visible)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Manual, x => x.TargetUsageSlider.Visibility,
|
||||
b => b ? Visibility.Collapsed : Visibility.Visible)
|
||||
.DisposeWith(disposable);
|
||||
|
||||
// Bind Values
|
||||
this.BindStrict(this.ViewModel, x => x.MaxCores, x => x.MaxCoresSpinner.Value,
|
||||
vmToViewConverter: x => x,
|
||||
viewToVmConverter: x => (byte)(x ?? 0))
|
||||
.DisposeWith(disposable);
|
||||
this.BindStrict(this.ViewModel, x => x.TargetUsage, x => x.TargetUsageSpinner.Value)
|
||||
.DisposeWith(disposable);
|
||||
this.BindStrict(this.ViewModel, x => x.TargetUsage, x => x.TargetUsageSlider.Value)
|
||||
.DisposeWith(disposable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<UserControl
|
||||
<rxui:ReactiveUserControl
|
||||
x:Class="Wabbajack.SettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
@ -6,9 +6,18 @@
|
||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:rxui="http://reactiveui.net"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:TypeArguments="local:SettingsVM"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Lucida Sans" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="47" />
|
||||
@ -27,11 +36,18 @@
|
||||
Margin="7,5,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Command="{Binding BackCommand}"
|
||||
Style="{StaticResource IconCircleButtonStyle}"
|
||||
ToolTip="Back to main menu">
|
||||
<iconPacks:PackIconMaterial Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Kind="ArrowLeft" />
|
||||
</Button>
|
||||
<local:LoginManagerView Grid.Row="1" DataContext="{Binding LoginManagerVM}" />
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
Focusable="False"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<WrapPanel>
|
||||
<local:LoginSettingsView x:Name="LoginView" />
|
||||
<local:PerformanceSettingsView x:Name="PerformanceView" />
|
||||
</WrapPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</rxui:ReactiveUserControl>
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@ -12,17 +14,27 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SettingsView.xaml
|
||||
/// </summary>
|
||||
public partial class SettingsView : UserControl
|
||||
public partial class SettingsView : ReactiveUserControl<SettingsVM>
|
||||
{
|
||||
public SettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(disposable =>
|
||||
{
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.BackCommand, x => x.BackButton.Command)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Login, x => x.LoginView.ViewModel)
|
||||
.DisposeWith(disposable);
|
||||
this.OneWayBindStrict(this.ViewModel, x => x.Performance, x => x.PerformanceView.ViewModel)
|
||||
.DisposeWith(disposable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,4 +40,28 @@ namespace Wabbajack
|
||||
control.RaisePropertyChanged(e.Property.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public class UserControlRx<TViewModel> : ReactiveUserControl<TViewModel>, IReactiveObject
|
||||
where TViewModel : class
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event PropertyChangingEventHandler PropertyChanging;
|
||||
|
||||
public void RaisePropertyChanging(PropertyChangingEventArgs args)
|
||||
{
|
||||
PropertyChanging?.Invoke(this, args);
|
||||
}
|
||||
|
||||
public void RaisePropertyChanged(PropertyChangedEventArgs args)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected static void WireNotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(d is UserControlRx control)) return;
|
||||
if (Equals(e.OldValue, e.NewValue)) return;
|
||||
control.RaisePropertyChanged(e.Property.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -176,6 +176,20 @@
|
||||
<Compile Include="UI\FilePickerVM.cs" />
|
||||
<Compile Include="UI\UIUtils.cs" />
|
||||
<Compile Include="Util\SystemParametersConstructor.cs" />
|
||||
<Compile Include="Converters\IntDownCastConverter.cs" />
|
||||
<Compile Include="Converters\CommandConverter.cs" />
|
||||
<Compile Include="Converters\ConverterRegistration.cs" />
|
||||
<Compile Include="Extensions\IViewForExt.cs" />
|
||||
<Compile Include="View Models\Interfaces\ICpuStatusVM.cs" />
|
||||
<Compile Include="Views\Settings\LoginItemView.xaml.cs">
|
||||
<DependentUpon>LoginItemView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Settings\LoginSettingsView.xaml.cs">
|
||||
<DependentUpon>LoginSettingsView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Settings\PerformanceSettingsView.xaml.cs">
|
||||
<DependentUpon>PerformanceSettingsView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="View Models\BackNavigatingVM.cs" />
|
||||
<Compile Include="View Models\Settings\SettingsVM.cs" />
|
||||
<Compile Include="Views\Settings\SettingsView.xaml.cs">
|
||||
@ -274,9 +288,6 @@
|
||||
<Compile Include="Views\ModListGalleryView.xaml.cs">
|
||||
<DependentUpon>ModListGalleryView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Settings\LoginManagerView.xaml.cs">
|
||||
<DependentUpon>LoginManagerView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\TextViewer.xaml.cs">
|
||||
<DependentUpon>TextViewer.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@ -290,6 +301,18 @@
|
||||
<Compile Include="Views\WebBrowserView.xaml.cs">
|
||||
<DependentUpon>WebBrowserView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Page Include="Views\Settings\LoginItemView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Settings\LoginSettingsView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Settings\PerformanceSettingsView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Settings\SettingsView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@ -396,10 +419,6 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Settings\LoginManagerView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Views\TextViewer.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
@ -500,6 +519,9 @@
|
||||
<PackageReference Include="DynamicData">
|
||||
<Version>6.14.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Extended.Wpf.Toolkit">
|
||||
<Version>3.7.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Fody">
|
||||
<Version>6.0.6</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
Loading…
Reference in New Issue
Block a user