Improve C# Console, key navigation on AutoCompleter

This commit is contained in:
Sinai 2021-05-15 01:21:07 +10:00
parent 1c216c0d86
commit 021db69409
8 changed files with 552 additions and 227 deletions

View File

@ -52,12 +52,12 @@ namespace UnityExplorer.UI.CSConsole
return; 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) while (start > 0)
{ {
start--; start--;
char c = InputField.Text[start]; char c = InputField.Text[start];
if (char.IsWhiteSpace(c) || delimiters.Contains(c)) if (delimiters.Contains(c))
{ {
start++; start++;
break; break;
@ -68,25 +68,25 @@ namespace UnityExplorer.UI.CSConsole
// Get MCS completions // Get MCS completions
string[] evaluatorCompletions = ConsoleController.Evaluator.GetCompletions(input, out string prefix); 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 suggestions.AddRange(from completion in evaluatorCompletions
select new Suggestion($"<color=cyan>{prefix}</color>{completion}", completion)); select new Suggestion(GetHighlightString(prefix, completion), completion));
} }
// Get manual keyword completions // Get manual keyword completions
foreach (var kw in KeywordLexer.keywords) 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, $"<color=#{SignatureHighlighter.keywordBlueHex}>{kw}</color>");
string completion = kw.Substring(input.Length, kw.Length - input.Length); string completion = kw.Substring(input.Length, kw.Length - input.Length);
suggestions.Add(new Suggestion( suggestions.Add(new Suggestion(keywordHighlights[kw], completion));
$"<color=cyan>{input}</color>" +
$"<color=#{SignatureHighlighter.keywordBlueHex}>{completion}</color>",
completion));
} }
} }
@ -100,5 +100,20 @@ namespace UnityExplorer.UI.CSConsole
AutoCompleteModal.Instance.ReleaseOwnership(this); AutoCompleteModal.Instance.ReleaseOwnership(this);
} }
} }
private readonly Dictionary<string, string> keywordHighlights = new Dictionary<string, string>();
private readonly StringBuilder highlightBuilder = new StringBuilder();
private const string OPEN_HIGHLIGHT = "<color=cyan>";
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();
}
} }
} }

View File

@ -38,9 +38,8 @@ namespace UnityExplorer.UI.CSConsole
{ {
"System", "System",
"System.Linq", "System.Linq",
"System.Collections", "System.Text",
"System.Collections.Generic", "System.Collections.Generic",
"System.Reflection",
"UnityEngine", "UnityEngine",
#if CPP #if CPP
"UnhollowerBaseLib", "UnhollowerBaseLib",
@ -68,16 +67,19 @@ namespace UnityExplorer.UI.CSConsole
Lexer = new LexerBuilder(); Lexer = new LexerBuilder();
Completer = new CSAutoCompleter(); Completer = new CSAutoCompleter();
SetupHelpInteraction();
Panel.OnInputChanged += OnInputChanged; Panel.OnInputChanged += OnInputChanged;
Panel.InputScroll.OnScroll += OnInputScrolled; Panel.InputScroll.OnScroll += OnInputScrolled;
Panel.OnCompileClicked += Evaluate; Panel.OnCompileClicked += Evaluate;
Panel.OnResetClicked += ResetConsole; Panel.OnResetClicked += ResetConsole;
Panel.OnHelpDropdownChanged += HelpSelected;
Panel.OnAutoIndentToggled += OnToggleAutoIndent; Panel.OnAutoIndentToggled += OnToggleAutoIndent;
Panel.OnCtrlRToggled += OnToggleCtrlRShortcut; Panel.OnCtrlRToggled += OnToggleCtrlRShortcut;
Panel.OnSuggestionsToggled += OnToggleSuggestions; Panel.OnSuggestionsToggled += OnToggleSuggestions;
} }
#region UI Listeners and options #region UI Listeners and options
// TODO save // TODO save
@ -99,81 +101,6 @@ namespace UnityExplorer.UI.CSConsole
#endregion #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 #region Evaluating
@ -216,19 +143,43 @@ namespace UnityExplorer.UI.CSConsole
{ {
try 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(); Evaluator.Run(input);
var outputSplit = output.Split('\n');
if (outputSplit.Length >= 2)
output = outputSplit[outputSplit.Length - 2];
evaluatorOutput.Clear();
if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) string output = ScriptEvaluator._textWriter.ToString();
throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); var outputSplit = output.Split('\n');
if (outputSplit.Length >= 2)
output = outputSplit[outputSplit.Length - 2];
evaluatorOutput.Clear();
//if (!supressLog) if (ScriptEvaluator._reportPrinter.ErrorsCount > 0)
// ExplorerCore.Log("Code executed successfully."); 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) catch (FormatException fex)
{ {
@ -245,6 +196,145 @@ namespace UnityExplorer.UI.CSConsole
#endregion #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 #region Lexer Highlighting
private static void HighlightVisibleInput() private static void HighlightVisibleInput()
@ -295,31 +385,28 @@ namespace UnityExplorer.UI.CSConsole
public static void InsertSuggestionAtCaret(string suggestion) public static void InsertSuggestionAtCaret(string suggestion)
{ {
settingAutoCompletion = true; settingCaretCoroutine = true;
Input.Text = Input.Text.Insert(LastCaretPosition, suggestion); Input.Text = Input.Text.Insert(LastCaretPosition, suggestion);
RuntimeProvider.Instance.StartCoroutine(SetAutocompleteCaret(LastCaretPosition + suggestion.Length)); SetCaretPosition(LastCaretPosition + suggestion.Length);
LastCaretPosition = Input.Component.caretPosition; LastCaretPosition = Input.Component.caretPosition;
} }
private static IEnumerator SetAutocompleteCaret(int caretPosition) private static void OnAutocompleteEnter()
{ {
var color = Input.Component.selectionColor; // Remove the new line
color.a = 0f; int lastIdx = Input.Component.caretPosition - 1;
Input.Component.selectionColor = color; Input.Text = Input.Text.Remove(lastIdx, 1);
yield return null;
EventSystem.current.SetSelectedGameObject(Panel.Input.UIRoot, null); // Use the selected suggestion
yield return null; Input.Component.caretPosition = LastCaretPosition;
Completer.OnSuggestionClicked(AutoCompleteModal.SelectedSuggestion);
}
Input.Component.caretPosition = caretPosition; private static void OnAutocompleteEscaped()
Input.Component.selectionFocusPosition = caretPosition; {
LastCaretPosition = Input.Component.caretPosition; AutoCompleteModal.Instance.ReleaseOwnership(Completer);
SetCaretPosition(LastCaretPosition);
color.a = defaultInputFieldAlpha;
Input.Component.selectionColor = color;
settingAutoCompletion = false;
} }
@ -359,6 +446,104 @@ namespace UnityExplorer.UI.CSConsole
#endregion #endregion
#region "Help" interaction
private static readonly Dictionary<string, string> helpDict = new Dictionary<string, string>();
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 = @"<color=#5d8556>// 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.</color>";
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
} }
} }

