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(k_LayoutPath).CloneTree(this); styleSheets.Add(AssetDatabase.LoadAssetAtPath(k_StylePath)); // Get the components defined in the style / layout files. m_SearchBar = this.Q(className: SearchBarUssClassName); m_RevisionSummaryBox = this.Q(className: TextFieldUssClassName); m_PublishButton = this.Q(className: PublishButtonUssClassName); m_EntryGroupsContainer = this.Q(className: EntryGroupsUssClassName); // Initialize the text strings. m_PublishButton.Text = StringAssets.publishButton; m_RevisionSummaryBox.Placeholder = StringAssets.publishSummaryPlaceholder; } /// 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(); } } } /// /// Setup events to communicate with the presenter. Must be called after presenter is set. /// 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); } /// public void SetBusyStatus(bool busy) { m_EntryGroupsContainer.SetEnabled(!busy); m_RevisionSummaryBox.SetEnabled(!busy); } /// protected override void SetActive() { Assert.IsFalse(m_Active, "The view is already active."); m_Active = true; m_Presenter?.Start(); } /// protected override void SetInactive() { Assert.IsTrue(m_Active, "The view is already inactive."); m_Active = false; m_Presenter?.Stop(); } /// 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; } /// public void SetRevisionSummary(string message) { m_RevisionSummaryBox.SetValueWithoutNotify(message); } /// public void SetConflicts(IReadOnlyList 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; } /// 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); } } } /// public void SetChanges(IReadOnlyList 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; } /// public void SetToggledCount(int count) { if (m_EntryToggleableGroup != null) { m_EntryToggleableGroup.SelectedEntryCount = count; } } /// 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; } /// public bool DisplayDialogue(string title, string message, string affirmative) { return EditorUtility.DisplayDialog(title, message, affirmative); } /// public bool DisplayDialogue(string title, string message, string affirmative, string negative) { return EditorUtility.DisplayDialog(title, message, affirmative, negative); } [UsedImplicitly] public new class UxmlFactory : UxmlFactory { } } }