DebugLogManager.cs 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. using UnityEngine.EventSystems;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. // Receives debug entries and custom events (e.g. Clear, Collapse, Filter by Type)
  7. // and notifies the recycled list view of changes to the list of debug entries
  8. //
  9. // - Vocabulary -
  10. // Debug/Log entry: a Debug.Log/LogError/LogWarning/LogException/LogAssertion request made by
  11. // the client and intercepted by this manager object
  12. // Debug/Log item: a visual (uGUI) representation of a debug entry
  13. //
  14. // There can be a lot of debug entries in the system but there will only be a handful of log items
  15. // to show their properties on screen (these log items are recycled as the list is scrolled)
  16. // An enum to represent filtered log types
  17. namespace IngameDebugConsole
  18. {
  19. public enum DebugLogFilter
  20. {
  21. None = 0,
  22. Info = 1,
  23. Warning = 2,
  24. Error = 4,
  25. All = 7
  26. }
  27. public class DebugLogManager : MonoBehaviour
  28. {
  29. private static DebugLogManager instance = null;
  30. #pragma warning disable 0649
  31. // Debug console will persist between scenes
  32. [Header( "Properties" )]
  33. [SerializeField]
  34. [HideInInspector]
  35. private bool singleton = true;
  36. // Minimum height of the console window
  37. [SerializeField]
  38. [HideInInspector]
  39. private float minimumHeight = 200f;
  40. [SerializeField]
  41. [HideInInspector]
  42. private bool enablePopup = true;
  43. [SerializeField]
  44. [HideInInspector]
  45. private bool startInPopupMode = true;
  46. [SerializeField]
  47. [HideInInspector]
  48. private bool startMinimized = false;
  49. [SerializeField]
  50. [HideInInspector]
  51. private bool toggleWithKey = false;
  52. [SerializeField]
  53. [HideInInspector]
  54. private KeyCode toggleKey = KeyCode.BackQuote;
  55. [SerializeField]
  56. [HideInInspector]
  57. private bool enableSearchbar = true;
  58. [SerializeField]
  59. [HideInInspector]
  60. private float topSearchbarMinWidth = 360f;
  61. // Should command input field be cleared after pressing Enter
  62. [SerializeField]
  63. [HideInInspector]
  64. private bool clearCommandAfterExecution = true;
  65. [SerializeField]
  66. [HideInInspector]
  67. private int commandHistorySize = 15;
  68. [SerializeField]
  69. [HideInInspector]
  70. private bool receiveLogcatLogsInAndroid = false;
  71. [SerializeField]
  72. [HideInInspector]
  73. private string logcatArguments;
  74. [SerializeField]
  75. private bool avoidScreenCutout = true;
  76. [SerializeField]
  77. private int maxLogLength = 10000;
  78. [Header( "Visuals" )]
  79. [SerializeField]
  80. private DebugLogItem logItemPrefab;
  81. // Visuals for different log types
  82. [SerializeField]
  83. private Sprite infoLog;
  84. [SerializeField]
  85. private Sprite warningLog;
  86. [SerializeField]
  87. private Sprite errorLog;
  88. private Dictionary<LogType, Sprite> logSpriteRepresentations;
  89. [SerializeField]
  90. private Color collapseButtonNormalColor;
  91. [SerializeField]
  92. private Color collapseButtonSelectedColor;
  93. [SerializeField]
  94. private Color filterButtonsNormalColor;
  95. [SerializeField]
  96. private Color filterButtonsSelectedColor;
  97. [Header( "Internal References" )]
  98. [SerializeField]
  99. private RectTransform logWindowTR;
  100. private RectTransform canvasTR;
  101. [SerializeField]
  102. private RectTransform logItemsContainer;
  103. [SerializeField]
  104. private InputField commandInputField;
  105. [SerializeField]
  106. private Image collapseButton;
  107. [SerializeField]
  108. private Image filterInfoButton;
  109. [SerializeField]
  110. private Image filterWarningButton;
  111. [SerializeField]
  112. private Image filterErrorButton;
  113. [SerializeField]
  114. private Text infoEntryCountText;
  115. [SerializeField]
  116. private Text warningEntryCountText;
  117. [SerializeField]
  118. private Text errorEntryCountText;
  119. [SerializeField]
  120. private RectTransform searchbar;
  121. [SerializeField]
  122. private RectTransform searchbarSlotTop;
  123. [SerializeField]
  124. private RectTransform searchbarSlotBottom;
  125. [SerializeField]
  126. private GameObject snapToBottomButton;
  127. // Canvas group to modify visibility of the log window
  128. [SerializeField]
  129. private CanvasGroup logWindowCanvasGroup;
  130. [SerializeField]
  131. private DebugLogPopup popupManager;
  132. [SerializeField]
  133. private ScrollRect logItemsScrollRect;
  134. private RectTransform logItemsScrollRectTR;
  135. private Vector2 logItemsScrollRectOriginalSize;
  136. // Recycled list view to handle the log items efficiently
  137. [SerializeField]
  138. private DebugLogRecycledListView recycledListView;
  139. #pragma warning restore 0649
  140. // Number of entries filtered by their types
  141. private int infoEntryCount = 0, warningEntryCount = 0, errorEntryCount = 0;
  142. private bool isLogWindowVisible = true;
  143. private bool screenDimensionsChanged = true;
  144. // Filters to apply to the list of debug entries to show
  145. private bool isCollapseOn = false;
  146. private DebugLogFilter logFilter = DebugLogFilter.All;
  147. // Search filter
  148. private string searchTerm;
  149. private bool isInSearchMode;
  150. // If the last log item is completely visible (scrollbar is at the bottom),
  151. // scrollbar will remain at the bottom when new debug entries are received
  152. private bool snapToBottom = true;
  153. // List of unique debug entries (duplicates of entries are not kept)
  154. private List<DebugLogEntry> collapsedLogEntries;
  155. // Dictionary to quickly find if a log already exists in collapsedLogEntries
  156. private Dictionary<DebugLogEntry, int> collapsedLogEntriesMap;
  157. // The order the collapsedLogEntries are received
  158. // (duplicate entries have the same index (value))
  159. private DebugLogIndexList uncollapsedLogEntriesIndices;
  160. // Filtered list of debug entries to show
  161. private DebugLogIndexList indicesOfListEntriesToShow;
  162. // Logs that should be registered in Update-loop
  163. private DynamicCircularBuffer<QueuedDebugLogEntry> queuedLogEntries;
  164. private object logEntriesLock;
  165. // Pools for memory efficiency
  166. private List<DebugLogEntry> pooledLogEntries;
  167. private List<DebugLogItem> pooledLogItems;
  168. // History of the previously entered commands
  169. private CircularBuffer<string> commandHistory;
  170. private int commandHistoryIndex = -1;
  171. // Required in ValidateScrollPosition() function
  172. private PointerEventData nullPointerEventData;
  173. #if UNITY_EDITOR
  174. private bool isQuittingApplication;
  175. #endif
  176. #if !UNITY_EDITOR && UNITY_ANDROID
  177. private DebugLogLogcatListener logcatListener;
  178. #endif
  179. private CanvasGroup m_CanvasGroup = null;
  180. public void ShowOrHide(bool value) {
  181. if (value) {
  182. m_CanvasGroup.alpha = 1;
  183. m_CanvasGroup.blocksRaycasts = true;
  184. m_CanvasGroup.interactable = true;
  185. } else {
  186. m_CanvasGroup.alpha = 0;
  187. m_CanvasGroup.blocksRaycasts = false;
  188. m_CanvasGroup.interactable = false;
  189. }
  190. }
  191. private void Awake()
  192. {
  193. // Only one instance of debug console is allowed
  194. if( instance == null )
  195. {
  196. instance = this;
  197. // If it is a singleton object, don't destroy it between scene changes
  198. if( singleton )
  199. DontDestroyOnLoad( gameObject );
  200. }
  201. else if( this != instance )
  202. {
  203. Destroy( gameObject );
  204. return;
  205. }
  206. pooledLogEntries = new List<DebugLogEntry>( 16 );
  207. pooledLogItems = new List<DebugLogItem>( 16 );
  208. queuedLogEntries = new DynamicCircularBuffer<QueuedDebugLogEntry>( 16 );
  209. commandHistory = new CircularBuffer<string>( commandHistorySize );
  210. logEntriesLock = new object();
  211. canvasTR = (RectTransform) transform;
  212. logItemsScrollRectTR = (RectTransform) logItemsScrollRect.transform;
  213. logItemsScrollRectOriginalSize = logItemsScrollRectTR.sizeDelta;
  214. // Associate sprites with log types
  215. logSpriteRepresentations = new Dictionary<LogType, Sprite>()
  216. {
  217. { LogType.Log, infoLog },
  218. { LogType.Warning, warningLog },
  219. { LogType.Error, errorLog },
  220. { LogType.Exception, errorLog },
  221. { LogType.Assert, errorLog }
  222. };
  223. // Initially, all log types are visible
  224. filterInfoButton.color = filterButtonsSelectedColor;
  225. filterWarningButton.color = filterButtonsSelectedColor;
  226. filterErrorButton.color = filterButtonsSelectedColor;
  227. collapsedLogEntries = new List<DebugLogEntry>( 128 );
  228. collapsedLogEntriesMap = new Dictionary<DebugLogEntry, int>( 128 );
  229. uncollapsedLogEntriesIndices = new DebugLogIndexList();
  230. indicesOfListEntriesToShow = new DebugLogIndexList();
  231. recycledListView.Initialize( this, collapsedLogEntries, indicesOfListEntriesToShow, logItemPrefab.Transform.sizeDelta.y );
  232. recycledListView.UpdateItemsInTheList( true );
  233. if( minimumHeight < 200f )
  234. minimumHeight = 200f;
  235. if( !enableSearchbar )
  236. {
  237. searchbar = null;
  238. searchbarSlotTop.gameObject.SetActive( false );
  239. searchbarSlotBottom.gameObject.SetActive( false );
  240. }
  241. nullPointerEventData = new PointerEventData( null );
  242. m_CanvasGroup = GetComponent<CanvasGroup>();
  243. }
  244. private void OnEnable()
  245. {
  246. // Intercept debug entries
  247. Application.logMessageReceivedThreaded -= ReceivedLog;
  248. Application.logMessageReceivedThreaded += ReceivedLog;
  249. // Listen for entered commands
  250. commandInputField.onValidateInput -= OnValidateCommand;
  251. commandInputField.onValidateInput += OnValidateCommand;
  252. if( receiveLogcatLogsInAndroid )
  253. {
  254. #if !UNITY_EDITOR && UNITY_ANDROID
  255. if( logcatListener == null )
  256. logcatListener = new DebugLogLogcatListener();
  257. logcatListener.Start( logcatArguments );
  258. #endif
  259. }
  260. DebugLogConsole.AddCommand( "save_logs", "Saves logs to a file", SaveLogsToFile );
  261. //Debug.LogAssertion( "assert" );
  262. //Debug.LogError( "error" );
  263. //Debug.LogException( new System.IO.EndOfStreamException() );
  264. //Debug.LogWarning( "warning" );
  265. //Debug.Log( "log" );
  266. }
  267. private void OnDisable()
  268. {
  269. if( instance != this )
  270. return;
  271. // Stop receiving debug entries
  272. Application.logMessageReceivedThreaded -= ReceivedLog;
  273. #if !UNITY_EDITOR && UNITY_ANDROID
  274. if( logcatListener != null )
  275. logcatListener.Stop();
  276. #endif
  277. // Stop receiving commands
  278. commandInputField.onValidateInput -= OnValidateCommand;
  279. DebugLogConsole.RemoveCommand( "save_logs" );
  280. }
  281. private void Start()
  282. {
  283. if( ( enablePopup && startInPopupMode ) || ( !enablePopup && startMinimized ) )
  284. ShowPopup();
  285. else
  286. ShowLogWindow();
  287. popupManager.gameObject.SetActive( enablePopup );
  288. }
  289. #if UNITY_EDITOR
  290. private void OnApplicationQuit()
  291. {
  292. isQuittingApplication = true;
  293. }
  294. #endif
  295. // Window is resized, update the list
  296. private void OnRectTransformDimensionsChange()
  297. {
  298. screenDimensionsChanged = true;
  299. }
  300. private void LateUpdate()
  301. {
  302. #if UNITY_EDITOR
  303. if( isQuittingApplication )
  304. return;
  305. #endif
  306. int queuedLogCount = queuedLogEntries.Count;
  307. if( queuedLogCount > 0 )
  308. {
  309. for( int i = 0; i < queuedLogCount; i++ )
  310. {
  311. QueuedDebugLogEntry logEntry;
  312. lock( logEntriesLock )
  313. {
  314. logEntry = queuedLogEntries.RemoveFirst();
  315. }
  316. ProcessLog( logEntry );
  317. }
  318. }
  319. if( screenDimensionsChanged )
  320. {
  321. // Update the recycled list view
  322. if( isLogWindowVisible )
  323. recycledListView.OnViewportDimensionsChanged();
  324. else
  325. popupManager.OnViewportDimensionsChanged();
  326. #if UNITY_ANDROID || UNITY_IOS
  327. CheckScreenCutout();
  328. #endif
  329. if( searchbar )
  330. {
  331. float logWindowWidth = logWindowTR.rect.width;
  332. if( logWindowWidth >= topSearchbarMinWidth )
  333. {
  334. if( searchbar.parent == searchbarSlotBottom )
  335. {
  336. searchbarSlotTop.gameObject.SetActive( true );
  337. searchbar.SetParent( searchbarSlotTop, false );
  338. searchbarSlotBottom.gameObject.SetActive( false );
  339. logItemsScrollRectTR.anchoredPosition = Vector2.zero;
  340. logItemsScrollRectTR.sizeDelta = logItemsScrollRectOriginalSize;
  341. }
  342. }
  343. else
  344. {
  345. if( searchbar.parent == searchbarSlotTop )
  346. {
  347. searchbarSlotBottom.gameObject.SetActive( true );
  348. searchbar.SetParent( searchbarSlotBottom, false );
  349. searchbarSlotTop.gameObject.SetActive( false );
  350. float searchbarHeight = searchbarSlotBottom.sizeDelta.y;
  351. logItemsScrollRectTR.anchoredPosition = new Vector2( 0f, searchbarHeight * -0.5f );
  352. logItemsScrollRectTR.sizeDelta = logItemsScrollRectOriginalSize - new Vector2( 0f, searchbarHeight );
  353. }
  354. }
  355. }
  356. screenDimensionsChanged = false;
  357. }
  358. // If snapToBottom is enabled, force the scrollbar to the bottom
  359. if( snapToBottom )
  360. {
  361. logItemsScrollRect.verticalNormalizedPosition = 0f;
  362. if( snapToBottomButton.activeSelf )
  363. snapToBottomButton.SetActive( false );
  364. }
  365. else
  366. {
  367. float scrollPos = logItemsScrollRect.verticalNormalizedPosition;
  368. if( snapToBottomButton.activeSelf != ( scrollPos > 1E-6f && scrollPos < 0.9999f ) )
  369. snapToBottomButton.SetActive( !snapToBottomButton.activeSelf );
  370. }
  371. if( toggleWithKey )
  372. {
  373. if( Input.GetKeyDown( toggleKey ) )
  374. {
  375. if( isLogWindowVisible )
  376. ShowPopup();
  377. else
  378. ShowLogWindow();
  379. }
  380. }
  381. if( isLogWindowVisible && commandInputField.isFocused )
  382. {
  383. if( Input.GetKeyDown( KeyCode.UpArrow ) )
  384. {
  385. if( commandHistoryIndex == -1 )
  386. commandHistoryIndex = commandHistory.Count - 1;
  387. else if( --commandHistoryIndex < 0 )
  388. commandHistoryIndex = 0;
  389. if( commandHistoryIndex >= 0 && commandHistoryIndex < commandHistory.Count )
  390. {
  391. commandInputField.text = commandHistory[commandHistoryIndex];
  392. commandInputField.caretPosition = commandInputField.text.Length;
  393. }
  394. }
  395. else if( Input.GetKeyDown( KeyCode.DownArrow ) )
  396. {
  397. if( commandHistoryIndex == -1 )
  398. commandHistoryIndex = commandHistory.Count - 1;
  399. else if( ++commandHistoryIndex >= commandHistory.Count )
  400. commandHistoryIndex = commandHistory.Count - 1;
  401. if( commandHistoryIndex >= 0 && commandHistoryIndex < commandHistory.Count )
  402. commandInputField.text = commandHistory[commandHistoryIndex];
  403. }
  404. }
  405. #if !UNITY_EDITOR && UNITY_ANDROID
  406. if( logcatListener != null )
  407. {
  408. string log;
  409. while( ( log = logcatListener.GetLog() ) != null )
  410. ReceivedLog( "LOGCAT: " + log, string.Empty, LogType.Log );
  411. }
  412. #endif
  413. }
  414. public void ShowLogWindow()
  415. {
  416. // Show the log window
  417. logWindowCanvasGroup.interactable = true;
  418. logWindowCanvasGroup.blocksRaycasts = true;
  419. logWindowCanvasGroup.alpha = 1f;
  420. popupManager.Hide();
  421. // Update the recycled list view
  422. // (in case new entries were intercepted while log window was hidden)
  423. recycledListView.OnLogEntriesUpdated( true );
  424. isLogWindowVisible = true;
  425. }
  426. public void ShowPopup()
  427. {
  428. // Hide the log window
  429. logWindowCanvasGroup.interactable = false;
  430. logWindowCanvasGroup.blocksRaycasts = false;
  431. logWindowCanvasGroup.alpha = 0f;
  432. popupManager.Show();
  433. commandHistoryIndex = -1;
  434. isLogWindowVisible = false;
  435. }
  436. // Command field input is changed, check if command is submitted
  437. public char OnValidateCommand( string text, int charIndex, char addedChar )
  438. {
  439. if( addedChar == '\t' ) // Autocomplete attempt
  440. {
  441. if( !string.IsNullOrEmpty( text ) )
  442. {
  443. string autoCompletedCommand = DebugLogConsole.GetAutoCompleteCommand( text );
  444. if( !string.IsNullOrEmpty( autoCompletedCommand ) )
  445. commandInputField.text = autoCompletedCommand;
  446. }
  447. return '\0';
  448. }
  449. else if( addedChar == '\n' ) // Command is submitted
  450. {
  451. // Clear the command field
  452. if( clearCommandAfterExecution )
  453. commandInputField.text = "";
  454. if( text.Length > 0 )
  455. {
  456. if( commandHistory.Count == 0 || commandHistory[commandHistory.Count - 1] != text )
  457. commandHistory.Add( text );
  458. commandHistoryIndex = -1;
  459. // Execute the command
  460. DebugLogConsole.ExecuteCommand( text );
  461. // Snap to bottom and select the latest entry
  462. SetSnapToBottom( true );
  463. }
  464. return '\0';
  465. }
  466. return addedChar;
  467. }
  468. // A debug entry is received
  469. private void ReceivedLog( string logString, string stackTrace, LogType logType )
  470. {
  471. #if UNITY_EDITOR
  472. if( isQuittingApplication )
  473. return;
  474. #endif
  475. // Truncate the log if it is longer than maxLogLength
  476. int logLength = logString.Length;
  477. if( stackTrace == null )
  478. {
  479. if( logLength > maxLogLength )
  480. logString = logString.Substring( 0, maxLogLength - 11 ) + "<truncated>";
  481. }
  482. else
  483. {
  484. logLength += stackTrace.Length;
  485. if( logLength > maxLogLength )
  486. {
  487. // Decide which log component(s) to truncate
  488. int halfMaxLogLength = maxLogLength / 2;
  489. if( logString.Length >= halfMaxLogLength )
  490. {
  491. if( stackTrace.Length >= halfMaxLogLength )
  492. {
  493. // Truncate both logString and stackTrace
  494. logString = logString.Substring( 0, halfMaxLogLength - 11 ) + "<truncated>";
  495. // If stackTrace doesn't end with a blank line, its last line won't be visible in the console for some reason
  496. stackTrace = stackTrace.Substring( 0, halfMaxLogLength - 12 ) + "<truncated>\n";
  497. }
  498. else
  499. {
  500. // Truncate logString
  501. logString = logString.Substring( 0, maxLogLength - stackTrace.Length - 11 ) + "<truncated>";
  502. }
  503. }
  504. else
  505. {
  506. // Truncate stackTrace
  507. stackTrace = stackTrace.Substring( 0, maxLogLength - logString.Length - 12 ) + "<truncated>\n";
  508. }
  509. }
  510. }
  511. QueuedDebugLogEntry queuedLogEntry = new QueuedDebugLogEntry( logString, stackTrace, logType );
  512. lock( logEntriesLock )
  513. {
  514. queuedLogEntries.Add( queuedLogEntry );
  515. }
  516. }
  517. // Present the log entry in the console
  518. private void ProcessLog( QueuedDebugLogEntry queuedLogEntry )
  519. {
  520. LogType logType = queuedLogEntry.logType;
  521. DebugLogEntry logEntry;
  522. if( pooledLogEntries.Count > 0 )
  523. {
  524. logEntry = pooledLogEntries[pooledLogEntries.Count - 1];
  525. pooledLogEntries.RemoveAt( pooledLogEntries.Count - 1 );
  526. }
  527. else
  528. logEntry = new DebugLogEntry();
  529. logEntry.Initialize( queuedLogEntry.logString, queuedLogEntry.stackTrace );
  530. // Check if this entry is a duplicate (i.e. has been received before)
  531. int logEntryIndex;
  532. bool isEntryInCollapsedEntryList = collapsedLogEntriesMap.TryGetValue( logEntry, out logEntryIndex );
  533. if( !isEntryInCollapsedEntryList )
  534. {
  535. // It is not a duplicate,
  536. // add it to the list of unique debug entries
  537. logEntry.logTypeSpriteRepresentation = logSpriteRepresentations[logType];
  538. logEntryIndex = collapsedLogEntries.Count;
  539. collapsedLogEntries.Add( logEntry );
  540. collapsedLogEntriesMap[logEntry] = logEntryIndex;
  541. }
  542. else
  543. {
  544. // It is a duplicate, pool the duplicate log entry and
  545. // increment the original debug item's collapsed count
  546. pooledLogEntries.Add( logEntry );
  547. logEntry = collapsedLogEntries[logEntryIndex];
  548. logEntry.count++;
  549. }
  550. // Add the index of the unique debug entry to the list
  551. // that stores the order the debug entries are received
  552. uncollapsedLogEntriesIndices.Add( logEntryIndex );
  553. // If this debug entry matches the current filters,
  554. // add it to the list of debug entries to show
  555. Sprite logTypeSpriteRepresentation = logEntry.logTypeSpriteRepresentation;
  556. if( isCollapseOn && isEntryInCollapsedEntryList )
  557. {
  558. if( isLogWindowVisible )
  559. {
  560. if( !isInSearchMode && logFilter == DebugLogFilter.All )
  561. recycledListView.OnCollapsedLogEntryAtIndexUpdated( logEntryIndex );
  562. else
  563. recycledListView.OnCollapsedLogEntryAtIndexUpdated( indicesOfListEntriesToShow.IndexOf( logEntryIndex ) );
  564. }
  565. }
  566. else if( ( !isInSearchMode || queuedLogEntry.MatchesSearchTerm( searchTerm ) ) && ( logFilter == DebugLogFilter.All ||
  567. ( logTypeSpriteRepresentation == infoLog && ( ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info ) ) ||
  568. ( logTypeSpriteRepresentation == warningLog && ( ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning ) ) ||
  569. ( logTypeSpriteRepresentation == errorLog && ( ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error ) ) ) )
  570. {
  571. indicesOfListEntriesToShow.Add( logEntryIndex );
  572. if( isLogWindowVisible )
  573. recycledListView.OnLogEntriesUpdated( false );
  574. }
  575. if( logType == LogType.Log )
  576. {
  577. infoEntryCount++;
  578. infoEntryCountText.text = infoEntryCount.ToString();
  579. // If debug popup is visible, notify it of the new debug entry
  580. if( !isLogWindowVisible )
  581. popupManager.NewInfoLogArrived();
  582. }
  583. else if( logType == LogType.Warning )
  584. {
  585. warningEntryCount++;
  586. warningEntryCountText.text = warningEntryCount.ToString();
  587. // If debug popup is visible, notify it of the new debug entry
  588. if( !isLogWindowVisible )
  589. popupManager.NewWarningLogArrived();
  590. }
  591. else
  592. {
  593. errorEntryCount++;
  594. errorEntryCountText.text = errorEntryCount.ToString();
  595. // If debug popup is visible, notify it of the new debug entry
  596. if( !isLogWindowVisible )
  597. popupManager.NewErrorLogArrived();
  598. }
  599. }
  600. // Value of snapToBottom is changed (user scrolled the list manually)
  601. public void SetSnapToBottom( bool snapToBottom )
  602. {
  603. this.snapToBottom = snapToBottom;
  604. }
  605. // Make sure the scroll bar of the scroll rect is adjusted properly
  606. public void ValidateScrollPosition()
  607. {
  608. logItemsScrollRect.OnScroll( nullPointerEventData );
  609. }
  610. // Hide button is clicked
  611. public void HideButtonPressed()
  612. {
  613. ShowPopup();
  614. }
  615. // Clear button is clicked
  616. public void ClearButtonPressed()
  617. {
  618. snapToBottom = true;
  619. infoEntryCount = 0;
  620. warningEntryCount = 0;
  621. errorEntryCount = 0;
  622. infoEntryCountText.text = "0";
  623. warningEntryCountText.text = "0";
  624. errorEntryCountText.text = "0";
  625. collapsedLogEntries.Clear();
  626. collapsedLogEntriesMap.Clear();
  627. uncollapsedLogEntriesIndices.Clear();
  628. indicesOfListEntriesToShow.Clear();
  629. recycledListView.DeselectSelectedLogItem();
  630. recycledListView.OnLogEntriesUpdated( true );
  631. }
  632. // Collapse button is clicked
  633. public void CollapseButtonPressed()
  634. {
  635. // Swap the value of collapse mode
  636. isCollapseOn = !isCollapseOn;
  637. snapToBottom = true;
  638. collapseButton.color = isCollapseOn ? collapseButtonSelectedColor : collapseButtonNormalColor;
  639. recycledListView.SetCollapseMode( isCollapseOn );
  640. // Determine the new list of debug entries to show
  641. FilterLogs();
  642. }
  643. // Filtering mode of info logs has changed
  644. public void FilterLogButtonPressed()
  645. {
  646. logFilter = logFilter ^ DebugLogFilter.Info;
  647. if( ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info )
  648. filterInfoButton.color = filterButtonsSelectedColor;
  649. else
  650. filterInfoButton.color = filterButtonsNormalColor;
  651. FilterLogs();
  652. }
  653. // Filtering mode of warning logs has changed
  654. public void FilterWarningButtonPressed()
  655. {
  656. logFilter = logFilter ^ DebugLogFilter.Warning;
  657. if( ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning )
  658. filterWarningButton.color = filterButtonsSelectedColor;
  659. else
  660. filterWarningButton.color = filterButtonsNormalColor;
  661. FilterLogs();
  662. }
  663. // Filtering mode of error logs has changed
  664. public void FilterErrorButtonPressed()
  665. {
  666. logFilter = logFilter ^ DebugLogFilter.Error;
  667. if( ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error )
  668. filterErrorButton.color = filterButtonsSelectedColor;
  669. else
  670. filterErrorButton.color = filterButtonsNormalColor;
  671. FilterLogs();
  672. }
  673. // Search term has changed
  674. public void SearchTermChanged( string searchTerm )
  675. {
  676. if( searchTerm != null )
  677. searchTerm = searchTerm.Trim();
  678. this.searchTerm = searchTerm;
  679. bool isInSearchMode = !string.IsNullOrEmpty( searchTerm );
  680. if( isInSearchMode || this.isInSearchMode )
  681. {
  682. this.isInSearchMode = isInSearchMode;
  683. FilterLogs();
  684. }
  685. }
  686. // Debug window is being resized,
  687. // Set the sizeDelta property of the window accordingly while
  688. // preventing window dimensions from going below the minimum dimensions
  689. public void Resize( BaseEventData dat )
  690. {
  691. PointerEventData eventData = (PointerEventData) dat;
  692. // Grab the resize button from top; 36f is the height of the resize button
  693. float newHeight = ( eventData.position.y - logWindowTR.position.y ) / -canvasTR.localScale.y + 36f;
  694. if( newHeight < minimumHeight )
  695. newHeight = minimumHeight;
  696. Vector2 anchorMin = logWindowTR.anchorMin;
  697. anchorMin.y = Mathf.Max( 0f, 1f - newHeight / canvasTR.sizeDelta.y );
  698. logWindowTR.anchorMin = anchorMin;
  699. // Update the recycled list view
  700. recycledListView.OnViewportDimensionsChanged();
  701. }
  702. // Determine the filtered list of debug entries to show on screen
  703. private void FilterLogs()
  704. {
  705. indicesOfListEntriesToShow.Clear();
  706. if( logFilter != DebugLogFilter.None )
  707. {
  708. if( logFilter == DebugLogFilter.All )
  709. {
  710. if( isCollapseOn )
  711. {
  712. if( !isInSearchMode )
  713. {
  714. // All the unique debug entries will be listed just once.
  715. // So, list of debug entries to show is the same as the
  716. // order these unique debug entries are added to collapsedLogEntries
  717. for( int i = 0, count = collapsedLogEntries.Count; i < count; i++ )
  718. indicesOfListEntriesToShow.Add( i );
  719. }
  720. else
  721. {
  722. for( int i = 0, count = collapsedLogEntries.Count; i < count; i++ )
  723. {
  724. if( collapsedLogEntries[i].MatchesSearchTerm( searchTerm ) )
  725. indicesOfListEntriesToShow.Add( i );
  726. }
  727. }
  728. }
  729. else
  730. {
  731. if( !isInSearchMode )
  732. {
  733. for( int i = 0, count = uncollapsedLogEntriesIndices.Count; i < count; i++ )
  734. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  735. }
  736. else
  737. {
  738. for( int i = 0, count = uncollapsedLogEntriesIndices.Count; i < count; i++ )
  739. {
  740. if( collapsedLogEntries[uncollapsedLogEntriesIndices[i]].MatchesSearchTerm( searchTerm ) )
  741. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  742. }
  743. }
  744. }
  745. }
  746. else
  747. {
  748. // Show only the debug entries that match the current filter
  749. bool isInfoEnabled = ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info;
  750. bool isWarningEnabled = ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning;
  751. bool isErrorEnabled = ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error;
  752. if( isCollapseOn )
  753. {
  754. for( int i = 0, count = collapsedLogEntries.Count; i < count; i++ )
  755. {
  756. DebugLogEntry logEntry = collapsedLogEntries[i];
  757. if( isInSearchMode && !logEntry.MatchesSearchTerm( searchTerm ) )
  758. continue;
  759. if( logEntry.logTypeSpriteRepresentation == infoLog )
  760. {
  761. if( isInfoEnabled )
  762. indicesOfListEntriesToShow.Add( i );
  763. }
  764. else if( logEntry.logTypeSpriteRepresentation == warningLog )
  765. {
  766. if( isWarningEnabled )
  767. indicesOfListEntriesToShow.Add( i );
  768. }
  769. else if( isErrorEnabled )
  770. indicesOfListEntriesToShow.Add( i );
  771. }
  772. }
  773. else
  774. {
  775. for( int i = 0, count = uncollapsedLogEntriesIndices.Count; i < count; i++ )
  776. {
  777. DebugLogEntry logEntry = collapsedLogEntries[uncollapsedLogEntriesIndices[i]];
  778. if( isInSearchMode && !logEntry.MatchesSearchTerm( searchTerm ) )
  779. continue;
  780. if( logEntry.logTypeSpriteRepresentation == infoLog )
  781. {
  782. if( isInfoEnabled )
  783. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  784. }
  785. else if( logEntry.logTypeSpriteRepresentation == warningLog )
  786. {
  787. if( isWarningEnabled )
  788. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  789. }
  790. else if( isErrorEnabled )
  791. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  792. }
  793. }
  794. }
  795. }
  796. // Update the recycled list view
  797. recycledListView.DeselectSelectedLogItem();
  798. recycledListView.OnLogEntriesUpdated( true );
  799. ValidateScrollPosition();
  800. }
  801. public string GetAllLogs()
  802. {
  803. int count = uncollapsedLogEntriesIndices.Count;
  804. int length = 0;
  805. int newLineLength = System.Environment.NewLine.Length;
  806. for( int i = 0; i < count; i++ )
  807. {
  808. DebugLogEntry entry = collapsedLogEntries[uncollapsedLogEntriesIndices[i]];
  809. length += entry.logString.Length + entry.stackTrace.Length + newLineLength * 3;
  810. }
  811. length += 100; // Just in case...
  812. System.Text.StringBuilder sb = new System.Text.StringBuilder( length );
  813. for( int i = 0; i < count; i++ )
  814. {
  815. DebugLogEntry entry = collapsedLogEntries[uncollapsedLogEntriesIndices[i]];
  816. sb.AppendLine( entry.logString ).AppendLine( entry.stackTrace ).AppendLine();
  817. }
  818. return sb.ToString();
  819. }
  820. private void SaveLogsToFile()
  821. {
  822. string path = Path.Combine( Application.persistentDataPath, System.DateTime.Now.ToString( "dd-MM-yyyy--HH-mm-ss" ) + ".txt" );
  823. File.WriteAllText( path, instance.GetAllLogs() );
  824. Debug.Log( "Logs saved to: " + path );
  825. }
  826. // If a cutout is intersecting with debug window on notch screens, shift the window downwards
  827. private void CheckScreenCutout()
  828. {
  829. if( !avoidScreenCutout )
  830. return;
  831. #if UNITY_2017_2_OR_NEWER && !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
  832. // Check if there is a cutout at the top of the screen
  833. int screenHeight = Screen.height;
  834. float safeYMax = Screen.safeArea.yMax;
  835. if( safeYMax < screenHeight - 1 ) // 1: a small threshold
  836. {
  837. // There is a cutout, shift the log window downwards
  838. float cutoutPercentage = ( screenHeight - safeYMax ) / Screen.height;
  839. float cutoutLocalSize = cutoutPercentage * canvasTR.rect.height;
  840. logWindowTR.anchoredPosition = new Vector2( 0f, -cutoutLocalSize );
  841. logWindowTR.sizeDelta = new Vector2( 0f, -cutoutLocalSize );
  842. }
  843. else
  844. {
  845. logWindowTR.anchoredPosition = Vector2.zero;
  846. logWindowTR.sizeDelta = Vector2.zero;
  847. }
  848. #endif
  849. }
  850. // Pool an unused log item
  851. public void PoolLogItem( DebugLogItem logItem )
  852. {
  853. logItem.gameObject.SetActive( false );
  854. pooledLogItems.Add( logItem );
  855. }
  856. // Fetch a log item from the pool
  857. public DebugLogItem PopLogItem()
  858. {
  859. DebugLogItem newLogItem;
  860. // If pool is not empty, fetch a log item from the pool,
  861. // create a new log item otherwise
  862. if( pooledLogItems.Count > 0 )
  863. {
  864. newLogItem = pooledLogItems[pooledLogItems.Count - 1];
  865. pooledLogItems.RemoveAt( pooledLogItems.Count - 1 );
  866. newLogItem.gameObject.SetActive( true );
  867. }
  868. else
  869. {
  870. newLogItem = (DebugLogItem) Instantiate( logItemPrefab, logItemsContainer, false );
  871. newLogItem.Initialize( recycledListView );
  872. }
  873. return newLogItem;
  874. }
  875. }
  876. }