Commit 1c847220 authored by jang dong hyeok's avatar jang dong hyeok
Browse files

.

parent 076f0c68
using System;
using System.Collections.Generic;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Components;
using Unity.Cloud.Collaborate.Components.Menus;
using Unity.Cloud.Collaborate.Models;
using Unity.Cloud.Collaborate.Models.Api;
using Unity.Cloud.Collaborate.Models.Enums;
using Unity.Cloud.Collaborate.Models.Providers;
using Unity.Cloud.Collaborate.Views;
using Unity.Cloud.Collaborate.Presenters;
using Unity.Cloud.Collaborate.Settings;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.UserInterface
{
internal class CollaborateWindow : EditorWindow
{
public const string UssClassName = "main-window";
public const string ContainerUssClassName = UssClassName + "__container";
public const string PackagePath = "Packages/com.unity.collab-proxy";
public const string UserInterfacePath = PackagePath + "/Editor/Collaborate/UserInterface";
public const string ResourcePath = PackagePath + "/Editor/Collaborate/Assets";
public const string LayoutPath = ResourcePath + "/Layouts";
public const string StylePath = ResourcePath + "/Styles";
public const string IconPath = ResourcePath + "/Icons";
public const string TestWindowPath = UserInterfacePath + "/TestWindows";
const string k_LayoutPath = LayoutPath + "/main-window.uxml";
public const string MainStylePath = StylePath + "/styles.uss";
MainPageView m_MainView;
ErrorPageView m_ErrorPageView;
StartPageView m_StartView;
VisualElement m_ViewContainer;
PageComponent m_ActivePage;
ISourceControlProvider m_Provider;
List<IModel> m_Models;
[MenuItem("Window/Collaborate")]
internal static void Init()
{
Init(FocusTarget.None);
}
internal static void Init(FocusTarget focusTarget)
{
var openLocation = CollabSettingsManager.Get(CollabSettings.settingDefaultOpenLocation, fallback: CollabSettings.OpenLocation.Docked);
CollaborateWindow window;
if (openLocation == CollabSettings.OpenLocation.Docked)
{
// Dock next to inspector, if available
var inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll");
window = GetWindow<CollaborateWindow>(inspectorType);
}
else
{
window = GetWindow<CollaborateWindow>();
}
// Set up window
window.titleContent = new GUIContent("Collaborate");
window.minSize = new Vector2(256, 400);
// Display window
window.Show();
window.Focus();
if (focusTarget != FocusTarget.None)
{
window.RequestFocus(focusTarget);
}
}
void OnDisable()
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload;
m_Provider.UpdatedProjectStatus -= OnUpdatedProjectStatus;
m_Models.ForEach(m => m.OnStop());
}
void CreateGUI()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload;
var root = rootVisualElement;
root.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(MainStylePath));
root.AddToClassList(EditorGUIUtility.isProSkin
? UiConstants.ussDark
: UiConstants.ussLight);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(root);
m_Provider = new Collab();
m_Provider.UpdatedProjectStatus += OnUpdatedProjectStatus;
m_ViewContainer = root.Q<VisualElement>(className: ContainerUssClassName);
// Create models and configure them.
var mainModel = new MainModel(m_Provider);
var startModel = new StartModel(m_Provider);
m_Models = new List<IModel> { mainModel, startModel };
m_Models.ForEach(m => m.OnStart());
// Get the views and configure them.
m_MainView = new MainPageView();
m_MainView.Presenter = new MainPresenter(m_MainView, mainModel);
m_StartView = new StartPageView();
m_StartView.Presenter = new StartPresenter(m_StartView, startModel);
m_ErrorPageView = new ErrorPageView();
// Add floating dialogue so it can be displayed anywhere in the window.
root.Add(FloatingDialogue.Instance);
OnUpdatedProjectStatus(m_Provider.GetProjectStatus());
}
/// <summary>
/// React to the play mode state changing. When in play mode, disable collab.
/// </summary>
/// <param name="state">Editor play mode state.</param>
void OnPlayModeStateChanged(PlayModeStateChange state)
{
bool enabled;
switch (state)
{
case PlayModeStateChange.EnteredEditMode:
case PlayModeStateChange.ExitingEditMode:
enabled = true;
break;
case PlayModeStateChange.EnteredPlayMode:
case PlayModeStateChange.ExitingPlayMode:
enabled = false;
break;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
m_ViewContainer.SetEnabled(enabled);
}
/// <summary>
/// Restore window state after assembly reload.
/// </summary>
void OnAfterAssemblyReload()
{
m_Models.ForEach(m => m.RestoreState(WindowCache.Instance));
}
/// <summary>
/// Save state before domain reload.
/// </summary>
void OnBeforeAssemblyReload()
{
m_Models.ForEach(m => m.SaveState(WindowCache.Instance));
WindowCache.Instance.Serialize();
}
/// <summary>
/// Respond to changes in the project status.
/// </summary>
/// <param name="status">New project status.</param>
void OnUpdatedProjectStatus(ProjectStatus status)
{
if (status == ProjectStatus.Ready)
{
UpdateDisplayMode(Display.Main);
}
else
{
WindowCache.Instance.Clear();
m_Models.ForEach(m => m.RestoreState(WindowCache.Instance));
UpdateDisplayMode(Display.Add);
}
}
void RequestFocus(FocusTarget focusTarget)
{
if (m_ActivePage != m_MainView)
{
// Cannot focus changes or history pane if we're not already on mainview
return;
}
if (focusTarget == FocusTarget.Changes)
{
m_MainView.SetTab(MainPageView.ChangesTabIndex);
}
else if (focusTarget == FocusTarget.History)
{
m_MainView.SetTab(MainPageView.HistoryTabIndex);
}
else
{
Debug.LogError("Collab Error: Attempting to focus unknown target.");
}
}
/// <summary>
/// Switch the view displayed in the window.
/// </summary>
/// <param name="newDisplay">Display to switch the window to.</param>
void UpdateDisplayMode(Display newDisplay)
{
m_ActivePage?.RemoveFromHierarchy();
m_ActivePage?.SetActive(false);
m_ViewContainer.Clear();
// Get new page to display
switch (newDisplay)
{
case Display.Add:
m_ActivePage = m_StartView;
break;
case Display.Error:
m_ActivePage = m_ErrorPageView;
break;
case Display.Main:
m_ActivePage = m_MainView;
break;
default:
throw new ArgumentOutOfRangeException();
}
m_ActivePage.SetActive(true);
m_ViewContainer.Add(m_ActivePage);
}
enum Display
{
Add,
Error,
Main
}
public enum FocusTarget
{
None,
History,
Changes
}
}
}
# Collaborate User Interface
This directory contains the logic to present the collaborate UI.
## Overview
This is the structure of the directory:
```none
<root>
├── TestWindows/
├── Bootstrap.cs
├── CollaborateWindow.cs
├── ToolbarButton.cs
└── WindowCache.cs
```
The `TestWindows/` directory contains testing windows and is not present in release builds.
`Bootstrap.cs` provides the code to initialize the toolbar button on start up.
`CollaborateWindow.cs` is the entry point for the user interface. It spawns a EditorWindow and sets up the UI.
`ToolbarButton.cs` contains the code to create, update, and handle the collaborate button in the toolbar.
`WindowCache.cs` provides a collection of fields that are preserved during domain reload and editor restart. Some
examples are the the current commit message and the currently selected items for the simple UI/UX. Any data that would
impact UX if lost during reload or exit, should be saved in here.
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.Collaboration;
using UnityEditor.Connect;
using UnityEngine;
namespace Unity.Cloud.Collaborate.UserInterface
{
internal class ToolbarButton : SubToolbar
{
protected enum ToolbarButtonState
{
NeedToEnableCollab,
UpToDate,
Conflict,
OperationError,
ServerHasChanges,
FilesToPush,
InProgress,
Disabled,
Offline
}
ToolbarButtonState m_CurrentState;
string m_ErrorMessage;
readonly Dictionary<ToolbarButtonState, GUIContent> m_IconCache = new Dictionary<ToolbarButtonState, GUIContent>();
ButtonWithAnimatedIconRotation m_CollabButton;
public ToolbarButton()
{
Collab.instance.StateChanged += OnCollabStateChanged;
UnityConnect.instance.StateChanged += OnUnityConnectStateChanged;
UnityConnect.instance.UserStateChanged += OnUnityConnectUserStateChanged;
}
~ToolbarButton()
{
Collab.instance.StateChanged -= OnCollabStateChanged;
UnityConnect.instance.StateChanged -= OnUnityConnectStateChanged;
UnityConnect.instance.UserStateChanged -= OnUnityConnectUserStateChanged;
}
void OnUnityConnectUserStateChanged(UserInfo state)
{
Update();
}
void OnUnityConnectStateChanged(ConnectInfo state)
{
Update();
}
void OnCollabStateChanged(CollabInfo info)
{
Update();
}
[CanBeNull]
static Texture LoadIcon([NotNull] string iconName)
{
var hidpi = EditorGUIUtility.pixelsPerPoint > 1f ? "@2x" : string.Empty;
return AssetDatabase.LoadAssetAtPath<Texture>($"{CollaborateWindow.IconPath}/{iconName}-{(EditorGUIUtility.isProSkin ? "dark" : "light")}{hidpi}.png");
}
[NotNull]
GUIContent GetIconForState()
{
// Get cached icon, or construct it.
if (!m_IconCache.TryGetValue(m_CurrentState, out var content))
{
string iconName;
string tooltip;
switch (m_CurrentState)
{
case ToolbarButtonState.NeedToEnableCollab:
iconName = "collaborate";
tooltip = "You need to enable collab.";
break;
case ToolbarButtonState.UpToDate:
iconName = "collaborate";
tooltip = "You are up to date.";
break;
case ToolbarButtonState.Conflict:
iconName = "collaborate-error";
tooltip = "Please fix your conflicts prior to publishing.";
break;
case ToolbarButtonState.OperationError:
iconName = "collaborate-error";
tooltip = "Last operation failed. Please retry later.";
break;
case ToolbarButtonState.ServerHasChanges:
iconName = "collaborate-incoming";
tooltip = "Please update, there are server changes.";
break;
case ToolbarButtonState.FilesToPush:
iconName = "collaborate-available-changes";
tooltip = "You have files to publish.";
break;
case ToolbarButtonState.InProgress:
iconName = "collaborate-progress";
tooltip = "Operation in progress.";
break;
case ToolbarButtonState.Disabled:
iconName = "collaborate";
tooltip = "Collab is disabled.";
break;
case ToolbarButtonState.Offline:
iconName = "collaborate-offline";
tooltip = "Please check your network connection.";
break;
default:
throw new ArgumentOutOfRangeException();
}
// Create icon with tooltip and cache.
content = new GUIContent(LoadIcon(iconName), $"Collaborate • {tooltip}");
m_IconCache[m_CurrentState] = content;
}
// Add error message tooltip if there's a message.
var icon = new GUIContent(content);
if (!string.IsNullOrEmpty(m_ErrorMessage))
{
icon.tooltip = $"Collaborate • {m_ErrorMessage}";
}
return icon;
}
public override void OnGUI(Rect rect)
{
GUIStyle collabButtonStyle = "AppCommand";
var disable = EditorApplication.isPlaying;
using (new EditorGUI.DisabledScope(disable))
{
var icon = GetIconForState();
EditorGUIUtility.SetIconSize(new Vector2(16, 16));
if (GUI.Button(rect, icon, collabButtonStyle))
{
CollaborateWindow.Init();
}
EditorGUIUtility.SetIconSize(Vector2.zero);
}
}
public void Update()
{
var currentState = GetCurrentState();
if (m_CurrentState == currentState) return;
m_CurrentState = currentState;
Toolbar.RepaintToolbar();
}
protected virtual ToolbarButtonState GetCurrentState()
{
var currentState = ToolbarButtonState.UpToDate;
var networkAvailable = UnityConnect.instance.connectInfo.online && UnityConnect.instance.connectInfo.loggedIn;
m_ErrorMessage = string.Empty;
if (UnityConnect.instance.isDisableCollabWindow)
{
currentState = ToolbarButtonState.Disabled;
}
else if (networkAvailable)
{
var collab = Collab.instance;
var currentInfo = collab.collabInfo;
if (!currentInfo.ready)
{
currentState = ToolbarButtonState.InProgress;
}
else if (collab.GetError(UnityConnect.UnityErrorFilter.ByContext | UnityConnect.UnityErrorFilter.ByChild, out var errInfo) &&
errInfo.priority <= (int)UnityConnect.UnityErrorPriority.Error)
{
currentState = ToolbarButtonState.OperationError;
m_ErrorMessage = errInfo.shortMsg;
}
else if (currentInfo.inProgress)
{
currentState = ToolbarButtonState.InProgress;
}
else
{
var collabEnabled = Collab.instance.IsCollabEnabledForCurrentProject();
if (UnityConnect.instance.projectInfo.projectBound == false || !collabEnabled)
{
currentState = ToolbarButtonState.NeedToEnableCollab;
}
else if (currentInfo.update)
{
currentState = ToolbarButtonState.ServerHasChanges;
}
else if (currentInfo.conflict)
{
currentState = ToolbarButtonState.Conflict;
}
else if (currentInfo.publish)
{
currentState = ToolbarButtonState.FilesToPush;
}
}
}
else
{
currentState = ToolbarButtonState.Offline;
}
return currentState;
}
}
}
using System;
using System.Collections.Generic;
using Unity.Cloud.Collaborate.Common;
using UnityEngine;
using UnityEngine.Serialization;
namespace Unity.Cloud.Collaborate.UserInterface
{
internal interface IWindowCache
{
void Clear();
SelectedItemsDictionary SimpleSelectedItems { get; set; }
string RevisionSummary { get; set; }
string ChangesSearchValue { get; set; }
string SelectedHistoryRevision { get; set; }
int HistoryPageNumber { get; set; }
int TabIndex { get; set; }
}
[Location("Cache/Window.yml", LocationAttribute.Location.LibraryFolder)]
internal class WindowCache : ScriptableObjectSingleton<WindowCache>, IWindowCache
{
public event Action<IWindowCache> BeforeSerialize;
public void Serialize()
{
BeforeSerialize?.Invoke(this);
Save();
}
public void Clear()
{
SimpleSelectedItems = default;
RevisionSummary = default;
ChangesSearchValue = default;
SelectedHistoryRevision = default;
HistoryPageNumber = default;
TabIndex = default;
}
SelectedItemsDictionary IWindowCache.SimpleSelectedItems
{
get => SimpleSelectedItems;
set => SimpleSelectedItems = value;
}
string IWindowCache.RevisionSummary
{
get => RevisionSummary;
set => RevisionSummary = value;
}
string IWindowCache.ChangesSearchValue
{
get => ChangesSearchValue;
set => ChangesSearchValue = value;
}
string IWindowCache.SelectedHistoryRevision
{
get => SelectedHistoryRevision;
set => SelectedHistoryRevision = value;
}
int IWindowCache.HistoryPageNumber
{
get => HistoryPageNumber;
set => HistoryPageNumber = value;
}
int IWindowCache.TabIndex
{
get => TabIndex;
set => TabIndex = value;
}
[SerializeField]
public SelectedItemsDictionary SimpleSelectedItems = new SelectedItemsDictionary();
[FormerlySerializedAs("CommitMessage")]
[SerializeField]
public string RevisionSummary;
[SerializeField]
public string ChangesSearchValue;
[SerializeField]
public string SelectedHistoryRevision;
[SerializeField]
public int HistoryPageNumber;
[SerializeField]
public int TabIndex;
}
[Serializable]
internal class SelectedItemsDictionary : SerializableDictionary<string, bool>
{
public SelectedItemsDictionary() { }
public SelectedItemsDictionary(IDictionary<string, bool> dictionary) : base(dictionary) { }
}
}
using System;
using System.Linq;
namespace Unity.Cloud.Collaborate.Utilities
{
static class ExtensionMethods
{
// Credit: https://stackoverflow.com/a/4405876
/// <summary>
/// Take the first letter of the string and capitalise it.
/// </summary>
/// <param name="input">String to work with.</param>
/// <returns>String with first letter capitalised.</returns>
/// <exception cref="ArgumentNullException">If string is null.</exception>
/// <exception cref="ArgumentException">If string is empty.</exception>
public static string FirstCharToUpper(this string input)
{
switch (input)
{
case null: throw new ArgumentNullException(nameof(input));
case "": throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input));
default: return input.First().ToString().ToUpper() + input.Substring(1);
}
}
}
}
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Utilities
{
static class MenuUtilities
{
/// <summary>
/// Corner of the anchor element the dialogue should anchor to.
/// </summary>
public enum AnchorPoint
{
TopLeft,
TopRight,
BottomLeft,
BottomRight
}
/// <summary>
/// Direction the dialogue should open from its anchor.
/// </summary>
public enum OpenDirection
{
UpLeft,
UpRight,
DownLeft,
DownRight
}
/// <summary>
/// Given an element and an anchor point, calculate the world coords to draw a menu at.
/// </summary>
/// <param name="e">Element to start at.</param>
/// <param name="anchorPoint">Corner of the element to calculate.</param>
/// <returns>World coordinates from the given values.</returns>
public static (float X, float Y) GetMenuPosition(VisualElement e, AnchorPoint anchorPoint)
{
// Calculate position of the start corner.
(float x, float y) anchorCoords;
switch (anchorPoint)
{
case AnchorPoint.TopLeft:
anchorCoords = (e.worldBound.xMin, e.worldBound.yMin);
break;
case AnchorPoint.TopRight:
anchorCoords = (e.worldBound.xMax, e.worldBound.yMin);
break;
case AnchorPoint.BottomLeft:
anchorCoords = (e.worldBound.xMin, e.worldBound.yMax);
break;
case AnchorPoint.BottomRight:
anchorCoords = (e.worldBound.xMax, e.worldBound.yMax);
break;
default:
throw new ArgumentOutOfRangeException(nameof(anchorPoint), anchorPoint, null);
}
return anchorCoords;
}
}
}
using System;
using UnityEditor.Connect;
namespace Unity.Cloud.Collaborate.Utilities
{
internal static class OpenLinksUtility
{
public static void OpenMembersLink()
{
string url;
var config = UnityConnect.instance.configuration;
switch (config)
{
case "development": url = "https://dev-developer.cloud.unity3d.com/orgs/{0}/projects/{1}/users"; break;
case "staging": url = "https://staging-developer.cloud.unity3d.com/orgs/{0}/projects/{1}/users"; break;
case "production": url = "https://developer.cloud.unity3d.com/orgs/{0}/projects/{1}/users"; break;
default:
UnityEngine.Debug.LogError($"Unexpected connection configuration {config}"); return;
}
// url = url.Replace("%%ORGID%%", UnityConnect.instance.projectInfo.organizationId).Replace("%%UPID%%", UnityConnect.instance.projectInfo.projectGUID);
url = string.Format(url, UnityConnect.instance.projectInfo.organizationId, UnityConnect.instance.projectInfo.projectGUID);
UnityConnect.instance.OpenAuthorizedURLInWebBrowser(url);
}
}
}
# Unity Collaborate Utility Code
This directory contains utility classes and logic for the package.
using System;
namespace Unity.Cloud.Collaborate.Utilities
{
static class StringUtility
{
public static string TrimAndToLower(string value)
{
return string.IsNullOrEmpty(value)
? string.Empty
: value.Trim().ToLower();
}
}
}
using System;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace Unity.Cloud.Collaborate.Utilities
{
internal static class Threading
{
/// <summary>
/// Returns true if the current thread is the main thread, false otherwise.
/// </summary>
public static bool IsMainThread => InternalEditorUtility.CurrentThreadIsMainThread();
/// <summary>
/// Ensure that the provided action is executed on the UI/main thread.
/// </summary>
/// <param name="action">Action to perform on the UI/main thread.</param>
public static void EnsureUiThread(Action action)
{
if (IsMainThread)
{
action();
}
else
{
EditorApplication.delayCall += () => action();
}
}
}
}
using System;
using System.Globalization;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Settings;
namespace Unity.Cloud.Collaborate.Utilities
{
/// <summary>
/// Static class that presents methods to provide timestamps for the UI.
/// </summary>
static class TimeStamp
{
/// <summary>
/// Bool to decide whether timestamps should be exact values or relative values.
/// </summary>
public static bool UseRelativeTimeStamps =>
CollabSettingsManager.Get(CollabSettings.settingRelativeTimestamp, fallback: true);
/// <summary>
/// Values to translate a number to a string representation.
/// </summary>
static readonly string[] k_UnitsMap = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" };
/// <summary>
/// Get the localized or relative timestamp for the given DateTime based on the current settings.
/// </summary>
/// <param name="dateTime">DateTime to convert.</param>
/// <returns>String representation of the given DateTime.</returns>
[NotNull]
public static string GetTimeStamp(DateTimeOffset dateTime)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
return UseRelativeTimeStamps
? GetElapsedTime(dateTime)
: GetLocalisedTimeStamp(dateTime);
}
/// <summary>
/// Get the localised timestamp for the given DateTime.
/// </summary>
/// <param name="dateTime">DateTime to convert.</param>
/// <returns>Localised string representation of the given DateTime.</returns>
[NotNull]
public static string GetLocalisedTimeStamp(DateTimeOffset dateTime)
{
return dateTime.ToString(CultureInfo.CurrentCulture.DateTimeFormat.FullDateTimePattern);
}
// Original credit: https://codereview.stackexchange.com/questions/93239/get-elapsed-time-as-human-friendly-string
/// <summary>
/// Convert a DateTime into a relative timestamp.
/// </summary>
/// <param name="dateTime">Datetime to calculate the timestamp from.</param>
/// <returns>Relative timestamp for the given DateTime.</returns>
[NotNull]
static string GetElapsedTime(DateTimeOffset dateTime)
{
var offset = DateTimeOffset.Now.Subtract(dateTime);
// The trick: make variable contain date and time representing the desired timespan,
// having +1 in each date component.
var date = DateTimeOffset.MinValue + offset;
return ProcessPeriod(date.Year - 1, date.Month - 1, "year")
?? ProcessPeriod(date.Month - 1, date.Day - 1, "month")
?? ProcessPeriod(date.Day - 1, date.Hour, "day", "Yesterday")
?? ProcessPeriod(date.Hour, date.Minute, "hour")
?? ProcessPeriod(date.Minute, date.Second, "minute")
?? ProcessPeriod(date.Second, 0, "second")
?? "Right now";
}
// Original credit: https://codereview.stackexchange.com/questions/93239/get-elapsed-time-as-human-friendly-string
/// <summary>
/// Output the string representation for the given time frame. If it's not in that time frame, it returns null.
/// </summary>
/// <param name="value">Bigger time value.</param>
/// <param name="subValue">Smaller time value eg: minutes if value is hours.</param>
/// <param name="name">Name of the period.</param>
/// <param name="singularName">Name for period that is singular. Null if it's not singular eg: yesterday.</param>
/// <returns>String representation of the period, or null if it's outside of it.</returns>
[CanBeNull]
static string ProcessPeriod(int value, int subValue, string name, string singularName = null)
{
// If the value is less than this time frame, skip.
if (value == 0)
{
return null;
}
// If a multiple of this time frame eg: 20 minutes.
if (value != 1)
{
// Convert values specified to string numbers.
var stringValue = value <k_UnitsMap.Length ? k_UnitsMap[value] : value.ToString();
return subValue == 0
? $"{stringValue.FirstCharToUpper()} {name}s ago"
: $"About {stringValue} {name}s ago";
}
// Special case for one-off names eg: yesterday.
if (!string.IsNullOrEmpty(singularName))
{
return singularName;
}
// Singular time frame eg: an hour, a minute.
var articleSuffix = name[0] == 'h' ? "n" : string.Empty;
return subValue == 0
? $"A{articleSuffix} {name} ago"
: $"About a{articleSuffix} {name} ago";
}
}
}
using System;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Views.Adapters {
internal interface IAdapter
{
int Height { get; }
Func<VisualElement> MakeItem { get; }
Action<VisualElement, int> BindItem { get; }
int GetEntryCount();
void RegisterObserver(IAdapterObserver observer);
void DeregisterObserver(IAdapterObserver observer);
}
}
using System;
namespace Unity.Cloud.Collaborate.Views.Adapters
{
internal interface IAdapterObserver
{
void NotifyDataSetChanged();
}
}
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Views.Adapters.ListAdapters
{
/// <summary>
/// Adapter used to provide entries to the AdapterListView. Allows the data to be kept separately to the layout
/// and visual elements.
/// </summary>
/// <typeparam name="T">Type of list element the adapter provides</typeparam>
internal abstract class BaseListAdapter<T> : IAdapter where T : VisualElement
{
readonly List<IAdapterObserver> m_AdapterObservers = new List<IAdapterObserver>();
#region PrivateInterfaceFields
Func<VisualElement> IAdapter.MakeItem => MakeItem;
Action<VisualElement, int> IAdapter.BindItem => (v, i) => BindItem((T)v, i);
#endregion
#region UserOverrides
/// <summary>
/// Provides the static height for each element.
/// </summary>
public abstract int Height { get; }
/// <summary>
/// Creates and returns the layout for the entry.
/// </summary>
/// <returns>Created entry layout.</returns>
protected abstract T MakeItem();
/// <summary>
/// Binds data to the entry at the given index.
/// </summary>
/// <param name="element">Entry to bind to.</param>
/// <param name="index">Index in the data.</param>
protected abstract void BindItem(T element, int index);
/// <summary>
/// Gets the count of the number of entries in the list.
/// </summary>
/// <returns>The entry count.</returns>
public abstract int GetEntryCount();
#endregion
/// <summary>
/// Register an observer for this adapter.
/// </summary>
/// <param name="observer">Observer to register.</param>
public void RegisterObserver(IAdapterObserver observer)
{
m_AdapterObservers.Add(observer);
}
/// <summary>
/// Deregister an observer for this adapter.
/// </summary>
/// <param name="observer">Observer to deregister.</param>
public void DeregisterObserver(IAdapterObserver observer)
{
m_AdapterObservers.Remove(observer);
}
/// <summary>
/// Notify that the data set in this adapter has changed.
/// </summary>
public void NotifyDataSetChanged()
{
foreach (var observer in m_AdapterObservers)
{
observer.NotifyDataSetChanged();
}
}
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Components.ChangeListEntries;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.Presenters;
using UnityEngine.Assertions;
namespace Unity.Cloud.Collaborate.Views.Adapters.ListAdapters
{
internal class ConflictedChangeListAdapter : BaseListAdapter<ConflictedChangeListElement>
{
IChangesPresenter m_Presenter;
[CanBeNull]
IReadOnlyList<IChangeEntryData> m_List;
public IReadOnlyList<IChangeEntryData> List
{
set
{
m_List = value;
NotifyDataSetChanged();
}
}
public ConflictedChangeListAdapter([NotNull] IChangesPresenter presenter)
{
m_Presenter = presenter;
}
public override int Height { get; } = UiConstants.ChangesListViewItemHeight;
protected override ConflictedChangeListElement MakeItem()
{
return new ConflictedChangeListElement();
}
protected override void BindItem(ConflictedChangeListElement element, int index)
{
Assert.IsNotNull(m_List, "List should not be null at this point.");
element.ClearData();
var changesEntry = m_List[index];
var path = changesEntry.All ? StringAssets.all : changesEntry.Entry.Path;
element.UpdateFilePath(path);
// Update status icon
element.statusIcon.ClearClassList();
element.statusIcon.AddToClassList(BaseChangeListElement.IconUssClassName);
element.statusIcon.AddToClassList(ToggleableChangeListElement.StatusIconUssClassName);
element.statusIcon.AddToClassList(changesEntry.Entry.StatusToString());
// Wire up buttons
element.showButton.Clicked += () => m_Presenter.RequestShowConflictedDifferences(changesEntry.Entry.Path);
element.chooseMergeButton.Clicked += () => m_Presenter.RequestChooseMerge(changesEntry.Entry.Path);
element.chooseMineButton.Clicked += () => m_Presenter.RequestChooseMine(changesEntry.Entry.Path);
element.chooseRemoteButton.Clicked += () => m_Presenter.RequestChooseRemote(changesEntry.Entry.Path);
}
public override int GetEntryCount()
{
return m_List?.Count ?? 0;
}
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Presenters;
using Unity.Cloud.Collaborate.Components.ChangeListEntries;
using Unity.Cloud.Collaborate.Models.Structures;
namespace Unity.Cloud.Collaborate.Views.Adapters.ListAdapters
{
internal class HistoryEntryChangeListAdapter : BaseListAdapter<HistoryChangeListElement>
{
string m_RevisionId;
IList<IChangeEntry> m_List;
readonly IHistoryPresenter m_Presenter;
public HistoryEntryChangeListAdapter([NotNull] IHistoryPresenter presenter, [NotNull] string revisionId, [NotNull] IList<IChangeEntry> list)
{
m_Presenter = presenter;
m_RevisionId = revisionId;
m_List = list;
}
public override int Height => UiConstants.HistoryListViewItemHeight;
protected override HistoryChangeListElement MakeItem()
{
return new HistoryChangeListElement();
}
protected override void BindItem(HistoryChangeListElement element, int index)
{
element.ClearData();
var entry = m_List[index];
element.UpdateFilePath(entry.Path);
// TODO: make status icon an object to handle this logic
element.statusIcon.ClearClassList();
element.statusIcon.AddToClassList(BaseChangeListElement.IconUssClassName);
element.statusIcon.AddToClassList(HistoryChangeListElement.StatusIconUssClassName);
element.statusIcon.AddToClassList(entry.StatusToString());
if (m_Presenter.SupportsRevert)
{
element.revertButton.Clicked += () => m_Presenter.RequestRevert(m_RevisionId, new List<string> { entry.Path });
}
else
{
element.revertButton.AddToClassList(UiConstants.ussHidden);
}
}
public override int GetEntryCount()
{
return m_List.Count;
}
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Components.ChangeListEntries;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.Presenters;
using UnityEngine.Assertions;
namespace Unity.Cloud.Collaborate.Views.Adapters.ListAdapters
{
internal class ToggleableChangeListAdapter : BaseListAdapter<ToggleableChangeListElement>
{
[CanBeNull]
IReadOnlyList<IChangeEntryData> m_List;
public IReadOnlyList<IChangeEntryData> List
{
set
{
m_List = value;
NotifyDataSetChanged();
}
}
readonly IChangesPresenter m_Presenter;
int m_LastBoundElementIndex;
public ToggleableChangeListAdapter(IChangesPresenter presenter)
{
m_Presenter = presenter;
}
public override int Height { get; } = UiConstants.ChangesListViewItemHeight;
protected override ToggleableChangeListElement MakeItem()
{
return new ToggleableChangeListElement();
}
protected override void BindItem(ToggleableChangeListElement element, int index)
{
Assert.IsNotNull(m_List, "List should not be null at this point.");
m_LastBoundElementIndex = index;
element.ClearData();
var changesEntry = m_List[index];
var path = changesEntry.All ? StringAssets.all : changesEntry.Entry.Path;
element.UpdateFilePath(path);
// Setup callbacks
element.SetToggleCallback(c => OnItemToggleChanged(index, c));
element.diffButton.Clicked += () => OnDiffClicked(index);
element.discardButton.RemoveFromClassList(UiConstants.ussHidden);
element.discardButton.Clicked += () => OnDiscardClicked(index);
// Update the toggle and tooltips.
if (changesEntry.ToggleReadOnly)
{
element.toggle.SetValueWithoutNotify(true);
element.toggle.SetEnabled(false);
element.toggle.parent.tooltip = StringAssets.includedToPublishByAnotherGitTool;
}
else
{
element.toggle.SetValueWithoutNotify(changesEntry.Toggled);
element.toggle.SetEnabled(true);
element.toggle.parent.tooltip = string.Empty;
}
// Update the visibility of the icon and discard button.
if (changesEntry.All)
{
element.buttons.AddToClassList(UiConstants.ussHidden);
element.statusIcon.AddToClassList(UiConstants.ussHidden);
}
else
{
element.buttons.RemoveFromClassList(UiConstants.ussHidden);
// TODO: make status icon an object to handle this logic
element.statusIcon.ClearClassList();
element.statusIcon.AddToClassList(BaseChangeListElement.IconUssClassName);
element.statusIcon.AddToClassList(ToggleableChangeListElement.StatusIconUssClassName);
element.statusIcon.AddToClassList(changesEntry.Entry.StatusToString());
}
}
public override int GetEntryCount()
{
return m_List?.Count ?? 0;
}
void OnItemToggleChanged(int index, bool toggled)
{
Assert.IsNotNull(m_List, "List should not be null at this point.");
var changeEntry = m_List[index];
var refresh = m_Presenter.UpdateEntryToggle(changeEntry.Entry.Path, toggled);
if (refresh) NotifyDataSetChanged();
}
void OnDiscardClicked(int index)
{
Assert.IsNotNull(m_List, "List should not be null at this point.");
var changeEntry = m_List[index];
m_Presenter.RequestDiscard(changeEntry.Entry);
}
public int GetLastBoundElementIndex()
{
return m_LastBoundElementIndex;
}
public int GetFirstToggledIndex()
{
Assert.IsNotNull(m_List, "List should not be null at this point.");
for (var i=0; i < m_List.Count; i++)
{
if(m_List[i].Toggled)
{
return i;
}
}
return -1;
}
void OnDiffClicked(int index)
{
Assert.IsNotNull(m_List, "List should not be null at this point.");
var changeEntry = m_List[index];
m_Presenter.RequestDiffChanges(changeEntry.Entry.Path);
}
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Views.Adapters.ListAdapters;
using Unity.Cloud.Collaborate.Components;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.Presenters;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Views
{
[UsedImplicitly]
internal class ChangesTabPageView : TabPageComponent, IChangesView
{
[CanBeNull]
IChangesPresenter m_Presenter;
public const string UssClassName = "changes-tab-page-view";
public const string SearchBarUssClassName = UssClassName + "__search-bar";
public const string EntryGroupsUssClassName = UssClassName + "__entry-groups";
public const string PublishButtonUssClassName = UssClassName + "__publish-button";
public const string TextFieldUssClassName = UssClassName + "__text-field";
public const string ListViewUssClassName = UssClassName + "__list-view";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(ChangesTabPageView)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(ChangesTabPageView)}.uss";
readonly IconTextButton m_PublishButton;
readonly BetterTextField m_RevisionSummaryBox;
readonly SearchBar m_SearchBar;
readonly VisualElement m_EntryGroupsContainer;
bool m_Active;
[CanBeNull]
ConflictedChangeListAdapter m_ConflictedChangeListAdapter;
[CanBeNull]
ToggleableChangeListAdapter m_ToggleableChangeListAdapter;
[CanBeNull]
ChangeEntryGroup m_EntryToggleableGroup;
[CanBeNull]
ChangeEntryGroup m_EntryConflictsGroup;
[CanBeNull]
VisualElement m_ActiveGroup;
public ChangesTabPageView()
{
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Get the components defined in the style / layout files.
m_SearchBar = this.Q<SearchBar>(className: SearchBarUssClassName);
m_RevisionSummaryBox = this.Q<BetterTextField>(className: TextFieldUssClassName);
m_PublishButton = this.Q<IconTextButton>(className: PublishButtonUssClassName);
m_EntryGroupsContainer = this.Q<VisualElement>(className: EntryGroupsUssClassName);
// Initialize the text strings.
m_PublishButton.Text = StringAssets.publishButton;
m_RevisionSummaryBox.Placeholder = StringAssets.publishSummaryPlaceholder;
}
/// <inheritdoc />
public IChangesPresenter Presenter
{
set
{
m_Presenter = value;
SetupEvents();
// If tab active before presenter has been added, call start once we have it.
if (Active)
{
value.Start();
}
}
}
/// <summary>
/// Setup events to communicate with the presenter. Must be called after presenter is set.
/// </summary>
void SetupEvents()
{
Assert.IsNotNull(m_Presenter, "Invalid changes page state.");
// Set up publish invocation.
m_PublishButton.Clicked += m_Presenter.RequestPublish;
// Send text values to the presenter.
m_SearchBar.Search += m_Presenter.SetSearchQuery;
m_RevisionSummaryBox.OnValueChangedHandler += s => m_Presenter.SetRevisionSummary(s);
}
/// <inheritdoc />
public void SetBusyStatus(bool busy)
{
m_EntryGroupsContainer.SetEnabled(!busy);
m_RevisionSummaryBox.SetEnabled(!busy);
}
/// <inheritdoc />
protected override void SetActive()
{
Assert.IsFalse(m_Active, "The view is already active.");
m_Active = true;
m_Presenter?.Start();
}
/// <inheritdoc />
protected override void SetInactive()
{
Assert.IsTrue(m_Active, "The view is already inactive.");
m_Active = false;
m_Presenter?.Stop();
}
/// <inheritdoc />
public void SetSearchQuery(string query)
{
Assert.IsNotNull(m_Presenter, "Invalid state when setting search query.");
m_SearchBar.SetValueWithoutNotify(query);
var isSearching = m_Presenter.Searching;
if (m_EntryConflictsGroup != null) m_EntryConflictsGroup.Searching = isSearching;
if (m_EntryToggleableGroup != null) m_EntryToggleableGroup.Searching = isSearching;
}
/// <inheritdoc />
public void SetRevisionSummary(string message)
{
m_RevisionSummaryBox.SetValueWithoutNotify(message);
}
/// <inheritdoc />
public void SetConflicts(IReadOnlyList<IChangeEntryData> list)
{
Assert.IsNotNull(m_Presenter, "Invalid state while creating conflict list.");
// Initialise conflicts group
if (m_EntryConflictsGroup == null)
{
var conflictsList = new AdapterListView { name = StringAssets.changeListConflictedList, SelectionType = SelectionType.None };
conflictsList.AddToClassList(ListViewUssClassName);
m_ConflictedChangeListAdapter = new ConflictedChangeListAdapter(m_Presenter);
conflictsList.SetAdapter(m_ConflictedChangeListAdapter);
m_EntryConflictsGroup = new ChangeEntryGroup(conflictsList) { Title = StringAssets.changeListConflictedHeader };
m_EntryConflictsGroup.SetOverflowCallback(m_Presenter.OnClickConflictGroupOverflow);
m_EntryConflictsGroup.Searching = m_Presenter.Searching;
}
Assert.IsTrue(m_ConflictedChangeListAdapter != null && m_EntryConflictsGroup != null, "Invalid state while setting conflicted list.");
// Ensure conflict list is displayed
if (m_ActiveGroup != m_EntryConflictsGroup)
{
m_ActiveGroup?.RemoveFromHierarchy();
m_EntryGroupsContainer.Add(m_EntryConflictsGroup);
m_ActiveGroup = m_EntryConflictsGroup;
}
m_ConflictedChangeListAdapter.List = list;
var count = m_Presenter.ConflictedCount;
m_EntryConflictsGroup.NumberMenuItems = m_Presenter.ConflictGroupOverflowEntryCount;
m_EntryConflictsGroup.SelectedEntryCount = count;
m_EntryConflictsGroup.EntryCount = count;
}
/// <inheritdoc />
public void SetSelectedChanges()
{
Assert.IsNotNull(m_Presenter, "Invalid state while setting selected items from toggleable list.");
if(m_ToggleableChangeListAdapter == null)
{
// we might be Selecting partial changes before the view loads the first time,
// so we just ignore it ....
return;
}
Assert.IsTrue(m_ToggleableChangeListAdapter != null && m_EntryToggleableGroup != null, "Invalid state while setting selected items in toggleable list");
var scrollToIndex = m_ToggleableChangeListAdapter.GetFirstToggledIndex();
m_ToggleableChangeListAdapter.NotifyDataSetChanged();
if (scrollToIndex != -1)
{
scrollToIndex = Math.Min(scrollToIndex, m_ToggleableChangeListAdapter.GetEntryCount() - 1);
m_EntryToggleableGroup.ScrollTo(scrollToIndex);
if(m_ToggleableChangeListAdapter.GetLastBoundElementIndex() < scrollToIndex + 3)
{
// the pool of the list is 14 elements .. but the list actually shows only 12 ..
// so the normal scrollTo call of the list view may stop 1 element short of the selected
// index if the scrolled to index is greater than the currently selected index.
m_EntryToggleableGroup.ScrollTo(scrollToIndex + 3);
}
}
}
/// <inheritdoc />
public void SetChanges(IReadOnlyList<IChangeEntryData> list)
{
Assert.IsNotNull(m_Presenter, "Invalid state while creating toggleable list.");
// Initialise the toggleable list if not already initialised.
if (m_EntryToggleableGroup == null)
{
var toggleableListView = new AdapterListView { SelectionType = SelectionType.None };
toggleableListView.AddToClassList(ListViewUssClassName);
m_ToggleableChangeListAdapter = new ToggleableChangeListAdapter(m_Presenter);
toggleableListView.SetAdapter(m_ToggleableChangeListAdapter);
m_EntryToggleableGroup = new ChangeEntryGroup(toggleableListView)
{ Title = StringAssets.changeListFullHeader };
m_EntryToggleableGroup.SetOverflowCallback(m_Presenter.OnClickGroupOverflow);
m_EntryToggleableGroup.Searching = m_Presenter.Searching;
}
Assert.IsTrue(m_ToggleableChangeListAdapter != null && m_EntryToggleableGroup != null, "Invalid state while setting toggleable list");
// Ensure single list is displayed
if (m_ActiveGroup != m_EntryToggleableGroup)
{
m_ActiveGroup?.RemoveFromHierarchy();
m_EntryGroupsContainer.Add(m_EntryToggleableGroup);
m_ActiveGroup = m_EntryToggleableGroup;
}
// Can use list.Count here since searching hides "All".
m_EntryToggleableGroup.EntryCount = m_Presenter.Searching ? list.Count : m_Presenter.TotalCount;
m_ToggleableChangeListAdapter.List = list;
m_EntryToggleableGroup.NumberMenuItems = m_Presenter.GroupOverflowEntryCount;
m_EntryToggleableGroup.SelectedEntryCount = m_Presenter.ToggledCount;
}
/// <inheritdoc />
public void SetToggledCount(int count)
{
if (m_EntryToggleableGroup != null)
{
m_EntryToggleableGroup.SelectedEntryCount = count;
}
}
/// <inheritdoc />
public void SetPublishEnabled(bool enabled, string reason = null)
{
m_PublishButton.SetEnabled(enabled);
// Disabled elements cannot have a tooltip so apply to a empty/dummy parent instead.
m_PublishButton.parent.tooltip = reason;
}
/// <inheritdoc />
public bool DisplayDialogue(string title, string message, string affirmative)
{
return EditorUtility.DisplayDialog(title, message, affirmative);
}
/// <inheritdoc />
public bool DisplayDialogue(string title, string message, string affirmative, string negative)
{
return EditorUtility.DisplayDialog(title, message, affirmative, negative);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ChangesTabPageView> { }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Components;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.Presenters;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using Unity.Cloud.Collaborate.Views.Adapters.ListAdapters;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Views
{
internal class HistoryTabPageView : TabPageComponent, IHistoryView
{
[CanBeNull]
IHistoryPresenter m_Presenter;
public const string UssClassName = "history-page";
public const string PaginatorUssClassName = UssClassName + "__paginator";
public const string ContentUssClassName = UssClassName + "__content";
public const string NoticeUssClassName = UssClassName + "__notice";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(HistoryTabPageView)}.uxml";
readonly ScrollView m_Content;
readonly ListNotice m_ListNotice;
readonly Paginator m_Paginator;
bool m_Active;
public HistoryTabPageView()
{
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
m_Paginator = this.Q<Paginator>(className: PaginatorUssClassName);
m_Paginator.AddToClassList(UiConstants.ussHidden);
m_Paginator.ClickedMovePage += OnClickedMovePage;
m_Content = this.Q<ScrollView>(className: ContentUssClassName);
// Add loading notice
m_ListNotice = this.Q<ListNotice>(className: NoticeUssClassName);
m_ListNotice.Text = StringAssets.loadingRevisions;
}
/// <inheritdoc />
public IHistoryPresenter Presenter
{
set
{
m_Presenter = value;
// If tab active before presenter has been added, call start once we have it.
if (Active)
{
m_Presenter.Start();
}
}
}
/// <inheritdoc />
public void SetBusyStatus(bool busy)
{
m_Paginator.SetEnabled(!busy);
m_Content.SetEnabled(!busy);
}
/// <inheritdoc />
public void SetHistoryList(IReadOnlyList<IHistoryEntry> list)
{
// Clear out old content
m_ListNotice.AddToClassList(UiConstants.ussHidden);
m_Content.Clear();
// Show paginator
m_Paginator.RemoveFromClassList(UiConstants.ussHidden);
// Handle empty list case
if (list.Count == 0)
{
m_ListNotice.Text = StringAssets.noticeNoRevisionsToDisplay;
m_Content.RemoveFromClassList(UiConstants.ussHidden);
return;
}
foreach (var entry in list)
{
// Add entry to the list
m_Content.Add(CreateHistoryEntry(entry, false));
}
}
/// <summary>
/// Event handler for receiving page change requests.
/// </summary>
/// <param name="pageChange">
/// Delta to change the page by: Paginator.MoveForwards, Paginator.MoveBackwards. Mapped to +1, -1 respectively.
/// </param>
void OnClickedMovePage(int pageChange)
{
Assert.IsNotNull(m_Presenter, "Invalid state when requesting page change.");
if (pageChange == Paginator.MoveBackwards)
{
m_Presenter.PrevPage();
}
else
{
m_Presenter.NextPage();
}
}
/// <inheritdoc />
public void SetPage(int page, int max)
{
m_Paginator.SetPage(page, max);
}
/// <inheritdoc />
public void SetSelection(IHistoryEntry entry)
{
// Hide paginator
m_Paginator.AddToClassList(UiConstants.ussHidden);
// Clear out old content
m_ListNotice.AddToClassList(UiConstants.ussHidden);
m_Content.Clear();
// Add new content
m_Content.Add(CreateHistoryEntry(entry, true));
}
/// <summary>
/// Takes a IHistoryEntry and binds it to a created HistoryEntryComponent to be used in the history list.
/// </summary>
/// <param name="entry">History entry to bind</param>
/// <param name="expanded">Whether or not to show its list of changed entries.</param>
/// <returns>Inflated and bound component.</returns>
HistoryEntryComponent CreateHistoryEntry([NotNull] IHistoryEntry entry, bool expanded)
{
Assert.IsNotNull(m_Presenter, "Invalid state when creating history entry");
var comp = new HistoryEntryComponent();
// Handle expanded vs compact layout
if (expanded)
{
// Hide fields used for compact view
comp.showFilesButton.AddToClassList(UiConstants.ussHidden);
comp.cloudStatusText.AddToClassList(UiConstants.ussHidden);
comp.changedFilesCount.text = $"Changes ( {entry.Changes.Count} )";
var listAdapter = new HistoryEntryChangeListAdapter(m_Presenter, entry.RevisionId, entry.Changes.ToList());
comp.changedFiles.SetAdapter(listAdapter);
listAdapter.NotifyDataSetChanged();
// Configure button
comp.gotoButton.text = entry.GetGotoText();
comp.gotoButton.clickable.clicked += () => m_Presenter.RequestGoto(entry.RevisionId, entry.Status);
}
else
{
// Hide fields used for expanded view
comp.changedFilesCount.AddToClassList(UiConstants.ussHidden);
comp.changedFiles.AddToClassList(UiConstants.ussHidden);
comp.gotoButton.text = string.Empty;
comp.gotoButton.AddToClassList(UiConstants.ussHidden);
// Setup show button
comp.showFilesButton.text = entry.Changes.Count == 1
? StringAssets.showChange
: string.Format(StringAssets.showChanges, entry.Changes.Count);
comp.showFilesButton.clickable.clicked += () => m_Presenter.SelectedRevisionId = entry.RevisionId;
// TODO: cloud status text
}
// Trim whitespace on either side and grab initial for profile circle
var trimmedAuthorName = entry.AuthorName.Trim();
comp.profileInitial.text = trimmedAuthorName.Substring(0, 1).ToUpper();
comp.authorName.text = trimmedAuthorName;
// Display relative or absolute timestamp. If relative, show absolute as a tooltip.
comp.timestamp.text = TimeStamp.GetTimeStamp(entry.Time);
if (TimeStamp.UseRelativeTimeStamps)
{
comp.timestamp.tooltip = TimeStamp.GetLocalisedTimeStamp(entry.Time);
}
// Display revision id and show full length id as a tooltip
comp.revisionId.text = $"ID: {entry.RevisionId.Substring(0, 10)}";
comp.revisionId.tooltip = entry.RevisionId;
comp.commitMessage.text = entry.Message;
return comp;
}
/// <inheritdoc />
public bool DisplayDialogue(string title, string message, string affirmative)
{
return EditorUtility.DisplayDialog(title, message, affirmative);
}
/// <inheritdoc />
public bool DisplayDialogue(string title, string message, string affirmative, string negative)
{
return EditorUtility.DisplayDialog(title, message, affirmative, negative);
}
/// <inheritdoc />
protected override void SetActive()
{
Assert.IsFalse(m_Active, "The view is already active.");
m_Active = true;
m_Presenter?.Start();
}
/// <inheritdoc />
protected override void SetInactive()
{
Assert.IsTrue(m_Active, "The view is already inactive.");
m_Active = false;
m_Presenter?.Stop();
}
}
}
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.Presenters;
namespace Unity.Cloud.Collaborate.Views
{
internal interface IChangesView : IView<IChangesPresenter>
{
/// <summary>
/// Set busy status in the view.
/// </summary>
/// <param name="busy">Whether or not the presenter is busy with a request.</param>
void SetBusyStatus(bool busy);
/// <summary>
/// Set the search query in the view.
/// </summary>
/// <param name="query">Latest search query to set.</param>
void SetSearchQuery([NotNull] string query);
/// <summary>
/// Set the revision summary in the view.
/// </summary>
/// <param name="message">Latest summary to set.</param>
void SetRevisionSummary([NotNull] string message);
/// <summary>
/// Set the conflicts to be displayed.
/// </summary>
/// <param name="list">List of conflicts to display.</param>
void SetConflicts([NotNull] IReadOnlyList<IChangeEntryData> list);
/// <summary>
/// Set the changes to be selected.
/// </summary>
void SetSelectedChanges();
/// <summary>
/// Set the changes to be displayed.
/// </summary>
/// <param name="list">List of changes to be displayed.</param>
void SetChanges([NotNull] IReadOnlyList<IChangeEntryData> list);
/// <summary>
/// Set the count of toggled entries.
/// </summary>
/// <param name="count">Latest toggled count.</param>
void SetToggledCount(int count);
/// <summary>
/// Enable or disable the publish button based on the provided values. The optional reason is to be used as a
/// hint to users about why the functionality is blocked.
/// </summary>
/// <param name="enabled">Whether or not the publish is to be enabled.</param>
/// <param name="reason">Reason for the publish to be disabled.</param>
void SetPublishEnabled(bool enabled, [CanBeNull] string reason = null);
/// <summary>
/// Display a dialogue to the user.
/// </summary>
/// <param name="title">Title for the dialogue.</param>
/// <param name="message">Message inside the dialogue.</param>
/// <param name="affirmative">Affirmative button text.</param>
/// <returns>True if affirmative is clicked.</returns>
bool DisplayDialogue([NotNull] string title, [NotNull] string message, [NotNull] string affirmative);
/// <summary>
/// Display a dialogue to the user.
/// </summary>
/// <param name="title">Title for the dialogue.</param>
/// <param name="message">Message inside the dialogue.</param>
/// <param name="affirmative">Affirmative button text.</param>
/// <param name="negative">Negative button text.</param>
/// <returns>True if affirmative is clicked.</returns>
bool DisplayDialogue([NotNull] string title, [NotNull] string message, [NotNull] string affirmative, [NotNull] string negative);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment