| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 | using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;// Handles the log items in an optimized way such that existing log items are// recycled within the list instead of creating a new log item at each chancenamespace IngameDebugConsole{	public class DebugLogRecycledListView : MonoBehaviour	{#pragma warning disable 0649		// Cached components		[SerializeField]		private RectTransform transformComponent;		[SerializeField]		private RectTransform viewportTransform;		[SerializeField]		private Color logItemNormalColor1;		[SerializeField]		private Color logItemNormalColor2;		[SerializeField]		private Color logItemSelectedColor;#pragma warning restore 0649		internal DebugLogManager manager;		private ScrollRect scrollView;		private float logItemHeight;		private DynamicCircularBuffer<DebugLogEntry> entriesToShow = null;		private DynamicCircularBuffer<DebugLogEntryTimestamp> timestampsOfEntriesToShow = null;		private DebugLogEntry selectedLogEntry;		private int indexOfSelectedLogEntry = int.MaxValue;		private float heightOfSelectedLogEntry;		private float DeltaHeightOfSelectedLogEntry { get { return heightOfSelectedLogEntry - logItemHeight; } }		/// These properties are used by <see cref="OnBeforeFilterLogs"/> and <see cref="OnAfterFilterLogs"/>.		private int collapsedOrderOfSelectedLogEntry;		private float scrollDistanceToSelectedLogEntry;		// Log items used to visualize the visible debug entries		private readonly DynamicCircularBuffer<DebugLogItem> visibleLogItems = new DynamicCircularBuffer<DebugLogItem>( 32 );		private bool isCollapseOn = false;		// Current indices of debug entries shown on screen		private int currentTopIndex = -1, currentBottomIndex = -1;		private System.Predicate<DebugLogItem> shouldRemoveLogItemPredicate;		private System.Action<DebugLogItem> poolLogItemAction;		public float ItemHeight { get { return logItemHeight; } }		public float SelectedItemHeight { get { return heightOfSelectedLogEntry; } }		private void Awake()		{			scrollView = viewportTransform.GetComponentInParent<ScrollRect>();			scrollView.onValueChanged.AddListener( ( pos ) =>			{				if( manager.IsLogWindowVisible )					UpdateItemsInTheList( false );			} );		}		public void Initialize( DebugLogManager manager, DynamicCircularBuffer<DebugLogEntry> entriesToShow, DynamicCircularBuffer<DebugLogEntryTimestamp> timestampsOfEntriesToShow, float logItemHeight )		{			this.manager = manager;			this.entriesToShow = entriesToShow;			this.timestampsOfEntriesToShow = timestampsOfEntriesToShow;			this.logItemHeight = logItemHeight;			shouldRemoveLogItemPredicate = ShouldRemoveLogItem;			poolLogItemAction = manager.PoolLogItem;		}		public void SetCollapseMode( bool collapse )		{			isCollapseOn = collapse;		}		// A log item is clicked, highlight it		public void OnLogItemClicked( DebugLogItem item )		{			OnLogItemClickedInternal( item.Index, item );		}		// Force expand the log item at specified index		public void SelectAndFocusOnLogItemAtIndex( int itemIndex )		{			if( indexOfSelectedLogEntry != itemIndex ) // Make sure that we aren't deselecting the target log item				OnLogItemClickedInternal( itemIndex );			float viewportHeight = viewportTransform.rect.height;			float transformComponentCenterYAtTop = viewportHeight * 0.5f;			float transformComponentCenterYAtBottom = transformComponent.sizeDelta.y - viewportHeight * 0.5f;			float transformComponentTargetCenterY = itemIndex * logItemHeight + viewportHeight * 0.5f;			if( transformComponentCenterYAtTop == transformComponentCenterYAtBottom )				scrollView.verticalNormalizedPosition = 0.5f;			else				scrollView.verticalNormalizedPosition = Mathf.Clamp01( Mathf.InverseLerp( transformComponentCenterYAtBottom, transformComponentCenterYAtTop, transformComponentTargetCenterY ) );			manager.SnapToBottom = false;		}		private void OnLogItemClickedInternal( int itemIndex, DebugLogItem referenceItem = null )		{			int indexOfPreviouslySelectedLogEntry = indexOfSelectedLogEntry;			DeselectSelectedLogItem();			if( indexOfPreviouslySelectedLogEntry != itemIndex )			{				selectedLogEntry = entriesToShow[itemIndex];				indexOfSelectedLogEntry = itemIndex;				CalculateSelectedLogEntryHeight( referenceItem );				manager.SnapToBottom = false;			}			CalculateContentHeight();			UpdateItemsInTheList( true );			manager.ValidateScrollPosition();		}		// Deselect the currently selected log item		public void DeselectSelectedLogItem()		{			selectedLogEntry = null;			indexOfSelectedLogEntry = int.MaxValue;			heightOfSelectedLogEntry = 0f;		}		/// <summary>		/// Cache the currently selected log item's properties so that its position can be restored after <see cref="OnAfterFilterLogs"/> is called.		/// </summary>		public void OnBeforeFilterLogs()		{			collapsedOrderOfSelectedLogEntry = 0;			scrollDistanceToSelectedLogEntry = 0f;			if( selectedLogEntry != null )			{				if( !isCollapseOn )				{					for( int i = 0; i < indexOfSelectedLogEntry; i++ )					{						if( entriesToShow[i] == selectedLogEntry )							collapsedOrderOfSelectedLogEntry++;					}				}				scrollDistanceToSelectedLogEntry = indexOfSelectedLogEntry * ItemHeight - transformComponent.anchoredPosition.y;			}		}		/// <summary>		/// See <see cref="OnBeforeFilterLogs"/>.		/// </summary>		public void OnAfterFilterLogs()		{			// Refresh selected log entry's index			int newIndexOfSelectedLogEntry = -1;			if( selectedLogEntry != null )			{				for( int i = 0; i < entriesToShow.Count; i++ )				{					if( entriesToShow[i] == selectedLogEntry && collapsedOrderOfSelectedLogEntry-- == 0 )					{						newIndexOfSelectedLogEntry = i;						break;					}				}			}			if( newIndexOfSelectedLogEntry < 0 )				DeselectSelectedLogItem();			else			{				indexOfSelectedLogEntry = newIndexOfSelectedLogEntry;				transformComponent.anchoredPosition = new Vector2( 0f, newIndexOfSelectedLogEntry * ItemHeight - scrollDistanceToSelectedLogEntry );			}		}		// Number of debug entries may have changed, update the list		public void OnLogEntriesUpdated( bool updateAllVisibleItemContents )		{			CalculateContentHeight();			UpdateItemsInTheList( updateAllVisibleItemContents );		}		// A single collapsed log entry at specified index is updated, refresh its item if visible		public void OnCollapsedLogEntryAtIndexUpdated( int index )		{			if( index >= currentTopIndex && index <= currentBottomIndex )			{				DebugLogItem logItem = GetLogItemAtIndex( index );				logItem.ShowCount();				if( timestampsOfEntriesToShow != null )					logItem.UpdateTimestamp( timestampsOfEntriesToShow[index] );			}		}		public void RefreshCollapsedLogEntryCounts()		{			for( int i = 0; i < visibleLogItems.Count; i++ )				visibleLogItems[i].ShowCount();		}		public void OnLogEntriesRemoved( int removedLogCount )		{			if( selectedLogEntry != null )			{				bool isSelectedLogEntryRemoved = isCollapseOn ? ( selectedLogEntry.count == 0 ) : ( indexOfSelectedLogEntry < removedLogCount );				if( isSelectedLogEntryRemoved )					DeselectSelectedLogItem();				else					indexOfSelectedLogEntry = isCollapseOn ? FindIndexOfLogEntryInReverseDirection( selectedLogEntry, indexOfSelectedLogEntry ) : ( indexOfSelectedLogEntry - removedLogCount );			}			if( !manager.IsLogWindowVisible && manager.SnapToBottom )			{				// When log window becomes visible, it refreshes all logs. So unless snap to bottom is disabled, we don't need to				// keep track of either the scroll position or the visible log items' positions.				visibleLogItems.TrimStart( visibleLogItems.Count, poolLogItemAction );			}			else if( !isCollapseOn )				visibleLogItems.TrimStart( Mathf.Clamp( removedLogCount - currentTopIndex, 0, visibleLogItems.Count ), poolLogItemAction );			else			{				visibleLogItems.RemoveAll( shouldRemoveLogItemPredicate );				if( visibleLogItems.Count > 0 )					removedLogCount = currentTopIndex - FindIndexOfLogEntryInReverseDirection( visibleLogItems[0].Entry, visibleLogItems[0].Index );			}			if( visibleLogItems.Count == 0 )			{				currentTopIndex = -1;				if( !manager.SnapToBottom )					transformComponent.anchoredPosition = Vector2.zero;			}			else			{				currentTopIndex = Mathf.Max( 0, currentTopIndex - removedLogCount );				currentBottomIndex = currentTopIndex + visibleLogItems.Count - 1;				float firstVisibleLogItemInitialYPos = visibleLogItems[0].Transform.anchoredPosition.y;				for( int i = 0; i < visibleLogItems.Count; i++ )				{					DebugLogItem logItem = visibleLogItems[i];					logItem.Index = currentTopIndex + i;					// If log window is visible, we need to manually refresh the visible items' visual properties. Otherwise, all log items will be refreshed when log window is opened					if( manager.IsLogWindowVisible )					{						RepositionLogItem( logItem );						ColorLogItem( logItem );						// Update collapsed count of the log items in collapsed mode						if( isCollapseOn )							logItem.ShowCount();					}				}				// Shift the ScrollRect				if( !manager.SnapToBottom )					transformComponent.anchoredPosition = new Vector2( 0f, Mathf.Max( 0f, transformComponent.anchoredPosition.y - ( visibleLogItems[0].Transform.anchoredPosition.y - firstVisibleLogItemInitialYPos ) ) );			}		}		private bool ShouldRemoveLogItem( DebugLogItem logItem )		{			if( logItem.Entry.count == 0 )			{				poolLogItemAction( logItem );				return true;			}			return false;		}		private int FindIndexOfLogEntryInReverseDirection( DebugLogEntry logEntry, int startIndex )		{			for( int i = Mathf.Min( startIndex, entriesToShow.Count - 1 ); i >= 0; i-- )			{				if( entriesToShow[i] == logEntry )					return i;			}			return -1;		}		// Log window's width has changed, update the expanded (currently selected) log's height		public void OnViewportWidthChanged()		{			if( indexOfSelectedLogEntry >= entriesToShow.Count )				return;			CalculateSelectedLogEntryHeight();			CalculateContentHeight();			UpdateItemsInTheList( true );			manager.ValidateScrollPosition();		}		// Log window's height has changed, update the list		public void OnViewportHeightChanged()		{			UpdateItemsInTheList( false );		}		private void CalculateContentHeight()		{			float newHeight = Mathf.Max( 1f, entriesToShow.Count * logItemHeight );			if( selectedLogEntry != null )				newHeight += DeltaHeightOfSelectedLogEntry;			transformComponent.sizeDelta = new Vector2( 0f, newHeight );		}		private void CalculateSelectedLogEntryHeight( DebugLogItem referenceItem = null )		{			if( !referenceItem )			{				if( visibleLogItems.Count == 0 )				{					UpdateItemsInTheList( false ); // Try to generate some DebugLogItems, we need one DebugLogItem to calculate the text height					if( visibleLogItems.Count == 0 ) // No DebugLogItems are generated, weird						return;				}				referenceItem = visibleLogItems[0];			}			heightOfSelectedLogEntry = referenceItem.CalculateExpandedHeight( selectedLogEntry, ( timestampsOfEntriesToShow != null ) ? timestampsOfEntriesToShow[indexOfSelectedLogEntry] : (DebugLogEntryTimestamp?) null );		}		// Calculate the indices of log entries to show		// and handle log items accordingly		private void UpdateItemsInTheList( bool updateAllVisibleItemContents )		{			if( entriesToShow.Count > 0 )			{				float contentPosTop = transformComponent.anchoredPosition.y - 1f;				float contentPosBottom = contentPosTop + viewportTransform.rect.height + 2f;				float positionOfSelectedLogEntry = indexOfSelectedLogEntry * logItemHeight;				if( positionOfSelectedLogEntry <= contentPosBottom )				{					if( positionOfSelectedLogEntry <= contentPosTop )					{						contentPosTop = Mathf.Max( contentPosTop - DeltaHeightOfSelectedLogEntry, positionOfSelectedLogEntry - 1f );						contentPosBottom = Mathf.Max( contentPosBottom - DeltaHeightOfSelectedLogEntry, contentPosTop + 2f );					}					else						contentPosBottom = Mathf.Max( contentPosBottom - DeltaHeightOfSelectedLogEntry, positionOfSelectedLogEntry + 1f );				}				int newBottomIndex = Mathf.Min( (int) ( contentPosBottom / logItemHeight ), entriesToShow.Count - 1 );				int newTopIndex = Mathf.Clamp( (int) ( contentPosTop / logItemHeight ), 0, newBottomIndex );				if( currentTopIndex == -1 )				{					// There are no log items visible on screen,					// just create the new log items					updateAllVisibleItemContents = true;					for( int i = 0, count = newBottomIndex - newTopIndex + 1; i < count; i++ )						visibleLogItems.Add( manager.PopLogItem() );				}				else				{					// There are some log items visible on screen					if( newBottomIndex < currentTopIndex || newTopIndex > currentBottomIndex )					{						// If user scrolled a lot such that, none of the log items are now within						// the bounds of the scroll view, pool all the previous log items and create						// new log items for the new list of visible debug entries						updateAllVisibleItemContents = true;						visibleLogItems.TrimStart( visibleLogItems.Count, poolLogItemAction );						for( int i = 0, count = newBottomIndex - newTopIndex + 1; i < count; i++ )							visibleLogItems.Add( manager.PopLogItem() );					}					else					{						// User did not scroll a lot such that, there are still some log items within						// the bounds of the scroll view. Don't destroy them but update their content,						// if necessary						if( newTopIndex > currentTopIndex )							visibleLogItems.TrimStart( newTopIndex - currentTopIndex, poolLogItemAction );						if( newBottomIndex < currentBottomIndex )							visibleLogItems.TrimEnd( currentBottomIndex - newBottomIndex, poolLogItemAction );						if( newTopIndex < currentTopIndex )						{							for( int i = 0, count = currentTopIndex - newTopIndex; i < count; i++ )								visibleLogItems.AddFirst( manager.PopLogItem() );							// If it is not necessary to update all the log items,							// then just update the newly created log items. Otherwise,							// wait for the major update							if( !updateAllVisibleItemContents )								UpdateLogItemContentsBetweenIndices( newTopIndex, currentTopIndex - 1, newTopIndex );						}						if( newBottomIndex > currentBottomIndex )						{							for( int i = 0, count = newBottomIndex - currentBottomIndex; i < count; i++ )								visibleLogItems.Add( manager.PopLogItem() );							// If it is not necessary to update all the log items,							// then just update the newly created log items. Otherwise,							// wait for the major update							if( !updateAllVisibleItemContents )								UpdateLogItemContentsBetweenIndices( currentBottomIndex + 1, newBottomIndex, newTopIndex );						}					}				}				currentTopIndex = newTopIndex;				currentBottomIndex = newBottomIndex;				if( updateAllVisibleItemContents )				{					// Update all the log items					UpdateLogItemContentsBetweenIndices( currentTopIndex, currentBottomIndex, newTopIndex );				}			}			else if( currentTopIndex != -1 )			{				// There is nothing to show but some log items are still visible; pool them				visibleLogItems.TrimStart( visibleLogItems.Count, poolLogItemAction );				currentTopIndex = -1;			}		}		private DebugLogItem GetLogItemAtIndex( int index )		{			return visibleLogItems[index - currentTopIndex];		}		private void UpdateLogItemContentsBetweenIndices( int topIndex, int bottomIndex, int logItemOffset )		{			for( int i = topIndex; i <= bottomIndex; i++ )			{				DebugLogItem logItem = visibleLogItems[i - logItemOffset];				logItem.SetContent( entriesToShow[i], ( timestampsOfEntriesToShow != null ) ? timestampsOfEntriesToShow[i] : (DebugLogEntryTimestamp?) null, i, i == indexOfSelectedLogEntry );				RepositionLogItem( logItem );				ColorLogItem( logItem );				if( isCollapseOn )					logItem.ShowCount();				else					logItem.HideCount();			}		}		private void RepositionLogItem( DebugLogItem logItem )		{			int index = logItem.Index;			Vector2 anchoredPosition = new Vector2( 1f, -index * logItemHeight );			if( index > indexOfSelectedLogEntry )				anchoredPosition.y -= DeltaHeightOfSelectedLogEntry;			logItem.Transform.anchoredPosition = anchoredPosition;		}		private void ColorLogItem( DebugLogItem logItem )		{			int index = logItem.Index;			if( index == indexOfSelectedLogEntry )				logItem.Image.color = logItemSelectedColor;			else if( index % 2 == 0 )				logItem.Image.color = logItemNormalColor1;			else				logItem.Image.color = logItemNormalColor2;		}	}}
 |