Initial Commit

This commit is contained in:
Sebastian Cabrera 2021-08-02 05:44:37 -04:00
parent 53eb92e9af
commit 270ab7d11f
15341 changed files with 700234 additions and 0 deletions

View file

@ -0,0 +1,84 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Views.Adapters;
using Unity.Cloud.Collaborate.Views.Adapters.ListAdapters;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class AdapterListView : VisualElement, IAdapterObserver
{
public const string UssClassName = "unity-adapter-list-view";
public const string ListViewUssClassName = UssClassName + "__list-view";
IAdapter m_Adapter;
readonly ListView m_ListView;
public AdapterListView()
{
AddToClassList(UssClassName);
m_ListView = new ListView();
m_ListView.style.flexGrow = new StyleFloat(1);
m_ListView.AddToClassList(ListViewUssClassName);
Add(m_ListView);
}
/// <summary>
/// Set the adapter for the list.
/// </summary>
/// <param name="adapter">Adapter for the list to use.</param>
/// <typeparam name="T">The type of the list entries.</typeparam>
public void SetAdapter<T>(BaseListAdapter<T> adapter) where T : VisualElement
{
Assert.IsNull(m_Adapter, "There cannot be more than one adapter at a time.");
m_Adapter = adapter;
m_Adapter.RegisterObserver(this);
m_ListView.makeItem = m_Adapter.MakeItem;
m_ListView.bindItem = m_Adapter.BindItem;
m_ListView.itemHeight = m_Adapter.Height;
NotifyDataSetChanged();
}
/// <summary>
/// Remove adapter from the list.
/// </summary>
public void RemoveAdapter()
{
Assert.IsNotNull(m_Adapter, "Cannot remove a non-existent adapter.");
m_Adapter.DeregisterObserver(this);
m_ListView.makeItem = null;
m_ListView.bindItem = null;
m_ListView.itemHeight = 0;
m_ListView.itemsSource = null;
m_Adapter = null;
}
/// <summary>
/// Set the selection type of the list.
/// </summary>
public SelectionType SelectionType
{
set => m_ListView.selectionType = value;
get => m_ListView.selectionType;
}
/// <summary>
/// Notify that the data in this list has changed.
/// </summary>
public void NotifyDataSetChanged()
{
// TODO: pagination support would be done here if it happens.
// Feed the ListView a dummy list of the correct length.
m_ListView.itemsSource = new bool[m_Adapter.GetEntryCount()];
}
public void ScrollToIndex(int idx)
{
m_ListView.ScrollToItem(idx);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<AdapterListView> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: daf8ab897cb58d144814920459a2e206
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
[UsedImplicitly]
internal class AlertBox : VisualElement
{
/// <summary>
/// Describes the severity of the alert. Used to set the icon.
/// </summary>
public enum AlertLevel
{
Info,
Warning,
Alert
}
public const string UssClassName = "alert-box";
public const string IconUssClassName = UssClassName + "__icon";
public const string TextUssClassName = UssClassName + "__text";
public const string ButtonUssClassName = UssClassName + "__button";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(AlertBox)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(AlertBox)}.uss";
readonly Button m_Button;
readonly VisualElement m_Icon;
readonly TextElement m_Text;
// Uss classes to set which icon is displayed.
const string k_UssIconInfo = "icon-info";
const string k_UssIconWarning = "icon-warning";
const string k_UssIconAlert = "icon-alert";
/// <summary>
/// Queue of alerts to be displayed.
/// </summary>
readonly SortedSet<AlertEntry> m_AlertEntryList;
/// <summary>
/// Initialize the box and hide it.
/// </summary>
public AlertBox()
{
// Get the layout
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Initialise fields
m_Icon = this.Q<VisualElement>(className: IconUssClassName);
m_Text = this.Q<TextElement>(className: TextUssClassName);
m_Button = this.Q<Button>(className: ButtonUssClassName);
m_AlertEntryList = new SortedSet<AlertEntry>();
// No alerts to display, so this hides the box.
UpdateAlertBox();
}
/// <summary>
/// Queue an alert to be displayed. Displayed immediately if there is currently none. If there is an existing
/// alert with the same name, it will be replaced with the latest one.
/// </summary>
/// <param name="id">Unique ID for the queued alert.</param>
/// <param name="level">Level of important of the alert.</param>
/// <param name="message">Message to be displayed.</param>
/// <param name="button">Info to populate optional button.</param>
public void QueueAlert([NotNull] string id, AlertLevel level, [NotNull] string message, (string text, Action action)? button = null)
{
// Search for existing alert.
var oldActive = m_AlertEntryList.Count == 0 ? (AlertEntry?)null : m_AlertEntryList.Max;
// Create new alert entry.
var entry = new AlertEntry(id, level, message, button == null
? (AlertEntry.AlertButton?)null
: new AlertEntry.AlertButton { Text = button.Value.text, Action = button.Value.action });
m_AlertEntryList.Add(entry);
UpdateAlertBox(oldActive?.Button?.Action);
}
/// <summary>
/// Remove existing alert. If current active one, switch to next one, or hide if none queued.
/// </summary>
/// <param name="id">Unique ID for the alert.</param>
public void DequeueAlert([NotNull] string id)
{
var oldAlert = m_AlertEntryList.Max;
m_AlertEntryList.RemoveWhere(e => e.Id == id);
UpdateAlertBox(oldAlert.Button?.Action);
}
/// <summary>
/// Display alert at the front of the queue, or hide if there are none.
/// </summary>
void UpdateAlertBox(Action previousButtonAction = null)
{
// Remove old event if it exists.
if (previousButtonAction != null)
{
m_Button.clickable.clicked -= previousButtonAction;
}
if (m_AlertEntryList.Count == 0)
{
m_Button.text = string.Empty;
m_Button.AddToClassList(UiConstants.ussHidden);
AddToClassList(UiConstants.ussHidden);
}
else
{
var activeAlert = m_AlertEntryList.Max;
m_Text.text = activeAlert.Message;
// Update state of optional button
if (activeAlert.Button?.Action != null)
{
m_Button.clickable.clicked += activeAlert.Button.Value.Action;
m_Button.text = activeAlert.Button.Value.Text;
m_Button.RemoveFromClassList(UiConstants.ussHidden);
}
else
{
m_Button.text = string.Empty;
m_Button.AddToClassList(UiConstants.ussHidden);
}
SetAlertLevel(activeAlert.Level);
RemoveFromClassList(UiConstants.ussHidden);
}
}
/// <summary>
/// Set the icon to the given severity level.
/// </summary>
/// <param name="level">Level of severity to make the icon.</param>
void SetAlertLevel(AlertLevel level)
{
// Remove old level
m_Icon.RemoveFromClassList(k_UssIconInfo);
m_Icon.RemoveFromClassList(k_UssIconWarning);
m_Icon.RemoveFromClassList(k_UssIconAlert);
// Set new one
switch (level)
{
case AlertLevel.Info:
m_Icon.AddToClassList(k_UssIconInfo);
break;
case AlertLevel.Warning:
m_Icon.AddToClassList(k_UssIconWarning);
break;
case AlertLevel.Alert:
m_Icon.AddToClassList(k_UssIconAlert);
break;
default:
throw new ArgumentOutOfRangeException(nameof(level), level, null);
}
}
struct AlertEntry : IComparable<AlertEntry>
{
public readonly string Id;
public readonly AlertLevel Level;
public readonly string Message;
public AlertButton? Button;
public AlertEntry(string id, AlertLevel level, string message, AlertButton? button)
{
Id = id;
Level = level;
Message = message;
Button = button;
}
public struct AlertButton
{
public string Text;
public Action Action;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public override bool Equals(object obj)
{
return obj is AlertEntry other && Id == other.Id;
}
public int CompareTo(AlertEntry other)
{
var value = Level.CompareTo(other.Level);
// If same level, compare by id.
return value != 0
? value
: string.Compare(Id, other.Id, StringComparison.Ordinal);
}
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<AlertBox> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 669cb0152bdadcd43a9741f5fd4b4970
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,121 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
// Hopefully these features will eventually be in the default TextField eventually.
internal class BetterTextField : TextField
{
/// <summary>
/// USS class name of elements of this type.
/// </summary>
public const string UssClassName = "unity-better-text-field";
/// <summary>
/// USS class name of placeholder elements of this type.
/// </summary>
public const string PlaceholderUssClassName = UssClassName + "__placeholder";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(BetterTextField)}.uss";
readonly Label m_PlaceholderLabel;
/// <summary>
/// Notify external subscribers that value of text property changed.
/// </summary>
public Action<string> OnValueChangedHandler;
public BetterTextField()
{
AddToClassList(UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Add and configure placeholder
m_PlaceholderLabel = new Label { pickingMode = PickingMode.Ignore };
m_PlaceholderLabel.AddToClassList(PlaceholderUssClassName);
Add(m_PlaceholderLabel);
RegisterCallback<FocusInEvent>(e => HidePlaceholder());
RegisterCallback<FocusOutEvent>(e =>
{
if (string.IsNullOrEmpty(text))
{
ShowPlaceholder();
}
});
this.RegisterValueChangedCallback(e => OnValueChangedHandler?.Invoke(e.newValue));
}
void UpdatePlaceholderVisibility()
{
if (string.IsNullOrEmpty(value))
{
// Value can be set before the focus control is initialised.
if (focusController?.focusedElement != this)
{
ShowPlaceholder();
}
}
else
{
HidePlaceholder();
}
}
void HidePlaceholder()
{
m_PlaceholderLabel?.AddToClassList(UiConstants.ussHidden);
}
void ShowPlaceholder()
{
m_PlaceholderLabel?.RemoveFromClassList(UiConstants.ussHidden);
}
public override string value
{
get => base.value;
set
{
// Catch case of value being set programatically.
base.value = value;
UpdatePlaceholderVisibility();
}
}
public string Placeholder
{
get => m_PlaceholderLabel.text;
set => m_PlaceholderLabel.text = value;
}
public override void SetValueWithoutNotify(string newValue)
{
base.SetValueWithoutNotify(newValue);
UpdatePlaceholderVisibility();
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<BetterTextField, UxmlTraits> { }
public new class UxmlTraits : TextField.UxmlTraits
{
readonly UxmlStringAttributeDescription m_Hint = new UxmlStringAttributeDescription { name = "placeholder" };
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var field = (BetterTextField)ve;
field.Placeholder = m_Hint.GetValueFromBag(bag, cc);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d106d071d829dd488dd1682159c76f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,114 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class ChangeEntryGroup : VisualElement
{
readonly AdapterListView m_ListView;
readonly ChangesGroupHeader m_GroupHeader;
readonly ListNotice m_ListNotice;
[NotNull]
string m_Title = string.Empty;
bool m_Searching;
int m_SelectedEntryCount;
int m_EntryCount;
public ChangeEntryGroup([NotNull] AdapterListView adapterListView)
{
m_ListView = adapterListView;
m_GroupHeader = new ChangesGroupHeader();
m_ListNotice = new ListNotice();
m_ListNotice.AddToClassList(UiConstants.ussHidden);
Add(m_GroupHeader);
Add(m_ListView);
Add(m_ListNotice);
}
[NotNull]
public string Title
{
set
{
m_Title = value;
UpdateTitle();
}
get => m_Title;
}
public bool Searching
{
set
{
m_Searching = value;
UpdateListNoticeText();
}
get => m_Searching;
}
public int NumberMenuItems
{
set => m_GroupHeader.SetEnableOverflowMenu(value != 0);
}
public int EntryCount
{
set
{
m_EntryCount = value;
UpdateListNotice();
}
get => m_EntryCount;
}
public int SelectedEntryCount
{
set
{
m_SelectedEntryCount = value;
UpdateTitle();
}
get => m_SelectedEntryCount;
}
void UpdateListNoticeText()
{
m_ListNotice.Text = Searching ? StringAssets.noticeNoResultsForQuery : StringAssets.noticeNoChangesToDisplay;
}
void UpdateTitle()
{
m_GroupHeader.UpdateGroupName(Searching
? StringAssets.searchResults
: string.Format(StringAssets.changeGroupHeaderFormat, Title, SelectedEntryCount));
}
void UpdateListNotice()
{
if (m_EntryCount != 0)
{
m_ListNotice.AddToClassList(UiConstants.ussHidden);
m_ListView.RemoveFromClassList(UiConstants.ussHidden);
}
else
{
m_ListNotice.RemoveFromClassList(UiConstants.ussHidden);
m_ListView.AddToClassList(UiConstants.ussHidden);
}
}
public void SetOverflowCallback(Action<float, float> callback)
{
m_GroupHeader.OnOverflowButtonClicked += callback;
}
public void ScrollTo(int idx)
{
m_ListView.ScrollToIndex(idx);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b80ebc79e3e2e66468766fe75c6f0aa0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ed49dd4f35b9c6b4aa4cfa9227df60a2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,63 @@
using System.IO;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.ChangeListEntries
{
[UsedImplicitly]
internal class BaseChangeListElement : VisualElement
{
public const string UssClassName = "base-change-list-element";
public const string FileNameUssClassName = UssClassName + "__file-name";
public const string FilePathUssClassName = UssClassName + "__file-path";
public const string IconsUssClassName = UssClassName + "__icons";
public const string ButtonsUssClassName = UssClassName + "__buttons";
// Styling class names
public const string IconUssClassName = UssClassName + "__icon";
public const string ButtonUssClassName = UssClassName + "__button";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(BaseChangeListElement)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(BaseChangeListElement)}.uss";
protected readonly Label m_FileName;
protected readonly Label m_FilePath;
public readonly VisualElement icons;
public readonly VisualElement buttons;
public BaseChangeListElement()
{
// Get the layout
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Initialise fields
m_FileName = this.Q<Label>(className: FileNameUssClassName);
m_FilePath = this.Q<Label>(className: FilePathUssClassName);
icons = this.Q<VisualElement>(className: IconsUssClassName);
buttons = this.Q<VisualElement>(className: ButtonsUssClassName);
}
public void UpdateFilePath([NotNull] string path)
{
var directoryName = Path.GetDirectoryName(path);
m_FileName.text = Path.GetFileName(path);
m_FilePath.text = directoryName;
m_FilePath.tooltip = directoryName;
}
public virtual void ClearData()
{
m_FileName.text = null;
m_FileName.tooltip = null;
m_FilePath.text = null;
m_FilePath.tooltip = null;
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<BaseChangeListElement> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f244cfcc2f79c6f43b7f088d6b0cd58e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,58 @@
using System;
using JetBrains.Annotations;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.ChangeListEntries
{
internal class ChangeListElement : BaseChangeListElement
{
public new const string UssClassName = "change-list-element";
public const string StatusIconUssClassName = UssClassName + "__icon";
public const string DiffButtonUssClassName = UssClassName + "__diff-button";
public const string DiscardButtonUssClassName = UssClassName + "__revert-button";
public readonly IconButton discardButton;
public readonly IconButton diffButton;
public readonly VisualElement statusIcon;
public ChangeListElement()
{
AddToClassList(UssClassName);
// Setup icons
statusIcon = new VisualElement();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
icons.Add(statusIcon);
// Setup buttons
diffButton = new IconButton();
diffButton.AddToClassList(IconButton.DiffUssCLassName);
diffButton.AddToClassList(ButtonUssClassName);
diffButton.AddToClassList(DiffButtonUssClassName);
discardButton = new IconButton();
discardButton.AddToClassList(IconButton.UndoUssClassName);
discardButton.AddToClassList(ButtonUssClassName);
discardButton.AddToClassList(DiscardButtonUssClassName);
buttons.Add(diffButton);
buttons.Add(discardButton);
}
public override void ClearData()
{
base.ClearData();
diffButton.UnregisterClickEvents();
discardButton.UnregisterClickEvents();
statusIcon.ClearClassList();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ChangeListElement> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f900b7b74696ee47878c9d599ecca54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,81 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.ChangeListEntries
{
internal class ConflictedChangeListElement : BaseChangeListElement
{
public new const string UssClassName = "conflicted-change-list-element";
public const string StatusIconUssClassName = UssClassName + "__icon";
public const string ShowButtonUssClassName = UssClassName + "__show-button";
public const string ChooseMergeButtonUssClassName = UssClassName + "__choose-merge-button";
public const string ChooseRemoteButtonUssClassName = UssClassName + "__choose-remote-button";
public const string ChooseMineButtonUssClassName = UssClassName + "__choose-mine-button";
public readonly VisualElement statusIcon;
public readonly IconButton showButton;
public readonly IconButton chooseMergeButton;
public readonly IconButton chooseRemoteButton;
public readonly IconButton chooseMineButton;
public ConflictedChangeListElement()
{
AddToClassList(UssClassName);
// Setup icons
statusIcon = new VisualElement();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
icons.Add(statusIcon);
// Setup buttons
showButton = new IconButton();
showButton.AddToClassList(IconButton.ShowUssClassName);
showButton.AddToClassList(ButtonUssClassName);
showButton.AddToClassList(ShowButtonUssClassName);
showButton.tooltip = StringAssets.viewDiff;
chooseMergeButton = new IconButton();
chooseMergeButton.AddToClassList(IconButton.MergeUssClassName);
chooseMergeButton.AddToClassList(ButtonUssClassName);
chooseMergeButton.AddToClassList(ChooseMergeButtonUssClassName);
chooseMergeButton.tooltip = StringAssets.useMergeTool;
chooseMineButton = new IconButton();
chooseMineButton.AddToClassList(IconButton.ChooseMineUssClassName);
chooseMineButton.AddToClassList(ButtonUssClassName);
chooseMineButton.AddToClassList(ChooseMineButtonUssClassName);
chooseMineButton.tooltip = StringAssets.useMyChanges;
chooseRemoteButton = new IconButton();
chooseRemoteButton.AddToClassList(IconButton.ChooseRemoteUssClassName);
chooseRemoteButton.AddToClassList(ButtonUssClassName);
chooseRemoteButton.AddToClassList(ChooseRemoteButtonUssClassName);
chooseRemoteButton.tooltip = StringAssets.useRemoteChanges;
buttons.Add(showButton);
buttons.Add(chooseMergeButton);
buttons.Add(chooseMineButton);
buttons.Add(chooseRemoteButton);
}
public override void ClearData()
{
base.ClearData();
showButton.UnregisterClickEvents();
chooseMergeButton.UnregisterClickEvents();
chooseRemoteButton.UnregisterClickEvents();
chooseMineButton.UnregisterClickEvents();
statusIcon.ClearClassList();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ConflictedChangeListElement> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63180fc0af43b4e48b71324e06f132e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,49 @@
using System;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.ChangeListEntries
{
internal class HistoryChangeListElement : BaseChangeListElement
{
public new const string UssClassName = "history-change-list-element";
public const string StatusIconUssClassName = UssClassName + "__icon";
public const string RevertButtonUssClassName = UssClassName + "__revert-button";
public readonly VisualElement statusIcon;
public readonly IconButton revertButton;
public HistoryChangeListElement()
{
AddToClassList(UssClassName);
// Setup icons
statusIcon = new VisualElement();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
icons.Add(statusIcon);
// Setup buttons
revertButton = new IconButton();
revertButton.AddToClassList(IconButton.UndoUssClassName);
revertButton.AddToClassList(ButtonUssClassName);
revertButton.AddToClassList(RevertButtonUssClassName);
buttons.Add(revertButton);
}
public override void ClearData()
{
base.ClearData();
revertButton.UnregisterClickEvents();
statusIcon.ClearClassList();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<HistoryChangeListElement> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d52c3973039b074697833c7052a6daa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,89 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.ChangeListEntries
{
internal class ToggleableChangeListElement : BaseChangeListElement
{
public new const string UssClassName = "toggleable-change-list-element";
public const string ToggleUssClassName = UssClassName + "__toggle";
public const string StatusIconUssClassName = UssClassName + "__icon";
public const string DiffButtonUssClassName = UssClassName + "__diff-button";
public const string DiscardButtonUssClassName = UssClassName + "__revert-button";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(ToggleableChangeListElement)}.uss";
public readonly Toggle toggle;
public readonly IconButton diffButton;
public readonly IconButton discardButton;
public readonly VisualElement statusIcon;
[CanBeNull]
EventCallback<ChangeEvent<bool>> m_ToggleCallback;
public ToggleableChangeListElement()
{
AddToClassList(UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Setup icons
var toggleContainer = new VisualElement();
toggle = new Toggle();
toggle.AddToClassList(ToggleUssClassName);
toggleContainer.Add(toggle);
statusIcon = new VisualElement();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
icons.Add(toggleContainer);
icons.Add(statusIcon);
// Setup buttons
diffButton = new IconButton();
diffButton.AddToClassList(IconButton.DiffUssCLassName);
diffButton.AddToClassList(ButtonUssClassName);
diffButton.AddToClassList(DiffButtonUssClassName);
discardButton = new IconButton();
discardButton.AddToClassList(IconButton.UndoUssClassName);
discardButton.AddToClassList(ButtonUssClassName);
discardButton.AddToClassList(DiscardButtonUssClassName);
buttons.Add(diffButton);
buttons.Add(discardButton);
}
public void SetToggleCallback(Action<bool> callback)
{
Assert.IsNull(m_ToggleCallback);
m_ToggleCallback = c => callback(c.newValue);
toggle.RegisterValueChangedCallback(m_ToggleCallback);
}
public override void ClearData()
{
base.ClearData();
diffButton.UnregisterClickEvents();
discardButton.UnregisterClickEvents();
statusIcon.ClearClassList();
statusIcon.AddToClassList(IconUssClassName);
statusIcon.AddToClassList(StatusIconUssClassName);
if (m_ToggleCallback != null)
{
toggle.UnregisterValueChangedCallback(m_ToggleCallback);
m_ToggleCallback = null;
}
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ToggleableChangeListElement> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f34d38c48fd092741a582a4f42473e29
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,67 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class ChangesGroupHeader : VisualElement
{
public const string UssClassName = "changes-group-header";
public const string NameUssClassName = UssClassName + "__name";
public const string OverflowButtonUssClassName = UssClassName + "__overflow-button";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(ChangesGroupHeader)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(ChangesGroupHeader)}.uss";
readonly Label m_GroupName;
readonly IconButton m_OverflowButton;
public event Action<float, float> OnOverflowButtonClicked;
public ChangesGroupHeader()
{
// Get the layout and style sheet
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Initialise fields
m_GroupName = this.Q<Label>(className: NameUssClassName);
m_OverflowButton = this.Q<IconButton>(className: OverflowButtonUssClassName);
// Wire up overflow button
m_OverflowButton.Clicked += TriggerOverflowMenu;
}
public void SetEnableOverflowMenu(bool enable)
{
if (enable)
{
m_OverflowButton.RemoveFromClassList(UiConstants.ussHidden);
}
else
{
m_OverflowButton.AddToClassList(UiConstants.ussHidden);
}
}
void TriggerOverflowMenu()
{
var (x, y) = MenuUtilities.GetMenuPosition(m_OverflowButton, MenuUtilities.AnchorPoint.BottomRight);
OnOverflowButtonClicked?.Invoke(x, y);
}
public void UpdateGroupName(string text)
{
m_GroupName.text = text;
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ChangesGroupHeader> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ce030b4d24aad64a92981d598c7cafd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,36 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class ErrorPageView : PageComponent
{
public const string UssClassName = "error-page-view";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(ErrorPageView)}.uxml";
bool m_Visible;
public ErrorPageView()
{
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
}
protected override void SetActive()
{
throw new NotImplementedException();
}
protected override void SetInactive()
{
throw new NotImplementedException();
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ErrorPageView> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2faa49cc094a0db47aeb66c4b57db5fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,65 @@
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine;
namespace Unity.Cloud.Collaborate.Components
{
internal class HistoryEntryComponent : VisualElement
{
public const string UssClassName = "history-entry-component";
public const string ProfileInitialUssClassName = UssClassName + "__profile-initial";
public const string AuthorNameUssClassName = UssClassName + "__author-name";
public const string TimestampUssClassName = UssClassName + "__timestamp";
public const string RevisionIdUssClassName = UssClassName + "__revision-id";
public const string CommitMessageUssClassName = UssClassName + "__commit-message";
public const string ChangedFilesCountUssClassName = UssClassName + "__changed-files-count";
public const string ChangedFilesUssClassName = UssClassName + "__changed-files";
public const string RollbackButtonUssClassName = UssClassName + "__goto-button";
public const string ShowFilesButtonUssClassName = UssClassName + "__files-button";
public const string BuildStatusUssClassName = UssClassName + "__cloud-build-status";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(HistoryEntryComponent)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(HistoryEntryComponent)}.uss";
public readonly Label profileInitial;
public readonly Label authorName;
public readonly Label timestamp;
public readonly Label revisionId;
public readonly Label commitMessage;
public readonly Button gotoButton;
public readonly Button showFilesButton;
public readonly Label cloudStatusText;
public readonly Label changedFilesCount;
public readonly AdapterListView changedFiles;
public HistoryEntryComponent()
{
// Get the layout
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Initialise fields
profileInitial = this.Q<Label>(className: ProfileInitialUssClassName);
authorName = this.Q<Label>(className: AuthorNameUssClassName);
timestamp = this.Q<Label>(className: TimestampUssClassName);
revisionId = this.Q<Label>(className: RevisionIdUssClassName);
commitMessage = this.Q<Label>(className: CommitMessageUssClassName);
changedFilesCount = this.Q<Label>(className: ChangedFilesCountUssClassName);
changedFiles = this.Q<AdapterListView>(className: ChangedFilesUssClassName);
gotoButton = this.Q<Button>(className: RollbackButtonUssClassName);
showFilesButton = this.Q<Button>(className: ShowFilesButtonUssClassName);
cloudStatusText = this.Q<Label>(className: BuildStatusUssClassName);
changedFiles.SelectionType = SelectionType.None;
gotoButton.text = StringAssets.rollback;
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<HistoryEntryComponent> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80ba60ec8e138684084eb2a266fb344f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,79 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class IconButton : Image
{
public const string UssClassName = "unity-icon-button";
public const string UndoUssClassName = "btn-undo";
public const string ShowUssClassName = "btn-show";
public const string MergeUssClassName = "btn-merge";
public const string ChooseMineUssClassName = "btn-choose-mine";
public const string ChooseRemoteUssClassName = "btn-choose-remote";
public const string DiffUssCLassName = "btn-diff";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(IconButton)}.uss";
public event Action Clicked;
public IconButton() : this(null)
{
}
public IconButton([CanBeNull] Action clickEvent = null)
{
AddToClassList(UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Setup Clickable
Clicked += clickEvent;
this.AddManipulator(new Clickable(OnClick));
}
void OnClick()
{
Clicked?.Invoke();
Blur();
}
/// <summary>
/// Remove all event handlers for the Clicked event.
/// </summary>
public void UnregisterClickEvents()
{
Clicked = null;
}
public Texture2D Image
{
get => style.backgroundImage.value.texture;
set => style.backgroundImage = value;
}
public override bool canGrabFocus { get; } = true;
/// <summary>
/// Catch the enter key event to allow for tab & enter UI navigation.
/// </summary>
/// <param name="evt">Event to check.</param>
protected override void ExecuteDefaultActionAtTarget(EventBase evt)
{
base.ExecuteDefaultActionAtTarget(evt);
// Catch enter key being pressed.
if (!(evt is KeyDownEvent downEvent)) return;
if ((downEvent.keyCode != KeyCode.KeypadEnter) && (downEvent.keyCode != KeyCode.Return)) return;
Clicked?.Invoke();
downEvent.StopPropagation();
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<IconButton> {}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ef4bd6847e224944b5d92f231935eb4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,94 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class IconTextButton : VisualElement
{
public const string ussClassName = "unity-icon-text-button";
public const string imageUssClassName = ussClassName + "__image";
public const string textUssClassName = ussClassName + "__text";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(IconTextButton)}.uss";
readonly TextElement m_TextElement;
readonly VisualElement m_Image;
public event Action Clicked;
public IconTextButton() : this(null)
{
}
public IconTextButton([CanBeNull] Action clickEvent = null)
{
AddToClassList(ussClassName);
AddToClassList(Button.ussClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
Add(m_Image = new Image());
Add(m_TextElement = new TextElement());
m_Image.AddToClassList(imageUssClassName);
m_TextElement.AddToClassList(textUssClassName);
// Setup Clickable
Clicked += clickEvent;
this.AddManipulator(new Clickable(OnClick));
}
void OnClick()
{
Clicked?.Invoke();
Blur();
}
public string Text
{
get => m_TextElement.text;
set => m_TextElement.text = value;
}
public Texture2D Image
{
get => m_Image.style.backgroundImage.value.texture;
set => m_Image.style.backgroundImage = value;
}
public override bool canGrabFocus { get; } = true;
/// <summary>
/// Catch the enter key event to allow for tab & enter UI navigation.
/// </summary>
/// <param name="evt">Event to check.</param>
protected override void ExecuteDefaultActionAtTarget(EventBase evt)
{
base.ExecuteDefaultActionAtTarget(evt);
// Catch enter key being pressed.
if (!(evt is KeyDownEvent downEvent)) return;
if ((downEvent.keyCode != KeyCode.KeypadEnter) && (downEvent.keyCode != KeyCode.Return)) return;
Clicked?.Invoke();
downEvent.StopPropagation();
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<IconTextButton, UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
readonly UxmlStringAttributeDescription m_Text = new UxmlStringAttributeDescription { name = "text" };
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var field = (IconTextButton)ve;
field.Text = m_Text.GetValueFromBag(bag, cc);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a717437fab0c8a740b983b17a55a31ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,35 @@
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class ListNotice : VisualElement
{
public const string UssClassName = "list-notice";
public const string ListNoticeTextUssClassName = UssClassName + "__text";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(ListNotice)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(ListNotice)}.uss";
readonly Label m_Text;
public ListNotice()
{
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
m_Text = this.Q<Label>(className: ListNoticeTextUssClassName);
}
public string Text
{
set => m_Text.text = value;
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ListNotice> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f23e95205bbd2614aaf12e748e3d1ff4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b0d78f8c2d53d134ca0d2e90c04e8ef8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,172 @@
using System;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.Menus
{
/// <summary>
/// Global element that is used to display dialogues throughout the window.
/// </summary>
internal class FloatingDialogue : VisualElement
{
/// <summary>
/// USS class name of elements of this type.
/// </summary>
public const string UssClassName = "unity-floating-dialogue";
/// <summary>
/// Location the uss file for this element is stored.
/// </summary>
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(FloatingDialogue)}.uss";
/// <summary>
/// Backing field for the singleton.
/// </summary>
static FloatingDialogue s_Instance;
/// <summary>
/// Singleton for accessing the global FloatingDialogue
/// </summary>
public static FloatingDialogue Instance => s_Instance ?? (s_Instance = new FloatingDialogue());
/// <summary>
/// Constructor used by the singleton.
/// </summary>
FloatingDialogue()
{
AddToClassList(UssClassName);
AddToClassList(UiConstants.ussHidden);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
}
/// <summary>
/// Allows focus to be set when the window opens. Allows for the options to be next up for tab.
/// </summary>
public override bool canGrabFocus => true;
/// <summary>
/// Set the position of the dialogue.
/// </summary>
/// <param name="x">World x coordinate.</param>
/// <param name="y">World y coordinate.</param>
/// <param name="content">Content of the window.</param>
/// <param name="openDirection">Direction to open the dialogue towards.</param>
void SetPosition(float x, float y, VisualElement content, MenuUtilities.OpenDirection openDirection)
{
// Correct for the top left corner of the element based on the direction it opens.
switch (openDirection)
{
case MenuUtilities.OpenDirection.UpLeft:
x -= content.worldBound.width;
y -= content.worldBound.height;
break;
case MenuUtilities.OpenDirection.UpRight:
y -= content.worldBound.height;
break;
case MenuUtilities.OpenDirection.DownLeft:
x -= content.worldBound.width;
break;
case MenuUtilities.OpenDirection.DownRight:
break;
default:
throw new ArgumentOutOfRangeException(nameof(openDirection), openDirection, null);
}
// Set the position.
style.top = y - parent.worldBound.yMin;
style.left = x - parent.worldBound.xMin;
style.right = new StyleLength(StyleKeyword.Auto);
style.bottom = new StyleLength(StyleKeyword.Auto);
}
/// <summary>
/// Open the dialogue.
/// </summary>
/// <param name="x">World x coordinate.</param>
/// <param name="y">World y coordinate.</param>
/// <param name="content">Content to display.</param>
/// <param name="openDirection">Direction to open the dialogue towards.</param>
internal void Open(float x, float y, VisualElement content, MenuUtilities.OpenDirection openDirection = MenuUtilities.OpenDirection.DownLeft)
{
// Set new content
Clear();
Add(content);
// Set visible and give focus
RemoveFromClassList(UiConstants.ussHidden);
Focus();
BringToFront();
// Catch scrolling to avoid menu becoming detached.
parent.RegisterCallback<WheelEvent>(OnScroll, TrickleDown.TrickleDown);
// Catch clicks outside of the dialogue and close it.
parent.RegisterCallback<MouseDownEvent>(OnMouseDown, TrickleDown.TrickleDown);
// Catch window size changes so that the menu position can be fixed.
parent.RegisterCallback<GeometryChangedEvent>(OnGeometryChange, TrickleDown.TrickleDown);
content.RegisterCallback<GeometryChangedEvent>(SizeKnownCallback);
void SizeKnownCallback(GeometryChangedEvent _)
{
// Unregister now that we know it has a size.
content.UnregisterCallback<GeometryChangedEvent>(SizeKnownCallback);
// Set the position in the window
SetPosition(x, y, content, openDirection);
}
}
/// <summary>
/// Close the dialogue.
/// </summary>
internal void Close()
{
AddToClassList(UiConstants.ussHidden);
Clear();
// Avoid wasting extra cycles when closed.
parent.UnregisterCallback<WheelEvent>(OnScroll, TrickleDown.TrickleDown);
parent.UnregisterCallback<MouseDownEvent>(OnMouseDown, TrickleDown.TrickleDown);
}
/// <summary>
/// On scroll event.
/// </summary>
/// <param name="evt">Event data.</param>
void OnScroll(WheelEvent evt)
{
// Close the window if the user scrolls outside the dialogue.
if (!worldBound.Contains(evt.mousePosition))
{
Close();
}
}
/// <summary>
/// Mouse down event.
/// </summary>
/// <param name="evt">Event data.</param>
void OnMouseDown(MouseDownEvent evt)
{
// Close the window if the user clicks outside the dialogue.
if (!worldBound.Contains(evt.mousePosition))
{
Close();
}
}
/// <summary>
/// Geometry changed event.
/// </summary>
/// <param name="evt">Event data.</param>
void OnGeometryChange(GeometryChangedEvent evt)
{
// Close to avoid the dialogue getting detached.
Close();
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1607da2b0288db44acdeed2ddaf0962
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.Menus
{
[UsedImplicitly]
internal class FloatingMenu
{
public const string ussClassName = "unity-floating-menu";
// Fields used to display the option list.
const float k_ItemHeight = 28f;
readonly List<(string Text, Action Action, bool Enabled)> m_Items;
/// <summary>
/// Location the uss file for this element is stored.
/// </summary>
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(FloatingMenu)}.uss";
/// <summary>
/// Container for the menu items.
/// </summary>
readonly VisualElement m_Content;
/// <summary>
/// Direction to open the menu.
/// </summary>
MenuUtilities.OpenDirection m_OpenDirection = MenuUtilities.OpenDirection.DownLeft;
/// <summary>
/// Create a new floating menu. Follows the builder pattern.
/// </summary>
public FloatingMenu()
{
m_Items = new List<(string Text, Action Action, bool Enabled)>();
m_Content = new VisualElement();
m_Content.AddToClassList(ussClassName);
m_Content.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
}
/// <summary>
/// Add a single option to the menu.
/// </summary>
/// <param name="text">Text in the option.</param>
/// <param name="action">Action to invoke on click.</param>
/// <param name="enabled">State of the entry.</param>
/// <returns>This.</returns>
public FloatingMenu AddEntry(string text, Action action, bool enabled)
{
m_Items.Add((text, action, enabled));
return this;
}
/// <summary>
/// Add a list of entries.
/// </summary>
/// <param name="items">Entries to add.</param>
/// <returns>This.</returns>
public FloatingMenu AddEntries(IEnumerable<(string Text, Action Action, bool Enabled)> items)
{
m_Items.AddRange(items);
return this;
}
/// <summary>
/// Sets the open direction of the menu.
/// </summary>
/// <param name="openDirection">Direction the menu opens towards.</param>
/// <returns>This.</returns>
public FloatingMenu SetOpenDirection(MenuUtilities.OpenDirection openDirection)
{
m_OpenDirection = openDirection;
return this;
}
/// <summary>
/// Opens the constructed menu.
/// </summary>
/// <param name="x">World x coordinate.</param>
/// <param name="y">World y coordinate.</param>
public void Open(float x, float y)
{
FloatingDialogue.Instance.Open(x, y, GenerateContent(), m_OpenDirection);
}
/// <summary>
/// Generate the visual element that displays the content of this menu.
/// </summary>
/// <returns>The constructed visual element.</returns>
VisualElement GenerateContent()
{
m_Content.Clear();
foreach (var item in m_Items)
{
// Ensure the dialogue closes once the option is selected
void Action()
{
FloatingDialogue.Instance.Close();
item.Action();
}
var elem = new FloatingMenuItem(item.Text, Action, item.Enabled, k_ItemHeight);
m_Content.Add(elem);
}
return m_Content;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3ec4bfe3e4b9e8479f84284679cb851
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,55 @@
using System;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components.Menus
{
internal class FloatingMenuItem : VisualElement
{
const string k_UssClassName = "unity-floating-menu-item";
/// <summary>
/// Location the uss file for this element is stored.
/// </summary>
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(FloatingMenuItem)}.uss";
public override bool canGrabFocus { get; } = true;
readonly Action m_Action;
public FloatingMenuItem(string text, Action action, bool enabled, float height)
{
AddToClassList(k_UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
m_Action = action;
focusable = true;
this.AddManipulator(new Clickable(OnExec));
SetEnabled(enabled);
Add(new Label(text));
style.height = height;
}
void OnExec()
{
m_Action();
}
/// <summary>
/// Catch the enter key event to allow for tab & enter UI navigation.
/// </summary>
/// <param name="evt">Event to check.</param>
protected override void ExecuteDefaultActionAtTarget(EventBase evt)
{
base.ExecuteDefaultActionAtTarget(evt);
// Catch enter key being pressed.
if (!(evt is KeyDownEvent downEvent)) return;
if (downEvent.keyCode != KeyCode.KeypadEnter && downEvent.keyCode != KeyCode.Return) return;
OnExec();
downEvent.StopPropagation();
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1beb42c8af804da40a4ab5490b3d61d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,39 @@
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal abstract class PageComponent : VisualElement
{
/// <summary>
/// Current active status for this page.
/// </summary>
protected bool Active { get; private set; }
/// <summary>
/// Set active status of this page.
/// </summary>
/// <param name="active">True if the page is to be active.</param>
public void SetActive(bool active)
{
Active = active;
if (Active)
{
SetActive();
}
else
{
SetInactive();
}
}
/// <summary>
/// Set this page active.
/// </summary>
protected abstract void SetActive();
/// <summary>
/// Set this page inactive.
/// </summary>
protected abstract void SetInactive();
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b7fff04ce91c4201bb8026c14fef0f73
timeCreated: 1573172302

View file

@ -0,0 +1,56 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
internal class Paginator : VisualElement
{
public const string UssClassName = "paginator";
public const string PageTextUssClassName = UssClassName + "__page-text";
public const string BackButtonUssClassName = UssClassName + "__back-button";
public const string ForwardsButtonUssClassName = UssClassName + "__forwards-button";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(Paginator)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(Paginator)}.uss";
public const int MoveBackwards = -1;
public const int MoveForwards = 1;
public event Action<int> ClickedMovePage;
readonly Label m_PageText;
readonly Button m_BackButton;
readonly Button m_ForwardsButton;
public Paginator()
{
// Get the layout
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
m_PageText = this.Q<Label>(className: PageTextUssClassName);
m_BackButton = this.Q<Button>(className: BackButtonUssClassName);
m_ForwardsButton = this.Q<Button>(className: ForwardsButtonUssClassName);
m_BackButton.text = "<";
m_ForwardsButton.text = ">";
m_BackButton.clickable.clicked += () => ClickedMovePage?.Invoke(MoveBackwards);
m_ForwardsButton.clickable.clicked += () => ClickedMovePage?.Invoke(MoveForwards);
}
public void SetPage(int page, int maxPage)
{
m_PageText.text = $"Page {page + 1} of {maxPage + 1}";
m_BackButton.SetEnabled(page != 0);
m_ForwardsButton.SetEnabled(page != maxPage);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<Paginator> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f62d542b43f5ead479d2da97d270fc96
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,68 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
[UsedImplicitly]
internal class ProgressView : VisualElement
{
public const string UssClassName = "progress-view";
public const string LabelUssClassName = UssClassName + "__label";
public const string ProgressBarUssClassName = UssClassName + "__progress-bar";
public const string ButtonUssClassName = UssClassName + "__button";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(ProgressView)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(ProgressView)}.uss";
readonly Label m_Label;
readonly ProgressBar m_ProgressBar;
readonly Button m_Button;
public ProgressView()
{
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
m_Label = this.Q<Label>(className: LabelUssClassName);
m_Label.text = string.Empty;
m_ProgressBar = this.Q<ProgressBar>(className: ProgressBarUssClassName);
m_Button = this.Q<Button>(className: ButtonUssClassName);
m_Button.text = StringAssets.cancel;
}
public void SetText(string text, string progressText)
{
m_Label.text = text;
m_ProgressBar.title = progressText;
}
public void SetPercentComplete(int percent)
{
m_ProgressBar.value = percent;
}
public void SetCancelCallback(Action callback)
{
m_Button.clickable.clicked += callback;
}
public void SetCancelButtonActive(bool active)
{
m_Button.SetEnabled(active);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<ProgressView> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c5685731ccadae4aa67597f51dd494d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,18 @@
# Resources
This directory contains the UIElements-based user interface components.
## Overview
Each component is defined as its own class and file in this directory.
## Adding a New Component
Each component is a C# class that extends the UiElements' VisualElement class and provides a UXML factory. If no UXML
parameters are required/desired, a simple factory like this (taken from AlertBar) works:
```csharp
public new class UxmlFactory : UxmlFactory<AlertBar> { }
```
Just adding this line to the bottom of the component class with the `<AlertBar>` replaced with the name of the class.
Adding UXML parameters used to be covered in the official docs. Until it is returned: look at the source code for
any UiElements class such as TextElement.
To use the component in UXML (with editor inspections) the xml schema needs to be updated within the Unity Editor.
Instructions on how to do that is contained in `../Assets/`.

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: abc8f1905434498ba511a0b3848192ad
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,134 @@
using System;
using System.Threading;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
class SearchBar : VisualElement
{
public const string UssClassName = "unity-search-bar";
public const string SearchFieldUssClassName = UssClassName + "__search-field";
public const string PlaceholderUssClassName = UssClassName + "__placeholder";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(SearchBar)}.uss";
public const int timeoutMilliseconds = 1000;
readonly ToolbarSearchField m_SearchField;
readonly Label m_Placeholder;
string m_LatestSearchValue;
bool m_SearchEventFlag;
readonly Timer m_SearchEventTimer;
bool m_Focused;
public event Action<string> Search = delegate { };
public SearchBar() : this(null)
{
}
public SearchBar([CanBeNull] Action<string> searchEvent = null)
{
// Setup delayed search event to throttle requests.
m_SearchEventTimer = new Timer(_ => EditorApplication.delayCall += () =>
{
m_SearchEventFlag = false;
Search(m_LatestSearchValue);
});
AddToClassList(UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
m_SearchField = new ToolbarSearchField();
m_Placeholder = new Label { text = StringAssets.search, pickingMode = PickingMode.Ignore };
m_SearchField.AddToClassList(SearchFieldUssClassName);
m_Placeholder.AddToClassList(PlaceholderUssClassName);
Add(m_SearchField);
Add(m_Placeholder);
// Setup search event
if (searchEvent != null)
{
Search += searchEvent;
}
// Setup events to hide/show placeholder.
var textField = m_SearchField.Q<TextField>(className: ToolbarSearchField.textUssClassName);
textField.RegisterCallback<FocusInEvent>(e =>
{
m_Focused = true;
UpdatePlaceholderVisibility();
});
textField.RegisterCallback<FocusOutEvent>(e =>
{
m_Focused = false;
UpdatePlaceholderVisibility();
});
m_SearchField.RegisterValueChangedCallback(SearchEventThrottle);
// Set initial placeholder hide/show status.
ShowPlaceholder();
}
void SearchEventThrottle(ChangeEvent<string> evt)
{
UpdatePlaceholderVisibility();
m_LatestSearchValue = evt.newValue;
if (m_SearchEventFlag) return;
m_SearchEventFlag = true;
m_SearchEventTimer.Change(timeoutMilliseconds, Timeout.Infinite);
}
public string Value
{
get => m_SearchField.value;
set
{
m_SearchField.value = value;
UpdatePlaceholderVisibility();
}
}
public void SetValueWithoutNotify(string value)
{
m_SearchField.SetValueWithoutNotify(value);
UpdatePlaceholderVisibility();
}
void UpdatePlaceholderVisibility()
{
if (string.IsNullOrEmpty(m_SearchField.value) && !m_Focused)
{
ShowPlaceholder();
}
else
{
HidePlaceholder();
}
}
void HidePlaceholder()
{
m_Placeholder.AddToClassList(UiConstants.ussHidden);
}
void ShowPlaceholder()
{
m_Placeholder.RemoveFromClassList(UiConstants.ussHidden);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<SearchBar> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 07827588a4ae9e54ca7dbe7d31451339
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,39 @@
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
abstract class TabPageComponent : VisualElement
{
/// <summary>
/// Current active status for this page.
/// </summary>
protected bool Active { get; private set; }
/// <summary>
/// Set active status of this page.
/// </summary>
/// <param name="active">True if the page is to be active.</param>
public void SetActive(bool active)
{
Active = active;
if (Active)
{
SetActive();
}
else
{
SetInactive();
}
}
/// <summary>
/// Set this tab page active.
/// </summary>
protected abstract void SetActive();
/// <summary>
/// Set this tab page inactive.
/// </summary>
protected abstract void SetInactive();
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0bb2ee71e160aed40a9fb96cc498006e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
[UsedImplicitly]
internal class TabView : VisualElement
{
public const string UssClassName = "unity-tab-view";
public const string ContentContainerUssClassName = UssClassName + "__content-container";
public const string TabHeaderUssClassName = UssClassName + "__tab-header";
public const string ToolbarUssClassName = UssClassName + "__toolbar";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(TabView)}.uss";
const int k_NoTabs = -1;
int m_ActiveTabIndex = k_NoTabs;
bool m_Active;
readonly VisualElement m_Content;
readonly VisualElement m_Toolbar;
readonly List<(TextButton button, TabPageComponent tab)> m_TabList;
public event Action<int> TabSwitched;
public TabView()
{
AddToClassList(UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
m_Toolbar = new VisualElement { name = "unity-tab-view-toolbar" };
m_Toolbar.AddToClassList(ToolbarUssClassName);
m_Toolbar.AddToClassList(UiConstants.ussDefaultInset);
hierarchy.Add(m_Toolbar);
m_Content = new VisualElement { name = "unity-content-container" };
m_Content.AddToClassList(ContentContainerUssClassName);
hierarchy.Add(m_Content);
m_TabList = new List<(TextButton button, TabPageComponent tab)>();
}
public void SetActive()
{
Assert.IsFalse(m_Active, "TabView is already active.");
m_Active = true;
if (m_ActiveTabIndex != k_NoTabs)
{
m_TabList[m_ActiveTabIndex].tab.SetActive(true);
}
}
public void SetInactive()
{
Assert.IsTrue(m_Active, "TabView is already inactive.");
m_Active = false;
if (m_ActiveTabIndex != k_NoTabs)
{
m_TabList[m_ActiveTabIndex].tab.SetActive(false);
}
}
/// <summary>
/// Add a tab to the view.
/// </summary>
/// <param name="tabName">Title of the tab.</param>
/// <param name="tab">Tab content to display.</param>
public void AddTab(string tabName, TabPageComponent tab)
{
// Get the tab index
var index = m_TabList.Count;
tab.style.flexGrow = 1;
tab.style.flexShrink = 1;
tab.style.flexBasis = new StyleLength(StyleKeyword.Auto);
// Copy value to avoid modification of the closure scope.
var indexCopy = index;
var btn = new TextButton(() => SwitchTabInternal(indexCopy)) { text = tabName };
btn.AddToClassList(TabHeaderUssClassName);
m_Toolbar.Add(btn);
// Add tab to list
m_TabList.Add((btn, tab));
// If no currently active tab, switch to this newly added one.
if (m_ActiveTabIndex == k_NoTabs)
{
SwitchToNextTab();
}
}
/// <summary>
/// Switch to the tab with the given index. Does nothing with an invalid index.
/// </summary>
/// <param name="index">Index of the tab to switch to.</param>
public void SwitchTab(int index)
{
// Sanitise index to be passed into the internal switch method.
if (index == k_NoTabs) return;
if (index < m_TabList.Count)
{
SwitchTabInternal(index);
}
}
/// <summary>
/// Switch to tab with the given index. Does *NOT* check the validity of the index.
/// </summary>
/// <param name="index">Index of the tab to switch to.</param>
void SwitchTabInternal(int index)
{
// Reset tab state of previously active content/button - if there was one.
if (m_ActiveTabIndex != k_NoTabs)
{
m_TabList[m_ActiveTabIndex].tab.RemoveFromHierarchy();
if (m_Active)
{
m_TabList[m_ActiveTabIndex].tab.SetActive(false);
}
m_TabList[m_ActiveTabIndex].button.RemoveFromClassList(UiConstants.ussActive);
}
// Set new active tab.
m_ActiveTabIndex = index;
// Set tab state for newly active content/button.
if (m_Active)
{
m_TabList[m_ActiveTabIndex].tab.SetActive(true);
}
m_TabList[m_ActiveTabIndex].button.AddToClassList(UiConstants.ussActive);
m_Content.Add(m_TabList[m_ActiveTabIndex].tab);
TabSwitched?.Invoke(index);
}
/// <summary>
/// Switch to the next valid tab. Wraps if there's no direct successor.
/// </summary>
void SwitchToNextTab()
{
var index = (m_ActiveTabIndex + 1) % m_TabList.Count;
SwitchTabInternal(index);
}
public override VisualElement contentContainer => m_Content;
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<TabView> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3375fb53b3a9ae45bc73008f351e005
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,57 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components {
internal class TextButton : TextElement
{
public const string UssClassName = "unity-text-button";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(TextButton)}.uss";
public event Action Clicked;
[UsedImplicitly]
public TextButton() : this(null)
{
}
public TextButton([CanBeNull] Action clickEvent = null)
{
AddToClassList(UssClassName);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
Clicked += clickEvent;
this.AddManipulator(new Clickable(OnClick));
}
void OnClick()
{
Clicked?.Invoke();
Blur();
}
public override bool canGrabFocus { get; } = true;
/// <summary>
/// Catch the enter key event to allow for tab & enter UI navigation.
/// </summary>
/// <param name="evt">Event to check.</param>
protected override void ExecuteDefaultActionAtTarget(EventBase evt)
{
base.ExecuteDefaultActionAtTarget(evt);
// Catch enter key being pressed.
if (!(evt is KeyDownEvent downEvent)) return;
if ((downEvent.keyCode != KeyCode.KeypadEnter) && (downEvent.keyCode != KeyCode.Return)) return;
Clicked?.Invoke();
downEvent.StopPropagation();
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<TextButton, UxmlTraits> {}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a728ce005e34d5e43935c0696f93de60
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,93 @@
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Assets;
using Unity.Cloud.Collaborate.Components.Menus;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Cloud.Collaborate.Components
{
[UsedImplicitly]
internal class TopBar : VisualElement
{
public const string UssClassName = "top-bar";
public const string IconUssClassName = UssClassName + "__icon";
public const string BranchInfoUssClassName = UssClassName + "__branch-info";
public const string OverflowMenuUssClassName = UssClassName + "__overflow-button";
public const string BackUssClassName = UssClassName + "__back";
public const string BackButtonUssClassName = UssClassName + "__back-button";
public const string BackTextUssClassName = UssClassName + "__back-text";
static readonly string k_LayoutPath = $"{CollaborateWindow.LayoutPath}/{nameof(TopBar)}.uxml";
static readonly string k_StylePath = $"{CollaborateWindow.StylePath}/{nameof(TopBar)}.uss";
public event Action BackButtonClicked;
readonly VisualElement m_Icon;
readonly TextElement m_BranchInfo;
readonly IconButton m_OverflowMenu;
readonly VisualElement m_BackContainer;
readonly IconButton m_BackButton;
readonly TextElement m_BackText;
[CanBeNull]
string m_BranchName;
public TopBar()
{
// Get the layout
AddToClassList(UssClassName);
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_LayoutPath).CloneTree(this);
styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath));
// Initialise fields
m_Icon = this.Q<VisualElement>(className: IconUssClassName);
m_BranchInfo = this.Q<TextElement>(className: BranchInfoUssClassName);
m_OverflowMenu = this.Q<IconButton>(className: OverflowMenuUssClassName);
m_BackContainer = this.Q<VisualElement>(className: BackUssClassName);
m_BackButton = this.Q<IconButton>(className: BackButtonUssClassName);
m_BackText = this.Q<TextElement>(className: BackTextUssClassName);
m_OverflowMenu.Clicked += ClickableOnClicked;
m_BackButton.Clicked += () => BackButtonClicked?.Invoke();
HideBackNavigation();
}
/// <summary>
/// Hide the current back navigation.
/// </summary>
public void HideBackNavigation()
{
m_BackContainer.AddToClassList(UiConstants.ussHidden);
m_BackButton.SetEnabled(false);
m_BackText.text = string.Empty;
}
/// <summary>
/// Register back navigation to be made available to the user to navigate backwards in the UI.
/// </summary>
/// <param name="text">Destination of the back navigation</param>
public void DisplayBackNavigation([NotNull] string text)
{
m_BackText.text = text;
m_BackButton.SetEnabled(true);
m_BackContainer.RemoveFromClassList(UiConstants.ussHidden);
}
void ClickableOnClicked()
{
var (x, y) = MenuUtilities.GetMenuPosition(m_OverflowMenu, MenuUtilities.AnchorPoint.BottomRight);
new FloatingMenu()
.AddEntry("Invite Teammate", OpenLinksUtility.OpenMembersLink, true)
.SetOpenDirection(MenuUtilities.OpenDirection.DownLeft)
.Open(x, y);
}
[UsedImplicitly]
public new class UxmlFactory : UxmlFactory<TopBar> { }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 382a4f9ad2723e14cad85369d33f3ce8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: