TheMuseumProject/Assets/DebugConsole/Scripts/DebugLogRecycledListView.cs

404 lines
16 KiB
C#

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
{
// Log items used to visualize the debug entries at specified indices
private readonly Dictionary<int, DebugLogItem> logItemsAtIndices = new(256);
// Unique debug entries
private List<DebugLogEntry> collapsedLogEntries;
// Current indices of debug entries shown on screen
private int currentTopIndex = -1, currentBottomIndex = -1;
private float deltaHeightOfSelectedLogEntry;
private int indexOfSelectedLogEntry = int.MaxValue;
// Indices of debug entries to show in collapsedLogEntries
private DebugLogIndexList<int> indicesOfEntriesToShow;
private bool isCollapseOn;
private float _1OverLogItemHeight;
internal DebugLogManager manager;
private float positionOfSelectedLogEntry = float.MaxValue;
private ScrollRect scrollView;
private DebugLogIndexList<DebugLogEntryTimestamp> timestampsOfEntriesToShow;
private float viewportHeight;
public float ItemHeight { get; private set; }
public float SelectedItemHeight { get; private set; }
private void Awake()
{
scrollView = viewportTransform.GetComponentInParent<ScrollRect>();
scrollView.onValueChanged.AddListener(pos => UpdateItemsInTheList(false));
viewportHeight = viewportTransform.rect.height;
}
public void Initialize(DebugLogManager manager, List<DebugLogEntry> collapsedLogEntries,
DebugLogIndexList<int> indicesOfEntriesToShow,
DebugLogIndexList<DebugLogEntryTimestamp> timestampsOfEntriesToShow, float logItemHeight)
{
this.manager = manager;
this.collapsedLogEntries = collapsedLogEntries;
this.indicesOfEntriesToShow = indicesOfEntriesToShow;
this.timestampsOfEntriesToShow = timestampsOfEntriesToShow;
ItemHeight = logItemHeight;
_1OverLogItemHeight = 1f / logItemHeight;
}
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);
var transformComponentCenterYAtTop = viewportHeight * 0.5f;
var transformComponentCenterYAtBottom = transformComponent.sizeDelta.y - viewportHeight * 0.5f;
var transformComponentTargetCenterY = itemIndex * ItemHeight + viewportHeight * 0.5f;
if (transformComponentCenterYAtTop == transformComponentCenterYAtBottom)
scrollView.verticalNormalizedPosition = 0.5f;
else
scrollView.verticalNormalizedPosition = Mathf.Clamp01(Mathf.InverseLerp(
transformComponentCenterYAtBottom, transformComponentCenterYAtTop,
transformComponentTargetCenterY));
manager.SetSnapToBottom(false);
}
private void OnLogItemClickedInternal(int itemIndex, DebugLogItem referenceItem = null)
{
if (indexOfSelectedLogEntry != itemIndex)
{
DeselectSelectedLogItem();
if (!referenceItem)
{
if (currentTopIndex == -1)
UpdateItemsInTheList(
false); // Try to generate some DebugLogItems, we need one DebugLogItem to calculate the text height
referenceItem = logItemsAtIndices[currentTopIndex];
}
indexOfSelectedLogEntry = itemIndex;
positionOfSelectedLogEntry = itemIndex * ItemHeight;
SelectedItemHeight = referenceItem.CalculateExpandedHeight(
collapsedLogEntries[indicesOfEntriesToShow[itemIndex]],
timestampsOfEntriesToShow != null ? timestampsOfEntriesToShow[itemIndex] : null);
deltaHeightOfSelectedLogEntry = SelectedItemHeight - ItemHeight;
manager.SetSnapToBottom(false);
}
else
{
DeselectSelectedLogItem();
}
if (indexOfSelectedLogEntry >= currentTopIndex && indexOfSelectedLogEntry <= currentBottomIndex)
ColorLogItem(logItemsAtIndices[indexOfSelectedLogEntry], indexOfSelectedLogEntry);
CalculateContentHeight();
HardResetItems();
UpdateItemsInTheList(true);
manager.ValidateScrollPosition();
}
// Deselect the currently selected log item
public void DeselectSelectedLogItem()
{
var indexOfPreviouslySelectedLogEntry = indexOfSelectedLogEntry;
indexOfSelectedLogEntry = int.MaxValue;
positionOfSelectedLogEntry = float.MaxValue;
SelectedItemHeight = deltaHeightOfSelectedLogEntry = 0f;
if (indexOfPreviouslySelectedLogEntry >= currentTopIndex &&
indexOfPreviouslySelectedLogEntry <= currentBottomIndex)
ColorLogItem(logItemsAtIndices[indexOfPreviouslySelectedLogEntry], indexOfPreviouslySelectedLogEntry);
}
// Number of debug entries may be changed, update the list
public void OnLogEntriesUpdated(bool updateAllVisibleItemContents)
{
CalculateContentHeight();
viewportHeight = viewportTransform.rect.height;
if (updateAllVisibleItemContents)
HardResetItems();
UpdateItemsInTheList(updateAllVisibleItemContents);
}
// A single collapsed log entry at specified index is updated, refresh its item if visible
public void OnCollapsedLogEntryAtIndexUpdated(int index)
{
DebugLogItem logItem;
if (logItemsAtIndices.TryGetValue(index, out logItem))
{
logItem.ShowCount();
if (timestampsOfEntriesToShow != null)
logItem.UpdateTimestamp(timestampsOfEntriesToShow[index]);
}
}
// Log window's width has changed, update the expanded (currently selected) log's height
public void OnViewportWidthChanged()
{
if (indexOfSelectedLogEntry >= indicesOfEntriesToShow.Count)
return;
if (currentTopIndex == -1)
{
UpdateItemsInTheList(
false); // Try to generate some DebugLogItems, we need one DebugLogItem to calculate the text height
if (currentTopIndex == -1) // No DebugLogItems are generated, weird
return;
}
var referenceItem = logItemsAtIndices[currentTopIndex];
SelectedItemHeight = referenceItem.CalculateExpandedHeight(
collapsedLogEntries[indicesOfEntriesToShow[indexOfSelectedLogEntry]],
timestampsOfEntriesToShow != null ? timestampsOfEntriesToShow[indexOfSelectedLogEntry] : null);
deltaHeightOfSelectedLogEntry = SelectedItemHeight - ItemHeight;
CalculateContentHeight();
HardResetItems();
UpdateItemsInTheList(true);
manager.ValidateScrollPosition();
}
// Log window's height has changed, update the list
public void OnViewportHeightChanged()
{
viewportHeight = viewportTransform.rect.height;
UpdateItemsInTheList(false);
}
private void HardResetItems()
{
if (currentTopIndex != -1)
{
DestroyLogItemsBetweenIndices(currentTopIndex, currentBottomIndex);
currentTopIndex = -1;
}
}
private void CalculateContentHeight()
{
var newHeight = Mathf.Max(1f, indicesOfEntriesToShow.Count * ItemHeight + deltaHeightOfSelectedLogEntry);
transformComponent.sizeDelta = new Vector2(0f, newHeight);
}
// Calculate the indices of log entries to show
// and handle log items accordingly
public void UpdateItemsInTheList(bool updateAllVisibleItemContents)
{
// If there is at least one log entry to show
if (indicesOfEntriesToShow.Count > 0)
{
var contentPosTop = transformComponent.anchoredPosition.y - 1f;
var contentPosBottom = contentPosTop + viewportHeight + 2f;
if (positionOfSelectedLogEntry <= contentPosBottom)
{
if (positionOfSelectedLogEntry <= contentPosTop)
{
contentPosTop -= deltaHeightOfSelectedLogEntry;
contentPosBottom -= deltaHeightOfSelectedLogEntry;
if (contentPosTop < positionOfSelectedLogEntry - 1f)
contentPosTop = positionOfSelectedLogEntry - 1f;
if (contentPosBottom < contentPosTop + 2f)
contentPosBottom = contentPosTop + 2f;
}
else
{
contentPosBottom -= deltaHeightOfSelectedLogEntry;
if (contentPosBottom < positionOfSelectedLogEntry + 1f)
contentPosBottom = positionOfSelectedLogEntry + 1f;
}
}
var newTopIndex = (int)(contentPosTop * _1OverLogItemHeight);
var newBottomIndex = (int)(contentPosBottom * _1OverLogItemHeight);
if (newTopIndex < 0)
newTopIndex = 0;
if (newBottomIndex > indicesOfEntriesToShow.Count - 1)
newBottomIndex = indicesOfEntriesToShow.Count - 1;
if (currentTopIndex == -1)
{
// There are no log items visible on screen,
// just create the new log items
updateAllVisibleItemContents = true;
currentTopIndex = newTopIndex;
currentBottomIndex = newBottomIndex;
CreateLogItemsBetweenIndices(newTopIndex, newBottomIndex);
}
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;
DestroyLogItemsBetweenIndices(currentTopIndex, currentBottomIndex);
CreateLogItemsBetweenIndices(newTopIndex, newBottomIndex);
}
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)
DestroyLogItemsBetweenIndices(currentTopIndex, newTopIndex - 1);
if (newBottomIndex < currentBottomIndex)
DestroyLogItemsBetweenIndices(newBottomIndex + 1, currentBottomIndex);
if (newTopIndex < currentTopIndex)
{
CreateLogItemsBetweenIndices(newTopIndex, currentTopIndex - 1);
// 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);
}
if (newBottomIndex > currentBottomIndex)
{
CreateLogItemsBetweenIndices(currentBottomIndex + 1, newBottomIndex);
// 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);
}
}
currentTopIndex = newTopIndex;
currentBottomIndex = newBottomIndex;
}
if (updateAllVisibleItemContents)
// Update all the log items
UpdateLogItemContentsBetweenIndices(currentTopIndex, currentBottomIndex);
}
else
{
HardResetItems();
}
}
private void CreateLogItemsBetweenIndices(int topIndex, int bottomIndex)
{
for (var i = topIndex; i <= bottomIndex; i++)
CreateLogItemAtIndex(i);
}
// Create (or unpool) a log item
private void CreateLogItemAtIndex(int index)
{
var logItem = manager.PopLogItem();
// Reposition the log item
var anchoredPosition = new Vector2(1f, -index * ItemHeight);
if (index > indexOfSelectedLogEntry)
anchoredPosition.y -= deltaHeightOfSelectedLogEntry;
logItem.Transform.anchoredPosition = anchoredPosition;
// Color the log item
ColorLogItem(logItem, index);
// To access this log item easily in the future, add it to the dictionary
logItemsAtIndices[index] = logItem;
}
private void DestroyLogItemsBetweenIndices(int topIndex, int bottomIndex)
{
for (var i = topIndex; i <= bottomIndex; i++)
manager.PoolLogItem(logItemsAtIndices[i]);
}
private void UpdateLogItemContentsBetweenIndices(int topIndex, int bottomIndex)
{
DebugLogItem logItem;
for (var i = topIndex; i <= bottomIndex; i++)
{
logItem = logItemsAtIndices[i];
logItem.SetContent(collapsedLogEntries[indicesOfEntriesToShow[i]],
timestampsOfEntriesToShow != null ? timestampsOfEntriesToShow[i] : null, i,
i == indexOfSelectedLogEntry);
if (isCollapseOn)
logItem.ShowCount();
else
logItem.HideCount();
}
}
// Color a log item using its index
private void ColorLogItem(DebugLogItem logItem, int index)
{
if (index == indexOfSelectedLogEntry)
logItem.Image.color = logItemSelectedColor;
else if (index % 2 == 0)
logItem.Image.color = logItemNormalColor1;
else
logItem.Image.color = logItemNormalColor2;
}
#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
}
}