diff --git a/src/UI/CSConsole/CSAutoCompleter.cs b/src/UI/CSConsole/CSAutoCompleter.cs index dbf0ac7..fa5f084 100644 --- a/src/UI/CSConsole/CSAutoCompleter.cs +++ b/src/UI/CSConsole/CSAutoCompleter.cs @@ -52,12 +52,12 @@ namespace UnityExplorer.UI.CSConsole return; } - // get the current composition string (from caret back to last delimiter or whitespace) + // get the current composition string (from caret back to last delimiter) while (start > 0) { start--; char c = InputField.Text[start]; - if (char.IsWhiteSpace(c) || delimiters.Contains(c)) + if (delimiters.Contains(c)) { start++; break; @@ -68,25 +68,25 @@ namespace UnityExplorer.UI.CSConsole // Get MCS completions string[] evaluatorCompletions = ConsoleController.Evaluator.GetCompletions(input, out string prefix); - - if (!string.IsNullOrEmpty(prefix) && evaluatorCompletions != null && evaluatorCompletions.Any()) + + if (evaluatorCompletions != null && evaluatorCompletions.Any()) { suggestions.AddRange(from completion in evaluatorCompletions - select new Suggestion($"{prefix}{completion}", completion)); + select new Suggestion(GetHighlightString(prefix, completion), completion)); } // Get manual keyword completions foreach (var kw in KeywordLexer.keywords) { - if (kw.StartsWith(input)) + if (kw.StartsWith(input) && kw.Length > input.Length) { + if (!keywordHighlights.ContainsKey(kw)) + keywordHighlights.Add(kw, $"{kw}"); + string completion = kw.Substring(input.Length, kw.Length - input.Length); - - suggestions.Add(new Suggestion( - $"{input}" + - $"{completion}", - completion)); + + suggestions.Add(new Suggestion(keywordHighlights[kw], completion)); } } @@ -100,5 +100,20 @@ namespace UnityExplorer.UI.CSConsole AutoCompleteModal.Instance.ReleaseOwnership(this); } } + + private readonly Dictionary keywordHighlights = new Dictionary(); + + private readonly StringBuilder highlightBuilder = new StringBuilder(); + private const string OPEN_HIGHLIGHT = ""; + + private string GetHighlightString(string prefix, string completion) + { + highlightBuilder.Clear(); + highlightBuilder.Append(OPEN_HIGHLIGHT); + highlightBuilder.Append(prefix); + highlightBuilder.Append(SignatureHighlighter.CLOSE_COLOR); + highlightBuilder.Append(completion); + return highlightBuilder.ToString(); + } } } diff --git a/src/UI/CSConsole/ConsoleController.cs b/src/UI/CSConsole/ConsoleController.cs index 9b786e5..6a1116b 100644 --- a/src/UI/CSConsole/ConsoleController.cs +++ b/src/UI/CSConsole/ConsoleController.cs @@ -38,9 +38,8 @@ namespace UnityExplorer.UI.CSConsole { "System", "System.Linq", - "System.Collections", + "System.Text", "System.Collections.Generic", - "System.Reflection", "UnityEngine", #if CPP "UnhollowerBaseLib", @@ -68,16 +67,19 @@ namespace UnityExplorer.UI.CSConsole Lexer = new LexerBuilder(); Completer = new CSAutoCompleter(); + SetupHelpInteraction(); + Panel.OnInputChanged += OnInputChanged; Panel.InputScroll.OnScroll += OnInputScrolled; Panel.OnCompileClicked += Evaluate; Panel.OnResetClicked += ResetConsole; + Panel.OnHelpDropdownChanged += HelpSelected; Panel.OnAutoIndentToggled += OnToggleAutoIndent; Panel.OnCtrlRToggled += OnToggleCtrlRShortcut; Panel.OnSuggestionsToggled += OnToggleSuggestions; - } + #region UI Listeners and options // TODO save @@ -99,81 +101,6 @@ namespace UnityExplorer.UI.CSConsole #endregion - // Updating and event listeners - - private static bool settingAutoCompletion; - - private static void OnInputScrolled() => HighlightVisibleInput(); - - // Invoked at most once per frame - private static void OnInputChanged(string value) - { - if (!settingAutoCompletion && EnableSuggestions) - Completer.CheckAutocompletes(); - - if (!settingAutoCompletion && EnableAutoIndent) - DoAutoIndent(); - - HighlightVisibleInput(); - } - - public static void Update() - { - UpdateCaret(out bool caretMoved); - - if (!settingAutoCompletion && EnableSuggestions && caretMoved) - { - Completer.CheckAutocompletes(); - } - - if (EnableCtrlRShortcut - && (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl)) - && InputManager.GetKeyDown(KeyCode.R)) - { - Evaluate(Panel.Input.Text); - } - } - - private const int CSCONSOLE_LINEHEIGHT = 18; - - private static void UpdateCaret(out bool caretMoved) - { - int prevCaret = LastCaretPosition; - caretMoved = false; - - if (Input.Component.isFocused) - { - LastCaretPosition = Input.Component.caretPosition; - caretMoved = LastCaretPosition != prevCaret; - } - - if (Input.Text.Length == 0) - return; - - // If caret moved, ensure caret is visible in the viewport - if (caretMoved) - { - var charInfo = Input.TextGenerator.characters[LastCaretPosition]; - var charTop = charInfo.cursorPos.y; - var charBot = charTop - CSCONSOLE_LINEHEIGHT; - - var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); - var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; - - float diff = 0f; - if (charTop > viewportMin) - diff = charTop - viewportMin; - else if (charBot < viewportMax) - diff = charBot - viewportMax; - - if (Math.Abs(diff) > 1) - { - var rect = Input.Rect; - rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, rect.anchoredPosition.y - diff); - } - } - } - #region Evaluating @@ -216,19 +143,43 @@ namespace UnityExplorer.UI.CSConsole { try { - Evaluator.Run(input); + // Try to "Compile" the code (tries to interpret it as REPL) + var evaluation = Evaluator.Compile(input); + if (evaluation != null) + { + // Valid REPL, we have a delegate to the evaluation. + try + { + object ret = null; + evaluation.Invoke(ref ret); + var result = ret?.ToString(); + if (!string.IsNullOrEmpty(result)) + ExplorerCore.Log($"Invoked REPL, result: {ret}"); + else + ExplorerCore.Log($"Invoked REPL (no return value)"); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception invoking REPL: {ex}"); + } + } + else + { + // The input was not recognized as an evaluation. Compile the code. - string output = ScriptEvaluator._textWriter.ToString(); - var outputSplit = output.Split('\n'); - if (outputSplit.Length >= 2) - output = outputSplit[outputSplit.Length - 2]; - evaluatorOutput.Clear(); + Evaluator.Run(input); - if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) - throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); + string output = ScriptEvaluator._textWriter.ToString(); + var outputSplit = output.Split('\n'); + if (outputSplit.Length >= 2) + output = outputSplit[outputSplit.Length - 2]; + evaluatorOutput.Clear(); - //if (!supressLog) - // ExplorerCore.Log("Code executed successfully."); + if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) + throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); + else if (!supressLog) + ExplorerCore.Log($"Code compiled without errors."); + } } catch (FormatException fex) { @@ -245,6 +196,145 @@ namespace UnityExplorer.UI.CSConsole #endregion + // Updating and event listeners + + private static bool settingCaretCoroutine; + + private static void OnInputScrolled() => HighlightVisibleInput(); + + private static string previousInput; + + // Invoked at most once per frame + private static void OnInputChanged(string value) + { + // prevent escape wiping input + if (InputManager.GetKeyDown(KeyCode.Escape)) + { + Input.Text = previousInput; + + if (EnableSuggestions && AutoCompleteModal.CheckEscape(Completer)) + OnAutocompleteEscaped(); + + return; + } + + previousInput = value; + + if (EnableSuggestions && AutoCompleteModal.CheckEnter(Completer)) + { + OnAutocompleteEnter(); + } + else if (!settingCaretCoroutine) + { + if (EnableSuggestions) + Completer.CheckAutocompletes(); + + if (EnableAutoIndent) + DoAutoIndent(); + } + + HighlightVisibleInput(); + } + + public static void Update() + { + UpdateCaret(out bool caretMoved); + + if (!settingCaretCoroutine && EnableSuggestions && AutoCompleteModal.CheckEscape(Completer)) + { + OnAutocompleteEscaped(); + return; + } + + if (!settingCaretCoroutine && EnableSuggestions && caretMoved) + { + Completer.CheckAutocompletes(); + } + + if (EnableCtrlRShortcut + && (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl)) + && InputManager.GetKeyDown(KeyCode.R)) + { + Evaluate(Panel.Input.Text); + } + } + + private const int CSCONSOLE_LINEHEIGHT = 18; + + private static void UpdateCaret(out bool caretMoved) + { + int prevCaret = LastCaretPosition; + caretMoved = false; + + // Override up/down arrow movement when autocompleting + if (EnableSuggestions && AutoCompleteModal.CheckNavigation(Completer)) + { + Input.Component.caretPosition = LastCaretPosition; + return; + } + + if (Input.Component.isFocused) + { + LastCaretPosition = Input.Component.caretPosition; + caretMoved = LastCaretPosition != prevCaret; + } + + if (Input.Text.Length == 0) + return; + + // If caret moved, ensure caret is visible in the viewport + if (caretMoved) + { + var charInfo = Input.TextGenerator.characters[LastCaretPosition]; + var charTop = charInfo.cursorPos.y; + var charBot = charTop - CSCONSOLE_LINEHEIGHT; + + var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); + var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; + + float diff = 0f; + if (charTop > viewportMin) + diff = charTop - viewportMin; + else if (charBot < viewportMax) + diff = charBot - viewportMax; + + if (Math.Abs(diff) > 1) + { + var rect = Input.Rect; + rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, rect.anchoredPosition.y - diff); + } + } + } + + private static void SetCaretPosition(int caretPosition) + { + settingCaretCoroutine = true; + RuntimeProvider.Instance.StartCoroutine(SetAutocompleteCaretCoro(caretPosition)); + } + + private static IEnumerator SetAutocompleteCaretCoro(int caretPosition) + { + var color = Input.Component.selectionColor; + color.a = 0f; + Input.Component.selectionColor = color; + EventSystem.current.SetSelectedGameObject(null, null); + yield return null; + + EventSystem.current.SetSelectedGameObject(Input.UIRoot, null); + Input.Component.Select(); + yield return null; + + Input.Component.caretPosition = caretPosition; + Input.Component.selectionFocusPosition = caretPosition; + LastCaretPosition = Input.Component.caretPosition; + + color.a = defaultInputFieldAlpha; + Input.Component.selectionColor = color; + + settingCaretCoroutine = false; + } + + #region Lexer Highlighting private static void HighlightVisibleInput() @@ -295,31 +385,28 @@ namespace UnityExplorer.UI.CSConsole public static void InsertSuggestionAtCaret(string suggestion) { - settingAutoCompletion = true; + settingCaretCoroutine = true; Input.Text = Input.Text.Insert(LastCaretPosition, suggestion); - RuntimeProvider.Instance.StartCoroutine(SetAutocompleteCaret(LastCaretPosition + suggestion.Length)); + SetCaretPosition(LastCaretPosition + suggestion.Length); LastCaretPosition = Input.Component.caretPosition; } - private static IEnumerator SetAutocompleteCaret(int caretPosition) + private static void OnAutocompleteEnter() { - var color = Input.Component.selectionColor; - color.a = 0f; - Input.Component.selectionColor = color; - yield return null; + // Remove the new line + int lastIdx = Input.Component.caretPosition - 1; + Input.Text = Input.Text.Remove(lastIdx, 1); - EventSystem.current.SetSelectedGameObject(Panel.Input.UIRoot, null); - yield return null; + // Use the selected suggestion + Input.Component.caretPosition = LastCaretPosition; + Completer.OnSuggestionClicked(AutoCompleteModal.SelectedSuggestion); + } - Input.Component.caretPosition = caretPosition; - Input.Component.selectionFocusPosition = caretPosition; - LastCaretPosition = Input.Component.caretPosition; - - color.a = defaultInputFieldAlpha; - Input.Component.selectionColor = color; - - settingAutoCompletion = false; + private static void OnAutocompleteEscaped() + { + AutoCompleteModal.Instance.ReleaseOwnership(Completer); + SetCaretPosition(LastCaretPosition); } @@ -359,6 +446,104 @@ namespace UnityExplorer.UI.CSConsole #endregion + #region "Help" interaction + private static readonly Dictionary helpDict = new Dictionary(); + + public static void SetupHelpInteraction() + { + var drop = Panel.HelpDropdown; + + helpDict.Add("Help", ""); + helpDict.Add("Usings", HELP_USINGS); + helpDict.Add("REPL", HELP_REPL); + helpDict.Add("Classes", HELP_CLASSES); + helpDict.Add("Coroutines", HELP_COROUTINES); + + foreach (var opt in helpDict) + drop.options.Add(new Dropdown.OptionData(opt.Key)); + } + + public static void HelpSelected(int index) + { + if (index == 0) + return; + + var helpText = helpDict.ElementAt(index); + + Input.Text = helpText.Value; + + Panel.HelpDropdown.value = 0; + } + + + internal const string STARTUP_TEXT = @"// Welcome to the UnityExplorer C# Console! +// It is recommended to use the Log panel (or a console log window) while using this tool. +// Use the Help dropdown to see detailed examples of how to use the console."; + + internal const string HELP_USINGS = @"// To add a using directive, simply compile it like you would in your IDE: +using UnityEngine.UI; + +// To see your current usings, evaluate ""GetUsing();"" as REPL. You cannot do this while adding usings. + +// To reset usings to default, press the Reset button."; + + internal const string HELP_REPL = @"/* REPL (Read-Evaluate-Print-Loop) is a way to execute code immediately. + * REPL code cannot contain any using directives or classes. + * The return value of the last line of your REPL will be printed to the log. + * Variables defined in REPL will exist until you Reset the console. +*/ + +// eg: This code would print 'Hello, World!', and then print 6 as the return value. +Log(""Hello, world!""); +var x = 5; +++x; + +/* The following helpers are available in REPL mode: + * CurrentTarget; - System.Object, the target of the active Inspector tab + * AllTargets; - System.Object[], the targets of all Inspector tabs + * Log(obj); - prints a message to the console log + * Inspect(obj); - inspect the object with the Inspector + * Inspect(someType); - inspect a Type with static reflection + * Start(enumerator); - starts the IEnumerator as a Coroutine + * GetUsing(); - prints the current using directives to the console log + * GetVars(); - prints the names and values of the REPL variables you have defined + * GetClasses(); - prints the names and members of the classes you have defined +*/"; + + internal const string HELP_CLASSES = @"// Classes you compile will exist until the application closes. +// You can soft-overwrite a class by compiling it again with the same name. The old class will still technically exist in memory. + +// Compiled classes can be accessed from both inside and outside this console. +// Note: in IL2CPP, injecting these classes with ClassInjector may crash the game! + +public class HelloWorld +{ + public static void Main() + { + UnityExplorer.ExplorerCore.Log(""Hello, world!""); + } +} + +// In REPL, you could call the example method above with ""HelloWorld.Main();"" +// Note: The compiler does not allow you to run REPL code and define classes at the same time."; + + internal const string HELP_COROUTINES = @"// To start a Coroutine, you can use an existing IEnumerator or define one yourself. +// You can start Coroutines from REPL by using ""Start(enumerator);"" + +// To define a coroutine, for example: +public class MyCoros +{ + public static IEnumerator Coro() + { + yield return null; + UnityExplorer.ExplorerCore.Log(""Hello, world after one frame!""); + } +} +// To run this Coroutine in REPL, it would look like ""Start(MyCoros.Coro());"" +// Note: You cannot define classes and run REPL code at the same time! +"; + + #endregion } } diff --git a/src/UI/CSConsole/LexerBuilder.cs b/src/UI/CSConsole/LexerBuilder.cs index b4443f0..e8eb5e0 100644 --- a/src/UI/CSConsole/LexerBuilder.cs +++ b/src/UI/CSConsole/LexerBuilder.cs @@ -115,6 +115,13 @@ namespace UnityExplorer.UI.CSConsole lastUnhighlighted = match.endIndex + 1; } + // Append trailing unhighlighted input + while (lastUnhighlighted <= endIdx) + { + sb.Append(input[lastUnhighlighted]); + lastUnhighlighted++; + } + return sb.ToString(); } diff --git a/src/UI/CSConsole/ScriptInteraction.cs b/src/UI/CSConsole/ScriptInteraction.cs index 52c77e9..340a955 100644 --- a/src/UI/CSConsole/ScriptInteraction.cs +++ b/src/UI/CSConsole/ScriptInteraction.cs @@ -7,33 +7,16 @@ using System.Linq; using UnityExplorer.Core.Runtime; using System.Text; +/* + Welcome to the UnityExplorer C# Console! + Use the Help dropdown to see detailed examples of how to use this console. + To see your output, use the Log panel or a Console Log window. +*/ + namespace UnityExplorer.UI.CSConsole { public class ScriptInteraction : InteractiveBase { - internal const string STARTUP_TEXT = @"// Compile a using directive to add it to the console (until Reset) -using SomeNamespace; - -// Compile a C# class and it will exist until Reset -public class SomeClass { - public static void SomeMethod() { - } -} - -// If not compiling any usings or classes, the code will run immediately (REPL). -// Variables you define in REPL mode will also exist until Reset. -// In REPL context, the following helpers are available: - -* System.Object CurrentTarget - the target of the active Inspector tab -* System.Object[] AllTargets - an array containing the targets of all Inspector tabs -* void Log(""message"") - prints a message to the console log -* void Inspect(someObject) - inspect an instance, eg. Inspect(Camera.main); -* void Inspect(typeof(SomeClass)) - inspect a Class with static reflection -* void StartCoroutine(ienumerator) - start the IEnumerator as a Coroutine -* void GetUsing() - prints the current using directives to the console log -* void GetVars() - prints the variables you have defined and their current values -* void GetClasses() - prints the names of the classes you have defined, and their members"; - public static void Log(object message) { ExplorerCore.Log(message); @@ -41,7 +24,7 @@ using SomeNamespace; public static object CurrentTarget => InspectorManager.ActiveInspector?.Target; - public static object[] AllTargets() => InspectorManager.Inspectors.Select(it => it.Target).ToArray(); + public static object[] AllTargets => InspectorManager.Inspectors.Select(it => it.Target).ToArray(); public static void Inspect(object obj) { @@ -53,7 +36,7 @@ using SomeNamespace; InspectorManager.Inspect(type); } - public static void StartCoroutine(IEnumerator ienumerator) + public static void Start(IEnumerator ienumerator) { RuntimeProvider.Instance.StartCoroutine(ienumerator); } @@ -65,7 +48,11 @@ using SomeNamespace; public static void GetVars() { - Log(Evaluator.GetVars()); + var vars = Evaluator.GetVars()?.Trim(); + if (string.IsNullOrEmpty(vars)) + ExplorerCore.LogWarning("No variables seem to be defined!"); + else + Log(vars); } public static void GetClasses() diff --git a/src/UI/Panels/CSConsolePanel.cs b/src/UI/Panels/CSConsolePanel.cs index 944ae25..bbadc66 100644 --- a/src/UI/Panels/CSConsolePanel.cs +++ b/src/UI/Panels/CSConsolePanel.cs @@ -15,7 +15,7 @@ namespace UnityExplorer.UI.Panels { public override string Name => "C# Console"; public override UIManager.Panels PanelType => UIManager.Panels.CSConsole; - public override int MinWidth => 750; + public override int MinWidth => 740; public override int MinHeight => 300; public InputFieldScroller InputScroll { get; private set; } @@ -23,10 +23,13 @@ namespace UnityExplorer.UI.Panels public Text InputText { get; private set; } public Text HighlightText { get; private set; } - public Action OnInputChanged; + public Dropdown HelpDropdown { get; private set; } + // events + public Action OnInputChanged; public Action OnResetClicked; public Action OnCompileClicked; + public Action OnHelpDropdownChanged; public Action OnCtrlRToggled; public Action OnSuggestionsToggled; public Action OnAutoIndentToggled; @@ -74,6 +77,25 @@ namespace UnityExplorer.UI.Panels default, TextAnchor.MiddleLeft); UIFactory.SetLayoutElement(toolsRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + // Buttons + + var compileButton = UIFactory.CreateButton(toolsRow, "CompileButton", "Compile", new Color(0.33f, 0.5f, 0.33f)); + UIFactory.SetLayoutElement(compileButton.Component.gameObject, minHeight: 28, minWidth: 130, flexibleHeight: 0); + compileButton.ButtonText.fontSize = 15; + compileButton.OnClick += () => { OnCompileClicked?.Invoke(); }; + + var resetButton = UIFactory.CreateButton(toolsRow, "ResetButton", "Reset", new Color(0.33f, 0.33f, 0.33f)); + UIFactory.SetLayoutElement(resetButton.Component.gameObject, minHeight: 28, minWidth: 80, flexibleHeight: 0); + resetButton.ButtonText.fontSize = 15; + resetButton.OnClick += () => { OnResetClicked?.Invoke(); }; + + // Help dropdown + + var helpDrop = UIFactory.CreateDropdown(toolsRow, out var dropdown, "Help", 14, null); + UIFactory.SetLayoutElement(helpDrop, minHeight: 25, minWidth: 100); + HelpDropdown = dropdown; + HelpDropdown.onValueChanged.AddListener((int val) => { this.OnHelpDropdownChanged?.Invoke(val); }); + // Enable Ctrl+R toggle var ctrlRToggleObj = UIFactory.CreateToggle(toolsRow, "CtrlRToggle", out var CtrlRToggle, out Text ctrlRToggleText); @@ -93,36 +115,26 @@ namespace UnityExplorer.UI.Panels // Enable Auto-indent toggle var autoIndentToggleObj = UIFactory.CreateToggle(toolsRow, "IndentToggle", out var AutoIndentToggle, out Text autoIndentToggleText); - UIFactory.SetLayoutElement(autoIndentToggleObj, minWidth: 180, flexibleWidth: 0, minHeight: 25); + UIFactory.SetLayoutElement(autoIndentToggleObj, minWidth: 120, flexibleWidth: 0, minHeight: 25); autoIndentToggleText.alignment = TextAnchor.UpperLeft; autoIndentToggleText.text = "Auto-indent"; AutoIndentToggle.onValueChanged.AddListener((bool val) => { OnAutoIndentToggled?.Invoke(val); }); - // Buttons - - var resetButton = UIFactory.CreateButton(toolsRow, "ResetButton", "Reset", new Color(0.33f, 0.33f, 0.33f)); - UIFactory.SetLayoutElement(resetButton.Component.gameObject, minHeight: 28, minWidth: 80, flexibleHeight: 0); - resetButton.ButtonText.fontSize = 15; - resetButton.OnClick += OnResetClicked; - - var compileButton = UIFactory.CreateButton(toolsRow, "CompileButton", "Compile", new Color(0.33f, 0.5f, 0.33f)); - UIFactory.SetLayoutElement(compileButton.Component.gameObject, minHeight: 28, minWidth: 130, flexibleHeight: 0); - compileButton.ButtonText.fontSize = 15; - compileButton.OnClick += OnCompileClicked; - // Console Input int fontSize = 16; - var inputObj = UIFactory.CreateSrollInputField(this.content, "ConsoleInput", ScriptInteraction.STARTUP_TEXT, out var inputScroller, fontSize); + var inputObj = UIFactory.CreateSrollInputField(this.content, "ConsoleInput", ConsoleController.STARTUP_TEXT, out var inputScroller, fontSize); InputScroll = inputScroller; ConsoleController.defaultInputFieldAlpha = Input.Component.selectionColor.a; Input.OnValueChanged += InvokeOnValueChanged; InputText = Input.Component.textComponent; InputText.supportRichText = false; - InputText.color = Color.white; Input.PlaceholderText.fontSize = fontSize; + InputText.color = Color.clear; + Input.Component.customCaretColor = true; + Input.Component.caretColor = Color.white; // Lexer highlight text overlay var highlightTextObj = UIFactory.CreateUIObject("HighlightText", InputText.gameObject); @@ -134,7 +146,7 @@ namespace UnityExplorer.UI.Panels highlightTextRect.offsetMax = Vector2.zero; HighlightText = highlightTextObj.AddComponent(); - HighlightText.color = Color.clear; + HighlightText.color = Color.white; HighlightText.supportRichText = true; HighlightText.fontSize = fontSize; diff --git a/src/UI/Panels/LogPanel.cs b/src/UI/Panels/LogPanel.cs index 66b7a7c..1e30027 100644 --- a/src/UI/Panels/LogPanel.cs +++ b/src/UI/Panels/LogPanel.cs @@ -80,7 +80,7 @@ namespace UnityExplorer.UI.Panels CurrentStreamPath = Path.Combine(path, fileName); - File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message)); + File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message).ToArray()); } // Logging diff --git a/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs b/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs index 439bad1..011b2a3 100644 --- a/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs +++ b/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs @@ -29,12 +29,17 @@ namespace UnityExplorer.UI.Widgets.AutoComplete public override bool ShouldSaveActiveState => false; public override bool NavButtonWanted => false; - public ISuggestionProvider CurrentHandler { get; private set; } + public static ISuggestionProvider CurrentHandler { get; private set; } - public ButtonListSource dataHandler; - public ScrollPool scrollPool; + public static ButtonListSource dataHandler; + public static ScrollPool scrollPool; - private List suggestions = new List(); + private static List Suggestions = new List(); + private static int SelectedIndex = 0; + + public static Suggestion SelectedSuggestion => Suggestions[SelectedIndex]; + + public static bool Suggesting(ISuggestionProvider handler) => CurrentHandler == handler && Instance.UIRoot.activeSelf; public AutoCompleteModal() { @@ -42,47 +47,6 @@ namespace UnityExplorer.UI.Widgets.AutoComplete OnClickedOutsidePanels += AutoCompleter_OnClickedOutsidePanels; } - private void AutoCompleter_OnClickedOutsidePanels() - { - if (!this.UIRoot || !this.UIRoot.activeInHierarchy) - return; - - if (CurrentHandler != null) - ReleaseOwnership(CurrentHandler); - else - UIRoot.SetActive(false); - } - - private void UIPanel_OnPanelsReordered() - { - if (!this.UIRoot || !this.UIRoot.activeInHierarchy) - return; - - if (this.UIRoot.transform.GetSiblingIndex() != UIManager.PanelHolder.transform.childCount - 1) - { - if (CurrentHandler != null) - ReleaseOwnership(CurrentHandler); - else - UIRoot.SetActive(false); - } - } - - public override void Update() - { - if (!UIRoot || !UIRoot.activeSelf) - return; - - if (suggestions.Any() && CurrentHandler != null) - { - if (!CurrentHandler.InputField.UIRoot.activeInHierarchy) - ReleaseOwnership(CurrentHandler); - else - { - UpdatePosition(); - } - } - } - public void TakeOwnership(ISuggestionProvider provider) { CurrentHandler = provider; @@ -100,43 +64,170 @@ namespace UnityExplorer.UI.Widgets.AutoComplete } } - private List GetEntries() => suggestions; - - private bool ShouldDisplay(Suggestion data, string filter) => true; - - public void SetSuggestions(IEnumerable collection) + public void SetSuggestions(IEnumerable suggestions) { - suggestions = collection as List ?? collection.ToList(); + Suggestions = suggestions as List ?? suggestions.ToList(); + SelectedIndex = 0; - if (!suggestions.Any()) - UIRoot.SetActive(false); + if (!Suggestions.Any()) + base.UIRoot.SetActive(false); else { - UIRoot.SetActive(true); - UIRoot.transform.SetAsLastSibling(); + base.UIRoot.SetActive(true); + base.UIRoot.transform.SetAsLastSibling(); dataHandler.RefreshData(); scrollPool.Refresh(true, true); } } + private static float timeOfLastNavHold = -1f; + + /// + /// Returns true if the AutoCompleteModal used the navigation input, false if not. + /// The navigation inputs are Control+Up/Down, and Control+Enter. + /// + public static bool CheckNavigation(ISuggestionProvider handler) + { + if (!Suggesting(handler)) + return false; + + if (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl)) + { + bool up = InputManager.GetKey(KeyCode.UpArrow); + bool down = InputManager.GetKey(KeyCode.DownArrow); + + if (up || down) + { + if (up) + { + if (InputManager.GetKeyDown(KeyCode.UpArrow)) + { + SetSelectedSuggestion(SelectedIndex - 1); + timeOfLastNavHold = Time.realtimeSinceStartup + 0.3f; + } + else if (timeOfLastNavHold.OccuredEarlierThan(0.05f)) + { + SetSelectedSuggestion(SelectedIndex - 1); + timeOfLastNavHold = Time.realtimeSinceStartup; + } + } + else + { + if (InputManager.GetKeyDown(KeyCode.DownArrow)) + { + SetSelectedSuggestion(SelectedIndex + 1); + timeOfLastNavHold = Time.realtimeSinceStartup + 0.3f; + } + else if (timeOfLastNavHold.OccuredEarlierThan(0.05f)) + { + SetSelectedSuggestion(SelectedIndex + 1); + timeOfLastNavHold = Time.realtimeSinceStartup; + } + } + + return true; + } + + return false; + } + + return !timeOfLastNavHold.OccuredEarlierThan(0.2f); + } + + public static bool CheckEnter(ISuggestionProvider handler) + { + return Suggesting(handler) && InputManager.GetKeyDown(KeyCode.Return); + } + + public static bool CheckEscape(ISuggestionProvider handler) + { + return Suggesting(handler) && InputManager.GetKeyDown(KeyCode.Escape); + } + + private static void SetSelectedSuggestion(int index) + { + if (index < 0 || index >= Suggestions.Count) + return; + + SelectedIndex = index; + scrollPool.Refresh(true, false); + } + + // Internal update + + public override void Update() + { + if (!UIRoot || !UIRoot.activeSelf) + return; + + if (Suggestions.Any() && CurrentHandler != null) + { + if (!CurrentHandler.InputField.UIRoot.activeInHierarchy) + ReleaseOwnership(CurrentHandler); + else + { + UpdatePosition(); + } + } + } + + // Setting autocomplete cell buttons + + private readonly Color selectedSuggestionColor = new Color(46/255f, 54/255f, 53/255f); + private readonly Color inactiveSuggestionColor = new Color(0.11f, 0.11f, 0.11f); + + private List GetEntries() => Suggestions; + + private bool ShouldDisplay(Suggestion data, string filter) => true; + private void OnCellClicked(int dataIndex) { - var suggestion = suggestions[dataIndex]; + var suggestion = Suggestions[dataIndex]; CurrentHandler.OnSuggestionClicked(suggestion); } + private bool setFirstCell; + private void SetCell(ButtonCell cell, int index) { - if (index < 0 || index >= suggestions.Count) + if (index < 0 || index >= Suggestions.Count) { cell.Disable(); return; } - var suggestion = suggestions[index]; + var suggestion = Suggestions[index]; cell.Button.ButtonText.text = suggestion.DisplayText; + + if (index == SelectedIndex && setFirstCell) + { + if (cell.Rect.MinY() > scrollPool.Viewport.MinY()) + { + // cell is too far down + var diff = cell.Rect.MinY() - scrollPool.Viewport.MinY(); + var pos = scrollPool.Content.anchoredPosition; + pos.y -= diff; + scrollPool.Content.anchoredPosition = pos; + } + else if (cell.Rect.MaxY() < scrollPool.Viewport.MaxY()) + { + // cell is too far up + var diff = cell.Rect.MaxY() - scrollPool.Viewport.MaxY(); + var pos = scrollPool.Content.anchoredPosition; + pos.y -= diff; + scrollPool.Content.anchoredPosition = pos; + } + + RuntimeProvider.Instance.SetColorBlock(cell.Button.Component, selectedSuggestionColor); + } + else + RuntimeProvider.Instance.SetColorBlock(cell.Button.Component, inactiveSuggestionColor); + + setFirstCell = true; } + // Updating panel position + private int lastCaretPosition; private Vector3 lastInputPosition; @@ -175,6 +266,35 @@ namespace UnityExplorer.UI.Widgets.AutoComplete this.Dragger.OnEndResize(); } + // Event listeners for panel + + private void AutoCompleter_OnClickedOutsidePanels() + { + if (!this.UIRoot || !this.UIRoot.activeInHierarchy) + return; + + if (CurrentHandler != null) + ReleaseOwnership(CurrentHandler); + else + UIRoot.SetActive(false); + } + + private void UIPanel_OnPanelsReordered() + { + if (!this.UIRoot || !this.UIRoot.activeInHierarchy) + return; + + if (this.UIRoot.transform.GetSiblingIndex() != UIManager.PanelHolder.transform.childCount - 1) + { + if (CurrentHandler != null) + ReleaseOwnership(CurrentHandler); + else + UIRoot.SetActive(false); + } + } + + // UI Construction + protected internal override void DoSetDefaultPosAndAnchors() { var mainRect = uiRoot.GetComponent(); @@ -190,9 +310,13 @@ namespace UnityExplorer.UI.Widgets.AutoComplete scrollPool = UIFactory.CreateScrollPool(this.content, "AutoCompleter", out GameObject scrollObj, out GameObject scrollContent); scrollPool.Initialize(dataHandler); UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999); - UIFactory.SetLayoutGroup(scrollContent, true, false, true, false); + var bottomRow = UIFactory.CreateHorizontalGroup(this.content, "BottomRow", true, true, true, true, 0, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(bottomRow, minHeight: 20, flexibleWidth: 9999); + UIFactory.CreateLabel(bottomRow, "HelpText", "Control+Up/Down to select, Enter to use, Esc to hide", + TextAnchor.MiddleLeft, Color.grey, false, 13); + UIRoot.SetActive(false); } diff --git a/src/UI/Widgets/AutoComplete/Suggestion.cs b/src/UI/Widgets/AutoComplete/Suggestion.cs index 55e7190..7b67c5f 100644 --- a/src/UI/Widgets/AutoComplete/Suggestion.cs +++ b/src/UI/Widgets/AutoComplete/Suggestion.cs @@ -11,17 +11,12 @@ namespace UnityExplorer.UI.Widgets.AutoComplete { public readonly string DisplayText; public readonly string UnderlyingValue; - //public int InsertIndex; - //public readonly string Prefix; - //public readonly string Addition; - //public string Full => Prefix + Addition; + public string Combined => DisplayText + UnderlyingValue; - public Suggestion(string displayText, /* string prefix, string addition, */ string underlyingValue) + public Suggestion(string displayText, string underlyingValue) { DisplayText = displayText; - //Addition = addition; - //Prefix = prefix; UnderlyingValue = underlyingValue; } }