Files
UnrealEngineUWP/Engine/Source/Programs/UnsyncUI/UnsyncUI/BuildSelectorControl.xaml.cs
UnrealBot 73409369c0 Branch snapshot for CL 21319338
[CL 21319338 in ue5-main branch]
2022-08-10 16:03:37 +00:00

435 lines
12 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace UnsyncUI
{
/// <summary>
/// Interaction logic for BuildSelectorControl.xaml
/// </summary>
public partial class BuildSelectorControl : UserControl
{
GridViewColumnHeader lastHeaderClicked = null;
ListSortDirection lastDirection = ListSortDirection.Ascending;
public BuildSelectorControl()
{
InitializeComponent();
}
private void SyncHyperlink_Click(object sender, RoutedEventArgs e)
{
(DataContext as ProjectModel).StartSync(SelectedPlatforms.SelectedItems.OfType<BuildPlatformModel>());
}
private void BuildsHeader_Click(object sender, RoutedEventArgs e)
{
var headerClicked = e.OriginalSource as GridViewColumnHeader;
if (headerClicked == null || headerClicked.Role == GridViewColumnHeaderRole.Padding)
return;
ListSortDirection direction;
if (headerClicked != lastHeaderClicked)
{
direction = ListSortDirection.Ascending;
}
else
{
direction = lastDirection == ListSortDirection.Ascending
? ListSortDirection.Descending
: ListSortDirection.Ascending;
}
if (headerClicked.Column == colStream)
{
Sort((colStream.Header as string, direction), (colCL.Header as string, ListSortDirection.Descending));
}
else
{
var columnBinding = headerClicked.Column.DisplayMemberBinding as Binding;
var sortBy = columnBinding?.Path.Path ?? headerClicked.Column.Header as string;
Sort((sortBy, direction));
}
lastHeaderClicked = headerClicked;
lastDirection = direction;
}
private void Sort(params (string field, ListSortDirection direction)[] args)
{
var dataView = CollectionViewSource.GetDefaultView(SelectedBuild.ItemsSource);
dataView.SortDescriptions.Clear();
foreach (var desc in args)
{
dataView.SortDescriptions.Add(new SortDescription(desc.field, desc.direction));
}
dataView.Refresh();
}
}
public sealed class BuildPlatformModel
{
public BuildModel Build { get; }
public string Platform { get; }
public string Flavor { get; }
public string Description { get; }
public string FullPath { get; }
public string DestPathRelative { get; init; }
public string Name => Build == null
? "Custom Sync"
: $"{Build.Stream} - {Platform} - CL {Build.CL}";
public string SafeName => Name.Replace('/', '+').Replace(" ", "");
public BuildPlatformModel(BuildModel build, string platform, string fullPath, string flavor)
{
Build = build;
Platform = platform;
FullPath = fullPath;
Flavor = flavor;
DestPathRelative = Flavor == null ? Platform : Platform + Flavor;
Description = Flavor == null ? Platform : $"{Platform} {Flavor}";
}
}
public sealed class BuildModel : BaseModel
{
private CancellationTokenSource cts;
public string Stream => Template.Stream;
public string CL => Template.CL;
public string Suffix => Template.Suffix;
public Config.Directory RootDir { get; }
public string Path { get; }
public Config.BuildTemplate Template { get; }
public Command OnRefreshPlatformsClicked { get; }
public Command OnStopRefreshPlatformsClicked { get; }
public ObservableCollection<BuildPlatformModel> Platforms { get; } = new ObservableCollection<BuildPlatformModel>();
public BuildModel(string path, Config.Directory rootDir, Config.BuildTemplate template)
{
Path = path;
RootDir = rootDir;
Template = template;
OnRefreshPlatformsClicked = new Command(() => PopulatePlatforms()) { Enabled = true };
OnStopRefreshPlatformsClicked = new Command(() =>
{
cts?.Cancel();
});
}
public bool HasPlatform(string platform) => Platforms.Where(p => p.Platform == platform).Any();
private async Task EnumeratePlatforms(Config.Directory currentDir, string path, Config.BuildTemplate template, CancellationTokenSource cancellationToken)
{
if (!currentDir.Parse(path, ref template))
return;
foreach (var childDir in await AsyncIO.EnumerateDirectoriesAsync(path, cancellationToken.Token))
{
if (System.IO.Path.GetFileName(childDir) == ".unsync")
{
// This folder is a valid build
Platforms.Add(new BuildPlatformModel(this, template.Platform, path, template.Flavor));
}
else
{
foreach (var subDir in currentDir.SubDirectories)
{
await EnumeratePlatforms(subDir, childDir, template, cancellationToken);
}
}
}
}
public async void PopulatePlatforms()
{
cts?.Cancel();
cts = new CancellationTokenSource();
OnRefreshPlatformsClicked.Enabled = false;
OnStopRefreshPlatformsClicked.Enabled = true;
Platforms.Clear();
try
{
await EnumeratePlatforms(RootDir, Path, Template, cts);
}
catch (OperationCanceledException)
{ }
finally
{
OnRefreshPlatformsClicked.Enabled = true;
OnStopRefreshPlatformsClicked.Enabled = false;
}
}
}
public sealed class ProjectModel : TabModel
{
private bool firstRefresh = true;
private Action<IEnumerable<(string DstPath, string[] Exclusions, BuildPlatformModel Model)>> onBuildsSelected;
public Config.Project Definition { get; }
public string Name => Definition.Name;
public Command OnRefreshBuildsClicked { get; }
public Command OnStopRefreshBuildsClicked { get; }
public Command<IList> OnStartSync { get; }
public Command CopyBuildCLClicked { get; }
public Command OpenBuildLocationClicked { get; }
public Command ResetDestinationClicked { get; }
public ObservableCollection<BuildModel> Builds { get; } = new ObservableCollection<BuildModel>();
public ICollectionView BuildsView { get; }
public bool BuildsViewFilter(object item)
{
BuildModel build = (BuildModel)item;
foreach (var term in searchTerms.Split(' '))
{
if (!build.CL.Contains(term, StringComparison.InvariantCultureIgnoreCase)
&& !build.Stream.Contains(term, StringComparison.InvariantCultureIgnoreCase)
&& !build.Suffix.Contains(term, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
}
return true;
}
private BuildModel selectedBuild = null;
public BuildModel SelectedBuild
{
get => selectedBuild;
set
{
SetProperty(ref selectedBuild, value);
if (selectedBuild != null)
{
selectedBuild.PopulatePlatforms();
}
}
}
private string searchTerms = "";
public string SearchTerms
{
get => searchTerms;
set
{
SetProperty(ref searchTerms, value);
BuildsView.Refresh();
}
}
private CancellationTokenSource cts = null;
/// Default download directory (either auto-detected or loaded from project config)
private string defaultDstPath = null;
/// Current download path selected by user or loaded from user preferences.
/// This value persists between application sessions (saved per project).
/// It can be reset to default by pressing "Reset" button in the UI.
private string dstPath = null;
public string DstPath
{
get => dstPath;
set
{
SetProperty(ref dstPath, value);
App.Current.UserConfig.ProjectDestinationMap[Name] = value;
ResetDestinationClicked.Enabled = CanResetDestinationToDefault;
}
}
public ProjectModel(Config.Project definition, Action<IEnumerable<(string DstPath, string[] Exclusions, BuildPlatformModel Model)>> onBuildsSelected)
{
this.Definition = definition;
this.onBuildsSelected = onBuildsSelected;
OnRefreshBuildsClicked = new Command(RefreshBuildsAsync) { Enabled = true };
OnStopRefreshBuildsClicked = new Command(StopRefreshBuilds) { Enabled = false };
OnStartSync = new Command<IList>(l => StartSync(l.OfType<BuildPlatformModel>())) { Enabled = true };
CopyBuildCLClicked = new Command(CopyBuildCL) { Enabled = true };
OpenBuildLocationClicked = new Command(OpenBuildLocation) { Enabled = true };
BuildsView = CollectionViewSource.GetDefaultView(Builds);
BuildsView.Filter = BuildsViewFilter;
BuildsView.SortDescriptions.Clear();
BuildsView.SortDescriptions.Add(new SortDescription("CL", ListSortDirection.Descending));
SearchTerms = App.Current.DefaultSearchTerms;
if (definition.Destination != null)
{
defaultDstPath = Path.GetFullPath(definition.Destination);
}
else
{
// If this is the first time running, choose the destination dir as the largest local disk
// @todo: remember user choice
var bestDrive = DriveInfo.GetDrives()
.Where(d => d.DriveType == DriveType.Fixed)
.Where(d => !d.VolumeLabel.Contains("google", StringComparison.InvariantCultureIgnoreCase)) // Ignore Google Drive (it appears as a 'fixed' disk)
.OrderByDescending(d => d.TotalSize).FirstOrDefault();
if (bestDrive != null)
{
defaultDstPath = Path.Combine(bestDrive.RootDirectory.FullName, "Unsync", definition.Name);
}
}
// If we failed to find a reasonable default destination then set it to a non-null empty string,
// which prevents sync command until user explicitly sets the path
if (defaultDstPath == null)
{
defaultDstPath = "";
}
string userDstPath = null;
if (App.Current.UserConfig.ProjectDestinationMap.TryGetValue(Name, out userDstPath))
{
dstPath = userDstPath;
}
else
{
dstPath = defaultDstPath;
}
ResetDestinationClicked = new Command(ResetDestinationToDefault) { Enabled = CanResetDestinationToDefault };
}
private bool CanResetDestinationToDefault => dstPath != defaultDstPath && defaultDstPath.Length != 0;
private void ResetDestinationToDefault()
{
DstPath = defaultDstPath;
}
private void StopRefreshBuilds()
{
if (cts != null)
{
cts.Cancel();
OnStopRefreshBuildsClicked.Enabled = false;
}
}
public async void RefreshBuildsAsync()
{
cts = new CancellationTokenSource();
OnRefreshBuildsClicked.Enabled = false;
OnStopRefreshBuildsClicked.Enabled = true;
Builds.Clear();
SelectedBuild = null;
try
{
var timer = new Stopwatch();
timer.Start();
var buildsPipe = Definition.EnumerateBuilds(cts.Token);
while (await buildsPipe.OutputAvailableAsync(cts.Token))
{
Builds.Add(await buildsPipe.ReceiveAsync());
}
timer.Stop();
Debug.WriteLine($"Search took {timer.Elapsed.TotalSeconds} seconds.");
}
catch (IOException)
{
// @todo show an error message
}
catch (OperationCanceledException)
{ }
finally
{
OnRefreshBuildsClicked.Enabled = true;
OnStopRefreshBuildsClicked.Enabled = false;
}
}
public void StartSync(IEnumerable<BuildPlatformModel> selectedBuilds)
{
onBuildsSelected?.Invoke(selectedBuilds.Select(s => (Path.Combine(dstPath, s.DestPathRelative), Definition.Exclusions?.ToArray(), s)).ToList());
}
public override void OnSelected()
{
if (firstRefresh)
{
RefreshBuildsAsync();
firstRefresh = false;
}
}
private void CopyBuildCL()
{
if (selectedBuild != null)
{
// Clipboard can be locked if timing is just right(wrong), retry a few times on failure.
int retryDelay = 0;
const int attempts = 7;
for (int attempt = 0; attempt < attempts; attempt++)
{
if (retryDelay > 0)
{
System.Threading.Thread.Sleep(retryDelay);
retryDelay *= 2;
}
else
{
retryDelay = 5;
}
try
{
Clipboard.SetText(selectedBuild.CL, TextDataFormat.Text);
break;
}
catch (System.Runtime.InteropServices.COMException)
{
}
}
}
}
private void OpenBuildLocation()
{
if (selectedBuild != null)
{
Shell.LaunchExplorer(selectedBuild.Path);
}
}
}
}