using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Unity.Cloud.Collaborate.Models.Api; using Unity.Cloud.Collaborate.Models.Structures; using Unity.Cloud.Collaborate.UserInterface; using Unity.Cloud.Collaborate.Utilities; using UnityEngine; using UnityEngine.Assertions; namespace Unity.Cloud.Collaborate.Models { internal class ChangesModel : IChangesModel { protected readonly ISourceControlProvider m_Provider; /// public event Action UpdatedChangeList; /// public event Action OnUpdatedSelectedChanges; /// public event Action BusyStatusUpdated; /// public event Action StateChanged; internal Dictionary entryData; internal Dictionary toggledEntries; IReadOnlyList m_Conflicted; readonly ChangeEntryData m_AllItem; readonly HashSet m_Requests; const string k_RequestNewList = "request-new-list"; /// public string SavedRevisionSummary { get; set; } /// public string SavedSearchQuery { get; set; } /// public int ToggledCount { get; private set; } /// public int TotalCount { get; private set; } /// public int ConflictedCount => m_Conflicted.Count; /// public bool Conflicted => m_Provider.GetConflictedState(); /// public bool Busy => m_Requests.Count != 0; public ChangesModel(ISourceControlProvider provider) { m_Provider = provider; m_Requests = new HashSet(); m_AllItem = new ChangeEntryData { Entry = new ChangeEntry(string.Empty), All = true }; entryData = new Dictionary(); m_Conflicted = new List(); toggledEntries = new Dictionary(); SavedSearchQuery = string.Empty; SavedRevisionSummary = string.Empty; } /// public void OnStart() { // Setup events. m_Provider.UpdatedChangeList += OnUpdatedChangeList; m_Provider.UpdatedSelectedChangeList += OnUpdatedSelectedChangesList; } /// public void OnStop() { // Clean up. m_Provider.UpdatedChangeList -= OnUpdatedChangeList; m_Provider.UpdatedSelectedChangeList -= OnUpdatedSelectedChangesList; } /// public void RestoreState(IWindowCache cache) { // Populate data from cache. SavedRevisionSummary = cache.RevisionSummary; SavedSearchQuery = cache.ChangesSearchValue; toggledEntries = cache.SimpleSelectedItems ?? new Dictionary(); StateChanged?.Invoke(); } /// public void SaveState(IWindowCache cache) { // Save data. cache.RevisionSummary = SavedRevisionSummary; cache.ChangesSearchValue = SavedSearchQuery; cache.SimpleSelectedItems = new SelectedItemsDictionary(toggledEntries); } /// /// Event handler for when the source control provider receives an updated history list. /// void OnUpdatedChangeList() { // Only one request at a time. if (!AddRequest(k_RequestNewList)) return; m_Provider.RequestChangeList(OnReceivedChangeList); } void OnUpdatedSelectedChangesList(IReadOnlyList list) { ToggleAllEntries(false); foreach (var path in list) { UpdateEntryToggle(path, true); } OnUpdatedSelectedChanges?.Invoke(); } /// /// Event handler to receive changes from the provider. /// /// Change list received. void OnReceivedChangeList([CanBeNull] IReadOnlyList list) { if (list != null) { UpdateChangeList(list); UpdatedChangeList?.Invoke(); } else { Debug.LogError("Failed to fetch latest change list."); } RemoveRequest(k_RequestNewList); } /// /// Convert and cache new list of changes. /// /// New list of changes. internal virtual void UpdateChangeList([NotNull] IReadOnlyList list) { TotalCount = list.Count; // Create a new set of containers. var newEntryData = new Dictionary { [string.Empty] = m_AllItem }; var newToggledEntries = new Dictionary(); var conflicted = new List(); var all = m_AllItem.Toggled; var toggledCount = 0; foreach (var entry in list) { // Transfer toggled state from old lookup into new. toggledEntries.TryGetValue(entry.Path, out var toggled); toggled = toggled || all || entry.Staged; newToggledEntries[entry.Path] = toggled; // Create a new data item for the entry. var item = new ChangeEntryData { Entry = entry, Toggled = toggled }; newEntryData.Add(entry.Path, item); // Update counts. if (toggled) { toggledCount++; } if (entry.Unmerged) { conflicted.Add(item); } } // Store the new containers. entryData = newEntryData; toggledEntries = newToggledEntries; ToggledCount = toggledCount; m_Conflicted = conflicted; UpdateAllItemToggle(); } /// public virtual bool UpdateEntryToggle(string path, bool toggled) { var entry = (ChangeEntryData)entryData[path]; // Toggle all items if needed. if (entry.All) { return ToggleAllEntries(toggled); } // Update the toggled count. if (entry.Toggled && !toggled) { ToggledCount--; } else if (!entry.Toggled && toggled) { ToggledCount++; } // Store the value in the dictionary and data item. toggledEntries[entry.Entry.Path] = toggled; entry.Toggled = toggled; // Update the "All" option if needed. return UpdateAllItemToggle(); } /// public IReadOnlyList GetToggledEntries(string query = null) { // Filter items by search query query = StringUtility.TrimAndToLower(query); return entryData.Values.Where(e => !e.All && e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList(); } /// public IReadOnlyList GetUntoggledEntries(string query = null) { // Filter items by search query query = StringUtility.TrimAndToLower(query); return entryData.Values.Where(e => !e.All && !e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList(); } /// public IReadOnlyList GetAllEntries(string query = null) { // Filter items by search query query = StringUtility.TrimAndToLower(query); return entryData.Values.Where(e => e.Entry.Path.ToLower().Contains(query)).ToList(); } /// public IReadOnlyList GetConflictedEntries(string query = null) { // Filter items by search query query = StringUtility.TrimAndToLower(query); return entryData.Values.Where(e => !e.All && e.Conflicted && e.Entry.Path.ToLower().Contains(query)) .ToList(); } /// /// Update the state of the "All" entry. If all entries are toggled, then "All" should be toggled too; /// otherwise, "All" should be untoggled. /// /// True if the "All" entry was modified. bool UpdateAllItemToggle() { // Update state of the "All" option var allItemToggled = m_AllItem.Toggled; if (entryData.Count == 0) return false; if (ToggledCount == entryData.Count - 1) { // If every entry is toggled, then set AllItem as toggled. toggledEntries[m_AllItem.Entry.Path] = true; m_AllItem.Toggled = true; return !allItemToggled; } // Otherwise, set AllItem as not toggled. toggledEntries[m_AllItem.Entry.Path] = false; m_AllItem.Toggled = false; return allItemToggled; } /// /// Toggle on or off all entries in the list. /// /// Whether to toggle off or on. /// True if the list has been modified. bool ToggleAllEntries(bool toggled) { // Update all values in the dictionary. toggledEntries.Keys.ToList().ForEach(x => toggledEntries[x] = toggled); // Compute the number of toggled items (excluding the single All). if (toggled) { ToggledCount = entryData.Count - 1; } else { ToggledCount = 0; } // Update all values in the list. foreach (var kv in entryData) { ((ChangeEntryData)kv.Value).Toggled = toggled; } return true; } /// /// Add a started request. /// /// Id of the request to add. /// False if the request already exists. bool AddRequest(string requestId) { if (m_Requests.Contains(requestId)) return false; m_Requests.Add(requestId); // Signal background activity if this is the only thing running. if (m_Requests.Count == 1) BusyStatusUpdated?.Invoke(true); return true; } /// /// Remove a finished request. /// /// Id of the request to remove. void RemoveRequest(string requestId) { Assert.IsTrue(m_Requests.Contains(requestId), $"Expects request to have first been made for it to have been finished: {requestId}"); m_Requests.Remove(requestId); // Signal no background activity if no requests in progress if (m_Requests.Count == 0) BusyStatusUpdated?.Invoke(false); } /// public void RequestInitialData() { // Only one request at a time. if (!AddRequest(k_RequestNewList)) return; m_Provider.RequestChangeList(OnReceivedChangeList); } /// public void RequestDiffChanges(string path) { m_Provider.RequestDiffChanges(path); } /// public void RequestDiscard(IChangeEntry entry) { m_Provider.RequestDiscard(entry); } /// public void RequestBulkDiscard(IReadOnlyList entries) { m_Provider.RequestBulkDiscard(entries); } /// public void RequestPublish(string message, IReadOnlyList changes) { m_Provider.RequestPublish(message, changes); } /// public void RequestShowConflictedDifferences(string path) { m_Provider.RequestShowConflictedDifferences(path); } /// public void RequestChooseMerge(string path) { m_Provider.RequestChooseMerge(path); } /// public void RequestChooseMine(string[] paths) { m_Provider.RequestChooseMine(paths); } /// public void RequestChooseRemote(string[] paths) { m_Provider.RequestChooseRemote(paths); } /// /// Implementation of IChangeEntryData with each field given a setter so that the data can be updated. /// class ChangeEntryData : IChangeEntryData { /// public IChangeEntry Entry { get; set; } /// public bool Toggled { get; set; } /// public bool All { get; set; } /// public bool ToggleReadOnly => Entry.Staged; /// public bool Conflicted => Entry.Unmerged; } } }