Files
2026-06-17 20:44:53 +03:00

1217 lines
42 KiB
C#

using QFSW.QC.Pooling;
using QFSW.QC.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace QFSW.QC
{
/// <summary>
/// Provides the UI and I/O interface for the QuantumConsoleProcessor. Invokes commands on the processor and displays the output.
/// </summary>
[DisallowMultipleComponent]
public class QuantumConsole : MonoBehaviour
{
/// <summary>
/// Singleton reference to the console. Only valid and set if the singleton option is enabled for the console.
/// </summary>
public static QuantumConsole Instance { get; private set; }
#pragma warning disable 0414, 0067, 0649
[SerializeField] private RectTransform _containerRect;
[SerializeField] private ScrollRect _scrollRect;
[SerializeField] private RectTransform _suggestionPopupRect;
[SerializeField] private RectTransform _jobCounterRect;
[SerializeField] private Image[] _panels;
[SerializeField] private QuantumTheme _theme;
[SerializeField] private QuantumKeyConfig _keyConfig;
[SerializeField] private QuantumLocalization _localization;
public QuantumTheme Theme => _theme;
public QuantumKeyConfig KeyConfig
{
get => _keyConfig;
set => _keyConfig = value;
}
public QuantumLocalization Localization
{
get => _localization;
set => _localization = value;
}
[Command("verbose-errors", "If errors caused by the Quantum Console Processor or commands should be logged in verbose mode.", MonoTargetType.Registry)]
[SerializeField] private bool _verboseErrors = false;
[Command("verbose-logging", "The minimum log severity required to use verbose logging.", MonoTargetType.Registry)]
[SerializeField] private LoggingThreshold _verboseLogging = LoggingThreshold.Never;
[Command("logging-level", "The minimum log severity required to intercept and display the log.", MonoTargetType.Registry)]
[SerializeField] private LoggingThreshold _loggingLevel = LoggingThreshold.Always;
[SerializeField] private LoggingThreshold _openOnLogLevel = LoggingThreshold.Never;
[SerializeField] private bool _interceptDebugLogger = true;
[SerializeField] private bool _interceptWhilstInactive = true;
[SerializeField] private bool _prependTimestamps = false;
[SerializeField] private SupportedState _supportedState = SupportedState.Always;
[SerializeField] private bool _activateOnStartup = true;
[SerializeField] private bool _initialiseOnStartup = false;
[SerializeField] private bool _focusOnActivate = true;
[SerializeField] private bool _closeOnSubmit = false;
[SerializeField] private bool _singletonMode = false;
[SerializeField] private AutoScrollOptions _autoScroll = AutoScrollOptions.OnInvoke;
[SerializeField] private bool _enableAutocomplete = true;
[SerializeField] private bool _showPopupDisplay = true;
[SerializeField] private SortOrder _suggestionDisplayOrder = SortOrder.Descending;
[SerializeField] private int _maxSuggestionDisplaySize = -1;
[SerializeField] private bool _useFuzzySearch = false;
[SerializeField] private bool _caseSensitiveSearch = true;
[SerializeField] private bool _collapseSuggestionOverloads = true;
[SerializeField] private bool _showCurrentJobs = true;
[SerializeField] private bool _blockOnAsync = false;
[SerializeField] private bool _storeCommandHistory = true;
[SerializeField] private bool _storeDuplicateCommands = true;
[SerializeField] private bool _storeAdjacentDuplicateCommands = false;
[SerializeField] private int _commandHistorySize = -1;
[SerializeField] private int _maxStoredLogs = 1024;
[SerializeField] private int _maxLogSize = 8192;
[SerializeField] private bool _showInitLogs = true;
[SerializeField] private TMP_InputField _consoleInput;
[SerializeField] private TextMeshProUGUI _inputPlaceholderText;
[SerializeField] private TextMeshProUGUI _consoleLogText;
[SerializeField] private TextMeshProUGUI _consoleSuggestionText;
[SerializeField] private TextMeshProUGUI _suggestionPopupText;
[SerializeField] private TextMeshProUGUI _jobCounterText;
/// <summary>
/// The maximum number of logs that may be stored in the log storage before old logs are removed.
/// </summary>
[Command("max-logs", MonoTargetType.Registry)]
[CommandDescription("The maximum number of logs that may be stored in the log storage before old logs are removed.")]
public int MaxStoredLogs
{
get => _maxStoredLogs;
set
{
_maxStoredLogs = value;
if (_logStorage != null) { _logStorage.MaxStoredLogs = value; }
if (_logQueue != null) { _logQueue.MaxStoredLogs = value; }
}
}
#pragma warning restore 0414, 0067, 0649
#region Callbacks
/// <summary>Callback executed when the QC state changes.</summary>
public event Action OnStateChange;
/// <summary>Callback executed when the QC invokes a command.</summary>
public event Action<string> OnInvoke;
/// <summary>Callback executed when the QC is cleared.</summary>
public event Action OnClear;
/// <summary>Callback executed when text has been logged to the QC.</summary>
public event Action<ILog> OnLog;
/// <summary>Callback executed when the QC is activated.</summary>
public event Action OnActivate;
/// <summary>Callback executed when the QC is deactivated.</summary>
public event Action OnDeactivate;
/// <summary>Callback executed when a new suggestion set is created. The user may modify the suggestion set.</summary>
public event Action<SuggestionSet> OnSuggestionSetGenerated;
#endregion
private bool IsBlockedByAsync => (_blockOnAsync
&& _currentTasks.Count > 0
|| _currentActions.Count > 0)
&& !_isHandlingUserResponse;
private readonly QuantumSerializer _serializer = new QuantumSerializer();
private SuggestionStack _suggestionStack;
private ILogStorage _logStorage;
private ILogQueue _logQueue;
public bool IsActive { get; private set; }
public bool IsFocused => IsActive && _consoleInput && _consoleInput.isFocused;
/// <summary>
/// If any actions are currently executing
/// </summary>
public bool AreActionsExecuting => _currentActions.Count > 0;
private readonly List<string> _previousCommands = new List<string>();
private readonly List<Task> _currentTasks = new List<Task>();
private readonly List<IEnumerator<ICommandAction>> _currentActions = new List<IEnumerator<ICommandAction>>();
private readonly StringBuilderPool _stringBuilderPool = new StringBuilderPool();
private int _selectedPreviousCommandIndex = -1;
private string _currentInput;
private string _previousInput;
private bool _isGeneratingTable;
private bool _consoleRequiresFlush;
private bool _isHandlingUserResponse = false;
private ResponseConfig _currentResponseConfig;
private Action<string> _onSubmitResponseCallback;
private TextMeshProUGUI[] _textComponents;
private readonly Type _voidTaskType = typeof(Task<>).MakeGenericType(Type.GetType("System.Threading.Tasks.VoidTaskResult"));
/// <summary>Applies a theme to the Quantum Console.</summary>
/// <param name="theme">The desired theme to apply.</param>
public void ApplyTheme(QuantumTheme theme, bool forceRefresh = false)
{
_theme = theme;
if (theme)
{
if (_textComponents == null || forceRefresh) { _textComponents = GetComponentsInChildren<TextMeshProUGUI>(true); }
foreach (TextMeshProUGUI text in _textComponents)
{
if (theme.Font)
{
text.font = theme.Font;
}
}
foreach (Image panel in _panels)
{
panel.material = theme.PanelMaterial;
panel.color = theme.PanelColor;
}
}
}
protected virtual void Update()
{
if (!IsActive)
{
if (_keyConfig.ShowConsoleKey.IsPressed() || _keyConfig.ToggleConsoleVisibilityKey.IsPressed())
{
Activate();
}
}
else
{
ProcessAsyncTasks();
ProcessActions();
HandleAsyncJobCounter();
if (_keyConfig.HideConsoleKey.IsPressed() || _keyConfig.ToggleConsoleVisibilityKey.IsPressed())
{
Deactivate();
return;
}
if (QuantumConsoleProcessor.TableIsGenerating)
{
_consoleInput.interactable = false;
string consoleText = $"{_logStorage.GetLogString()}\n{GetTableGenerationText()}".Trim();
if (consoleText != _consoleLogText.text)
{
if (_showInitLogs)
{
OnStateChange?.Invoke();
_consoleLogText.text = consoleText;
}
if (_inputPlaceholderText) { _inputPlaceholderText.text = _localization.Loading; }
}
return;
}
else if (IsBlockedByAsync)
{
OnStateChange?.Invoke();
_consoleInput.interactable = false;
if (_inputPlaceholderText) { _inputPlaceholderText.text = _localization.ExecutingAsyncCommand; }
}
else if (!_consoleInput.interactable)
{
OnStateChange?.Invoke();
_consoleInput.interactable = true;
if (_inputPlaceholderText) { _inputPlaceholderText.text = _localization.EnterCommand; }
OverrideConsoleInput(string.Empty);
if (_isGeneratingTable)
{
if (_showInitLogs)
{
AppendLog(new Log(GetTableGenerationText()));
_consoleLogText.text = _logStorage.GetLogString();
}
_isGeneratingTable = false;
ScrollConsoleToLatest();
}
}
_previousInput = _currentInput;
_currentInput = _consoleInput.text;
if (_currentInput != _previousInput)
{
OnInputChange();
}
else if (!IsBlockedByAsync)
{
if (InputHelper.GetKeyDown(_keyConfig.SubmitCommandKey)) { InvokeCommand(); }
if (_storeCommandHistory) { ProcessCommandHistory(); }
ProcessAutocomplete();
}
}
}
private void LateUpdate()
{
if (IsActive)
{
FlushQueuedLogs();
FlushToConsoleText();
}
}
private string GetTableGenerationText()
{
string text = string.Format(_localization.InitializationProgress, QuantumConsoleProcessor.LoadedCommandCount);
if (QuantumConsoleProcessor.TableIsGenerating)
{
text += "...";
}
else
{
string completionText =
_theme == null
? _localization.InitializationComplete
: _localization.InitializationComplete.ColorText(_theme.SuccessColor);
text += $"\n{completionText}";
}
return text;
}
private void ProcessCommandHistory()
{
if (InputHelper.GetKeyDown(_keyConfig.NextCommandKey) || InputHelper.GetKeyDown(_keyConfig.PreviousCommandKey))
{
if (InputHelper.GetKeyDown(_keyConfig.NextCommandKey)) { _selectedPreviousCommandIndex++; }
else if (_selectedPreviousCommandIndex > 0) { _selectedPreviousCommandIndex--; }
_selectedPreviousCommandIndex = Mathf.Clamp(_selectedPreviousCommandIndex, -1, _previousCommands.Count - 1);
if (_selectedPreviousCommandIndex > -1)
{
string command = _previousCommands[_previousCommands.Count - _selectedPreviousCommandIndex - 1];
OverrideConsoleInput(command);
}
}
}
private void UpdateSuggestions()
{
// don't show suggestions when the user is responding to a prompt
if (_isHandlingUserResponse)
{
ClearSuggestions();
ClearPopup();
return;
}
SuggestorOptions options = new SuggestorOptions
{
CaseSensitive = _caseSensitiveSearch,
Fuzzy = _useFuzzySearch,
CollapseOverloads = _collapseSuggestionOverloads,
};
_suggestionStack.UpdateStack(_currentInput, options);
UpdateSuggestionText();
if (_showPopupDisplay)
{
UpdatePopupDisplay();
}
}
private void ProcessAutocomplete()
{
if (!_enableAutocomplete)
{
return;
}
if (_keyConfig.SelectNextSuggestionKey.IsPressed() || _keyConfig.SelectPreviousSuggestionKey.IsPressed())
{
SuggestionSet set = _suggestionStack.TopmostSuggestionSet;
if (set != null && set.Suggestions.Count > 0)
{
if (_keyConfig.SelectNextSuggestionKey.IsPressed()) { set.SelectionIndex++; }
if (_keyConfig.SelectPreviousSuggestionKey.IsPressed()) { set.SelectionIndex--; }
set.SelectionIndex += set.Suggestions.Count;
set.SelectionIndex %= set.Suggestions.Count;
SetSuggestion(set.SelectionIndex);
}
}
}
private void FormatSuggestion(IQcSuggestion suggestion, bool selected, StringBuilder buffer)
{
if (!_theme)
{
buffer.Append(suggestion.FullSignature);
return;
}
Color primaryColor = Color.white;
Color secondaryColor = _theme.SuggestionColor;
if (selected)
{
primaryColor *= _theme.SelectedSuggestionColor;
secondaryColor *= _theme.SelectedSuggestionColor;
}
buffer.AppendColoredText(suggestion.PrimarySignature, primaryColor);
buffer.AppendColoredText(suggestion.SecondarySignature, secondaryColor);
}
private string GetFormattedSuggestions(SuggestionSet suggestionSet)
{
StringBuilder buffer = _stringBuilderPool.GetStringBuilder();
GetFormattedSuggestions(suggestionSet, buffer);
return _stringBuilderPool.ReleaseAndToString(buffer);
}
private void GetFormattedSuggestions(SuggestionSet suggestionSet, StringBuilder buffer)
{
int displaySize = suggestionSet.Suggestions.Count;
if (_maxSuggestionDisplaySize > 0)
{
displaySize = Mathf.Min(displaySize, _maxSuggestionDisplaySize + 1);
}
for (int i = 0; i < displaySize; i++)
{
if (_maxSuggestionDisplaySize > 0 && i >= _maxSuggestionDisplaySize)
{
const string remainingSuggestion = "...";
if (_theme && suggestionSet.SelectionIndex >= _maxSuggestionDisplaySize)
{
buffer.AppendColoredText(remainingSuggestion, _theme.SelectedSuggestionColor);
}
else
{
buffer.Append(remainingSuggestion);
}
}
else
{
bool selected = i == suggestionSet.SelectionIndex;
buffer.Append("<link=");
buffer.Append(i);
buffer.Append(">");
FormatSuggestion(suggestionSet.Suggestions[i], selected, buffer);
buffer.AppendLine("</link>");
}
}
}
private void UpdatePopupDisplay()
{
SuggestionSet suggestionSet = _suggestionStack.TopmostSuggestionSet;
if (suggestionSet == null || suggestionSet.Suggestions.Count == 0)
{
ClearPopup();
}
else
{
if (_suggestionPopupRect && _suggestionPopupText)
{
string formattedSuggestions = GetFormattedSuggestions(suggestionSet);
if (_suggestionDisplayOrder == SortOrder.Ascending)
{
formattedSuggestions = formattedSuggestions.ReverseItems('\n');
}
_suggestionPopupRect.gameObject.SetActive(true);
_suggestionPopupText.text = formattedSuggestions;
}
}
}
/// <summary>
/// Sets the selected suggestion on the console.
/// </summary>
/// <param name="suggestionIndex">The index of the suggestion to set.</param>
public void SetSuggestion(int suggestionIndex)
{
if (!_suggestionStack.SetSuggestionIndex(suggestionIndex))
{
throw new ArgumentException($"Cannot set suggestion to index {suggestionIndex}.");
}
OverrideConsoleInput(_suggestionStack.GetCompletion());
UpdateSuggestionText();
}
private void UpdateSuggestionText()
{
Color suggestionColor = _theme
? _theme.SuggestionColor
: Color.gray;
StringBuilder buffer = _stringBuilderPool.GetStringBuilder();
buffer.AppendColoredText(_currentInput, Color.clear);
buffer.AppendColoredText(_suggestionStack.GetCompletionTail(), suggestionColor);
_consoleSuggestionText.text = _stringBuilderPool.ReleaseAndToString(buffer);
}
/// <summary>
/// Overrides the console input field.
/// </summary>
/// <param name="newInput">The text to override the current input with.</param>
/// <param name="shouldFocus">If the input field should be automatically focused.</param>
public void OverrideConsoleInput(string newInput, bool shouldFocus = true)
{
_currentInput = newInput;
_previousInput = newInput;
_consoleInput.text = newInput;
if (shouldFocus)
{
FocusConsoleInput();
}
OnInputChange();
}
/// <summary>
/// Selects and focuses the input field for the console.
/// </summary>
public void FocusConsoleInput()
{
_consoleInput.Select();
_consoleInput.caretPosition = _consoleInput.text.Length;
_consoleInput.selectionAnchorPosition = _consoleInput.text.Length;
_consoleInput.MoveTextEnd(false);
_consoleInput.ActivateInputField();
}
private void OnInputChange()
{
if (_selectedPreviousCommandIndex >= 0 && _currentInput.Trim() !=
_previousCommands[_previousCommands.Count - _selectedPreviousCommandIndex - 1])
{
ClearHistoricalSuggestions();
}
if (_enableAutocomplete)
{
UpdateSuggestions();
}
}
private void ClearHistoricalSuggestions()
{
_selectedPreviousCommandIndex = -1;
}
private void ClearSuggestions()
{
_suggestionStack.Clear();
_consoleSuggestionText.text = string.Empty;
}
private void ClearPopup()
{
if (_suggestionPopupRect) { _suggestionPopupRect.gameObject.SetActive(false); }
if (_suggestionPopupText) { _suggestionPopupText.text = string.Empty; }
}
/// <summary>
/// Invokes the command currently inputted into the Quantum Console.
/// </summary>
public void InvokeCommand()
{
string userInput = _consoleInput.text;
// invoke command
if (!string.IsNullOrWhiteSpace(userInput))
{
string command = userInput.Trim();
if (_isHandlingUserResponse)
{
HandleUserResponse(command);
}
else
{
InvokeCommand(command);
OverrideConsoleInput(string.Empty);
StoreCommand(command);
}
}
}
private void HandleUserResponse(string command)
{
if (_currentResponseConfig.LogInput)
{
LogUserInput(command);
StoreCommand(command);
}
// reset state to accept a command as usual
_onSubmitResponseCallback(command); // pushes the input back to user code
_onSubmitResponseCallback = null;
_consoleInput.interactable = false;
_isHandlingUserResponse = false;
OnStateChange?.Invoke();
}
private void LogUserInput(string input)
{
ILog commandLog = GenerateCommandLog(input);
LogToConsole(commandLog);
}
protected ILog GenerateCommandLog(string command)
{
string format =
_theme != null
? _theme.CommandLogFormat
: "> {0}";
if (command.Contains("<"))
{
// Add no parse as rich tags are possible
command = $"<noparse>{command}</noparse>";
}
string logValue = string.Format(format, command);
if (_theme)
{
logValue = logValue.ColorText(_theme.CommandLogColor);
}
return new Log(logValue);
}
/// <summary>
/// Invokes the given command.
/// </summary>
/// <param name="command">The command to invoke.</param>
/// <returns>The return value, if any, of the invoked command.</returns>
public object InvokeCommand(string command)
{
object commandResult = null;
if (!string.IsNullOrWhiteSpace(command))
{
LogUserInput(command);
string logTrace = string.Empty;
try
{
commandResult = QuantumConsoleProcessor.InvokeCommand(command);
switch (commandResult)
{
case Task task: _currentTasks.Add(task); break;
case IEnumerator<ICommandAction> action: StartAction(action); break;
case IEnumerable<ICommandAction> action: StartAction(action.GetEnumerator()); break;
default: logTrace = Serialize(commandResult); break;
}
}
catch (System.Reflection.TargetInvocationException e) { logTrace = GetInvocationErrorMessage(e.InnerException); }
catch (Exception e) { logTrace = GetErrorMessage(e); }
LogToConsole(logTrace);
OnInvoke?.Invoke(command);
if (_autoScroll == AutoScrollOptions.OnInvoke) { ScrollConsoleToLatest(); }
if (_closeOnSubmit) { Deactivate(); }
}
else { OverrideConsoleInput(string.Empty); }
return commandResult;
}
[Command("qc-script-extern", "Executes an external source of QC script file, where each line is a separate QC command.", MonoTargetType.Registry, Platform.AllPlatforms ^ Platform.WebGLPlayer)]
public async Task InvokeExternalCommandsAsync(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
while (!reader.EndOfStream)
{
string command = await reader.ReadLineAsync();
if (InvokeCommand(command) is Task ret)
{
await ret;
ProcessAsyncTasks();
}
}
}
}
/// <summary>
/// Invokes a sequence of commands, only starting a new command when the previous is complete.
/// </summary>
/// <param name="commands">The commands to invoke.</param>
public async Task InvokeCommandsAsync(IEnumerable<string> commands)
{
foreach (string command in commands)
{
if (InvokeCommand(command) is Task ret)
{
await ret;
ProcessAsyncTasks();
}
}
}
private string GetErrorMessage(Exception e)
{
return GetErrorMessage(e, _localization.ConsoleError);
}
private string GetInvocationErrorMessage(Exception e)
{
return GetErrorMessage(e, _localization.CommandError);
}
private string GetErrorMessage(Exception e, string label)
{
string message = _verboseErrors
? $"{label} ({e.GetType()}): {e.Message}\n{e.StackTrace}"
: $"{label}: {e.Message}";
return _theme
? message.ColorText(_theme.ErrorColor)
: message;
}
/// <summary>Thread safe API to format and log text to the Quantum Console.</summary>
/// <param name="logText">Text to be logged.</param>
/// <param name="logType">The type of the log to be logged.</param>
public void LogToConsoleAsync(string logText, LogType logType = LogType.Log)
{
if (!string.IsNullOrWhiteSpace(logText))
{
Log log = new Log(logText, logType);
LogToConsoleAsync(log);
}
}
/// <summary>Thread safe API to format and log text to the Quantum Console.</summary>
/// <param name="log">Log to be logged.</param>
public void LogToConsoleAsync(ILog log)
{
OnLog?.Invoke(log);
_logQueue.QueueLog(log);
}
private void FlushQueuedLogs()
{
bool scroll = false;
bool open = false;
while (_logQueue.TryDequeue(out ILog log))
{
AppendLog(log);
LoggingThreshold severity = log.Type.ToLoggingThreshold();
scroll |= _autoScroll == AutoScrollOptions.Always;
open |= severity <= _openOnLogLevel;
}
if (scroll) { ScrollConsoleToLatest(); }
if (open) { Activate(false); }
}
private void ProcessAsyncTasks()
{
for (int i = _currentTasks.Count - 1; i >= 0; i--)
{
if (_currentTasks[i].IsCompleted)
{
if (_currentTasks[i].IsFaulted)
{
foreach (Exception e in _currentTasks[i].Exception.InnerExceptions)
{
string error = GetInvocationErrorMessage(e);
LogToConsole(error);
}
}
else
{
Type taskType = _currentTasks[i].GetType();
if (taskType.IsGenericTypeOf(typeof(Task<>)) && !_voidTaskType.IsAssignableFrom(taskType))
{
System.Reflection.PropertyInfo resultProperty = _currentTasks[i].GetType().GetProperty("Result");
object result = resultProperty.GetValue(_currentTasks[i]);
string log = _serializer.SerializeFormatted(result, _theme);
LogToConsole(log);
}
}
_currentTasks.RemoveAt(i);
}
}
}
/// <summary>
/// Begin accepting text input from the user as a response.
/// </summary>
/// <param name="onSubmitResponseCallback">Method that will provide the submitted response text when it is submitted.</param>
/// <param name="config">The configuration to use for this response.</param>
/// <exception cref="ArgumentNullException"/>
public void BeginResponse(Action<string> onSubmitResponseCallback, ResponseConfig config)
{
// validate args
if (onSubmitResponseCallback == null)
{
throw new ArgumentNullException(nameof(onSubmitResponseCallback));
}
// update internal state
_onSubmitResponseCallback = onSubmitResponseCallback;
_currentResponseConfig = config;
_isHandlingUserResponse = true;
OnStateChange?.Invoke();
// change state of input text to allow for response
_consoleInput.interactable = true;
if (_inputPlaceholderText)
{
_inputPlaceholderText.text = _currentResponseConfig.InputPrompt;
}
FocusConsoleInput();
}
/// <summary>
/// Starts executing an action.
/// </summary>
/// <param name="action">The action to start.</param>
public void StartAction(IEnumerator<ICommandAction> action)
{
_currentActions.Add(action);
ProcessActions();
}
/// <summary>
/// Cancels any actions currently executing.
/// </summary>
public void CancelAllActions()
{
_currentActions.Clear();
}
private void ProcessActions()
{
if (_keyConfig.CancelActionsKey.IsPressed())
{
CancelAllActions();
return;
}
ActionContext context = new ActionContext
{
Console = this
};
for (int i = _currentActions.Count - 1; i >= 0; i--)
{
IEnumerator<ICommandAction> action = _currentActions[i];
try
{
if (action.Execute(context) != ActionState.Running)
{
_currentActions.RemoveAt(i);
}
}
catch (Exception e)
{
_currentActions.RemoveAt(i);
string error = GetInvocationErrorMessage(e);
LogToConsole(error);
break;
}
}
}
private void HandleAsyncJobCounter()
{
if (_showCurrentJobs)
{
if (_jobCounterRect && _jobCounterText)
{
if (_currentTasks.Count == 0) { _jobCounterRect.gameObject.SetActive(false); }
else
{
_jobCounterRect.gameObject.SetActive(true);
_jobCounterText.text = $"{_currentTasks.Count} job{(_currentTasks.Count == 1 ? "" : "s")} in progress";
}
}
}
}
/// <summary>
/// Serializes a value using the current serializer and theme.
/// </summary>
/// <param name="value">The value to the serialize.</param>
/// <returns>The serialized value.</returns>
public string Serialize(object value)
{
return _serializer.SerializeFormatted(value, _theme);
}
/// <summary>
/// Logs text to the Quantum Console.
/// </summary>
/// <param name="logText">Text to be logged.</param>
/// <param name="newLine">If a newline should be ins</param>
public void LogToConsole(string logText, bool newLine = true)
{
bool logExists = !string.IsNullOrEmpty(logText);
if (logExists)
{
LogToConsole(new Log(logText, LogType.Log, newLine));
}
}
/// <summary>
/// Logs text to the Quantum Console.
/// </summary>
/// <param name="log">Log to be logged.</param>
public void LogToConsole(ILog log)
{
FlushQueuedLogs();
AppendLog(log);
OnLog?.Invoke(log);
if (_autoScroll == AutoScrollOptions.Always)
{
ScrollConsoleToLatest();
}
}
private void FlushToConsoleText()
{
if (_consoleRequiresFlush)
{
_consoleRequiresFlush = false;
_consoleLogText.text = _logStorage.GetLogString();
}
}
private ILog TruncateLog(ILog log)
{
if (log.Text.Length <= _maxLogSize || _maxLogSize < 0)
{
return log;
}
string msg = string.Format(_localization.MaxLogSizeExceeded, log.Text.Length, _maxLogSize);
if (_theme)
{
msg = msg.ColorText(_theme.ErrorColor);
}
return new Log(msg, LogType.Error);
}
protected void AppendLog(ILog log)
{
_logStorage.AddLog(TruncateLog(log));
RequireFlush();
}
protected void RequireFlush()
{
_consoleRequiresFlush = true;
}
/// <summary>
/// Removes the last log from the console.
/// </summary>
public void RemoveLogTrace()
{
_logStorage.RemoveLog();
RequireFlush();
}
private void ScrollConsoleToLatest()
{
if (_scrollRect)
{
_scrollRect.verticalNormalizedPosition = 0;
}
}
private void StoreCommand(string command)
{
if (_storeCommandHistory)
{
if (!_storeDuplicateCommands) { _previousCommands.Remove(command); }
if (_storeAdjacentDuplicateCommands || _previousCommands.Count == 0 || _previousCommands[_previousCommands.Count - 1] != command) { _previousCommands.Add(command); }
if (_commandHistorySize > 0 && _previousCommands.Count > _commandHistorySize) { _previousCommands.RemoveAt(0); }
}
}
/// <summary>
/// Clears the Quantum Console.
/// </summary>
[Command("clear", "Clears the Quantum Console", MonoTargetType.Registry)]
public void ClearConsole()
{
_logStorage.Clear();
_logQueue.Clear();
_consoleLogText.text = string.Empty;
_consoleLogText.SetLayoutDirty();
ClearBuffers();
OnClear?.Invoke();
}
public string GetConsoleText()
{
return _consoleLogText.text;
}
protected virtual void ClearBuffers()
{
ClearHistoricalSuggestions();
ClearSuggestions();
ClearPopup();
}
private void Awake()
{
InitializeLogging();
}
private void OnEnable()
{
QuantumRegistry.RegisterObject(this);
Application.logMessageReceivedThreaded += DebugIntercept;
if (IsSupportedState())
{
if (_singletonMode)
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else if (Instance != this)
{
Destroy(gameObject);
}
}
if (_activateOnStartup)
{
bool shouldFocus = SystemInfo.deviceType == DeviceType.Desktop;
Activate(shouldFocus);
}
else
{
if (_initialiseOnStartup) { Initialize(); }
Deactivate();
}
}
else { DisableQC(); }
}
private bool IsSupportedState()
{
#if QC_DISABLED
return false;
#endif
SupportedState currentState = SupportedState.Always;
#if DEVELOPMENT_BUILD
currentState = SupportedState.Development;
#elif UNITY_EDITOR
currentState = SupportedState.Editor;
#endif
return _supportedState <= currentState;
}
private void OnDisable()
{
QuantumRegistry.DeregisterObject(this);
Application.logMessageReceivedThreaded -= DebugIntercept;
Deactivate();
}
private void DisableQC()
{
Deactivate();
enabled = false;
}
private void Initialize()
{
if (!QuantumConsoleProcessor.TableGenerated)
{
QuantumConsoleProcessor.GenerateCommandTable(true);
_consoleInput.interactable = false;
_isGeneratingTable = true;
}
InitializeSuggestionStack();
InitializeLogging();
_consoleLogText.richText = true;
_consoleSuggestionText.richText = true;
ApplyTheme(_theme);
if (!_keyConfig) { _keyConfig = ScriptableObject.CreateInstance<QuantumKeyConfig>(); }
if (!_localization) { _localization = ScriptableObject.CreateInstance<QuantumLocalization>(); }
}
private void InitializeSuggestionStack()
{
if (_suggestionStack == null)
{
_suggestionStack = CreateSuggestionStack();
_suggestionStack.OnSuggestionSetCreated += OnSuggestionSetGenerated;
}
}
private void InitializeLogging()
{
_logStorage = _logStorage ?? CreateLogStorage();
_logQueue = _logQueue ?? CreateLogQueue();
}
protected virtual ILogStorage CreateLogStorage() => new LogStorage(_maxStoredLogs);
protected virtual ILogQueue CreateLogQueue() => new LogQueue(_maxStoredLogs);
protected virtual SuggestionStack CreateSuggestionStack() => new SuggestionStack();
/// <summary>
/// Toggles the Quantum Console.
/// </summary>
public void Toggle()
{
if (IsActive) { Deactivate(); }
else { Activate(); }
}
/// <summary>
/// Activates the Quantum Console.
/// </summary>
public void Activate()
{
Activate(_focusOnActivate);
}
/// <summary>
/// Activates the Quantum Console.
/// </summary>
/// <param name="shouldFocus">If the input field should be automatically focused.</param>
public void Activate(bool shouldFocus)
{
Initialize();
IsActive = true;
_containerRect.gameObject.SetActive(true);
OverrideConsoleInput(string.Empty, shouldFocus);
if (!EventSystem.current)
{
Debug.LogWarning("Quantum Console's UI requires an EventSystem in the scene but there were none present.");
}
OnActivate?.Invoke();
}
/// <summary>
/// Deactivates the Quantum Console.
/// </summary>
public void Deactivate()
{
IsActive = false;
_containerRect.gameObject.SetActive(false);
OnDeactivate?.Invoke();
}
private void DebugIntercept(string condition, string stackTrace, LogType type)
{
if (_interceptDebugLogger && (IsActive || _interceptWhilstInactive) && _loggingLevel >= type.ToLoggingThreshold())
{
bool appendStackTrace = _verboseLogging >= type.ToLoggingThreshold();
ILog log = ConstructDebugLog(condition, stackTrace, type, _prependTimestamps, appendStackTrace);
LogToConsoleAsync(log);
}
}
protected virtual ILog ConstructDebugLog(string condition, string stackTrace, LogType type, bool prependTimeStamp, bool appendStackTrace)
{
if (prependTimeStamp)
{
DateTime now = DateTime.Now;
string format = _theme
? _theme.TimestampFormat
: "[{0:00}:{1:00}:{2:00}]";
condition = $"{string.Format(format, now.Hour, now.Minute, now.Second)} {condition}";
}
if (appendStackTrace)
{
condition += $"\n{stackTrace}";
}
if (_theme)
{
switch (type)
{
case LogType.Warning:
{
condition = ColorExtensions.ColorText(condition, _theme.WarningColor);
break;
}
case LogType.Error:
case LogType.Assert:
case LogType.Exception:
{
condition = ColorExtensions.ColorText(condition, _theme.ErrorColor);
break;
}
}
}
return new Log(condition, type, true);
}
protected virtual void OnValidate()
{
MaxStoredLogs = _maxStoredLogs;
}
}
}