View File

@ -115,6 +115,13 @@ namespace UnityExplorer.UI.CSConsole
lastUnhighlighted = match.endIndex + 1; lastUnhighlighted = match.endIndex + 1;
} }
// Append trailing unhighlighted input
while (lastUnhighlighted <= endIdx)
{
sb.Append(input[lastUnhighlighted]);
lastUnhighlighted++;
}
return sb.ToString(); return sb.ToString();
} }

View File

@ -7,33 +7,16 @@ using System.Linq;
using UnityExplorer.Core.Runtime; using UnityExplorer.Core.Runtime;
using System.Text; 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 namespace UnityExplorer.UI.CSConsole
{ {
public class ScriptInteraction : InteractiveBase public class ScriptInteraction : InteractiveBase
{ {
internal const string STARTUP_TEXT = @"<color=#5d8556>// Compile a using directive to add it to the console (until Reset)</color>
using SomeNamespace;
<color=#5d8556>// Compile a C# class and it will exist until Reset</color>
<color=#5a728c>public class</color> SomeClass {
<color=#5a728c>public static void</color> SomeMethod() {
}
}
<color=#5d8556>// 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:</color>
* System.Object <color=#add490>CurrentTarget</color> - the target of the active Inspector tab
* System.Object[] <color=#add490>AllTargets</color> - an array containing the targets of all Inspector tabs
* void <color=#add490>Log(""message"")</color> - prints a message to the console log
* void <color=#add490>Inspect(someObject)</color> - inspect an instance, eg. Inspect(Camera.main);
* void <color=#add490>Inspect(typeof(SomeClass))</color> - inspect a Class with static reflection
* void <color=#add490>StartCoroutine(ienumerator)</color> - start the IEnumerator as a Coroutine
* void <color=#add490>GetUsing()</color> - prints the current using directives to the console log
* void <color=#add490>GetVars()</color> - prints the variables you have defined and their current values
* void <color=#add490>GetClasses()</color> - prints the names of the classes you have defined, and their members";
public static void Log(object message) public static void Log(object message)
{ {
ExplorerCore.Log(message); ExplorerCore.Log(message);
@ -41,7 +24,7 @@ using SomeNamespace;
public static object CurrentTarget => InspectorManager.ActiveInspector?.Target; 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) public static void Inspect(object obj)
{ {
@ -53,7 +36,7 @@ using SomeNamespace;
InspectorManager.Inspect(type); InspectorManager.Inspect(type);
} }
public static void StartCoroutine(IEnumerator ienumerator) public static void Start(IEnumerator ienumerator)
{ {
RuntimeProvider.Instance.StartCoroutine(ienumerator); RuntimeProvider.Instance.StartCoroutine(ienumerator);
} }
@ -65,7 +48,11 @@ using SomeNamespace;
public static void GetVars() 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() public static void GetClasses()

View File

@ -15,7 +15,7 @@ namespace UnityExplorer.UI.Panels
{ {
public override string Name => "C# Console"; public override string Name => "C# Console";
public override UIManager.Panels PanelType => UIManager.Panels.CSConsole; public override UIManager.Panels PanelType => UIManager.Panels.CSConsole;
public override int MinWidth => 750; public override int MinWidth => 740;
public override int MinHeight => 300; public override int MinHeight => 300;
public InputFieldScroller InputScroll { get; private set; } public InputFieldScroller InputScroll { get; private set; }
@ -23,10 +23,13 @@ namespace UnityExplorer.UI.Panels
public Text InputText { get; private set; } public Text InputText { get; private set; }
public Text HighlightText { get; private set; } public Text HighlightText { get; private set; }
public Action<string> OnInputChanged; public Dropdown HelpDropdown { get; private set; }
// events
public Action<string> OnInputChanged;
public Action OnResetClicked; public Action OnResetClicked;
public Action OnCompileClicked; public Action OnCompileClicked;
public Action<int> OnHelpDropdownChanged;
public Action<bool> OnCtrlRToggled; public Action<bool> OnCtrlRToggled;
public Action<bool> OnSuggestionsToggled; public Action<bool> OnSuggestionsToggled;
public Action<bool> OnAutoIndentToggled; public Action<bool> OnAutoIndentToggled;
@ -74,6 +77,25 @@ namespace UnityExplorer.UI.Panels
default, TextAnchor.MiddleLeft); default, TextAnchor.MiddleLeft);
UIFactory.SetLayoutElement(toolsRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); 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 // Enable Ctrl+R toggle
var ctrlRToggleObj = UIFactory.CreateToggle(toolsRow, "CtrlRToggle", out var CtrlRToggle, out Text ctrlRToggleText); var ctrlRToggleObj = UIFactory.CreateToggle(toolsRow, "CtrlRToggle", out var CtrlRToggle, out Text ctrlRToggleText);
@ -93,36 +115,26 @@ namespace UnityExplorer.UI.Panels
// Enable Auto-indent toggle // Enable Auto-indent toggle
var autoIndentToggleObj = UIFactory.CreateToggle(toolsRow, "IndentToggle", out var AutoIndentToggle, out Text autoIndentToggleText); 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.alignment = TextAnchor.UpperLeft;
autoIndentToggleText.text = "Auto-indent"; autoIndentToggleText.text = "Auto-indent";
AutoIndentToggle.onValueChanged.AddListener((bool val) => { OnAutoIndentToggled?.Invoke(val); }); 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 // Console Input
int fontSize = 16; 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; InputScroll = inputScroller;
ConsoleController.defaultInputFieldAlpha = Input.Component.selectionColor.a; ConsoleController.defaultInputFieldAlpha = Input.Component.selectionColor.a;
Input.OnValueChanged += InvokeOnValueChanged; Input.OnValueChanged += InvokeOnValueChanged;
InputText = Input.Component.textComponent; InputText = Input.Component.textComponent;
InputText.supportRichText = false; InputText.supportRichText = false;
InputText.color = Color.white;
Input.PlaceholderText.fontSize = fontSize; Input.PlaceholderText.fontSize = fontSize;
InputText.color = Color.clear;
Input.Component.customCaretColor = true;
Input.Component.caretColor = Color.white;
// Lexer highlight text overlay // Lexer highlight text overlay
var highlightTextObj = UIFactory.CreateUIObject("HighlightText", InputText.gameObject); var highlightTextObj = UIFactory.CreateUIObject("HighlightText", InputText.gameObject);
@ -134,7 +146,7 @@ namespace UnityExplorer.UI.Panels
highlightTextRect.offsetMax = Vector2.zero; highlightTextRect.offsetMax = Vector2.zero;
HighlightText = highlightTextObj.AddComponent<Text>(); HighlightText = highlightTextObj.AddComponent<Text>();
HighlightText.color = Color.clear; HighlightText.color = Color.white;
HighlightText.supportRichText = true; HighlightText.supportRichText = true;
HighlightText.fontSize = fontSize; HighlightText.fontSize = fontSize;

View File

@ -80,7 +80,7 @@ namespace UnityExplorer.UI.Panels
CurrentStreamPath = Path.Combine(path, fileName); CurrentStreamPath = Path.Combine(path, fileName);
File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message)); File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message).ToArray());
} }
// Logging // Logging

