DebugLogManager.cs 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877
  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. using TMPro;
  9. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  10. using UnityEngine.InputSystem;
  11. #endif
  12. #if UNITY_EDITOR && UNITY_2021_1_OR_NEWER
  13. using Screen = UnityEngine.Device.Screen; // To support Device Simulator on Unity 2021.1+
  14. #endif
  15. // Receives debug entries and custom events (e.g. Clear, Collapse, Filter by Type)
  16. // and notifies the recycled list view of changes to the list of debug entries
  17. //
  18. // - Vocabulary -
  19. // Debug/Log entry: a Debug.Log/LogError/LogWarning/LogException/LogAssertion request made by
  20. // the client and intercepted by this manager object
  21. // Debug/Log item: a visual (uGUI) representation of a debug entry
  22. //
  23. // There can be a lot of debug entries in the system but there will only be a handful of log items
  24. // to show their properties on screen (these log items are recycled as the list is scrolled)
  25. // An enum to represent filtered log types
  26. namespace IngameDebugConsole
  27. {
  28. public enum DebugLogFilter
  29. {
  30. None = 0,
  31. Info = 1,
  32. Warning = 2,
  33. Error = 4,
  34. All = ~0
  35. }
  36. public enum PopupVisibility
  37. {
  38. Always = 0,
  39. WhenLogReceived = 1,
  40. Never = 2
  41. }
  42. public class DebugLogManager : MonoBehaviour
  43. {
  44. public static DebugLogManager Instance { get; private set; }
  45. #pragma warning disable 0649
  46. [Header( "Properties" )]
  47. [SerializeField]
  48. [HideInInspector]
  49. [Tooltip( "If enabled, console window will persist between scenes (i.e. not be destroyed when scene changes)" )]
  50. private bool singleton = true;
  51. [SerializeField]
  52. [HideInInspector]
  53. [Tooltip( "Minimum height of the console window" )]
  54. private float minimumHeight = 200f;
  55. [SerializeField]
  56. [HideInInspector]
  57. [Tooltip( "If enabled, console window can be resized horizontally, as well" )]
  58. private bool enableHorizontalResizing = false;
  59. [SerializeField]
  60. [HideInInspector]
  61. [Tooltip( "If enabled, console window's resize button will be located at bottom-right corner. Otherwise, it will be located at bottom-left corner" )]
  62. private bool resizeFromRight = true;
  63. [SerializeField]
  64. [HideInInspector]
  65. [Tooltip( "Minimum width of the console window" )]
  66. private float minimumWidth = 240f;
  67. [SerializeField]
  68. [HideInInspector]
  69. [Tooltip( "Opacity of the console window" )]
  70. [Range( 0f, 1f )]
  71. private float logWindowOpacity = 1f;
  72. [SerializeField]
  73. [HideInInspector]
  74. [Tooltip( "Opacity of the popup" )]
  75. [Range( 0f, 1f )]
  76. internal float popupOpacity = 1f;
  77. [SerializeField]
  78. [HideInInspector]
  79. [Tooltip( "Determines when the popup will show up (after the console window is closed)" )]
  80. private PopupVisibility popupVisibility = PopupVisibility.Always;
  81. [SerializeField]
  82. [HideInInspector]
  83. [Tooltip( "Determines which log types will show the popup on screen" )]
  84. private DebugLogFilter popupVisibilityLogFilter = DebugLogFilter.All;
  85. [SerializeField]
  86. [HideInInspector]
  87. [Tooltip( "If enabled, console window will initially be invisible" )]
  88. private bool startMinimized = true;
  89. [SerializeField]
  90. [HideInInspector]
  91. [Tooltip( "If enabled, pressing the Toggle Key will show/hide (i.e. toggle) the console window at runtime" )]
  92. private bool toggleWithKey = false;
  93. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  94. [SerializeField]
  95. [HideInInspector]
  96. public InputAction toggleBinding = new InputAction( "Toggle Binding", type: InputActionType.Button, binding: "<Keyboard>/backquote", expectedControlType: "Button" );
  97. #else
  98. [SerializeField]
  99. [HideInInspector]
  100. private KeyCode toggleKey = KeyCode.BackQuote;
  101. #endif
  102. [SerializeField]
  103. [HideInInspector]
  104. [Tooltip( "If enabled, the console window will have a searchbar" )]
  105. private bool enableSearchbar = true;
  106. [SerializeField]
  107. [HideInInspector]
  108. [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" )]
  109. private float topSearchbarMinWidth = 360f;
  110. [SerializeField, HideInInspector]
  111. [Tooltip("If enabled, clicking the resize button of the console window will copy all logs to clipboard. It'll also play a scale animation to give feedback.")]
  112. internal bool copyAllLogsOnResizeButtonClick;
  113. [SerializeField]
  114. [HideInInspector]
  115. [Tooltip( "If enabled, the console window will continue receiving logs in the background even if its GameObject is inactive. But the console window's GameObject needs to be activated at least once because its Awake function must be triggered for this to work" )]
  116. private bool receiveLogsWhileInactive = false;
  117. [SerializeField]
  118. [HideInInspector]
  119. private bool receiveInfoLogs = true, receiveWarningLogs = true, receiveErrorLogs = true, receiveExceptionLogs = true;
  120. [SerializeField]
  121. [HideInInspector]
  122. [Tooltip( "If enabled, the arrival times of logs will be recorded and displayed when a log is expanded" )]
  123. private bool captureLogTimestamps = false;
  124. [SerializeField]
  125. [HideInInspector]
  126. [Tooltip( "If enabled, timestamps will be displayed for logs even if they aren't expanded" )]
  127. internal bool alwaysDisplayTimestamps = false;
  128. [SerializeField]
  129. [HideInInspector]
  130. [Tooltip( "If the number of logs reach this limit, the oldest log(s) will be deleted to limit the RAM usage. It's recommended to set this value as low as possible" )]
  131. private int maxLogCount = int.MaxValue;
  132. [SerializeField]
  133. [HideInInspector]
  134. [Tooltip( "How many log(s) to delete when the threshold is reached (all logs are iterated during this operation so it should neither be too low nor too high)" )]
  135. private int logsToRemoveAfterMaxLogCount = 16;
  136. [SerializeField]
  137. [HideInInspector]
  138. [Tooltip( "While the console window is hidden, incoming logs will be queued but not immediately processed until the console window is opened (to avoid wasting CPU resources). When the log queue exceeds this limit, the first logs in the queue will be processed to enforce this limit. Processed logs won't increase RAM usage if they've been seen before (i.e. collapsible logs) but this is not the case for queued logs, so if a log is spammed every frame, it will fill the whole queue in an instant. Which is why there is a queue limit" )]
  139. private int queuedLogLimit = 256;
  140. [SerializeField]
  141. [HideInInspector]
  142. [Tooltip( "If enabled, the command input field at the bottom of the console window will automatically be cleared after entering a command" )]
  143. private bool clearCommandAfterExecution = true;
  144. [SerializeField]
  145. [HideInInspector]
  146. [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)" )]
  147. private int commandHistorySize = 15;
  148. [SerializeField]
  149. [HideInInspector]
  150. [Tooltip( "If enabled, while typing a command, all of the matching commands' signatures will be displayed in a popup" )]
  151. private bool showCommandSuggestions = true;
  152. [SerializeField]
  153. [HideInInspector]
  154. [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)" )]
  155. private bool receiveLogcatLogsInAndroid = false;
  156. #pragma warning disable 0414
  157. #pragma warning disable 0169
  158. [SerializeField]
  159. [HideInInspector]
  160. [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" )]
  161. private string logcatArguments;
  162. #pragma warning restore 0169
  163. #pragma warning restore 0414
  164. [SerializeField]
  165. [HideInInspector]
  166. [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" )]
  167. private bool avoidScreenCutout = true;
  168. [SerializeField]
  169. [HideInInspector]
  170. [Tooltip( "If enabled, on Android and iOS devices with notch screens, the console window's popup won't be obscured by the screen cutouts" )]
  171. internal bool popupAvoidsScreenCutout = false;
  172. [SerializeField]
  173. [Tooltip("If a log that isn't expanded is longer than this limit, it will be truncated. This greatly optimizes scrolling speed of collapsed logs if their log messages are long.")]
  174. internal int maxCollapsedLogLength = 200;
  175. [SerializeField, UnityEngine.Serialization.FormerlySerializedAs("maxLogLength")]
  176. [Tooltip("If an expanded log is longer than this limit, it will be truncated. This optimizes scrolling speed while an expanded log is visible.")]
  177. internal int maxExpandedLogLength = 10000;
  178. #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL
  179. [SerializeField]
  180. [HideInInspector]
  181. [Tooltip( "If enabled, on standalone platforms, command input field will automatically be focused (start receiving keyboard input) after opening the console window" )]
  182. private bool autoFocusOnCommandInputField = true;
  183. #endif
  184. [Header( "Visuals" )]
  185. [SerializeField]
  186. private DebugLogItem logItemPrefab;
  187. [SerializeField]
  188. internal TMP_FontAsset logItemFontOverride;
  189. [SerializeField]
  190. private TextMeshProUGUI commandSuggestionPrefab;
  191. // Visuals for different log types
  192. [SerializeField]
  193. private Sprite infoLog;
  194. [SerializeField]
  195. private Sprite warningLog;
  196. [SerializeField]
  197. private Sprite errorLog;
  198. internal static Sprite[] logSpriteRepresentations;
  199. // Visuals for resize button
  200. [SerializeField]
  201. private Sprite resizeIconAllDirections;
  202. [SerializeField]
  203. private Sprite resizeIconVerticalOnly;
  204. [SerializeField]
  205. private Color collapseButtonNormalColor;
  206. [SerializeField]
  207. private Color collapseButtonSelectedColor;
  208. [SerializeField]
  209. private Color filterButtonsNormalColor;
  210. [SerializeField]
  211. private Color filterButtonsSelectedColor;
  212. [SerializeField]
  213. private string commandSuggestionHighlightStart = "<color=orange>";
  214. [SerializeField]
  215. private string commandSuggestionHighlightEnd = "</color>";
  216. [Header( "Internal References" )]
  217. [SerializeField]
  218. private RectTransform logWindowTR;
  219. internal RectTransform canvasTR;
  220. [SerializeField]
  221. private RectTransform logItemsContainer;
  222. [SerializeField]
  223. private RectTransform commandSuggestionsContainer;
  224. [SerializeField]
  225. private TMP_InputField commandInputField;
  226. [SerializeField]
  227. private Button hideButton;
  228. [SerializeField]
  229. private Button clearButton;
  230. [SerializeField]
  231. private Image collapseButton;
  232. [SerializeField]
  233. private Image filterInfoButton;
  234. [SerializeField]
  235. private Image filterWarningButton;
  236. [SerializeField]
  237. private Image filterErrorButton;
  238. [SerializeField]
  239. private TextMeshProUGUI infoEntryCountText;
  240. [SerializeField]
  241. private TextMeshProUGUI warningEntryCountText;
  242. [SerializeField]
  243. private TextMeshProUGUI errorEntryCountText;
  244. [SerializeField]
  245. private RectTransform searchbar;
  246. [SerializeField]
  247. private RectTransform searchbarSlotTop;
  248. [SerializeField]
  249. private RectTransform searchbarSlotBottom;
  250. [SerializeField]
  251. private Image resizeButton;
  252. [SerializeField]
  253. private GameObject snapToBottomButton;
  254. // Canvas group to modify visibility of the log window
  255. [SerializeField]
  256. private CanvasGroup logWindowCanvasGroup;
  257. [SerializeField]
  258. private DebugLogPopup popupManager;
  259. [SerializeField]
  260. private ScrollRect logItemsScrollRect;
  261. private RectTransform logItemsScrollRectTR;
  262. private Vector2 logItemsScrollRectOriginalSize;
  263. // Recycled list view to handle the log items efficiently
  264. [SerializeField]
  265. private DebugLogRecycledListView recycledListView;
  266. #pragma warning restore 0649
  267. private bool isLogWindowVisible = true;
  268. public bool IsLogWindowVisible { get { return isLogWindowVisible; } }
  269. public bool PopupEnabled
  270. {
  271. get { return popupManager.gameObject.activeSelf; }
  272. set { popupManager.gameObject.SetActive( value ); }
  273. }
  274. private bool screenDimensionsChanged = true;
  275. private float logWindowPreviousWidth;
  276. // Number of entries filtered by their types
  277. private int infoEntryCount = 0, warningEntryCount = 0, errorEntryCount = 0;
  278. private bool entryCountTextsDirty;
  279. // Number of new entries received this frame
  280. private int newInfoEntryCount = 0, newWarningEntryCount = 0, newErrorEntryCount = 0;
  281. // Filters to apply to the list of debug entries to show
  282. private bool isCollapseOn = false;
  283. private DebugLogFilter logFilter = DebugLogFilter.All;
  284. // Search filter
  285. private string searchTerm;
  286. private bool isInSearchMode;
  287. // If the last log item is completely visible (scrollbar is at the bottom),
  288. // scrollbar will remain at the bottom when new debug entries are received
  289. [System.NonSerialized]
  290. public bool SnapToBottom = true;
  291. // List of unique debug entries (duplicates of entries are not kept)
  292. private DynamicCircularBuffer<DebugLogEntry> collapsedLogEntries;
  293. private DynamicCircularBuffer<DebugLogEntryTimestamp> collapsedLogEntriesTimestamps;
  294. // Dictionary to quickly find if a log already exists in collapsedLogEntries
  295. private Dictionary<DebugLogEntry, DebugLogEntry> collapsedLogEntriesMap;
  296. // The order the collapsedLogEntries are received
  297. // (duplicate entries have the same value)
  298. private DynamicCircularBuffer<DebugLogEntry> uncollapsedLogEntries;
  299. private DynamicCircularBuffer<DebugLogEntryTimestamp> uncollapsedLogEntriesTimestamps;
  300. // Filtered list of debug entries to show
  301. private DynamicCircularBuffer<DebugLogEntry> logEntriesToShow;
  302. private DynamicCircularBuffer<DebugLogEntryTimestamp> timestampsOfLogEntriesToShow;
  303. // The log entry that must be focused this frame
  304. private int indexOfLogEntryToSelectAndFocus = -1;
  305. // Whether or not logs list view should be updated this frame
  306. private bool shouldUpdateRecycledListView = true;
  307. // Logs that should be registered in Update-loop
  308. private DynamicCircularBuffer<QueuedDebugLogEntry> queuedLogEntries;
  309. private DynamicCircularBuffer<DebugLogEntryTimestamp> queuedLogEntriesTimestamps;
  310. private object logEntriesLock;
  311. private int pendingLogToAutoExpand;
  312. // Command suggestions that match the currently entered command
  313. private List<TextMeshProUGUI> commandSuggestionInstances;
  314. private int visibleCommandSuggestionInstances = 0;
  315. private List<ConsoleMethodInfo> matchingCommandSuggestions;
  316. private List<int> commandCaretIndexIncrements;
  317. private string commandInputFieldPrevCommand;
  318. private string commandInputFieldPrevCommandName;
  319. private int commandInputFieldPrevParamCount = -1;
  320. private int commandInputFieldPrevCaretPos = -1;
  321. private int commandInputFieldPrevCaretArgumentIndex = -1;
  322. // Value of the command input field when autocomplete was first requested
  323. private string commandInputFieldAutoCompleteBase;
  324. private bool commandInputFieldAutoCompletedNow;
  325. // Pools for memory efficiency
  326. private Stack<DebugLogEntry> pooledLogEntries;
  327. private Stack<DebugLogItem> pooledLogItems;
  328. /// Variables used by <see cref="RemoveOldestLogs"/>
  329. private bool anyCollapsedLogRemoved;
  330. private int removedLogEntriesToShowCount;
  331. // History of the previously entered commands
  332. private CircularBuffer<string> commandHistory;
  333. private int commandHistoryIndex = -1;
  334. private string unfinishedCommand;
  335. // StringBuilder used by various functions
  336. internal StringBuilder sharedStringBuilder;
  337. /// <summary>
  338. /// Used for <see cref="TMP_Text.SetText(char[])"/>.
  339. /// </summary>
  340. [System.NonSerialized]
  341. internal char[] textBuffer = new char[4096];
  342. // Offset of DateTime.Now from DateTime.UtcNow
  343. private System.TimeSpan localTimeUtcOffset;
  344. // Last recorded values of Time.realtimeSinceStartup and Time.frameCount on the main thread (because these Time properties can't be accessed from other threads)
  345. #if !IDG_OMIT_ELAPSED_TIME
  346. private float lastElapsedSeconds;
  347. #endif
  348. #if !IDG_OMIT_FRAMECOUNT
  349. private int lastFrameCount;
  350. #endif
  351. private DebugLogEntryTimestamp dummyLogEntryTimestamp;
  352. // Required in ValidateScrollPosition() function
  353. private PointerEventData nullPointerEventData;
  354. private System.Action<DebugLogEntry> poolLogEntryAction;
  355. private System.Action<DebugLogEntry> removeUncollapsedLogEntryAction;
  356. private System.Predicate<DebugLogEntry> shouldRemoveCollapsedLogEntryPredicate;
  357. private System.Predicate<DebugLogEntry> shouldRemoveLogEntryToShowPredicate;
  358. private System.Action<DebugLogEntry, int> updateLogEntryCollapsedIndexAction;
  359. // Callbacks for log window show/hide events
  360. public System.Action OnLogWindowShown, OnLogWindowHidden;
  361. private bool isQuittingApplication;
  362. #if !UNITY_EDITOR && UNITY_ANDROID && UNITY_ANDROID_JNI
  363. private DebugLogLogcatListener logcatListener;
  364. #endif
  365. private void Awake()
  366. {
  367. // Only one instance of debug console is allowed
  368. if( !Instance )
  369. {
  370. Instance = this;
  371. // If it is a singleton object, don't destroy it between scene changes
  372. if( singleton )
  373. DontDestroyOnLoad( gameObject );
  374. }
  375. else if( Instance != this )
  376. {
  377. Destroy( gameObject );
  378. return;
  379. }
  380. pooledLogEntries = new Stack<DebugLogEntry>( 64 );
  381. pooledLogItems = new Stack<DebugLogItem>( 16 );
  382. commandSuggestionInstances = new List<TextMeshProUGUI>( 8 );
  383. matchingCommandSuggestions = new List<ConsoleMethodInfo>( 8 );
  384. commandCaretIndexIncrements = new List<int>( 8 );
  385. queuedLogEntries = new DynamicCircularBuffer<QueuedDebugLogEntry>( Mathf.Clamp( queuedLogLimit, 16, 4096 ) );
  386. commandHistory = new CircularBuffer<string>( commandHistorySize );
  387. logEntriesLock = new object();
  388. sharedStringBuilder = new StringBuilder( 1024 );
  389. canvasTR = (RectTransform) transform;
  390. logItemsScrollRectTR = (RectTransform) logItemsScrollRect.transform;
  391. logItemsScrollRectOriginalSize = logItemsScrollRectTR.sizeDelta;
  392. // Associate sprites with log types
  393. logSpriteRepresentations = new Sprite[5];
  394. logSpriteRepresentations[(int) LogType.Log] = infoLog;
  395. logSpriteRepresentations[(int) LogType.Warning] = warningLog;
  396. logSpriteRepresentations[(int) LogType.Error] = errorLog;
  397. logSpriteRepresentations[(int) LogType.Exception] = errorLog;
  398. logSpriteRepresentations[(int) LogType.Assert] = errorLog;
  399. // Initially, all log types are visible
  400. filterInfoButton.color = filterButtonsSelectedColor;
  401. filterWarningButton.color = filterButtonsSelectedColor;
  402. filterErrorButton.color = filterButtonsSelectedColor;
  403. resizeButton.sprite = enableHorizontalResizing ? resizeIconAllDirections : resizeIconVerticalOnly;
  404. collapsedLogEntries = new DynamicCircularBuffer<DebugLogEntry>( 128 );
  405. collapsedLogEntriesMap = new Dictionary<DebugLogEntry, DebugLogEntry>( 128, new DebugLogEntryContentEqualityComparer() );
  406. uncollapsedLogEntries = new DynamicCircularBuffer<DebugLogEntry>( 256 );
  407. logEntriesToShow = new DynamicCircularBuffer<DebugLogEntry>( 256 );
  408. if( captureLogTimestamps )
  409. {
  410. collapsedLogEntriesTimestamps = new DynamicCircularBuffer<DebugLogEntryTimestamp>( 128 );
  411. uncollapsedLogEntriesTimestamps = new DynamicCircularBuffer<DebugLogEntryTimestamp>( 256 );
  412. timestampsOfLogEntriesToShow = new DynamicCircularBuffer<DebugLogEntryTimestamp>( 256 );
  413. queuedLogEntriesTimestamps = new DynamicCircularBuffer<DebugLogEntryTimestamp>( queuedLogEntries.Capacity );
  414. }
  415. recycledListView.Initialize( this, logEntriesToShow, timestampsOfLogEntriesToShow, logItemPrefab.Transform.sizeDelta.y );
  416. if( minimumWidth < 100f )
  417. minimumWidth = 100f;
  418. if( minimumHeight < 200f )
  419. minimumHeight = 200f;
  420. if( !resizeFromRight )
  421. {
  422. RectTransform resizeButtonTR = (RectTransform) resizeButton.GetComponentInParent<DebugLogResizeListener>().transform;
  423. resizeButtonTR.anchorMin = new Vector2( 0f, resizeButtonTR.anchorMin.y );
  424. resizeButtonTR.anchorMax = new Vector2( 0f, resizeButtonTR.anchorMax.y );
  425. resizeButtonTR.pivot = new Vector2( 0f, resizeButtonTR.pivot.y );
  426. ( (RectTransform) commandInputField.transform ).anchoredPosition += new Vector2( resizeButtonTR.sizeDelta.x, 0f );
  427. }
  428. if( enableSearchbar )
  429. searchbar.GetComponent<TMP_InputField>().onValueChanged.AddListener( SearchTermChanged );
  430. else
  431. {
  432. searchbar = null;
  433. searchbarSlotTop.gameObject.SetActive( false );
  434. searchbarSlotBottom.gameObject.SetActive( false );
  435. }
  436. filterInfoButton.gameObject.SetActive( receiveInfoLogs );
  437. filterWarningButton.gameObject.SetActive( receiveWarningLogs );
  438. filterErrorButton.gameObject.SetActive( receiveErrorLogs || receiveExceptionLogs );
  439. if( commandSuggestionsContainer.gameObject.activeSelf )
  440. commandSuggestionsContainer.gameObject.SetActive( false );
  441. // Register to UI events
  442. commandInputField.onValidateInput += OnValidateCommand;
  443. commandInputField.onValueChanged.AddListener( OnEditCommand );
  444. commandInputField.onEndEdit.AddListener( OnEndEditCommand );
  445. hideButton.onClick.AddListener( HideLogWindow );
  446. clearButton.onClick.AddListener( ClearLogs );
  447. collapseButton.GetComponent<Button>().onClick.AddListener( CollapseButtonPressed );
  448. filterInfoButton.GetComponent<Button>().onClick.AddListener( FilterLogButtonPressed );
  449. filterWarningButton.GetComponent<Button>().onClick.AddListener( FilterWarningButtonPressed );
  450. filterErrorButton.GetComponent<Button>().onClick.AddListener( FilterErrorButtonPressed );
  451. snapToBottomButton.GetComponent<Button>().onClick.AddListener( () => SnapToBottom = true );
  452. localTimeUtcOffset = System.DateTime.Now - System.DateTime.UtcNow;
  453. dummyLogEntryTimestamp = new DebugLogEntryTimestamp();
  454. nullPointerEventData = new PointerEventData( null );
  455. poolLogEntryAction = PoolLogEntry;
  456. removeUncollapsedLogEntryAction = RemoveUncollapsedLogEntry;
  457. shouldRemoveCollapsedLogEntryPredicate = ShouldRemoveCollapsedLogEntry;
  458. shouldRemoveLogEntryToShowPredicate = ShouldRemoveLogEntryToShow;
  459. updateLogEntryCollapsedIndexAction = UpdateLogEntryCollapsedIndex;
  460. if( receiveLogsWhileInactive )
  461. {
  462. Application.logMessageReceivedThreaded -= ReceivedLog;
  463. Application.logMessageReceivedThreaded += ReceivedLog;
  464. }
  465. // OnApplicationQuit isn't reliable on some Unity versions when Application.wantsToQuit is used; Application.quitting is the only reliable solution on those versions
  466. // https://issuetracker.unity3d.com/issues/onapplicationquit-method-is-called-before-application-dot-wantstoquit-event-is-raised
  467. Application.quitting += OnApplicationQuitting;
  468. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  469. toggleBinding.performed += ( context ) =>
  470. {
  471. if( toggleWithKey )
  472. {
  473. if( isLogWindowVisible )
  474. HideLogWindow();
  475. else
  476. ShowLogWindow();
  477. }
  478. };
  479. // On new Input System, scroll sensitivity is much higher than legacy Input system
  480. logItemsScrollRect.scrollSensitivity *= 0.25f;
  481. #endif
  482. }
  483. private void OnEnable()
  484. {
  485. if( Instance != this )
  486. return;
  487. if( !receiveLogsWhileInactive )
  488. {
  489. Application.logMessageReceivedThreaded -= ReceivedLog;
  490. Application.logMessageReceivedThreaded += ReceivedLog;
  491. }
  492. if( receiveLogcatLogsInAndroid )
  493. {
  494. #if UNITY_ANDROID
  495. #if UNITY_ANDROID_JNI
  496. #if !UNITY_EDITOR
  497. if( logcatListener == null )
  498. logcatListener = new DebugLogLogcatListener();
  499. logcatListener.Start( logcatArguments );
  500. #endif
  501. #else
  502. Debug.LogWarning( "Android JNI module must be enabled in Package Manager for \"Receive Logcat Logs In Android\" to work." );
  503. #endif
  504. #endif
  505. }
  506. #if IDG_ENABLE_HELPER_COMMANDS || IDG_ENABLE_LOGS_SAVE_COMMAND
  507. DebugLogConsole.AddCommand( "logs.save", "Saves logs to persistentDataPath", SaveLogsToFile );
  508. DebugLogConsole.AddCommand<string>( "logs.save", "Saves logs to the specified file", SaveLogsToFile );
  509. #endif
  510. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  511. if( toggleWithKey )
  512. toggleBinding.Enable();
  513. #endif
  514. //Debug.LogAssertion( "assert" );
  515. //Debug.LogError( "error" );
  516. //Debug.LogException( new System.IO.EndOfStreamException() );
  517. //Debug.LogWarning( "warning" );
  518. //Debug.Log( "log" );
  519. }
  520. private void OnDisable()
  521. {
  522. if( Instance != this )
  523. return;
  524. if( !receiveLogsWhileInactive )
  525. Application.logMessageReceivedThreaded -= ReceivedLog;
  526. #if !UNITY_EDITOR && UNITY_ANDROID && UNITY_ANDROID_JNI
  527. if( logcatListener != null )
  528. logcatListener.Stop();
  529. #endif
  530. DebugLogConsole.RemoveCommand( "logs.save" );
  531. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  532. if( toggleBinding.enabled )
  533. toggleBinding.Disable();
  534. #endif
  535. }
  536. private void Start()
  537. {
  538. if( startMinimized )
  539. {
  540. HideLogWindow();
  541. if( popupVisibility != PopupVisibility.Always )
  542. popupManager.Hide();
  543. }
  544. else
  545. ShowLogWindow();
  546. PopupEnabled = ( popupVisibility != PopupVisibility.Never );
  547. }
  548. private void OnDestroy()
  549. {
  550. if( Instance == this )
  551. Instance = null;
  552. if( receiveLogsWhileInactive )
  553. Application.logMessageReceivedThreaded -= ReceivedLog;
  554. Application.quitting -= OnApplicationQuitting;
  555. }
  556. #if UNITY_EDITOR
  557. private void OnValidate()
  558. {
  559. maxLogCount = Mathf.Max( 2, maxLogCount );
  560. logsToRemoveAfterMaxLogCount = Mathf.Max( 1, logsToRemoveAfterMaxLogCount );
  561. queuedLogLimit = Mathf.Max( 0, queuedLogLimit );
  562. if( UnityEditor.EditorApplication.isPlaying )
  563. {
  564. resizeButton.sprite = enableHorizontalResizing ? resizeIconAllDirections : resizeIconVerticalOnly;
  565. filterInfoButton.gameObject.SetActive( receiveInfoLogs );
  566. filterWarningButton.gameObject.SetActive( receiveWarningLogs );
  567. filterErrorButton.gameObject.SetActive( receiveErrorLogs || receiveExceptionLogs );
  568. }
  569. }
  570. #endif
  571. private void OnApplicationQuitting()
  572. {
  573. isQuittingApplication = true;
  574. }
  575. // Window is resized, update the list
  576. private void OnRectTransformDimensionsChange()
  577. {
  578. screenDimensionsChanged = true;
  579. }
  580. private void Update()
  581. {
  582. #if !IDG_OMIT_ELAPSED_TIME
  583. lastElapsedSeconds = Time.realtimeSinceStartup;
  584. #endif
  585. #if !IDG_OMIT_FRAMECOUNT
  586. lastFrameCount = Time.frameCount;
  587. #endif
  588. #if !UNITY_EDITOR && UNITY_ANDROID && UNITY_ANDROID_JNI
  589. if( logcatListener != null )
  590. {
  591. string log;
  592. while( ( log = logcatListener.GetLog() ) != null )
  593. ReceivedLog( "LOGCAT: " + log, string.Empty, LogType.Log );
  594. }
  595. #endif
  596. #if !ENABLE_INPUT_SYSTEM || ENABLE_LEGACY_INPUT_MANAGER
  597. // Toggling the console with toggleKey is handled in Update instead of LateUpdate because
  598. // when we hide the console, we don't want the commandInputField to capture the toggleKey.
  599. // InputField captures input in LateUpdate so deactivating it in Update ensures that
  600. // no further input is captured
  601. if( toggleWithKey )
  602. {
  603. if( Input.GetKeyDown( toggleKey ) )
  604. {
  605. if( isLogWindowVisible )
  606. HideLogWindow();
  607. else
  608. ShowLogWindow();
  609. }
  610. }
  611. #endif
  612. }
  613. private void LateUpdate()
  614. {
  615. if( isQuittingApplication )
  616. return;
  617. int numberOfLogsToProcess = isLogWindowVisible ? queuedLogEntries.Count : ( queuedLogEntries.Count - queuedLogLimit );
  618. ProcessQueuedLogs( numberOfLogsToProcess );
  619. if( uncollapsedLogEntries.Count >= maxLogCount )
  620. {
  621. /// If log window isn't visible, remove the logs over time (i.e. don't remove more than <see cref="logsToRemoveAfterMaxLogCount"/>) to avoid performance issues.
  622. int numberOfLogsToRemove = Mathf.Min( !isLogWindowVisible ? logsToRemoveAfterMaxLogCount : ( uncollapsedLogEntries.Count - maxLogCount + logsToRemoveAfterMaxLogCount ), uncollapsedLogEntries.Count );
  623. RemoveOldestLogs( numberOfLogsToRemove );
  624. }
  625. // Don't perform CPU heavy tasks if neither the log window nor the popup is visible
  626. if( !isLogWindowVisible && !PopupEnabled )
  627. return;
  628. int newInfoEntryCount, newWarningEntryCount, newErrorEntryCount;
  629. lock( logEntriesLock )
  630. {
  631. newInfoEntryCount = this.newInfoEntryCount;
  632. newWarningEntryCount = this.newWarningEntryCount;
  633. newErrorEntryCount = this.newErrorEntryCount;
  634. this.newInfoEntryCount = 0;
  635. this.newWarningEntryCount = 0;
  636. this.newErrorEntryCount = 0;
  637. }
  638. // Update entry count texts in a single batch
  639. if( newInfoEntryCount > 0 || newWarningEntryCount > 0 || newErrorEntryCount > 0 )
  640. {
  641. if( newInfoEntryCount > 0 )
  642. {
  643. infoEntryCount += newInfoEntryCount;
  644. if( isLogWindowVisible )
  645. infoEntryCountText.text = infoEntryCount.ToString();
  646. }
  647. if( newWarningEntryCount > 0 )
  648. {
  649. warningEntryCount += newWarningEntryCount;
  650. if( isLogWindowVisible )
  651. warningEntryCountText.text = warningEntryCount.ToString();
  652. }
  653. if( newErrorEntryCount > 0 )
  654. {
  655. errorEntryCount += newErrorEntryCount;
  656. if( isLogWindowVisible )
  657. errorEntryCountText.text = errorEntryCount.ToString();
  658. }
  659. // If debug popup is visible, notify it of the new debug entries
  660. if( !isLogWindowVisible )
  661. {
  662. entryCountTextsDirty = true;
  663. if( popupVisibility == PopupVisibility.WhenLogReceived && !popupManager.IsVisible )
  664. {
  665. if( ( newInfoEntryCount > 0 && ( popupVisibilityLogFilter & DebugLogFilter.Info ) == DebugLogFilter.Info ) ||
  666. ( newWarningEntryCount > 0 && ( popupVisibilityLogFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning ) ||
  667. ( newErrorEntryCount > 0 && ( popupVisibilityLogFilter & DebugLogFilter.Error ) == DebugLogFilter.Error ) )
  668. {
  669. popupManager.Show();
  670. }
  671. }
  672. if( popupManager.IsVisible )
  673. popupManager.NewLogsArrived( newInfoEntryCount, newWarningEntryCount, newErrorEntryCount );
  674. }
  675. }
  676. if( isLogWindowVisible )
  677. {
  678. // Update visible logs if necessary
  679. if( shouldUpdateRecycledListView )
  680. OnLogEntriesUpdated( false, false );
  681. // Automatically expand the target log (if any)
  682. if( indexOfLogEntryToSelectAndFocus >= 0 )
  683. {
  684. if( indexOfLogEntryToSelectAndFocus < logEntriesToShow.Count )
  685. recycledListView.SelectAndFocusOnLogItemAtIndex( indexOfLogEntryToSelectAndFocus );
  686. indexOfLogEntryToSelectAndFocus = -1;
  687. }
  688. if( entryCountTextsDirty )
  689. {
  690. infoEntryCountText.text = infoEntryCount.ToString();
  691. warningEntryCountText.text = warningEntryCount.ToString();
  692. errorEntryCountText.text = errorEntryCount.ToString();
  693. entryCountTextsDirty = false;
  694. }
  695. float logWindowWidth = logWindowTR.rect.width;
  696. if( !Mathf.Approximately( logWindowWidth, logWindowPreviousWidth ) )
  697. {
  698. logWindowPreviousWidth = logWindowWidth;
  699. if( searchbar )
  700. {
  701. if( logWindowWidth >= topSearchbarMinWidth )
  702. {
  703. if( searchbar.parent == searchbarSlotBottom )
  704. {
  705. searchbarSlotTop.gameObject.SetActive( true );
  706. searchbar.SetParent( searchbarSlotTop, false );
  707. searchbarSlotBottom.gameObject.SetActive( false );
  708. logItemsScrollRectTR.anchoredPosition = Vector2.zero;
  709. logItemsScrollRectTR.sizeDelta = logItemsScrollRectOriginalSize;
  710. }
  711. }
  712. else
  713. {
  714. if( searchbar.parent == searchbarSlotTop )
  715. {
  716. searchbarSlotBottom.gameObject.SetActive( true );
  717. searchbar.SetParent( searchbarSlotBottom, false );
  718. searchbarSlotTop.gameObject.SetActive( false );
  719. float searchbarHeight = searchbarSlotBottom.sizeDelta.y;
  720. logItemsScrollRectTR.anchoredPosition = new Vector2( 0f, searchbarHeight * -0.5f );
  721. logItemsScrollRectTR.sizeDelta = logItemsScrollRectOriginalSize - new Vector2( 0f, searchbarHeight );
  722. }
  723. }
  724. }
  725. recycledListView.OnViewportWidthChanged();
  726. }
  727. // If SnapToBottom is enabled, force the scrollbar to the bottom
  728. if( SnapToBottom )
  729. {
  730. logItemsScrollRect.verticalNormalizedPosition = 0f;
  731. if( snapToBottomButton.activeSelf )
  732. snapToBottomButton.SetActive( false );
  733. }
  734. else
  735. {
  736. float scrollPos = logItemsScrollRect.verticalNormalizedPosition;
  737. if( snapToBottomButton.activeSelf != ( scrollPos > 1E-6f && scrollPos < 0.9999f ) )
  738. snapToBottomButton.SetActive( !snapToBottomButton.activeSelf );
  739. }
  740. if( showCommandSuggestions && commandInputField.isFocused && commandInputField.caretPosition != commandInputFieldPrevCaretPos )
  741. RefreshCommandSuggestions( commandInputField.text );
  742. if( commandInputField.isFocused && commandHistory.Count > 0 )
  743. {
  744. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  745. if( Keyboard.current != null )
  746. #endif
  747. {
  748. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  749. if( Keyboard.current[Key.UpArrow].wasPressedThisFrame )
  750. #else
  751. if( Input.GetKeyDown( KeyCode.UpArrow ) )
  752. #endif
  753. {
  754. if( commandHistoryIndex == -1 )
  755. {
  756. commandHistoryIndex = commandHistory.Count - 1;
  757. unfinishedCommand = commandInputField.text;
  758. }
  759. else if( --commandHistoryIndex < 0 )
  760. commandHistoryIndex = 0;
  761. commandInputField.text = commandHistory[commandHistoryIndex];
  762. commandInputField.caretPosition = commandInputField.text.Length;
  763. }
  764. #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER
  765. else if( Keyboard.current[Key.DownArrow].wasPressedThisFrame && commandHistoryIndex != -1 )
  766. #else
  767. else if( Input.GetKeyDown( KeyCode.DownArrow ) && commandHistoryIndex != -1 )
  768. #endif
  769. {
  770. if( ++commandHistoryIndex < commandHistory.Count )
  771. commandInputField.text = commandHistory[commandHistoryIndex];
  772. else
  773. {
  774. commandHistoryIndex = -1;
  775. commandInputField.text = unfinishedCommand ?? string.Empty;
  776. }
  777. }
  778. }
  779. }
  780. }
  781. if( screenDimensionsChanged )
  782. {
  783. // Update the recycled list view
  784. if( isLogWindowVisible )
  785. recycledListView.OnViewportHeightChanged();
  786. else
  787. popupManager.UpdatePosition( true );
  788. #if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS
  789. CheckScreenCutout();
  790. #endif
  791. screenDimensionsChanged = false;
  792. }
  793. }
  794. public void ShowLogWindow()
  795. {
  796. // Show the log window
  797. logWindowCanvasGroup.blocksRaycasts = true;
  798. logWindowCanvasGroup.alpha = logWindowOpacity;
  799. popupManager.Hide();
  800. // Update the recycled list view
  801. // (in case new entries were intercepted while log window was hidden)
  802. OnLogEntriesUpdated( true, true );
  803. #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL
  804. // Focus on the command input field on standalone platforms when the console is opened
  805. if( autoFocusOnCommandInputField )
  806. StartCoroutine( ActivateCommandInputFieldCoroutine() );
  807. #endif
  808. isLogWindowVisible = true;
  809. if( OnLogWindowShown != null )
  810. OnLogWindowShown();
  811. }
  812. public void HideLogWindow()
  813. {
  814. // Hide the log window
  815. logWindowCanvasGroup.blocksRaycasts = false;
  816. logWindowCanvasGroup.alpha = 0f;
  817. if( commandInputField.isFocused )
  818. commandInputField.DeactivateInputField();
  819. if( popupVisibility == PopupVisibility.Always )
  820. popupManager.Show();
  821. isLogWindowVisible = false;
  822. // Deselect the currently selected UI object (if any) when the log window is hidden to avoid edge cases: https://github.com/yasirkula/UnityIngameDebugConsole/pull/85
  823. if( EventSystem.current != null )
  824. EventSystem.current.SetSelectedGameObject( null );
  825. if( OnLogWindowHidden != null )
  826. OnLogWindowHidden();
  827. }
  828. // Command field input is changed, check if command is submitted
  829. private char OnValidateCommand( string text, int charIndex, char addedChar )
  830. {
  831. if( addedChar == '\t' ) // Autocomplete attempt
  832. {
  833. if( !string.IsNullOrEmpty( text ) )
  834. {
  835. if( string.IsNullOrEmpty( commandInputFieldAutoCompleteBase ) )
  836. commandInputFieldAutoCompleteBase = text;
  837. string autoCompletedCommand = DebugLogConsole.GetAutoCompleteCommand( commandInputFieldAutoCompleteBase, text );
  838. if( !string.IsNullOrEmpty( autoCompletedCommand ) && autoCompletedCommand != text )
  839. {
  840. commandInputFieldAutoCompletedNow = true;
  841. commandInputField.text = autoCompletedCommand;
  842. commandInputField.stringPosition = autoCompletedCommand.Length;
  843. }
  844. }
  845. return '\0';
  846. }
  847. else if( addedChar == '\n' ) // Command is submitted
  848. {
  849. // Clear the command field
  850. if( clearCommandAfterExecution )
  851. commandInputField.text = string.Empty;
  852. if( text.Length > 0 )
  853. {
  854. if( commandHistory.Count == 0 || commandHistory[commandHistory.Count - 1] != text )
  855. commandHistory.Add( text );
  856. commandHistoryIndex = -1;
  857. unfinishedCommand = null;
  858. // Execute the command
  859. DebugLogConsole.ExecuteCommand( text );
  860. // Snap to bottom and select the latest entry
  861. SnapToBottom = true;
  862. }
  863. return '\0';
  864. }
  865. return addedChar;
  866. }
  867. // A debug entry is received
  868. public void ReceivedLog( string logString, string stackTrace, LogType logType )
  869. {
  870. if( isQuittingApplication )
  871. return;
  872. switch( logType )
  873. {
  874. case LogType.Log: if( !receiveInfoLogs ) return; break;
  875. case LogType.Warning: if( !receiveWarningLogs ) return; break;
  876. case LogType.Error: if( !receiveErrorLogs ) return; break;
  877. case LogType.Assert:
  878. case LogType.Exception: if( !receiveExceptionLogs ) return; break;
  879. }
  880. QueuedDebugLogEntry queuedLogEntry = new QueuedDebugLogEntry( logString, stackTrace, logType );
  881. DebugLogEntryTimestamp queuedLogEntryTimestamp;
  882. if( queuedLogEntriesTimestamps != null )
  883. {
  884. // It is 10 times faster to cache local time's offset from UtcNow and add it to UtcNow to get local time at any time
  885. System.DateTime dateTime = System.DateTime.UtcNow + localTimeUtcOffset;
  886. #if !IDG_OMIT_ELAPSED_TIME && !IDG_OMIT_FRAMECOUNT
  887. queuedLogEntryTimestamp = new DebugLogEntryTimestamp( dateTime, lastElapsedSeconds, lastFrameCount );
  888. #elif !IDG_OMIT_ELAPSED_TIME
  889. queuedLogEntryTimestamp = new DebugLogEntryTimestamp( dateTime, lastElapsedSeconds );
  890. #elif !IDG_OMIT_FRAMECOUNT
  891. queuedLogEntryTimestamp = new DebugLogEntryTimestamp( dateTime, lastFrameCount );
  892. #else
  893. queuedLogEntryTimestamp = new DebugLogEntryTimestamp( dateTime );
  894. #endif
  895. }
  896. else
  897. queuedLogEntryTimestamp = dummyLogEntryTimestamp;
  898. lock( logEntriesLock )
  899. {
  900. /// Enforce <see cref="maxLogCount"/> in queued logs, as well. That's because when it's exceeded, the oldest queued logs will
  901. /// be removed by <see cref="RemoveOldestLogs"/> immediately after they're processed anyways (i.e. waste of CPU and RAM).
  902. if( queuedLogEntries.Count + 1 >= maxLogCount )
  903. {
  904. LogType removedLogType = queuedLogEntries.RemoveFirst().logType;
  905. if( removedLogType == LogType.Log )
  906. newInfoEntryCount--;
  907. else if( removedLogType == LogType.Warning )
  908. newWarningEntryCount--;
  909. else
  910. newErrorEntryCount--;
  911. if( queuedLogEntriesTimestamps != null )
  912. queuedLogEntriesTimestamps.RemoveFirst();
  913. }
  914. queuedLogEntries.Add( queuedLogEntry );
  915. if( queuedLogEntriesTimestamps != null )
  916. queuedLogEntriesTimestamps.Add( queuedLogEntryTimestamp );
  917. if( logType == LogType.Log )
  918. newInfoEntryCount++;
  919. else if( logType == LogType.Warning )
  920. newWarningEntryCount++;
  921. else
  922. newErrorEntryCount++;
  923. }
  924. }
  925. // Process a number of logs waiting in the pending logs queue
  926. private void ProcessQueuedLogs( int numberOfLogsToProcess )
  927. {
  928. for( int i = 0; i < numberOfLogsToProcess; i++ )
  929. {
  930. QueuedDebugLogEntry logEntry;
  931. DebugLogEntryTimestamp timestamp;
  932. lock( logEntriesLock )
  933. {
  934. logEntry = queuedLogEntries.RemoveFirst();
  935. timestamp = queuedLogEntriesTimestamps != null ? queuedLogEntriesTimestamps.RemoveFirst() : dummyLogEntryTimestamp;
  936. }
  937. ProcessLog( logEntry, timestamp );
  938. }
  939. }
  940. // Present the log entry in the console
  941. private void ProcessLog( QueuedDebugLogEntry queuedLogEntry, DebugLogEntryTimestamp timestamp )
  942. {
  943. LogType logType = queuedLogEntry.logType;
  944. DebugLogEntry logEntry;
  945. if( pooledLogEntries.Count > 0 )
  946. logEntry = pooledLogEntries.Pop();
  947. else
  948. logEntry = new DebugLogEntry();
  949. logEntry.Initialize( queuedLogEntry.logString, queuedLogEntry.stackTrace );
  950. // Check if this entry is a duplicate (i.e. has been received before)
  951. DebugLogEntry existingLogEntry;
  952. bool isEntryInCollapsedEntryList = collapsedLogEntriesMap.TryGetValue( logEntry, out existingLogEntry );
  953. if( !isEntryInCollapsedEntryList )
  954. {
  955. // It is not a duplicate,
  956. // add it to the list of unique debug entries
  957. logEntry.logType = logType;
  958. logEntry.collapsedIndex = collapsedLogEntries.Count;
  959. collapsedLogEntries.Add( logEntry );
  960. collapsedLogEntriesMap[logEntry] = logEntry;
  961. if( collapsedLogEntriesTimestamps != null )
  962. collapsedLogEntriesTimestamps.Add( timestamp );
  963. }
  964. else
  965. {
  966. // It is a duplicate, pool the duplicate log entry and
  967. // increment the original debug item's collapsed count
  968. PoolLogEntry( logEntry );
  969. logEntry = existingLogEntry;
  970. logEntry.count++;
  971. if( collapsedLogEntriesTimestamps != null )
  972. collapsedLogEntriesTimestamps[logEntry.collapsedIndex] = timestamp;
  973. }
  974. uncollapsedLogEntries.Add( logEntry );
  975. if( uncollapsedLogEntriesTimestamps != null )
  976. uncollapsedLogEntriesTimestamps.Add( timestamp );
  977. // If this debug entry matches the current filters,
  978. // add it to the list of debug entries to show
  979. int logEntryIndexInEntriesToShow = -1;
  980. if( isCollapseOn && isEntryInCollapsedEntryList )
  981. {
  982. if( isLogWindowVisible || timestampsOfLogEntriesToShow != null )
  983. {
  984. if( !isInSearchMode && logFilter == DebugLogFilter.All )
  985. logEntryIndexInEntriesToShow = logEntry.collapsedIndex;
  986. else
  987. logEntryIndexInEntriesToShow = logEntriesToShow.IndexOf( logEntry );
  988. if( logEntryIndexInEntriesToShow >= 0 )
  989. {
  990. if( timestampsOfLogEntriesToShow != null )
  991. timestampsOfLogEntriesToShow[logEntryIndexInEntriesToShow] = timestamp;
  992. if( isLogWindowVisible )
  993. recycledListView.OnCollapsedLogEntryAtIndexUpdated( logEntryIndexInEntriesToShow );
  994. }
  995. }
  996. }
  997. else if( ( !isInSearchMode || queuedLogEntry.MatchesSearchTerm( searchTerm ) ) && ( logFilter == DebugLogFilter.All ||
  998. ( logType == LogType.Log && ( ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info ) ) ||
  999. ( logType == LogType.Warning && ( ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning ) ) ||
  1000. ( logType != LogType.Log && logType != LogType.Warning && ( ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error ) ) ) )
  1001. {
  1002. logEntriesToShow.Add( logEntry );
  1003. logEntryIndexInEntriesToShow = logEntriesToShow.Count - 1;
  1004. if( timestampsOfLogEntriesToShow != null )
  1005. timestampsOfLogEntriesToShow.Add( timestamp );
  1006. shouldUpdateRecycledListView = true;
  1007. }
  1008. // Automatically expand this log if necessary
  1009. if( pendingLogToAutoExpand > 0 && --pendingLogToAutoExpand <= 0 && logEntryIndexInEntriesToShow >= 0 )
  1010. indexOfLogEntryToSelectAndFocus = logEntryIndexInEntriesToShow;
  1011. }
  1012. private void RemoveOldestLogs( int numberOfLogsToRemove )
  1013. {
  1014. if( numberOfLogsToRemove <= 0 )
  1015. return;
  1016. DebugLogEntry logEntryToSelectAndFocus = ( indexOfLogEntryToSelectAndFocus >= 0 && indexOfLogEntryToSelectAndFocus < logEntriesToShow.Count ) ? logEntriesToShow[indexOfLogEntryToSelectAndFocus] : null;
  1017. anyCollapsedLogRemoved = false;
  1018. removedLogEntriesToShowCount = 0;
  1019. uncollapsedLogEntries.TrimStart( numberOfLogsToRemove, removeUncollapsedLogEntryAction );
  1020. if( uncollapsedLogEntriesTimestamps != null )
  1021. uncollapsedLogEntriesTimestamps.TrimStart( numberOfLogsToRemove );
  1022. if( removedLogEntriesToShowCount > 0 )
  1023. {
  1024. logEntriesToShow.TrimStart( removedLogEntriesToShowCount );
  1025. if( timestampsOfLogEntriesToShow != null )
  1026. timestampsOfLogEntriesToShow.TrimStart( removedLogEntriesToShowCount );
  1027. }
  1028. if( anyCollapsedLogRemoved )
  1029. {
  1030. collapsedLogEntries.RemoveAll( shouldRemoveCollapsedLogEntryPredicate, updateLogEntryCollapsedIndexAction, collapsedLogEntriesTimestamps );
  1031. if( isCollapseOn )
  1032. removedLogEntriesToShowCount = logEntriesToShow.RemoveAll( shouldRemoveLogEntryToShowPredicate, null, timestampsOfLogEntriesToShow );
  1033. }
  1034. if( removedLogEntriesToShowCount > 0 )
  1035. {
  1036. if( logEntryToSelectAndFocus == null || logEntryToSelectAndFocus.count == 0 )
  1037. indexOfLogEntryToSelectAndFocus = -1;
  1038. else
  1039. {
  1040. for( int i = Mathf.Min( indexOfLogEntryToSelectAndFocus, logEntriesToShow.Count - 1 ); i >= 0; i-- )
  1041. {
  1042. if( logEntriesToShow[i] == logEntryToSelectAndFocus )
  1043. {
  1044. indexOfLogEntryToSelectAndFocus = i;
  1045. break;
  1046. }
  1047. }
  1048. }
  1049. recycledListView.OnLogEntriesRemoved( removedLogEntriesToShowCount );
  1050. if( isLogWindowVisible )
  1051. OnLogEntriesUpdated( false, true );
  1052. }
  1053. else if( isLogWindowVisible && isCollapseOn )
  1054. recycledListView.RefreshCollapsedLogEntryCounts();
  1055. entryCountTextsDirty = true;
  1056. }
  1057. private void RemoveUncollapsedLogEntry( DebugLogEntry logEntry )
  1058. {
  1059. if( --logEntry.count <= 0 )
  1060. anyCollapsedLogRemoved = true;
  1061. if( !isCollapseOn && logEntriesToShow[removedLogEntriesToShowCount] == logEntry )
  1062. removedLogEntriesToShowCount++;
  1063. if( logEntry.logType == LogType.Log )
  1064. infoEntryCount--;
  1065. else if( logEntry.logType == LogType.Warning )
  1066. warningEntryCount--;
  1067. else
  1068. errorEntryCount--;
  1069. }
  1070. private bool ShouldRemoveCollapsedLogEntry( DebugLogEntry logEntry )
  1071. {
  1072. if( logEntry.count <= 0 )
  1073. {
  1074. PoolLogEntry( logEntry );
  1075. collapsedLogEntriesMap.Remove( logEntry );
  1076. return true;
  1077. }
  1078. return false;
  1079. }
  1080. private bool ShouldRemoveLogEntryToShow( DebugLogEntry logEntry )
  1081. {
  1082. return logEntry.count <= 0;
  1083. }
  1084. private void UpdateLogEntryCollapsedIndex( DebugLogEntry logEntry, int collapsedIndex )
  1085. {
  1086. logEntry.collapsedIndex = collapsedIndex;
  1087. }
  1088. private void OnLogEntriesUpdated( bool updateAllVisibleItemContents, bool validateScrollPosition )
  1089. {
  1090. recycledListView.OnLogEntriesUpdated( updateAllVisibleItemContents );
  1091. shouldUpdateRecycledListView = false;
  1092. if( validateScrollPosition )
  1093. ValidateScrollPosition();
  1094. }
  1095. private void PoolLogEntry( DebugLogEntry logEntry )
  1096. {
  1097. if( pooledLogEntries.Count < 4096 )
  1098. {
  1099. logEntry.Clear();
  1100. pooledLogEntries.Push( logEntry );
  1101. }
  1102. }
  1103. // Make sure the scroll bar of the scroll rect is adjusted properly
  1104. internal void ValidateScrollPosition()
  1105. {
  1106. // When scrollbar is snapped to the very bottom of the scroll view, sometimes OnScroll alone doesn't work
  1107. if( logItemsScrollRect.verticalNormalizedPosition <= Mathf.Epsilon )
  1108. logItemsScrollRect.verticalNormalizedPosition = 0.0001f;
  1109. logItemsScrollRect.OnScroll( nullPointerEventData );
  1110. }
  1111. // Modifies certain properties of the most recently received log
  1112. public void AdjustLatestPendingLog( bool autoExpand, bool stripStackTrace )
  1113. {
  1114. lock( logEntriesLock )
  1115. {
  1116. if( queuedLogEntries.Count == 0 )
  1117. return;
  1118. if( autoExpand ) // Automatically expand the latest log in queuedLogEntries
  1119. pendingLogToAutoExpand = queuedLogEntries.Count;
  1120. if( stripStackTrace ) // Omit the latest log's stack trace
  1121. {
  1122. QueuedDebugLogEntry log = queuedLogEntries[queuedLogEntries.Count - 1];
  1123. queuedLogEntries[queuedLogEntries.Count - 1] = new QueuedDebugLogEntry( log.logString, string.Empty, log.logType );
  1124. }
  1125. }
  1126. }
  1127. // Clear all the logs
  1128. public void ClearLogs()
  1129. {
  1130. SnapToBottom = true;
  1131. indexOfLogEntryToSelectAndFocus = -1;
  1132. infoEntryCount = 0;
  1133. warningEntryCount = 0;
  1134. errorEntryCount = 0;
  1135. infoEntryCountText.text = "0";
  1136. warningEntryCountText.text = "0";
  1137. errorEntryCountText.text = "0";
  1138. collapsedLogEntries.ForEach( poolLogEntryAction );
  1139. collapsedLogEntries.Clear();
  1140. collapsedLogEntriesMap.Clear();
  1141. uncollapsedLogEntries.Clear();
  1142. logEntriesToShow.Clear();
  1143. if( collapsedLogEntriesTimestamps != null )
  1144. {
  1145. collapsedLogEntriesTimestamps.Clear();
  1146. uncollapsedLogEntriesTimestamps.Clear();
  1147. timestampsOfLogEntriesToShow.Clear();
  1148. }
  1149. recycledListView.DeselectSelectedLogItem();
  1150. OnLogEntriesUpdated( true, true );
  1151. }
  1152. // Collapse button is clicked
  1153. private void CollapseButtonPressed()
  1154. {
  1155. // Swap the value of collapse mode
  1156. isCollapseOn = !isCollapseOn;
  1157. collapseButton.color = isCollapseOn ? collapseButtonSelectedColor : collapseButtonNormalColor;
  1158. recycledListView.SetCollapseMode( isCollapseOn );
  1159. // Determine the new list of debug entries to show
  1160. FilterLogs();
  1161. }
  1162. // Filtering mode of info logs has changed
  1163. private void FilterLogButtonPressed()
  1164. {
  1165. logFilter = logFilter ^ DebugLogFilter.Info;
  1166. if( ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info )
  1167. filterInfoButton.color = filterButtonsSelectedColor;
  1168. else
  1169. filterInfoButton.color = filterButtonsNormalColor;
  1170. FilterLogs();
  1171. }
  1172. // Filtering mode of warning logs has changed
  1173. private void FilterWarningButtonPressed()
  1174. {
  1175. logFilter = logFilter ^ DebugLogFilter.Warning;
  1176. if( ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning )
  1177. filterWarningButton.color = filterButtonsSelectedColor;
  1178. else
  1179. filterWarningButton.color = filterButtonsNormalColor;
  1180. FilterLogs();
  1181. }
  1182. // Filtering mode of error logs has changed
  1183. private void FilterErrorButtonPressed()
  1184. {
  1185. logFilter = logFilter ^ DebugLogFilter.Error;
  1186. if( ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error )
  1187. filterErrorButton.color = filterButtonsSelectedColor;
  1188. else
  1189. filterErrorButton.color = filterButtonsNormalColor;
  1190. FilterLogs();
  1191. }
  1192. // Search term has changed
  1193. private void SearchTermChanged( string searchTerm )
  1194. {
  1195. if( searchTerm != null )
  1196. searchTerm = searchTerm.Trim();
  1197. this.searchTerm = searchTerm;
  1198. bool isInSearchMode = !string.IsNullOrEmpty( searchTerm );
  1199. if( isInSearchMode || this.isInSearchMode )
  1200. {
  1201. this.isInSearchMode = isInSearchMode;
  1202. FilterLogs();
  1203. }
  1204. }
  1205. // Show suggestions for the currently entered command
  1206. private void RefreshCommandSuggestions( string command )
  1207. {
  1208. if( !showCommandSuggestions )
  1209. return;
  1210. commandInputFieldPrevCaretPos = commandInputField.caretPosition;
  1211. // Don't recalculate the command suggestions if the input command hasn't changed (i.e. only caret's position has changed)
  1212. bool commandChanged = command != commandInputFieldPrevCommand;
  1213. bool commandNameOrParametersChanged = false;
  1214. if( commandChanged )
  1215. {
  1216. commandInputFieldPrevCommand = command;
  1217. matchingCommandSuggestions.Clear();
  1218. commandCaretIndexIncrements.Clear();
  1219. string prevCommandName = commandInputFieldPrevCommandName;
  1220. int numberOfParameters;
  1221. DebugLogConsole.GetCommandSuggestions( command, matchingCommandSuggestions, commandCaretIndexIncrements, ref commandInputFieldPrevCommandName, out numberOfParameters );
  1222. if( prevCommandName != commandInputFieldPrevCommandName || numberOfParameters != commandInputFieldPrevParamCount )
  1223. {
  1224. commandInputFieldPrevParamCount = numberOfParameters;
  1225. commandNameOrParametersChanged = true;
  1226. }
  1227. }
  1228. int caretArgumentIndex = 0;
  1229. int caretPos = commandInputField.caretPosition;
  1230. for( int i = 0; i < commandCaretIndexIncrements.Count && caretPos > commandCaretIndexIncrements[i]; i++ )
  1231. caretArgumentIndex++;
  1232. if( caretArgumentIndex != commandInputFieldPrevCaretArgumentIndex )
  1233. commandInputFieldPrevCaretArgumentIndex = caretArgumentIndex;
  1234. else if( !commandChanged || !commandNameOrParametersChanged )
  1235. {
  1236. // Command suggestions don't need to be updated if:
  1237. // a) neither the entered command nor the argument that the caret is hovering has changed
  1238. // b) entered command has changed but command's name hasn't changed, parameter count hasn't changed and the argument
  1239. // that the caret is hovering hasn't changed (i.e. user has continued typing a parameter's value)
  1240. return;
  1241. }
  1242. if( matchingCommandSuggestions.Count == 0 )
  1243. OnEndEditCommand( command );
  1244. else
  1245. {
  1246. if( !commandSuggestionsContainer.gameObject.activeSelf )
  1247. commandSuggestionsContainer.gameObject.SetActive( true );
  1248. int suggestionInstancesCount = commandSuggestionInstances.Count;
  1249. int suggestionsCount = matchingCommandSuggestions.Count;
  1250. for( int i = 0; i < suggestionsCount; i++ )
  1251. {
  1252. if( i >= visibleCommandSuggestionInstances )
  1253. {
  1254. if( i >= suggestionInstancesCount )
  1255. commandSuggestionInstances.Add( Instantiate( commandSuggestionPrefab, commandSuggestionsContainer, false ) );
  1256. else
  1257. commandSuggestionInstances[i].gameObject.SetActive( true );
  1258. visibleCommandSuggestionInstances++;
  1259. }
  1260. ConsoleMethodInfo suggestedCommand = matchingCommandSuggestions[i];
  1261. sharedStringBuilder.Length = 0;
  1262. if( caretArgumentIndex > 0 )
  1263. sharedStringBuilder.Append( suggestedCommand.command );
  1264. else
  1265. sharedStringBuilder.Append( commandSuggestionHighlightStart ).Append( matchingCommandSuggestions[i].command ).Append( commandSuggestionHighlightEnd );
  1266. if( suggestedCommand.parameters.Length > 0 )
  1267. {
  1268. sharedStringBuilder.Append( " " );
  1269. // If the command name wasn't highlighted, a parameter must always be highlighted
  1270. int caretParameterIndex = caretArgumentIndex - 1;
  1271. if( caretParameterIndex >= suggestedCommand.parameters.Length )
  1272. caretParameterIndex = suggestedCommand.parameters.Length - 1;
  1273. for( int j = 0; j < suggestedCommand.parameters.Length; j++ )
  1274. {
  1275. if( caretParameterIndex != j )
  1276. sharedStringBuilder.Append( suggestedCommand.parameters[j] );
  1277. else
  1278. sharedStringBuilder.Append( commandSuggestionHighlightStart ).Append( suggestedCommand.parameters[j] ).Append( commandSuggestionHighlightEnd );
  1279. }
  1280. }
  1281. commandSuggestionInstances[i].text = sharedStringBuilder.ToString();
  1282. }
  1283. for( int i = visibleCommandSuggestionInstances - 1; i >= suggestionsCount; i-- )
  1284. commandSuggestionInstances[i].gameObject.SetActive( false );
  1285. visibleCommandSuggestionInstances = suggestionsCount;
  1286. }
  1287. }
  1288. // Command input field's text has changed
  1289. private void OnEditCommand( string command )
  1290. {
  1291. RefreshCommandSuggestions( command );
  1292. if( !commandInputFieldAutoCompletedNow )
  1293. commandInputFieldAutoCompleteBase = null;
  1294. else // This change was caused by autocomplete
  1295. commandInputFieldAutoCompletedNow = false;
  1296. }
  1297. // Command input field has lost focus
  1298. private void OnEndEditCommand( string command )
  1299. {
  1300. if( commandSuggestionsContainer.gameObject.activeSelf )
  1301. commandSuggestionsContainer.gameObject.SetActive( false );
  1302. }
  1303. // Debug window is being resized,
  1304. // Set the sizeDelta property of the window accordingly while
  1305. // preventing window dimensions from going below the minimum dimensions
  1306. internal void Resize( PointerEventData eventData )
  1307. {
  1308. Vector2 localPoint;
  1309. if( !RectTransformUtility.ScreenPointToLocalPointInRectangle( canvasTR, eventData.position, eventData.pressEventCamera, out localPoint ) )
  1310. return;
  1311. // To be able to maximize the log window easily:
  1312. // - When enableHorizontalResizing is true and resizing horizontally, resize button will be grabbed from its left edge (if resizeFromRight is true) or its right edge
  1313. // - While resizing vertically, resize button will be grabbed from its top edge
  1314. Rect resizeButtonRect = ( (RectTransform) resizeButton.rectTransform.parent ).rect;
  1315. float resizeButtonWidth = resizeButtonRect.width;
  1316. float resizeButtonHeight = resizeButtonRect.height;
  1317. Vector2 canvasPivot = canvasTR.pivot;
  1318. Vector2 canvasSize = canvasTR.rect.size;
  1319. Vector2 anchorMin = logWindowTR.anchorMin;
  1320. // Horizontal resizing
  1321. if( enableHorizontalResizing )
  1322. {
  1323. if( resizeFromRight )
  1324. {
  1325. localPoint.x += canvasPivot.x * canvasSize.x + resizeButtonWidth;
  1326. if( localPoint.x < minimumWidth )
  1327. localPoint.x = minimumWidth;
  1328. Vector2 anchorMax = logWindowTR.anchorMax;
  1329. anchorMax.x = Mathf.Clamp01( localPoint.x / canvasSize.x );
  1330. logWindowTR.anchorMax = anchorMax;
  1331. }
  1332. else
  1333. {
  1334. localPoint.x += canvasPivot.x * canvasSize.x - resizeButtonWidth;
  1335. if( localPoint.x > canvasSize.x - minimumWidth )
  1336. localPoint.x = canvasSize.x - minimumWidth;
  1337. anchorMin.x = Mathf.Clamp01( localPoint.x / canvasSize.x );
  1338. }
  1339. }
  1340. // Vertical resizing
  1341. float notchHeight = -logWindowTR.sizeDelta.y; // Size of notch screen cutouts at the top of the screen
  1342. localPoint.y += canvasPivot.y * canvasSize.y - resizeButtonHeight;
  1343. if( localPoint.y > canvasSize.y - minimumHeight - notchHeight )
  1344. localPoint.y = canvasSize.y - minimumHeight - notchHeight;
  1345. anchorMin.y = Mathf.Clamp01( localPoint.y / canvasSize.y );
  1346. logWindowTR.anchorMin = anchorMin;
  1347. // Update the recycled list view
  1348. recycledListView.OnViewportHeightChanged();
  1349. }
  1350. // Determine the filtered list of debug entries to show on screen
  1351. private void FilterLogs()
  1352. {
  1353. recycledListView.OnBeforeFilterLogs();
  1354. logEntriesToShow.Clear();
  1355. if( timestampsOfLogEntriesToShow != null )
  1356. timestampsOfLogEntriesToShow.Clear();
  1357. if( logFilter != DebugLogFilter.None )
  1358. {
  1359. DynamicCircularBuffer<DebugLogEntry> targetLogEntries = isCollapseOn ? collapsedLogEntries : uncollapsedLogEntries;
  1360. DynamicCircularBuffer<DebugLogEntryTimestamp> targetLogEntriesTimestamps = isCollapseOn ? collapsedLogEntriesTimestamps : uncollapsedLogEntriesTimestamps;
  1361. if( logFilter == DebugLogFilter.All )
  1362. {
  1363. if( !isInSearchMode )
  1364. {
  1365. logEntriesToShow.AddRange( targetLogEntries );
  1366. if( timestampsOfLogEntriesToShow != null )
  1367. timestampsOfLogEntriesToShow.AddRange( targetLogEntriesTimestamps );
  1368. }
  1369. else
  1370. {
  1371. for( int i = 0, count = targetLogEntries.Count; i < count; i++ )
  1372. {
  1373. if( targetLogEntries[i].MatchesSearchTerm( searchTerm ) )
  1374. {
  1375. logEntriesToShow.Add( targetLogEntries[i] );
  1376. if( timestampsOfLogEntriesToShow != null )
  1377. timestampsOfLogEntriesToShow.Add( targetLogEntriesTimestamps[i] );
  1378. }
  1379. }
  1380. }
  1381. }
  1382. else
  1383. {
  1384. // Show only the debug entries that match the current filter
  1385. bool isInfoEnabled = ( logFilter & DebugLogFilter.Info ) == DebugLogFilter.Info;
  1386. bool isWarningEnabled = ( logFilter & DebugLogFilter.Warning ) == DebugLogFilter.Warning;
  1387. bool isErrorEnabled = ( logFilter & DebugLogFilter.Error ) == DebugLogFilter.Error;
  1388. for( int i = 0, count = targetLogEntries.Count; i < count; i++ )
  1389. {
  1390. DebugLogEntry logEntry = targetLogEntries[i];
  1391. if( isInSearchMode && !logEntry.MatchesSearchTerm( searchTerm ) )
  1392. continue;
  1393. bool shouldShowLog = false;
  1394. if( logEntry.logType == LogType.Log )
  1395. {
  1396. if( isInfoEnabled )
  1397. shouldShowLog = true;
  1398. }
  1399. else if( logEntry.logType == LogType.Warning )
  1400. {
  1401. if( isWarningEnabled )
  1402. shouldShowLog = true;
  1403. }
  1404. else if( isErrorEnabled )
  1405. shouldShowLog = true;
  1406. if( shouldShowLog )
  1407. {
  1408. logEntriesToShow.Add( logEntry );
  1409. if( timestampsOfLogEntriesToShow != null )
  1410. timestampsOfLogEntriesToShow.Add( targetLogEntriesTimestamps[i] );
  1411. }
  1412. }
  1413. }
  1414. }
  1415. // Update the recycled list view
  1416. recycledListView.OnAfterFilterLogs();
  1417. OnLogEntriesUpdated( true, true );
  1418. }
  1419. public string GetAllLogs()
  1420. {
  1421. return GetAllLogs(int.MaxValue, float.PositiveInfinity);
  1422. }
  1423. /// <param name="maxLogCount">Maximum allowed log count.</param>
  1424. /// <param name="maxElapsedTime">Maximum allowed time interval (in seconds) between now and the logs' arrival time (requires <see cref="captureLogTimestamps"/> to be enabled).</param>
  1425. public string GetAllLogs(int maxLogCount, float maxElapsedTime)
  1426. {
  1427. // Process all pending logs since we want to return "all" logs
  1428. ProcessQueuedLogs( queuedLogEntries.Count );
  1429. int startIndex = uncollapsedLogEntries.Count - Mathf.Min(uncollapsedLogEntries.Count, maxLogCount);
  1430. if (uncollapsedLogEntriesTimestamps != null)
  1431. {
  1432. float currentElapsedSeconds = Time.realtimeSinceStartup;
  1433. while (startIndex < uncollapsedLogEntries.Count && currentElapsedSeconds - uncollapsedLogEntriesTimestamps[startIndex].elapsedSeconds > maxElapsedTime)
  1434. startIndex++;
  1435. }
  1436. int length = 0;
  1437. int newLineLength = System.Environment.NewLine.Length;
  1438. for (int i = startIndex; i < uncollapsedLogEntries.Count; i++)
  1439. {
  1440. DebugLogEntry entry = uncollapsedLogEntries[i];
  1441. length += entry.logString.Length + entry.stackTrace.Length + newLineLength * 3;
  1442. }
  1443. if (uncollapsedLogEntriesTimestamps != null)
  1444. length += (uncollapsedLogEntries.Count - startIndex) * 30;
  1445. length += 200; // Just in case...
  1446. StringBuilder sb = new StringBuilder( length );
  1447. for (int i = startIndex; i < uncollapsedLogEntries.Count; i++)
  1448. {
  1449. DebugLogEntry entry = uncollapsedLogEntries[i];
  1450. if( uncollapsedLogEntriesTimestamps != null )
  1451. {
  1452. uncollapsedLogEntriesTimestamps[i].AppendFullTimestamp( sb );
  1453. sb.Append( ": " );
  1454. }
  1455. sb.AppendLine( entry.logString ).AppendLine( entry.stackTrace ).AppendLine();
  1456. }
  1457. sb.Append( "Current time: " ).AppendLine( ( System.DateTime.UtcNow + localTimeUtcOffset ).ToString( "F" ) );
  1458. sb.Append( "Version: " ).AppendLine( Application.version );
  1459. return sb.ToString();
  1460. }
  1461. /// <param name="logTimestamps">Is <c>null</c> if <see cref="captureLogTimestamps"/> is <c>false</c>. Indices are in sync with <paramref name="logEntries"/>.</param>
  1462. /// <remarks>You mustn't modify the returned buffers in any way.</remarks>
  1463. public void GetAllLogs( out DynamicCircularBuffer<DebugLogEntry> logEntries, out DynamicCircularBuffer<DebugLogEntryTimestamp> logTimestamps )
  1464. {
  1465. // Process all pending logs since we want to return "all" logs
  1466. ProcessQueuedLogs( queuedLogEntries.Count );
  1467. logEntries = uncollapsedLogEntries;
  1468. logTimestamps = uncollapsedLogEntriesTimestamps;
  1469. }
  1470. public void SaveLogsToFile()
  1471. {
  1472. SaveLogsToFile( Path.Combine( Application.persistentDataPath, System.DateTime.Now.ToString( "dd-MM-yyyy--HH-mm-ss" ) + ".txt" ) );
  1473. }
  1474. public void SaveLogsToFile( string filePath )
  1475. {
  1476. File.WriteAllText( filePath, GetAllLogs() );
  1477. Debug.Log( "Logs saved to: " + filePath );
  1478. }
  1479. // If a cutout is intersecting with debug window on notch screens, shift the window downwards
  1480. private void CheckScreenCutout()
  1481. {
  1482. if( !avoidScreenCutout )
  1483. return;
  1484. #if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS
  1485. // Check if there is a cutout at the top of the screen
  1486. int screenHeight = Screen.height;
  1487. float safeYMax = Screen.safeArea.yMax;
  1488. if( safeYMax < screenHeight - 1 ) // 1: a small threshold
  1489. {
  1490. // There is a cutout, shift the log window downwards
  1491. float cutoutPercentage = ( screenHeight - safeYMax ) / Screen.height;
  1492. float cutoutLocalSize = cutoutPercentage * canvasTR.rect.height;
  1493. logWindowTR.anchoredPosition = new Vector2( 0f, -cutoutLocalSize );
  1494. logWindowTR.sizeDelta = new Vector2( 0f, -cutoutLocalSize );
  1495. }
  1496. else
  1497. {
  1498. logWindowTR.anchoredPosition = Vector2.zero;
  1499. logWindowTR.sizeDelta = Vector2.zero;
  1500. }
  1501. #endif
  1502. }
  1503. #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL
  1504. private IEnumerator ActivateCommandInputFieldCoroutine()
  1505. {
  1506. // Waiting 1 frame before activating commandInputField ensures that the toggleKey isn't captured by it
  1507. yield return null;
  1508. commandInputField.ActivateInputField();
  1509. yield return null;
  1510. commandInputField.MoveTextEnd( false );
  1511. }
  1512. #endif
  1513. // Pool an unused log item
  1514. internal void PoolLogItem( DebugLogItem logItem )
  1515. {
  1516. logItem.CanvasGroup.alpha = 0f;
  1517. logItem.CanvasGroup.blocksRaycasts = false;
  1518. pooledLogItems.Push( logItem );
  1519. }
  1520. // Fetch a log item from the pool
  1521. internal DebugLogItem PopLogItem()
  1522. {
  1523. DebugLogItem newLogItem;
  1524. // If pool is not empty, fetch a log item from the pool,
  1525. // create a new log item otherwise
  1526. if( pooledLogItems.Count > 0 )
  1527. {
  1528. newLogItem = pooledLogItems.Pop();
  1529. newLogItem.CanvasGroup.alpha = 1f;
  1530. newLogItem.CanvasGroup.blocksRaycasts = true;
  1531. }
  1532. else
  1533. {
  1534. newLogItem = (DebugLogItem) Instantiate( logItemPrefab, logItemsContainer, false );
  1535. newLogItem.Initialize( recycledListView );
  1536. }
  1537. return newLogItem;
  1538. }
  1539. }
  1540. }