ChangesModel.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using JetBrains.Annotations;
  5. using Unity.Cloud.Collaborate.Models.Api;
  6. using Unity.Cloud.Collaborate.Models.Structures;
  7. using Unity.Cloud.Collaborate.UserInterface;
  8. using Unity.Cloud.Collaborate.Utilities;
  9. using UnityEngine;
  10. using UnityEngine.Assertions;
  11. namespace Unity.Cloud.Collaborate.Models
  12. {
  13. internal class ChangesModel : IChangesModel
  14. {
  15. protected readonly ISourceControlProvider m_Provider;
  16. /// <inheritdoc />
  17. public event Action UpdatedChangeList;
  18. /// <inheritdoc />
  19. public event Action OnUpdatedSelectedChanges;
  20. /// <inheritdoc />
  21. public event Action<bool> BusyStatusUpdated;
  22. /// <inheritdoc />
  23. public event Action StateChanged;
  24. internal Dictionary<string, IChangeEntryData> entryData;
  25. internal Dictionary<string, bool> toggledEntries;
  26. IReadOnlyList<IChangeEntryData> m_Conflicted;
  27. readonly ChangeEntryData m_AllItem;
  28. readonly HashSet<string> m_Requests;
  29. const string k_RequestNewList = "request-new-list";
  30. /// <inheritdoc />
  31. public string SavedRevisionSummary { get; set; }
  32. /// <inheritdoc />
  33. public string SavedSearchQuery { get; set; }
  34. /// <inheritdoc />
  35. public int ToggledCount { get; private set; }
  36. /// <inheritdoc />
  37. public int TotalCount { get; private set; }
  38. /// <inheritdoc />
  39. public int ConflictedCount => m_Conflicted.Count;
  40. /// <inheritdoc />
  41. public bool Conflicted => m_Provider.GetConflictedState();
  42. /// <inheritdoc />
  43. public bool Busy => m_Requests.Count != 0;
  44. public ChangesModel(ISourceControlProvider provider)
  45. {
  46. m_Provider = provider;
  47. m_Requests = new HashSet<string>();
  48. m_AllItem = new ChangeEntryData { Entry = new ChangeEntry(string.Empty), All = true };
  49. entryData = new Dictionary<string, IChangeEntryData>();
  50. m_Conflicted = new List<IChangeEntryData>();
  51. toggledEntries = new Dictionary<string, bool>();
  52. SavedSearchQuery = string.Empty;
  53. SavedRevisionSummary = string.Empty;
  54. }
  55. /// <inheritdoc />
  56. public void OnStart()
  57. {
  58. // Setup events.
  59. m_Provider.UpdatedChangeList += OnUpdatedChangeList;
  60. m_Provider.UpdatedSelectedChangeList += OnUpdatedSelectedChangesList;
  61. }
  62. /// <inheritdoc />
  63. public void OnStop()
  64. {
  65. // Clean up.
  66. m_Provider.UpdatedChangeList -= OnUpdatedChangeList;
  67. m_Provider.UpdatedSelectedChangeList -= OnUpdatedSelectedChangesList;
  68. }
  69. /// <inheritdoc />
  70. public void RestoreState(IWindowCache cache)
  71. {
  72. // Populate data from cache.
  73. SavedRevisionSummary = cache.RevisionSummary;
  74. SavedSearchQuery = cache.ChangesSearchValue;
  75. toggledEntries = cache.SimpleSelectedItems ?? new Dictionary<string, bool>();
  76. StateChanged?.Invoke();
  77. }
  78. /// <inheritdoc />
  79. public void SaveState(IWindowCache cache)
  80. {
  81. // Save data.
  82. cache.RevisionSummary = SavedRevisionSummary;
  83. cache.ChangesSearchValue = SavedSearchQuery;
  84. cache.SimpleSelectedItems = new SelectedItemsDictionary(toggledEntries);
  85. }
  86. /// <summary>
  87. /// Event handler for when the source control provider receives an updated history list.
  88. /// </summary>
  89. void OnUpdatedChangeList()
  90. {
  91. // Only one request at a time.
  92. if (!AddRequest(k_RequestNewList)) return;
  93. m_Provider.RequestChangeList(OnReceivedChangeList);
  94. }
  95. void OnUpdatedSelectedChangesList(IReadOnlyList<string> list)
  96. {
  97. ToggleAllEntries(false);
  98. foreach (var path in list)
  99. {
  100. UpdateEntryToggle(path, true);
  101. }
  102. OnUpdatedSelectedChanges?.Invoke();
  103. }
  104. /// <summary>
  105. /// Event handler to receive changes from the provider.
  106. /// </summary>
  107. /// <param name="list">Change list received.</param>
  108. void OnReceivedChangeList([CanBeNull] IReadOnlyList<IChangeEntry> list)
  109. {
  110. if (list != null)
  111. {
  112. UpdateChangeList(list);
  113. UpdatedChangeList?.Invoke();
  114. }
  115. else
  116. {
  117. Debug.LogError("Failed to fetch latest change list.");
  118. }
  119. RemoveRequest(k_RequestNewList);
  120. }
  121. /// <summary>
  122. /// Convert and cache new list of changes.
  123. /// </summary>
  124. /// <param name="list">New list of changes.</param>
  125. internal virtual void UpdateChangeList([NotNull] IReadOnlyList<IChangeEntry> list)
  126. {
  127. TotalCount = list.Count;
  128. // Create a new set of containers.
  129. var newEntryData = new Dictionary<string, IChangeEntryData> { [string.Empty] = m_AllItem };
  130. var newToggledEntries = new Dictionary<string, bool>();
  131. var conflicted = new List<IChangeEntryData>();
  132. var all = m_AllItem.Toggled;
  133. var toggledCount = 0;
  134. foreach (var entry in list)
  135. {
  136. // Transfer toggled state from old lookup into new.
  137. toggledEntries.TryGetValue(entry.Path, out var toggled);
  138. toggled = toggled || all || entry.Staged;
  139. newToggledEntries[entry.Path] = toggled;
  140. // Create a new data item for the entry.
  141. var item = new ChangeEntryData { Entry = entry, Toggled = toggled };
  142. newEntryData.Add(entry.Path, item);
  143. // Update counts.
  144. if (toggled)
  145. {
  146. toggledCount++;
  147. }
  148. if (entry.Unmerged)
  149. {
  150. conflicted.Add(item);
  151. }
  152. }
  153. // Store the new containers.
  154. entryData = newEntryData;
  155. toggledEntries = newToggledEntries;
  156. ToggledCount = toggledCount;
  157. m_Conflicted = conflicted;
  158. UpdateAllItemToggle();
  159. }
  160. /// <inheritdoc />
  161. public virtual bool UpdateEntryToggle(string path, bool toggled)
  162. {
  163. var entry = (ChangeEntryData)entryData[path];
  164. // Toggle all items if needed.
  165. if (entry.All)
  166. {
  167. return ToggleAllEntries(toggled);
  168. }
  169. // Update the toggled count.
  170. if (entry.Toggled && !toggled)
  171. {
  172. ToggledCount--;
  173. }
  174. else if (!entry.Toggled && toggled)
  175. {
  176. ToggledCount++;
  177. }
  178. // Store the value in the dictionary and data item.
  179. toggledEntries[entry.Entry.Path] = toggled;
  180. entry.Toggled = toggled;
  181. // Update the "All" option if needed.
  182. return UpdateAllItemToggle();
  183. }
  184. /// <inheritdoc />
  185. public IReadOnlyList<IChangeEntryData> GetToggledEntries(string query = null)
  186. {
  187. // Filter items by search query
  188. query = StringUtility.TrimAndToLower(query);
  189. return entryData.Values.Where(e => !e.All && e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList();
  190. }
  191. /// <inheritdoc />
  192. public IReadOnlyList<IChangeEntryData> GetUntoggledEntries(string query = null)
  193. {
  194. // Filter items by search query
  195. query = StringUtility.TrimAndToLower(query);
  196. return entryData.Values.Where(e => !e.All && !e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList();
  197. }
  198. /// <inheritdoc />
  199. public IReadOnlyList<IChangeEntryData> GetAllEntries(string query = null)
  200. {
  201. // Filter items by search query
  202. query = StringUtility.TrimAndToLower(query);
  203. return entryData.Values.Where(e => e.Entry.Path.ToLower().Contains(query)).ToList();
  204. }
  205. /// <inheritdoc />
  206. public IReadOnlyList<IChangeEntryData> GetConflictedEntries(string query = null)
  207. {
  208. // Filter items by search query
  209. query = StringUtility.TrimAndToLower(query);
  210. return entryData.Values.Where(e => !e.All && e.Conflicted && e.Entry.Path.ToLower().Contains(query))
  211. .ToList();
  212. }
  213. /// <summary>
  214. /// Update the state of the "All" entry. If all entries are toggled, then "All" should be toggled too;
  215. /// otherwise, "All" should be untoggled.
  216. /// </summary>
  217. /// <returns>True if the "All" entry was modified.</returns>
  218. bool UpdateAllItemToggle()
  219. {
  220. // Update state of the "All" option
  221. var allItemToggled = m_AllItem.Toggled;
  222. if (entryData.Count == 0) return false;
  223. if (ToggledCount == entryData.Count - 1)
  224. {
  225. // If every entry is toggled, then set AllItem as toggled.
  226. toggledEntries[m_AllItem.Entry.Path] = true;
  227. m_AllItem.Toggled = true;
  228. return !allItemToggled;
  229. }
  230. // Otherwise, set AllItem as not toggled.
  231. toggledEntries[m_AllItem.Entry.Path] = false;
  232. m_AllItem.Toggled = false;
  233. return allItemToggled;
  234. }
  235. /// <summary>
  236. /// Toggle on or off all entries in the list.
  237. /// </summary>
  238. /// <param name="toggled">Whether to toggle off or on.</param>
  239. /// <returns>True if the list has been modified.</returns>
  240. bool ToggleAllEntries(bool toggled)
  241. {
  242. // Update all values in the dictionary.
  243. toggledEntries.Keys.ToList().ForEach(x => toggledEntries[x] = toggled);
  244. // Compute the number of toggled items (excluding the single All).
  245. if (toggled)
  246. {
  247. ToggledCount = entryData.Count - 1;
  248. }
  249. else
  250. {
  251. ToggledCount = 0;
  252. }
  253. // Update all values in the list.
  254. foreach (var kv in entryData)
  255. {
  256. ((ChangeEntryData)kv.Value).Toggled = toggled;
  257. }
  258. return true;
  259. }
  260. /// <summary>
  261. /// Add a started request.
  262. /// </summary>
  263. /// <param name="requestId">Id of the request to add.</param>
  264. /// <returns>False if the request already exists.</returns>
  265. bool AddRequest(string requestId)
  266. {
  267. if (m_Requests.Contains(requestId)) return false;
  268. m_Requests.Add(requestId);
  269. // Signal background activity if this is the only thing running.
  270. if (m_Requests.Count == 1)
  271. BusyStatusUpdated?.Invoke(true);
  272. return true;
  273. }
  274. /// <summary>
  275. /// Remove a finished request.
  276. /// </summary>
  277. /// <param name="requestId">Id of the request to remove.</param>
  278. void RemoveRequest(string requestId)
  279. {
  280. Assert.IsTrue(m_Requests.Contains(requestId), $"Expects request to have first been made for it to have been finished: {requestId}");
  281. m_Requests.Remove(requestId);
  282. // Signal no background activity if no requests in progress
  283. if (m_Requests.Count == 0)
  284. BusyStatusUpdated?.Invoke(false);
  285. }
  286. /// <inheritdoc />
  287. public void RequestInitialData()
  288. {
  289. // Only one request at a time.
  290. if (!AddRequest(k_RequestNewList)) return;
  291. m_Provider.RequestChangeList(OnReceivedChangeList);
  292. }
  293. /// <inheritdoc />
  294. public void RequestDiffChanges(string path)
  295. {
  296. m_Provider.RequestDiffChanges(path);
  297. }
  298. /// <inheritdoc />
  299. public void RequestDiscard(IChangeEntry entry)
  300. {
  301. m_Provider.RequestDiscard(entry);
  302. }
  303. /// <inheritdoc />
  304. public void RequestBulkDiscard(IReadOnlyList<IChangeEntry> entries)
  305. {
  306. m_Provider.RequestBulkDiscard(entries);
  307. }
  308. /// <inheritdoc />
  309. public void RequestPublish(string message, IReadOnlyList<IChangeEntry> changes)
  310. {
  311. m_Provider.RequestPublish(message, changes);
  312. }
  313. /// <inheritdoc />
  314. public void RequestShowConflictedDifferences(string path)
  315. {
  316. m_Provider.RequestShowConflictedDifferences(path);
  317. }
  318. /// <inheritdoc />
  319. public void RequestChooseMerge(string path)
  320. {
  321. m_Provider.RequestChooseMerge(path);
  322. }
  323. /// <inheritdoc />
  324. public void RequestChooseMine(string[] paths)
  325. {
  326. m_Provider.RequestChooseMine(paths);
  327. }
  328. /// <inheritdoc />
  329. public void RequestChooseRemote(string[] paths)
  330. {
  331. m_Provider.RequestChooseRemote(paths);
  332. }
  333. /// <summary>
  334. /// Implementation of IChangeEntryData with each field given a setter so that the data can be updated.
  335. /// </summary>
  336. class ChangeEntryData : IChangeEntryData
  337. {
  338. /// <inheritdoc />
  339. public IChangeEntry Entry { get; set; }
  340. /// <inheritdoc />
  341. public bool Toggled { get; set; }
  342. /// <inheritdoc />
  343. public bool All { get; set; }
  344. /// <inheritdoc />
  345. public bool ToggleReadOnly => Entry.Staged;
  346. /// <inheritdoc />
  347. public bool Conflicted => Entry.Unmerged;
  348. }
  349. }
  350. }