init
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
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 chance
|
||||
namespace 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;
|
||||
|
||||
private bool viewportSizeChanged;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private void OnRectTransformDimensionsChange()
|
||||
{
|
||||
viewportSizeChanged = true;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (viewportSizeChanged)
|
||||
{
|
||||
viewportSizeChanged = false;
|
||||
OnViewportSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewportSizeChanged()
|
||||
{
|
||||
if (indexOfSelectedLogEntry >= entriesToShow.Count)
|
||||
{
|
||||
UpdateItemsInTheList(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the expanded (currently selected) log's height
|
||||
CalculateSelectedLogEntryHeight();
|
||||
CalculateContentHeight();
|
||||
UpdateItemsInTheList( true );
|
||||
|
||||
manager.ValidateScrollPosition();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user