DebugLogManager.cs 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. using UnityEngine;
  6. using UnityEngine.UI;
  7. using UnityEngine.EventSystems;
  8. // Receives debug entries and custom events (e.g. Clear, Collapse, Filter by Type)
  9. // and notifies the recycled list view of changes to the list of debug entries
  10. //
  11. // - Vocabulary -
  12. // Debug/Log entry: a Debug.Log/LogError/LogWarning/LogException/LogAssertion request made by
  13. // the client and intercepted by this manager object
  14. // Debug/Log item: a visual (uGUI) representation of a debug entry
  15. //
  16. // There can be a lot of debug entries in the system but there will only be a handful of log items
  17. // to show their properties on screen (these log items are recycled as the list is scrolled)
  18. // An enum to represent filtered log types
  19. namespace IngameDebugConsole
  20. {
  21. public enum DebugLogFilter
  22. {
  23. None = 0,
  24. Info = 1,
  25. Warning = 2,
  26. Error = 4,
  27. All = 7
  28. }
  29. public class DebugLogManager : MonoBehaviour
  30. {
  31. public static DebugLogManager Instance { get; private set; }
  32. #pragma warning disable 0649
  33. [Header( "Properties" )]
  34. [SerializeField]
  35. [HideInInspector]
  36. [Tooltip( "If enabled, console window will persist between scenes (i.e. not be destroyed when scene changes)" )]
  37. private bool singleton = true;
  38. [SerializeField]
  39. [HideInInspector]
  40. [Tooltip( "Minimum height of the console window" )]
  41. private float minimumHeight = 200f;
  42. [SerializeField]
  43. [HideInInspector]
  44. [Tooltip( "If disabled, no popup will be shown when the console window is hidden" )]
  45. private bool enablePopup = true;
  46. [SerializeField]
  47. [HideInInspector]
  48. [Tooltip( "If enabled, console will be initialized as a popup" )]
  49. private bool startInPopupMode = true;
  50. [SerializeField]
  51. [HideInInspector]
  52. [Tooltip( "If enabled, console window will initially be invisible" )]
  53. private bool startMinimized = false;
  54. [SerializeField]
  55. [HideInInspector]
  56. [Tooltip( "If enabled, pressing the Toggle Key will show/hide (i.e. toggle) the console window at runtime" )]
  57. private bool toggleWithKey = false;
  58. [SerializeField]
  59. [HideInInspector]
  60. private KeyCode toggleKey = KeyCode.BackQuote;
  61. [SerializeField]
  62. [HideInInspector]
  63. [Tooltip( "If enabled, the console window will have a searchbar" )]
  64. private bool enableSearchbar = true;
  65. [SerializeField]
  66. [HideInInspector]
  67. [Tooltip( "Width of the canvas determines whether the searchbar will be located inside the menu bar or underneath the menu bar. This way, the menu bar doesn't get too crowded on narrow screens. This value determines the minimum width of the canvas for the searchbar to appear inside the menu bar" )]
  68. private float topSearchbarMinWidth = 360f;
  69. [SerializeField]
  70. [HideInInspector]
  71. [Tooltip( "If enabled, the command input field at the bottom of the console window will automatically be cleared after entering a command" )]
  72. private bool clearCommandAfterExecution = true;
  73. [SerializeField]
  74. [HideInInspector]
  75. [Tooltip( "Console keeps track of the previously entered commands. This value determines the capacity of the command history (you can scroll through the history via up and down arrow keys while the command input field is focused)" )]
  76. private int commandHistorySize = 15;
  77. [SerializeField]
  78. [HideInInspector]
  79. [Tooltip( "If enabled, while typing a command, all of the matching commands' signatures will be displayed in a popup" )]
  80. private bool showCommandSuggestions = true;
  81. [SerializeField]
  82. [HideInInspector]
  83. [Tooltip( "If enabled, on Android platform, logcat entries of the application will also be logged to the console with the prefix \"LOGCAT: \". This may come in handy especially if you want to access the native logs of your Android plugins (like Admob)" )]
  84. private bool receiveLogcatLogsInAndroid = false;
  85. #pragma warning disable 0414
  86. [SerializeField]
  87. [HideInInspector]
  88. [Tooltip( "Native logs will be filtered using these arguments. If left blank, all native logs of the application will be logged to the console. But if you want to e.g. see Admob's logs only, you can enter \"-s Ads\" (without quotes) here" )]
  89. private string logcatArguments;
  90. #pragma warning restore 0414
  91. [SerializeField]
  92. [Tooltip( "If enabled, on Android and iOS devices with notch screens, the console window will be repositioned so that the cutout(s) don't obscure it" )]
  93. private bool avoidScreenCutout = true;
  94. [SerializeField]
  95. [Tooltip( "If a log is longer than this limit, it will be truncated. This helps avoid reaching Unity's 65000 vertex limit for UI canvases" )]
  96. private int maxLogLength = 10000;
  97. [SerializeField]
  98. [Tooltip( "If enabled, on standalone platforms, command input field will automatically be focused (start receiving keyboard input) after opening the console window" )]
  99. private bool autoFocusOnCommandInputField = true;
  100. [Header( "Visuals" )]
  101. [SerializeField]
  102. private DebugLogItem logItemPrefab;
  103. [SerializeField]
  104. private Text commandSuggestionPrefab;
  105. // Visuals for different log types
  106. [SerializeField]
  107. private Sprite infoLog;
  108. [SerializeField]
  109. private Sprite warningLog;
  110. [SerializeField]
  111. private Sprite errorLog;
  112. private Dictionary<LogType, Sprite> logSpriteRepresentations;
  113. [SerializeField]
  114. private Color collapseButtonNormalColor;
  115. [SerializeField]
  116. private Color collapseButtonSelectedColor;
  117. [SerializeField]
  118. private Color filterButtonsNormalColor;
  119. [SerializeField]
  120. private Color filterButtonsSelectedColor;
  121. [SerializeField]
  122. private string commandSuggestionHighlightStart = "<color=orange>";
  123. [SerializeField]
  124. private string commandSuggestionHighlightEnd = "</color>";
  125. [Header( "Internal References" )]
  126. [SerializeField]
  127. private RectTransform logWindowTR;
  128. private RectTransform canvasTR;
  129. [SerializeField]
  130. private RectTransform logItemsContainer;
  131. [SerializeField]
  132. private RectTransform commandSuggestionsContainer;
  133. [SerializeField]
  134. private InputField commandInputField;
  135. [SerializeField]
  136. private Button hideButton;
  137. [SerializeField]
  138. private Button clearButton;
  139. [SerializeField]
  140. private Image collapseButton;
  141. [SerializeField]
  142. private Image filterInfoButton;
  143. [SerializeField]
  144. private Image filterWarningButton;
  145. [SerializeField]
  146. private Image filterErrorButton;
  147. [SerializeField]
  148. private Text infoEntryCountText;
  149. [SerializeField]
  150. private Text warningEntryCountText;
  151. [SerializeField]
  152. private Text errorEntryCountText;
  153. [SerializeField]
  154. private RectTransform searchbar;
  155. [SerializeField]
  156. private RectTransform searchbarSlotTop;
  157. [SerializeField]
  158. private RectTransform searchbarSlotBottom;
  159. [SerializeField]
  160. private GameObject snapToBottomButton;
  161. // Canvas group to modify visibility of the log window
  162. [SerializeField]
  163. private CanvasGroup logWindowCanvasGroup;
  164. [SerializeField]
  165. private DebugLogPopup popupManager;
  166. [SerializeField]
  167. private ScrollRect logItemsScrollRect;
  168. private RectTransform logItemsScrollRectTR;
  169. private Vector2 logItemsScrollRectOriginalSize;
  170. // Recycled list view to handle the log items efficiently
  171. [SerializeField]
  172. private DebugLogRecycledListView recycledListView;
  173. #pragma warning restore 0649
  174. private bool isLogWindowVisible = true;
  175. public bool IsLogWindowVisible { get { return isLogWindowVisible; } }
  176. public bool PopupEnabled
  177. {
  178. get { return popupManager.gameObject.activeSelf; }
  179. set { popupManager.gameObject.SetActive( value ); }
  180. }
  181. private bool screenDimensionsChanged = true;
  182. // Number of entries filtered by their types
  183. private int infoEntryCount = 0, warningEntryCount = 0, errorEntryCount = 0;
  184. // Filters to apply to the list of debug entries to show
  185. private bool isCollapseOn = false;
  186. private DebugLogFilter logFilter = DebugLogFilter.All;
  187. // Search filter
  188. private string searchTerm;
  189. private bool isInSearchMode;
  190. // If the last log item is completely visible (scrollbar is at the bottom),
  191. // scrollbar will remain at the bottom when new debug entries are received
  192. private bool snapToBottom = true;
  193. // List of unique debug entries (duplicates of entries are not kept)
  194. private List<DebugLogEntry> collapsedLogEntries;
  195. // Dictionary to quickly find if a log already exists in collapsedLogEntries
  196. private Dictionary<DebugLogEntry, int> collapsedLogEntriesMap;
  197. // The order the collapsedLogEntries are received
  198. // (duplicate entries have the same index (value))
  199. private DebugLogIndexList uncollapsedLogEntriesIndices;
  200. // Filtered list of debug entries to show
  201. private DebugLogIndexList indicesOfListEntriesToShow;
  202. // Logs that should be registered in Update-loop
  203. private DynamicCircularBuffer<QueuedDebugLogEntry> queuedLogEntries;
  204. private object logEntriesLock;
  205. private int pendingLogToAutoExpand;
  206. // Command suggestions that match the currently entered command
  207. private List<Text> commandSuggestionInstances;
  208. private int visibleCommandSuggestionInstances = 0;
  209. private List<ConsoleMethodInfo> matchingCommandSuggestions;
  210. private List<int> commandCaretIndexIncrements;
  211. private StringBuilder commandSuggestionsStringBuilder;
  212. private string commandInputFieldPrevCommand;
  213. private string commandInputFieldPrevCommandName;
  214. private int commandInputFieldPrevParamCount = -1;
  215. private int commandInputFieldPrevCaretPos = -1;
  216. private int commandInputFieldPrevCaretArgumentIndex = -1;
  217. // Pools for memory efficiency
  218. private List<DebugLogEntry> pooledLogEntries;
  219. private List<DebugLogItem> pooledLogItems;
  220. // History of the previously entered commands
  221. private CircularBuffer<string> commandHistory;
  222. private int commandHistoryIndex = -1;
  223. // Required in ValidateScrollPosition() function
  224. private PointerEventData nullPointerEventData;
  225. #if UNITY_EDITOR
  226. private bool isQuittingApplication;
  227. #endif
  228. #if !UNITY_EDITOR && UNITY_ANDROID
  229. private DebugLogLogcatListener logcatListener;
  230. #endif
  231. #region 显示FPS
  232. private float _fps;
  233. private Color _fpsColor = Color.white;
  234. private int _frameNumber;
  235. private float _lastShowFPSTime;
  236. #endregion
  237. private void Awake()
  238. {
  239. // Only one instance of debug console is allowed
  240. if( !Instance )
  241. {
  242. Instance = this;
  243. // If it is a singleton object, don't destroy it between scene changes
  244. if( singleton )
  245. DontDestroyOnLoad( gameObject );
  246. }
  247. else if( Instance != this )
  248. {
  249. Destroy( gameObject );
  250. return;
  251. }
  252. pooledLogEntries = new List<DebugLogEntry>( 16 );
  253. pooledLogItems = new List<DebugLogItem>( 16 );
  254. commandSuggestionInstances = new List<Text>( 8 );
  255. matchingCommandSuggestions = new List<ConsoleMethodInfo>( 8 );
  256. commandCaretIndexIncrements = new List<int>( 8 );
  257. queuedLogEntries = new DynamicCircularBuffer<QueuedDebugLogEntry>( 16 );
  258. commandHistory = new CircularBuffer<string>( commandHistorySize );
  259. logEntriesLock = new object();
  260. commandSuggestionsStringBuilder = new StringBuilder( 128 );
  261. canvasTR = (RectTransform) transform;
  262. logItemsScrollRectTR = (RectTransform) logItemsScrollRect.transform;
  263. logItemsScrollRectOriginalSize = logItemsScrollRectTR.sizeDelta;
  264. // Associate sprites with log types
  265. logSpriteRepresentations = new Dictionary<LogType, Sprite>()
  266. {
  267. { LogType.Log, infoLog },
  268. { LogType.Warning, warningLog },
  269. { LogType.Error, errorLog },
  270. { LogType.Exception, errorLog },
  271. { LogType.Assert, errorLog }
  272. };
  273. // Initially, all log types are visible
  274. filterInfoButton.color = filterButtonsSelectedColor;
  275. filterWarningButton.color = filterButtonsSelectedColor;
  276. filterErrorButton.color = filterButtonsSelectedColor;
  277. collapsedLogEntries = new List<DebugLogEntry>( 128 );
  278. collapsedLogEntriesMap = new Dictionary<DebugLogEntry, int>( 128 );
  279. uncollapsedLogEntriesIndices = new DebugLogIndexList();
  280. indicesOfListEntriesToShow = new DebugLogIndexList();
  281. recycledListView.Initialize( this, collapsedLogEntries, indicesOfListEntriesToShow, logItemPrefab.Transform.sizeDelta.y );
  282. recycledListView.UpdateItemsInTheList( true );
  283. if( minimumHeight < 200f )
  284. minimumHeight = 200f;
  285. if( !enableSearchbar )
  286. {
  287. searchbar = null;
  288. searchbarSlotTop.gameObject.SetActive( false );
  289. searchbarSlotBottom.gameObject.SetActive( false );
  290. }
  291. if( commandSuggestionsContainer.gameObject.activeSelf )
  292. commandSuggestionsContainer.gameObject.SetActive( false );
  293. // Register to UI events
  294. commandInputField.onValidateInput += OnValidateCommand;
  295. commandInputField.onValueChanged.AddListener( RefreshCommandSuggestions );
  296. commandInputField.onEndEdit.AddListener( OnEndEditCommand );
  297. searchbar.GetComponent<InputField>().onValueChanged.AddListener( SearchTermChanged );
  298. hideButton.onClick.AddListener( HideLogWindow );
  299. clearButton.onClick.AddListener( ClearLogs );
  300. collapseButton.GetComponent<Button>().onClick.AddListener( CollapseButtonPressed );
  301. filterInfoButton.GetComponent<Button>().onClick.AddListener( FilterLogButtonPressed );
  302. filterWarningButton.GetComponent<Button>().onClick.AddListener( FilterWarningButtonPressed );
  303. filterErrorButton.GetComponent<Button>().onClick.AddListener( FilterErrorButtonPressed );
  304. snapToBottomButton.GetComponent<Button>().onClick.AddListener( () => SetSnapToBottom( true ) );
  305. nullPointerEventData = new PointerEventData( null );
  306. }
  307. private void OnEnable()
  308. {
  309. if( Instance != this )
  310. return;
  311. // Intercept debug entries
  312. Application.logMessageReceivedThreaded -= ReceivedLog;
  313. Application.logMessageReceivedThreaded += ReceivedLog;
  314. if( receiveLogcatLogsInAndroid )
  315. {
  316. #if !UNITY_EDITOR && UNITY_ANDROID
  317. if( logcatListener == null )
  318. logcatListener = new DebugLogLogcatListener();
  319. logcatListener.Start( logcatArguments );
  320. #endif
  321. }
  322. DebugLogConsole.AddCommand( "save_logs", "Saves logs to a file", SaveLogsToFile );
  323. //Debug.LogAssertion( "assert" );
  324. //Debug.LogError( "error" );
  325. //Debug.LogException( new System.IO.EndOfStreamException() );
  326. //Debug.LogWarning( "warning" );
  327. //Debug.Log( "log" );
  328. }
  329. private void OnDisable()
  330. {
  331. if( Instance != this )
  332. return;
  333. // Stop receiving debug entries
  334. Application.logMessageReceivedThreaded -= ReceivedLog;
  335. #if !UNITY_EDITOR && UNITY_ANDROID
  336. if( logcatListener != null )
  337. logcatListener.Stop();
  338. #endif
  339. DebugLogConsole.RemoveCommand( "save_logs" );
  340. }
  341. private void Start()
  342. {
  343. if( ( enablePopup && startInPopupMode ) || ( !enablePopup && startMinimized ) )
  344. HideLogWindow();
  345. else
  346. ShowLogWindow();
  347. PopupEnabled = enablePopup;
  348. }
  349. #if UNITY_EDITOR
  350. private void OnApplicationQuit()
  351. {
  352. isQuittingApplication = true;
  353. }
  354. #endif
  355. // Window is resized, update the list
  356. private void OnRectTransformDimensionsChange()
  357. {
  358. screenDimensionsChanged = true;
  359. }
  360. private void Update()
  361. {
  362. // Toggling the console with toggleKey is handled in Update instead of LateUpdate because
  363. // when we hide the console, we don't want the commandInputField to capture the toggleKey.
  364. // InputField captures input in LateUpdate so deactivating it in Update ensures that
  365. // no further input is captured
  366. if( toggleWithKey )
  367. {
  368. if( Input.GetKeyDown( toggleKey ) )
  369. {
  370. if( isLogWindowVisible )
  371. HideLogWindow();
  372. else
  373. ShowLogWindow();
  374. }
  375. }
  376. this._frameNumber += 1;
  377. float time = Time.realtimeSinceStartup - this._lastShowFPSTime;
  378. if (!(time >= 1)) return;
  379. this._fps = (this._frameNumber / time);
  380. this._frameNumber = 0;
  381. this._lastShowFPSTime = Time.realtimeSinceStartup;
  382. this.popupManager.NewFPSCountArrived(this._fps);
  383. }
  384. private void LateUpdate()
  385. {
  386. #if UNITY_EDITOR
  387. if( isQuittingApplication )
  388. return;
  389. #endif
  390. int queuedLogCount = queuedLogEntries.Count;
  391. if( queuedLogCount > 0 )
  392. {
  393. for( int i = 0; i < queuedLogCount; i++ )
  394. {
  395. QueuedDebugLogEntry logEntry;
  396. lock( logEntriesLock )
  397. {
  398. logEntry = queuedLogEntries.RemoveFirst();
  399. }
  400. ProcessLog( logEntry );
  401. }
  402. }
  403. if( showCommandSuggestions && commandInputField.isFocused && commandInputField.caretPosition != commandInputFieldPrevCaretPos )
  404. RefreshCommandSuggestions( commandInputField.text );
  405. if( screenDimensionsChanged )
  406. {
  407. // Update the recycled list view
  408. if( isLogWindowVisible )
  409. recycledListView.OnViewportDimensionsChanged();
  410. else
  411. popupManager.OnViewportDimensionsChanged();
  412. #if UNITY_ANDROID || UNITY_IOS
  413. CheckScreenCutout();
  414. #endif
  415. if( searchbar )
  416. {
  417. float logWindowWidth = logWindowTR.rect.width;
  418. if( logWindowWidth >= topSearchbarMinWidth )
  419. {
  420. if( searchbar.parent == searchbarSlotBottom )
  421. {
  422. searchbarSlotTop.gameObject.SetActive( true );
  423. searchbar.SetParent( searchbarSlotTop, false );
  424. searchbarSlotBottom.gameObject.SetActive( false );
  425. logItemsScrollRectTR.anchoredPosition = Vector2.zero;
  426. logItemsScrollRectTR.sizeDelta = logItemsScrollRectOriginalSize;
  427. }
  428. }
  429. else
  430. {
  431. if( searchbar.parent == searchbarSlotTop )
  432. {
  433. searchbarSlotBottom.gameObject.SetActive( true );
  434. searchbar.SetParent( searchbarSlotBottom, false );
  435. searchbarSlotTop.gameObject.SetActive( false );
  436. float searchbarHeight = searchbarSlotBottom.sizeDelta.y;
  437. logItemsScrollRectTR.anchoredPosition = new Vector2( 0f, searchbarHeight * -0.5f );
  438. logItemsScrollRectTR.sizeDelta = logItemsScrollRectOriginalSize - new Vector2( 0f, searchbarHeight );
  439. }
  440. }
  441. }
  442. screenDimensionsChanged = false;
  443. }
  444. // If snapToBottom is enabled, force the scrollbar to the bottom
  445. if( snapToBottom )
  446. {
  447. logItemsScrollRect.verticalNormalizedPosition = 0f;
  448. if( snapToBottomButton.activeSelf )
  449. snapToBottomButton.SetActive( false );
  450. }
  451. else
  452. {
  453. float scrollPos = logItemsScrollRect.verticalNormalizedPosition;
  454. if( snapToBottomButton.activeSelf != ( scrollPos > 1E-6f && scrollPos < 0.9999f ) )
  455. snapToBottomButton.SetActive( !snapToBottomButton.activeSelf );
  456. }
  457. if( isLogWindowVisible && commandInputField.isFocused )
  458. {
  459. if( Input.GetKeyDown( KeyCode.UpArrow ) )
  460. {
  461. if( commandHistoryIndex == -1 )
  462. commandHistoryIndex = commandHistory.Count - 1;
  463. else if( --commandHistoryIndex < 0 )
  464. commandHistoryIndex = 0;
  465. if( commandHistoryIndex >= 0 && commandHistoryIndex < commandHistory.Count )
  466. {
  467. commandInputField.text = commandHistory[commandHistoryIndex];
  468. commandInputField.caretPosition = commandInputField.text.Length;
  469. }
  470. }
  471. else if( Input.GetKeyDown( KeyCode.DownArrow ) )
  472. {
  473. if( commandHistoryIndex == -1 )
  474. commandHistoryIndex = commandHistory.Count - 1;
  475. else if( ++commandHistoryIndex >= commandHistory.Count )
  476. commandHistoryIndex = commandHistory.Count - 1;
  477. if( commandHistoryIndex >= 0 && commandHistoryIndex < commandHistory.Count )
  478. commandInputField.text = commandHistory[commandHistoryIndex];
  479. }
  480. }
  481. #if !UNITY_EDITOR && UNITY_ANDROID
  482. if( logcatListener != null )
  483. {
  484. string log;
  485. while( ( log = logcatListener.GetLog() ) != null )
  486. ReceivedLog( "LOGCAT: " + log, string.Empty, LogType.Log );
  487. }
  488. #endif
  489. }
  490. public void ShowLogWindow()
  491. {
  492. // Show the log window
  493. logWindowCanvasGroup.interactable = true;
  494. logWindowCanvasGroup.blocksRaycasts = true;
  495. logWindowCanvasGroup.alpha = 1f;
  496. popupManager.Hide();
  497. // Update the recycled list view
  498. // (in case new entries were intercepted while log window was hidden)
  499. recycledListView.OnLogEntriesUpdated( true );
  500. #if UNITY_EDITOR || UNITY_STANDALONE
  501. // Focus on the command input field on standalone platforms when the console is opened
  502. if( autoFocusOnCommandInputField )
  503. StartCoroutine( ActivateCommandInputFieldCoroutine() );
  504. #endif
  505. isLogWindowVisible = true;
  506. }
  507. public void HideLogWindow()
  508. {
  509. // Hide the log window
  510. logWindowCanvasGroup.interactable = false;
  511. logWindowCanvasGroup.blocksRaycasts = false;
  512. logWindowCanvasGroup.alpha = 0f;
  513. if( commandInputField.isFocused )
  514. commandInputField.DeactivateInputField();
  515. popupManager.Show();
  516. commandHistoryIndex = -1;
  517. isLogWindowVisible = false;
  518. }
  519. // Command field input is changed, check if command is submitted
  520. private char OnValidateCommand( string text, int charIndex, char addedChar )
  521. {
  522. if( addedChar == '\t' ) // Autocomplete attempt
  523. {
  524. if( !string.IsNullOrEmpty( text ) )
  525. {
  526. string autoCompletedCommand = DebugLogConsole.GetAutoCompleteCommand( text );
  527. if( !string.IsNullOrEmpty( autoCompletedCommand ) )
  528. commandInputField.text = autoCompletedCommand;
  529. }
  530. return '\0';
  531. }
  532. else if( addedChar == '\n' ) // Command is submitted
  533. {
  534. // Clear the command field
  535. if( clearCommandAfterExecution )
  536. commandInputField.text = "";
  537. if( text.Length > 0 )
  538. {
  539. if( commandHistory.Count == 0 || commandHistory[commandHistory.Count - 1] != text )
  540. commandHistory.Add( text );
  541. commandHistoryIndex = -1;
  542. // Execute the command
  543. DebugLogConsole.ExecuteCommand( text );
  544. // Snap to bottom and select the latest entry
  545. SetSnapToBottom( true );
  546. }
  547. return '\0';
  548. }
  549. return addedChar;
  550. }
  551. // A debug entry is received
  552. private void ReceivedLog( string logString, string stackTrace, LogType logType )
  553. {
  554. #if UNITY_EDITOR
  555. if( isQuittingApplication )
  556. return;
  557. #endif
  558. // Truncate the log if it is longer than maxLogLength
  559. int logLength = logString.Length;
  560. if( stackTrace == null )
  561. {
  562. if( logLength > maxLogLength )
  563. logString = logString.Substring( 0, maxLogLength - 11 ) + "<truncated>";
  564. }
  565. else
  566. {
  567. logLength += stackTrace.Length;
  568. if( logLength > maxLogLength )
  569. {
  570. // Decide which log component(s) to truncate
  571. int halfMaxLogLength = maxLogLength / 2;
  572. if( logString.Length >= halfMaxLogLength )
  573. {
  574. if( stackTrace.Length >= halfMaxLogLength )
  575. {
  576. // Truncate both logString and stackTrace
  577. logString = logString.Substring( 0, halfMaxLogLength - 11 ) + "<truncated>";
  578. // If stackTrace doesn't end with a blank line, its last line won't be visible in the console for some reason
  579. stackTrace = stackTrace.Substring( 0, halfMaxLogLength - 12 ) + "<truncated>\n";
  580. }
  581. else
  582. {
  583. // Truncate logString
  584. logString = logString.Substring( 0, maxLogLength - stackTrace.Length - 11 ) + "<truncated>";
  585. }
  586. }
  587. else
  588. {
  589. // Truncate stackTrace
  590. stackTrace = stackTrace.Substring( 0, maxLogLength - logString.Length - 12 ) + "<truncated>\n";
  591. }
  592. }
  593. }
  594. QueuedDebugLogEntry queuedLogEntry = new QueuedDebugLogEntry( logString, stackTrace, logType );
  595. lock( logEntriesLock )
  596. {
  597. queuedLogEntries.Add( queuedLogEntry );
  598. }
  599. }
  600. // Present the log entry in the console
  601. private void ProcessLog( QueuedDebugLogEntry queuedLogEntry )
  602. {
  603. LogType logType = queuedLogEntry.logType;
  604. DebugLogEntry logEntry;
  605. if( pooledLogEntries.Count > 0 )
  606. {
  607. logEntry = pooledLogEntries[pooledLogEntries.Count - 1];
  608. pooledLogEntries.RemoveAt( pooledLogEntries.Count - 1 );
  609. }
  610. else
  611. logEntry = new DebugLogEntry();
  612. logEntry.Initialize( queuedLogEntry.logString, queuedLogEntry.stackTrace );
  613. // Check if this entry is a duplicate (i.e. has been received before)
  614. int logEntryIndex;
  615. bool isEntryInCollapsedEntryList = collapsedLogEntriesMap.TryGetValue( logEntry, out logEntryIndex );
  616. if( !isEntryInCollapsedEntryList )
  617. {
  618. // It is not a duplicate,
  619. // add it to the list of unique debug entries
  620. logEntry.logTypeSpriteRepresentation = logSpriteRepresentations[logType];
  621. logEntryIndex = collapsedLogEntries.Count;
  622. collapsedLogEntries.Add( logEntry );
  623. collapsedLogEntriesMap[logEntry] = logEntryIndex;
  624. }
  625. else
  626. {
  627. // It is a duplicate, pool the duplicate log entry and
  628. // increment the original debug item's collapsed count
  629. pooledLogEntries.Add( logEntry );
  630. logEntry = collapsedLogEntries[logEntryIndex];
  631. logEntry.count++;
  632. }
  633. // Add the index of the unique debug entry to the list
  634. // that stores the order the debug entries are received
  635. uncollapsedLogEntriesIndices.Add( logEntryIndex );
  636. // If this debug entry matches the current filters,
  637. // add it to the list of debug entries to show
  638. int logEntryIndexInEntriesToShow = -1;
  639. Sprite logTypeSpriteRepresentation = logEntry.logTypeSpriteRepresentation;
  640. if( isCollapseOn && isEntryInCollapsedEntryList )
  641. {
  642. if( isLogWindowVisible )
  643. {
  644. if( !isInSearchMode && logFilter == DebugLogFilter.All )
  645. logEntryIndexInEntriesToShow = logEntryIndex;
  646. else
  647. logEntryIndexInEntriesToShow = indicesOfListEntriesToShow.IndexOf( logEntryIndex );
  648. recycledListView.OnCollapsedLogEntryAtIndexUpdated( logEntryIndexInEntriesToShow );
  649. }
  650. }
  651. else if( ( !isInSearchMode || queuedLogEntry.MatchesSearchTerm( searchTerm ) ) && ( logFilter == DebugLogFilter.All ||
  652. ( logTypeSpriteRepresentation == infoLog && ( ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info ) ) ||
  653. ( logTypeSpriteRepresentation == warningLog && ( ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning ) ) ||
  654. ( logTypeSpriteRepresentation == errorLog && ( ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error ) ) ) )
  655. {
  656. indicesOfListEntriesToShow.Add( logEntryIndex );
  657. logEntryIndexInEntriesToShow = indicesOfListEntriesToShow.Count - 1;
  658. if( isLogWindowVisible )
  659. recycledListView.OnLogEntriesUpdated( false );
  660. }
  661. if( logType == LogType.Log )
  662. {
  663. infoEntryCount++;
  664. infoEntryCountText.text = infoEntryCount.ToString();
  665. // If debug popup is visible, notify it of the new debug entry
  666. if( !isLogWindowVisible )
  667. popupManager.NewInfoLogArrived();
  668. }
  669. else if( logType == LogType.Warning )
  670. {
  671. warningEntryCount++;
  672. warningEntryCountText.text = warningEntryCount.ToString();
  673. // If debug popup is visible, notify it of the new debug entry
  674. if( !isLogWindowVisible )
  675. popupManager.NewWarningLogArrived();
  676. }
  677. else
  678. {
  679. errorEntryCount++;
  680. errorEntryCountText.text = errorEntryCount.ToString();
  681. // If debug popup is visible, notify it of the new debug entry
  682. if( !isLogWindowVisible )
  683. popupManager.NewErrorLogArrived();
  684. }
  685. // Automatically expand this log if necessary
  686. if( pendingLogToAutoExpand > 0 && --pendingLogToAutoExpand <= 0 && isLogWindowVisible && logEntryIndexInEntriesToShow >= 0 )
  687. recycledListView.SelectAndFocusOnLogItemAtIndex( logEntryIndexInEntriesToShow );
  688. }
  689. // Value of snapToBottom is changed (user scrolled the list manually)
  690. public void SetSnapToBottom( bool snapToBottom )
  691. {
  692. this.snapToBottom = snapToBottom;
  693. }
  694. // Make sure the scroll bar of the scroll rect is adjusted properly
  695. internal void ValidateScrollPosition()
  696. {
  697. logItemsScrollRect.OnScroll( nullPointerEventData );
  698. }
  699. // Automatically expand the latest log in queuedLogEntries
  700. internal void ExpandLatestPendingLog()
  701. {
  702. pendingLogToAutoExpand = queuedLogEntries.Count;
  703. }
  704. // Clear all the logs
  705. public void ClearLogs()
  706. {
  707. snapToBottom = true;
  708. infoEntryCount = 0;
  709. warningEntryCount = 0;
  710. errorEntryCount = 0;
  711. infoEntryCountText.text = "0";
  712. warningEntryCountText.text = "0";
  713. errorEntryCountText.text = "0";
  714. collapsedLogEntries.Clear();
  715. collapsedLogEntriesMap.Clear();
  716. uncollapsedLogEntriesIndices.Clear();
  717. indicesOfListEntriesToShow.Clear();
  718. recycledListView.DeselectSelectedLogItem();
  719. recycledListView.OnLogEntriesUpdated( true );
  720. }
  721. // Collapse button is clicked
  722. private void CollapseButtonPressed()
  723. {
  724. // Swap the value of collapse mode
  725. isCollapseOn = !isCollapseOn;
  726. snapToBottom = true;
  727. collapseButton.color = isCollapseOn ? collapseButtonSelectedColor : collapseButtonNormalColor;
  728. recycledListView.SetCollapseMode( isCollapseOn );
  729. // Determine the new list of debug entries to show
  730. FilterLogs();
  731. }
  732. // Filtering mode of info logs has changed
  733. private void FilterLogButtonPressed()
  734. {
  735. logFilter = logFilter ^ DebugLogFilter.Info;
  736. if( ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info )
  737. filterInfoButton.color = filterButtonsSelectedColor;
  738. else
  739. filterInfoButton.color = filterButtonsNormalColor;
  740. FilterLogs();
  741. }
  742. // Filtering mode of warning logs has changed
  743. private void FilterWarningButtonPressed()
  744. {
  745. logFilter = logFilter ^ DebugLogFilter.Warning;
  746. if( ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning )
  747. filterWarningButton.color = filterButtonsSelectedColor;
  748. else
  749. filterWarningButton.color = filterButtonsNormalColor;
  750. FilterLogs();
  751. }
  752. // Filtering mode of error logs has changed
  753. private void FilterErrorButtonPressed()
  754. {
  755. logFilter = logFilter ^ DebugLogFilter.Error;
  756. if( ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error )
  757. filterErrorButton.color = filterButtonsSelectedColor;
  758. else
  759. filterErrorButton.color = filterButtonsNormalColor;
  760. FilterLogs();
  761. }
  762. // Search term has changed
  763. private void SearchTermChanged( string searchTerm )
  764. {
  765. if( searchTerm != null )
  766. searchTerm = searchTerm.Trim();
  767. this.searchTerm = searchTerm;
  768. bool isInSearchMode = !string.IsNullOrEmpty( searchTerm );
  769. if( isInSearchMode || this.isInSearchMode )
  770. {
  771. this.isInSearchMode = isInSearchMode;
  772. FilterLogs();
  773. }
  774. }
  775. // Show suggestions for the currently entered command
  776. private void RefreshCommandSuggestions( string command )
  777. {
  778. if( !showCommandSuggestions )
  779. return;
  780. commandInputFieldPrevCaretPos = commandInputField.caretPosition;
  781. // Don't recalculate the command suggestions if the input command hasn't changed (i.e. only caret's position has changed)
  782. bool commandChanged = command != commandInputFieldPrevCommand;
  783. bool commandNameOrParametersChanged = false;
  784. if( commandChanged )
  785. {
  786. commandInputFieldPrevCommand = command;
  787. matchingCommandSuggestions.Clear();
  788. commandCaretIndexIncrements.Clear();
  789. string prevCommandName = commandInputFieldPrevCommandName;
  790. int numberOfParameters;
  791. DebugLogConsole.GetCommandSuggestions( command, matchingCommandSuggestions, commandCaretIndexIncrements, ref commandInputFieldPrevCommandName, out numberOfParameters );
  792. if( prevCommandName != commandInputFieldPrevCommandName || numberOfParameters != commandInputFieldPrevParamCount )
  793. {
  794. commandInputFieldPrevParamCount = numberOfParameters;
  795. commandNameOrParametersChanged = true;
  796. }
  797. }
  798. int caretArgumentIndex = 0;
  799. int caretPos = commandInputField.caretPosition;
  800. for( int i = 0; i < commandCaretIndexIncrements.Count && caretPos > commandCaretIndexIncrements[i]; i++ )
  801. caretArgumentIndex++;
  802. if( caretArgumentIndex != commandInputFieldPrevCaretArgumentIndex )
  803. commandInputFieldPrevCaretArgumentIndex = caretArgumentIndex;
  804. else if( !commandChanged || !commandNameOrParametersChanged )
  805. {
  806. // Command suggestions don't need to be updated if:
  807. // a) neither the entered command nor the argument that the caret is hovering has changed
  808. // b) entered command has changed but command's name hasn't changed, parameter count hasn't changed and the argument
  809. // that the caret is hovering hasn't changed (i.e. user has continued typing a parameter's value)
  810. return;
  811. }
  812. if( matchingCommandSuggestions.Count == 0 )
  813. OnEndEditCommand( command );
  814. else
  815. {
  816. if( !commandSuggestionsContainer.gameObject.activeSelf )
  817. commandSuggestionsContainer.gameObject.SetActive( true );
  818. int suggestionInstancesCount = commandSuggestionInstances.Count;
  819. int suggestionsCount = matchingCommandSuggestions.Count;
  820. for( int i = 0; i < suggestionsCount; i++ )
  821. {
  822. if( i >= visibleCommandSuggestionInstances )
  823. {
  824. if( i >= suggestionInstancesCount )
  825. commandSuggestionInstances.Add( (Text) Instantiate( commandSuggestionPrefab, commandSuggestionsContainer, false ) );
  826. else
  827. commandSuggestionInstances[i].gameObject.SetActive( true );
  828. visibleCommandSuggestionInstances++;
  829. }
  830. ConsoleMethodInfo suggestedCommand = matchingCommandSuggestions[i];
  831. commandSuggestionsStringBuilder.Length = 0;
  832. if( caretArgumentIndex > 0 )
  833. commandSuggestionsStringBuilder.Append( suggestedCommand.command );
  834. else
  835. commandSuggestionsStringBuilder.Append( commandSuggestionHighlightStart ).Append( matchingCommandSuggestions[i].command ).Append( commandSuggestionHighlightEnd );
  836. if( suggestedCommand.parameters.Length > 0 )
  837. {
  838. commandSuggestionsStringBuilder.Append( " " );
  839. // If the command name wasn't highlighted, a parameter must always be highlighted
  840. int caretParameterIndex = caretArgumentIndex - 1;
  841. if( caretParameterIndex >= suggestedCommand.parameters.Length )
  842. caretParameterIndex = suggestedCommand.parameters.Length - 1;
  843. for( int j = 0; j < suggestedCommand.parameters.Length; j++ )
  844. {
  845. if( caretParameterIndex != j )
  846. commandSuggestionsStringBuilder.Append( suggestedCommand.parameters[j] );
  847. else
  848. commandSuggestionsStringBuilder.Append( commandSuggestionHighlightStart ).Append( suggestedCommand.parameters[j] ).Append( commandSuggestionHighlightEnd );
  849. }
  850. }
  851. commandSuggestionInstances[i].text = commandSuggestionsStringBuilder.ToString();
  852. }
  853. for( int i = visibleCommandSuggestionInstances - 1; i >= suggestionsCount; i-- )
  854. commandSuggestionInstances[i].gameObject.SetActive( false );
  855. visibleCommandSuggestionInstances = suggestionsCount;
  856. }
  857. }
  858. // Command input field has lost focus
  859. private void OnEndEditCommand( string command )
  860. {
  861. if( commandSuggestionsContainer.gameObject.activeSelf )
  862. commandSuggestionsContainer.gameObject.SetActive( false );
  863. }
  864. // Debug window is being resized,
  865. // Set the sizeDelta property of the window accordingly while
  866. // preventing window dimensions from going below the minimum dimensions
  867. internal void Resize( PointerEventData eventData )
  868. {
  869. // Grab the resize button from top; 36f is the height of the resize button
  870. float newHeight = ( eventData.position.y - logWindowTR.position.y ) / -canvasTR.localScale.y + 36f;
  871. if( newHeight < minimumHeight )
  872. newHeight = minimumHeight;
  873. Vector2 anchorMin = logWindowTR.anchorMin;
  874. anchorMin.y = Mathf.Max( 0f, 1f - newHeight / canvasTR.sizeDelta.y );
  875. logWindowTR.anchorMin = anchorMin;
  876. // Update the recycled list view
  877. recycledListView.OnViewportDimensionsChanged();
  878. }
  879. // Determine the filtered list of debug entries to show on screen
  880. private void FilterLogs()
  881. {
  882. indicesOfListEntriesToShow.Clear();
  883. if( logFilter != DebugLogFilter.None )
  884. {
  885. if( logFilter == DebugLogFilter.All )
  886. {
  887. if( isCollapseOn )
  888. {
  889. if( !isInSearchMode )
  890. {
  891. // All the unique debug entries will be listed just once.
  892. // So, list of debug entries to show is the same as the
  893. // order these unique debug entries are added to collapsedLogEntries
  894. for( int i = 0, count = collapsedLogEntries.Count; i < count; i++ )
  895. indicesOfListEntriesToShow.Add( i );
  896. }
  897. else
  898. {
  899. for( int i = 0, count = collapsedLogEntries.Count; i < count; i++ )
  900. {
  901. if( collapsedLogEntries[i].MatchesSearchTerm( searchTerm ) )
  902. indicesOfListEntriesToShow.Add( i );
  903. }
  904. }
  905. }
  906. else
  907. {
  908. if( !isInSearchMode )
  909. {
  910. for( int i = 0, count = uncollapsedLogEntriesIndices.Count; i < count; i++ )
  911. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  912. }
  913. else
  914. {
  915. for( int i = 0, count = uncollapsedLogEntriesIndices.Count; i < count; i++ )
  916. {
  917. if( collapsedLogEntries[uncollapsedLogEntriesIndices[i]].MatchesSearchTerm( searchTerm ) )
  918. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  919. }
  920. }
  921. }
  922. }
  923. else
  924. {
  925. // Show only the debug entries that match the current filter
  926. bool isInfoEnabled = ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info;
  927. bool isWarningEnabled = ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning;
  928. bool isErrorEnabled = ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error;
  929. if( isCollapseOn )
  930. {
  931. for( int i = 0, count = collapsedLogEntries.Count; i < count; i++ )
  932. {
  933. DebugLogEntry logEntry = collapsedLogEntries[i];
  934. if( isInSearchMode && !logEntry.MatchesSearchTerm( searchTerm ) )
  935. continue;
  936. if( logEntry.logTypeSpriteRepresentation == infoLog )
  937. {
  938. if( isInfoEnabled )
  939. indicesOfListEntriesToShow.Add( i );
  940. }
  941. else if( logEntry.logTypeSpriteRepresentation == warningLog )
  942. {
  943. if( isWarningEnabled )
  944. indicesOfListEntriesToShow.Add( i );
  945. }
  946. else if( isErrorEnabled )
  947. indicesOfListEntriesToShow.Add( i );
  948. }
  949. }
  950. else
  951. {
  952. for( int i = 0, count = uncollapsedLogEntriesIndices.Count; i < count; i++ )
  953. {
  954. DebugLogEntry logEntry = collapsedLogEntries[uncollapsedLogEntriesIndices[i]];
  955. if( isInSearchMode && !logEntry.MatchesSearchTerm( searchTerm ) )
  956. continue;
  957. if( logEntry.logTypeSpriteRepresentation == infoLog )
  958. {
  959. if( isInfoEnabled )
  960. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  961. }
  962. else if( logEntry.logTypeSpriteRepresentation == warningLog )
  963. {
  964. if( isWarningEnabled )
  965. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  966. }
  967. else if( isErrorEnabled )
  968. indicesOfListEntriesToShow.Add( uncollapsedLogEntriesIndices[i] );
  969. }
  970. }
  971. }
  972. }
  973. // Update the recycled list view
  974. recycledListView.DeselectSelectedLogItem();
  975. recycledListView.OnLogEntriesUpdated( true );
  976. ValidateScrollPosition();
  977. }
  978. public string GetAllLogs()
  979. {
  980. int count = uncollapsedLogEntriesIndices.Count;
  981. int length = 0;
  982. int newLineLength = System.Environment.NewLine.Length;
  983. for( int i = 0; i < count; i++ )
  984. {
  985. DebugLogEntry entry = collapsedLogEntries[uncollapsedLogEntriesIndices[i]];
  986. length += entry.logString.Length + entry.stackTrace.Length + newLineLength * 3;
  987. }
  988. length += 100; // Just in case...
  989. StringBuilder sb = new StringBuilder( length );
  990. for( int i = 0; i < count; i++ )
  991. {
  992. DebugLogEntry entry = collapsedLogEntries[uncollapsedLogEntriesIndices[i]];
  993. sb.AppendLine( entry.logString ).AppendLine( entry.stackTrace ).AppendLine();
  994. }
  995. return sb.ToString();
  996. }
  997. private void SaveLogsToFile()
  998. {
  999. string path = Path.Combine( Application.persistentDataPath, System.DateTime.Now.ToString( "dd-MM-yyyy--HH-mm-ss" ) + ".txt" );
  1000. File.WriteAllText( path, GetAllLogs() );
  1001. Debug.Log( "Logs saved to: " + path );
  1002. }
  1003. // If a cutout is intersecting with debug window on notch screens, shift the window downwards
  1004. private void CheckScreenCutout()
  1005. {
  1006. if( !avoidScreenCutout )
  1007. return;
  1008. #if UNITY_2017_2_OR_NEWER && !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
  1009. // Check if there is a cutout at the top of the screen
  1010. int screenHeight = Screen.height;
  1011. float safeYMax = Screen.safeArea.yMax;
  1012. if( safeYMax < screenHeight - 1 ) // 1: a small threshold
  1013. {
  1014. // There is a cutout, shift the log window downwards
  1015. float cutoutPercentage = ( screenHeight - safeYMax ) / Screen.height;
  1016. float cutoutLocalSize = cutoutPercentage * canvasTR.rect.height;
  1017. logWindowTR.anchoredPosition = new Vector2( 0f, -cutoutLocalSize );
  1018. logWindowTR.sizeDelta = new Vector2( 0f, -cutoutLocalSize );
  1019. }
  1020. else
  1021. {
  1022. logWindowTR.anchoredPosition = Vector2.zero;
  1023. logWindowTR.sizeDelta = Vector2.zero;
  1024. }
  1025. #endif
  1026. }
  1027. #if UNITY_EDITOR || UNITY_STANDALONE
  1028. private IEnumerator ActivateCommandInputFieldCoroutine()
  1029. {
  1030. // Waiting 1 frame before activating commandInputField ensures that the toggleKey isn't captured by it
  1031. yield return null;
  1032. commandInputField.ActivateInputField();
  1033. yield return null;
  1034. commandInputField.MoveTextEnd( false );
  1035. }
  1036. #endif
  1037. // Pool an unused log item
  1038. internal void PoolLogItem( DebugLogItem logItem )
  1039. {
  1040. logItem.gameObject.SetActive( false );
  1041. pooledLogItems.Add( logItem );
  1042. }
  1043. // Fetch a log item from the pool
  1044. internal DebugLogItem PopLogItem()
  1045. {
  1046. DebugLogItem newLogItem;
  1047. // If pool is not empty, fetch a log item from the pool,
  1048. // create a new log item otherwise
  1049. if( pooledLogItems.Count > 0 )
  1050. {
  1051. newLogItem = pooledLogItems[pooledLogItems.Count - 1];
  1052. pooledLogItems.RemoveAt( pooledLogItems.Count - 1 );
  1053. newLogItem.gameObject.SetActive( true );
  1054. }
  1055. else
  1056. {
  1057. newLogItem = (DebugLogItem) Instantiate( logItemPrefab, logItemsContainer, false );
  1058. newLogItem.Initialize( recycledListView );
  1059. }
  1060. return newLogItem;
  1061. }
  1062. }
  1063. }