View File

@ -29,12 +29,17 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
public override bool ShouldSaveActiveState => false; public override bool ShouldSaveActiveState => false;
public override bool NavButtonWanted => false; public override bool NavButtonWanted => false;
public ISuggestionProvider CurrentHandler { get; private set; } public static ISuggestionProvider CurrentHandler { get; private set; }
public ButtonListSource<Suggestion> dataHandler; public static ButtonListSource<Suggestion> dataHandler;
public ScrollPool<ButtonCell> scrollPool; public static ScrollPool<ButtonCell> scrollPool;
private List<Suggestion> suggestions = new List<Suggestion>(); private static List<Suggestion> Suggestions = new List<Suggestion>();
private static int SelectedIndex = 0;
public static Suggestion SelectedSuggestion => Suggestions[SelectedIndex];
public static bool Suggesting(ISuggestionProvider handler) => CurrentHandler == handler && Instance.UIRoot.activeSelf;
public AutoCompleteModal() public AutoCompleteModal()
{ {
@ -42,47 +47,6 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
OnClickedOutsidePanels += AutoCompleter_OnClickedOutsidePanels; 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) public void TakeOwnership(ISuggestionProvider provider)
{ {
CurrentHandler = provider; CurrentHandler = provider;
@ -100,43 +64,170 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
} }
} }
private List<Suggestion> GetEntries() => suggestions; public void SetSuggestions(IEnumerable<Suggestion> suggestions)
private bool ShouldDisplay(Suggestion data, string filter) => true;
public void SetSuggestions(IEnumerable<Suggestion> collection)
{ {
suggestions = collection as List<Suggestion> ?? collection.ToList(); Suggestions = suggestions as List<Suggestion> ?? suggestions.ToList();
SelectedIndex = 0;
if (!suggestions.Any()) if (!Suggestions.Any())
UIRoot.SetActive(false); base.UIRoot.SetActive(false);
else else
{ {
UIRoot.SetActive(true); base.UIRoot.SetActive(true);
UIRoot.transform.SetAsLastSibling(); base.UIRoot.transform.SetAsLastSibling();
dataHandler.RefreshData(); dataHandler.RefreshData();
scrollPool.Refresh(true, true); scrollPool.Refresh(true, true);
} }
} }
private static float timeOfLastNavHold = -1f;
/// <summary>
/// Returns true if the AutoCompleteModal used the navigation input, false if not.
/// The navigation inputs are Control+Up/Down, and Control+Enter.
/// </summary>
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<Suggestion> GetEntries() => Suggestions;
private bool ShouldDisplay(Suggestion data, string filter) => true;
private void OnCellClicked(int dataIndex) private void OnCellClicked(int dataIndex)
{ {
var suggestion = suggestions[dataIndex]; var suggestion = Suggestions[dataIndex];
CurrentHandler.OnSuggestionClicked(suggestion); CurrentHandler.OnSuggestionClicked(suggestion);
} }
private bool setFirstCell;
private void SetCell(ButtonCell cell, int index) private void SetCell(ButtonCell cell, int index)
{ {
if (index < 0 || index >= suggestions.Count) if (index < 0 || index >= Suggestions.Count)
{ {
cell.Disable(); cell.Disable();
return; return;
} }
var suggestion = suggestions[index]; var suggestion = Suggestions[index];
cell.Button.ButtonText.text = suggestion.DisplayText; 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 int lastCaretPosition;
private Vector3 lastInputPosition; private Vector3 lastInputPosition;
@ -175,6 +266,35 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
this.Dragger.OnEndResize(); 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() protected internal override void DoSetDefaultPosAndAnchors()
{ {
var mainRect = uiRoot.GetComponent<RectTransform>(); var mainRect = uiRoot.GetComponent<RectTransform>();
@ -190,9 +310,13 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
scrollPool = UIFactory.CreateScrollPool<ButtonCell>(this.content, "AutoCompleter", out GameObject scrollObj, out GameObject scrollContent); scrollPool = UIFactory.CreateScrollPool<ButtonCell>(this.content, "AutoCompleter", out GameObject scrollObj, out GameObject scrollContent);
scrollPool.Initialize(dataHandler); scrollPool.Initialize(dataHandler);
UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999); UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999);
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(scrollContent, true, false, true, false); UIFactory.SetLayoutGroup<VerticalLayoutGroup>(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); UIRoot.SetActive(false);
} }

View File

@ -11,17 +11,12 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
{ {
public readonly string DisplayText; public readonly string DisplayText;
public readonly string UnderlyingValue; 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; DisplayText = displayText;
//Addition = addition;
//Prefix = prefix;
UnderlyingValue = underlyingValue; UnderlyingValue = underlyingValue;
} }
} }