DebugLogRecycledListView.cs 14.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
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; } }

		// 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;
		}

		// 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;
		}
	}
}