mirror of
https://github.com/GrahamKracker/UnityExplorer.git
synced 2025-07-01 11:12:49 +08:00
492 lines
18 KiB
C#
492 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using Explorer.UI.Main.Pages.Console;
|
|
using ExplorerBeta;
|
|
using ExplorerBeta.UI;
|
|
using ExplorerBeta.UI.Main;
|
|
using ExplorerBeta.Unstrip.Resources;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
#if CPP
|
|
using UnhollowerRuntimeLib;
|
|
#endif
|
|
|
|
namespace Explorer.UI.Main.Pages
|
|
{
|
|
// TODO: Maybe add interface for managing the Using directives of the console.
|
|
// Otherwise it's pretty much done.
|
|
|
|
public class ConsolePage : BaseMenuPage
|
|
{
|
|
public override string Name => "C# Console";
|
|
|
|
public static ConsolePage Instance { get; private set; }
|
|
|
|
public static bool EnableSuggestions { get; set; } = true;
|
|
public static bool EnableAutoIndent { get; set; } = true;
|
|
|
|
public CodeEditor m_codeEditor;
|
|
public ScriptEvaluator m_evaluator;
|
|
|
|
public static List<AutoComplete> AutoCompletes = new List<AutoComplete>();
|
|
public static List<string> UsingDirectives;
|
|
|
|
public static readonly string[] DefaultUsing = new string[]
|
|
{
|
|
"System",
|
|
"UnityEngine",
|
|
"System.Linq",
|
|
"System.Collections",
|
|
"System.Collections.Generic",
|
|
"System.Reflection"
|
|
};
|
|
|
|
public override void Init()
|
|
{
|
|
Instance = this;
|
|
|
|
try
|
|
{
|
|
ResetConsole();
|
|
|
|
foreach (var use in DefaultUsing)
|
|
{
|
|
AddUsing(use);
|
|
}
|
|
|
|
ConstructUI();
|
|
|
|
AutoCompleter.Init();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ExplorerCore.LogWarning($"Error setting up console!\r\nMessage: {e.Message}");
|
|
// TODO
|
|
}
|
|
}
|
|
|
|
public override void Update()
|
|
{
|
|
m_codeEditor?.Update();
|
|
|
|
AutoCompleter.Update();
|
|
}
|
|
|
|
internal string AsmToUsing(string asm) => $"using {asm};";
|
|
|
|
public void AddUsing(string asm)
|
|
{
|
|
if (!UsingDirectives.Contains(asm))
|
|
{
|
|
UsingDirectives.Add(asm);
|
|
Evaluate(AsmToUsing(asm), true);
|
|
}
|
|
}
|
|
|
|
public object Evaluate(string str, bool suppressWarning = false)
|
|
{
|
|
object ret = VoidType.Value;
|
|
|
|
m_evaluator.Compile(str, out var compiled);
|
|
|
|
try
|
|
{
|
|
if (compiled == null)
|
|
{
|
|
throw new Exception("Mono.Csharp Service was unable to compile the code provided.");
|
|
}
|
|
|
|
compiled.Invoke(ref ret);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (!suppressWarning)
|
|
{
|
|
ExplorerCore.LogWarning(e.GetType() + ", " + e.Message);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
public void ResetConsole()
|
|
{
|
|
if (m_evaluator != null)
|
|
{
|
|
m_evaluator.Dispose();
|
|
}
|
|
|
|
m_evaluator = new ScriptEvaluator(new StringWriter(new StringBuilder())) { InteractiveBaseClass = typeof(ScriptInteraction) };
|
|
|
|
UsingDirectives = new List<string>();
|
|
}
|
|
|
|
internal void OnInputChanged()
|
|
{
|
|
AutoCompleter.CheckAutocomplete();
|
|
|
|
AutoCompleter.SetSuggestions(AutoCompletes.ToArray());
|
|
}
|
|
|
|
|
|
public void UseAutocomplete(string suggestion)
|
|
{
|
|
int cursorIndex = m_codeEditor.InputField.caretPosition;
|
|
var input = m_codeEditor.InputField.text;
|
|
input = input.Insert(cursorIndex, suggestion);
|
|
m_codeEditor.InputField.text = input;
|
|
m_codeEditor.InputField.caretPosition += suggestion.Length;
|
|
|
|
AutoCompleter.ClearAutocompletes();
|
|
}
|
|
|
|
#region UI Construction
|
|
|
|
public void ConstructUI()
|
|
{
|
|
Content = UIFactory.CreateUIObject("C# Console", MainMenu.Instance.PageViewport);
|
|
|
|
var mainLayout = Content.AddComponent<LayoutElement>();
|
|
mainLayout.preferredHeight = 300;
|
|
mainLayout.flexibleHeight = 4;
|
|
|
|
var mainGroup = Content.AddComponent<VerticalLayoutGroup>();
|
|
mainGroup.childControlHeight = true;
|
|
mainGroup.childControlWidth = true;
|
|
mainGroup.childForceExpandHeight = true;
|
|
mainGroup.childForceExpandWidth = true;
|
|
|
|
#region TOP BAR
|
|
|
|
// Main group object
|
|
|
|
var topBarObj = UIFactory.CreateHorizontalGroup(Content);
|
|
var topBarLayout = topBarObj.AddComponent<LayoutElement>();
|
|
topBarLayout.minHeight = 50;
|
|
topBarLayout.flexibleHeight = 0;
|
|
|
|
var topBarGroup = topBarObj.GetComponent<HorizontalLayoutGroup>();
|
|
topBarGroup.padding.left = 30;
|
|
topBarGroup.padding.right = 30;
|
|
topBarGroup.padding.top = 8;
|
|
topBarGroup.padding.bottom = 8;
|
|
topBarGroup.spacing = 10;
|
|
topBarGroup.childForceExpandHeight = true;
|
|
topBarGroup.childForceExpandWidth = true;
|
|
topBarGroup.childControlWidth = true;
|
|
topBarGroup.childControlHeight = true;
|
|
topBarGroup.childAlignment = TextAnchor.LowerCenter;
|
|
|
|
var topBarLabel = UIFactory.CreateLabel(topBarObj, TextAnchor.MiddleLeft);
|
|
var topBarLabelLayout = topBarLabel.AddComponent<LayoutElement>();
|
|
topBarLabelLayout.preferredWidth = 800;
|
|
topBarLabelLayout.flexibleWidth = 10;
|
|
var topBarText = topBarLabel.GetComponent<Text>();
|
|
topBarText.text = "C# Console";
|
|
topBarText.fontSize = 20;
|
|
|
|
// Enable Suggestions toggle
|
|
|
|
var suggestToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle suggestToggle, out Text suggestToggleText);
|
|
#if CPP
|
|
suggestToggle.onValueChanged.AddListener(new Action<bool>(SuggestToggleCallback));
|
|
#else
|
|
suggestToggle.onValueChanged.AddListener(SuggestToggleCallback);
|
|
#endif
|
|
void SuggestToggleCallback(bool val)
|
|
{
|
|
EnableSuggestions = val;
|
|
AutoCompleter.Update();
|
|
}
|
|
|
|
suggestToggleText.text = "Suggestions";
|
|
suggestToggleText.alignment = TextAnchor.UpperLeft;
|
|
var suggestTextPos = suggestToggleText.transform.localPosition;
|
|
suggestTextPos.y = -14;
|
|
suggestToggleText.transform.localPosition = suggestTextPos;
|
|
|
|
var suggestLayout = suggestToggleObj.AddComponent<LayoutElement>();
|
|
suggestLayout.minWidth = 120;
|
|
suggestLayout.flexibleWidth = 0;
|
|
|
|
var suggestRect = suggestToggleObj.transform.Find("Background");
|
|
var suggestPos = suggestRect.localPosition;
|
|
suggestPos.y = -14;
|
|
suggestRect.localPosition = suggestPos;
|
|
|
|
// Enable Auto-indent toggle
|
|
|
|
var autoIndentToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle autoIndentToggle, out Text autoIndentToggleText);
|
|
#if CPP
|
|
autoIndentToggle.onValueChanged.AddListener(new Action<bool>((bool val) =>
|
|
{
|
|
EnableAutoIndent = val;
|
|
}));
|
|
#else
|
|
autoIndentToggle.onValueChanged.AddListener(OnIndentChanged);
|
|
|
|
void OnIndentChanged(bool val) => EnableAutoIndent = val;
|
|
#endif
|
|
autoIndentToggleText.text = "Auto-indent";
|
|
autoIndentToggleText.alignment = TextAnchor.UpperLeft;
|
|
var autoIndentTextPos = autoIndentToggleText.transform.localPosition;
|
|
autoIndentTextPos.y = -14;
|
|
autoIndentToggleText.transform.localPosition = autoIndentTextPos;
|
|
|
|
var autoIndentLayout = autoIndentToggleObj.AddComponent<LayoutElement>();
|
|
autoIndentLayout.minWidth = 120;
|
|
autoIndentLayout.flexibleWidth = 0;
|
|
|
|
var autoIndentRect = autoIndentToggleObj.transform.Find("Background");
|
|
suggestPos = autoIndentRect.localPosition;
|
|
suggestPos.y = -14;
|
|
autoIndentRect.localPosition = suggestPos;
|
|
|
|
#endregion
|
|
|
|
#region CONSOLE INPUT
|
|
|
|
var consoleBase = UIFactory.CreateUIObject("CodeEditor", Content);
|
|
|
|
var consoleLayout = consoleBase.AddComponent<LayoutElement>();
|
|
consoleLayout.preferredHeight = 500;
|
|
consoleLayout.flexibleHeight = 50;
|
|
|
|
consoleBase.AddComponent<RectMask2D>();
|
|
|
|
var mainRect = consoleBase.GetComponent<RectTransform>();
|
|
mainRect.pivot = Vector2.one * 0.5f;
|
|
mainRect.anchorMin = Vector2.zero;
|
|
mainRect.anchorMax = Vector2.one;
|
|
mainRect.offsetMin = Vector2.zero;
|
|
mainRect.offsetMax = Vector2.zero;
|
|
|
|
var mainBg = UIFactory.CreateUIObject("MainBackground", consoleBase);
|
|
|
|
var mainBgRect = mainBg.GetComponent<RectTransform>();
|
|
mainBgRect.pivot = new Vector2(0, 1);
|
|
mainBgRect.anchorMin = Vector2.zero;
|
|
mainBgRect.anchorMax = Vector2.one;
|
|
mainBgRect.offsetMin = Vector2.zero;
|
|
mainBgRect.offsetMax = Vector2.zero;
|
|
|
|
var mainBgImage = mainBg.AddComponent<Image>();
|
|
|
|
var lineHighlight = UIFactory.CreateUIObject("LineHighlight", consoleBase);
|
|
|
|
var lineHighlightRect = lineHighlight.GetComponent<RectTransform>();
|
|
lineHighlightRect.pivot = new Vector2(0.5f, 1);
|
|
lineHighlightRect.anchorMin = new Vector2(0, 1);
|
|
lineHighlightRect.anchorMax = new Vector2(1, 1);
|
|
lineHighlightRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 21);
|
|
|
|
var lineHighlightImage = lineHighlight.GetComponent<Image>();
|
|
if (!lineHighlightImage)
|
|
lineHighlightImage = lineHighlight.AddComponent<Image>();
|
|
|
|
var linesBg = UIFactory.CreateUIObject("LinesBackground", consoleBase);
|
|
var linesBgRect = linesBg.GetComponent<RectTransform>();
|
|
linesBgRect.anchorMin = Vector2.zero;
|
|
linesBgRect.anchorMax = new Vector2(0, 1);
|
|
linesBgRect.offsetMin = new Vector2(-17.5f, 0);
|
|
linesBgRect.offsetMax = new Vector2(17.5f, 0);
|
|
linesBgRect.sizeDelta = new Vector2(65, 0);
|
|
|
|
var linesBgImage = linesBg.AddComponent<Image>();
|
|
|
|
var inputObj = UIFactory.CreateTMPInput(consoleBase);
|
|
|
|
var inputField = inputObj.GetComponent<TMP_InputField>();
|
|
inputField.richText = false;
|
|
|
|
var inputRect = inputObj.GetComponent<RectTransform>();
|
|
inputRect.pivot = new Vector2(0.5f, 0.5f);
|
|
inputRect.anchorMin = Vector2.zero;
|
|
inputRect.anchorMax = new Vector2(0.92f, 1);
|
|
inputRect.offsetMin = new Vector2(20, 0);
|
|
inputRect.offsetMax = new Vector2(14, 0);
|
|
inputRect.anchoredPosition = new Vector2(40, 0);
|
|
|
|
var textAreaObj = inputObj.transform.Find("TextArea");
|
|
var textAreaRect = textAreaObj.GetComponent<RectTransform>();
|
|
textAreaRect.pivot = new Vector2(0.5f, 0.5f);
|
|
textAreaRect.anchorMin = Vector2.zero;
|
|
textAreaRect.anchorMax = Vector2.one;
|
|
|
|
var mainTextObj = textAreaObj.transform.Find("Text");
|
|
var mainTextRect = mainTextObj.GetComponent<RectTransform>();
|
|
mainTextRect.pivot = new Vector2(0.5f, 0.5f);
|
|
mainTextRect.anchorMin = Vector2.zero;
|
|
mainTextRect.anchorMax = Vector2.one;
|
|
mainTextRect.offsetMin = Vector2.zero;
|
|
mainTextRect.offsetMax = Vector2.zero;
|
|
|
|
var mainTextInput = mainTextObj.GetComponent<TextMeshProUGUI>();
|
|
mainTextInput.fontSize = 18;
|
|
|
|
var placeHolderText = textAreaObj.transform.Find("Placeholder").GetComponent<TextMeshProUGUI>();
|
|
placeHolderText.text = @"Welcome to the Explorer C# Console!
|
|
|
|
Use the <color=#c74e26>Help();</color> command for a list of available helper methods, or <color=#c74e26>Log(""[message]"");</color> to print something to console.";
|
|
|
|
var linesTextObj = UIFactory.CreateUIObject("LinesText", mainTextObj.gameObject);
|
|
var linesTextRect = linesTextObj.GetComponent<RectTransform>();
|
|
|
|
var linesTextInput = linesTextObj.AddComponent<TextMeshProUGUI>();
|
|
linesTextInput.fontSize = 18;
|
|
|
|
var highlightTextObj = UIFactory.CreateUIObject("HighlightText", mainTextObj.gameObject);
|
|
var highlightTextRect = highlightTextObj.GetComponent<RectTransform>();
|
|
highlightTextRect.anchorMin = Vector2.zero;
|
|
highlightTextRect.anchorMax = Vector2.one;
|
|
highlightTextRect.offsetMin = Vector2.zero;
|
|
highlightTextRect.offsetMax = Vector2.zero;
|
|
|
|
var highlightTextInput = highlightTextObj.AddComponent<TextMeshProUGUI>();
|
|
highlightTextInput.fontSize = 18;
|
|
|
|
var scroll = UIFactory.CreateScrollbar(consoleBase);
|
|
|
|
var scrollRect = scroll.GetComponent<RectTransform>();
|
|
scrollRect.anchorMin = new Vector2(1, 0);
|
|
scrollRect.anchorMax = new Vector2(1, 1);
|
|
scrollRect.pivot = new Vector2(0.5f, 1);
|
|
scrollRect.offsetMin = new Vector2(-25f, 0);
|
|
|
|
var scroller = scroll.GetComponent<Scrollbar>();
|
|
scroller.direction = Scrollbar.Direction.TopToBottom;
|
|
var scrollColors = scroller.colors;
|
|
scrollColors.normalColor = new Color(0.6f, 0.6f, 0.6f, 1.0f);
|
|
scroller.colors = scrollColors;
|
|
|
|
var scrollImage = scroll.GetComponent<Image>();
|
|
|
|
var tmpInput = inputObj.GetComponent<TMP_InputField>();
|
|
tmpInput.scrollSensitivity = 15;
|
|
tmpInput.verticalScrollbar = scroller;
|
|
|
|
// set lines text anchors here after UI is fleshed out
|
|
linesTextRect.pivot = Vector2.zero;
|
|
linesTextRect.anchorMin = new Vector2(0, 0);
|
|
linesTextRect.anchorMax = new Vector2(1, 1);
|
|
linesTextRect.offsetMin = Vector2.zero;
|
|
linesTextRect.offsetMax = Vector2.zero;
|
|
linesTextRect.anchoredPosition = new Vector2(-40, 0);
|
|
|
|
tmpInput.GetComponentInChildren<RectMask2D>().enabled = false;
|
|
inputObj.GetComponent<Image>().enabled = false;
|
|
|
|
#endregion
|
|
|
|
#region COMPILE BUTTON
|
|
|
|
var compileBtnObj = UIFactory.CreateButton(Content);
|
|
var compileBtnLayout = compileBtnObj.AddComponent<LayoutElement>();
|
|
compileBtnLayout.preferredWidth = 80;
|
|
compileBtnLayout.flexibleWidth = 0;
|
|
compileBtnLayout.minHeight = 45;
|
|
compileBtnLayout.flexibleHeight = 0;
|
|
var compileButton = compileBtnObj.GetComponent<Button>();
|
|
var compileBtnColors = compileButton.colors;
|
|
compileBtnColors.normalColor = new Color(14f / 255f, 80f / 255f, 14f / 255f);
|
|
compileButton.colors = compileBtnColors;
|
|
var btnText = compileBtnObj.GetComponentInChildren<Text>();
|
|
btnText.text = "Run";
|
|
btnText.fontSize = 18;
|
|
btnText.color = Color.white;
|
|
|
|
// Set compile button callback now that we have the Input Field reference
|
|
#if CPP
|
|
compileButton.onClick.AddListener(new Action(() =>
|
|
{
|
|
if (!string.IsNullOrEmpty(tmpInput.text))
|
|
{
|
|
Evaluate(tmpInput.text.Trim());
|
|
}
|
|
}));
|
|
#else
|
|
compileButton.onClick.AddListener(CompileCallback);
|
|
|
|
void CompileCallback()
|
|
{
|
|
if (!string.IsNullOrEmpty(tmpInput.text))
|
|
{
|
|
Evaluate(tmpInput.text.Trim());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#endregion
|
|
|
|
#region FONT
|
|
|
|
TMP_FontAsset fontToUse = null;
|
|
#if CPP
|
|
var fonts = ResourcesUnstrip.FindObjectsOfTypeAll(Il2CppType.Of<TMP_FontAsset>());
|
|
foreach (var font in fonts)
|
|
{
|
|
var fontCast = font.Il2CppCast(typeof(TMP_FontAsset)) as TMP_FontAsset;
|
|
|
|
if (fontCast.name.Contains("LiberationSans"))
|
|
{
|
|
fontToUse = fontCast;
|
|
break;
|
|
}
|
|
}
|
|
#else
|
|
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
|
|
foreach (var font in fonts)
|
|
{
|
|
if (font.name.Contains("LiberationSans"))
|
|
{
|
|
fontToUse = font;
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
if (fontToUse != null)
|
|
{
|
|
var faceInfo = fontToUse.faceInfo;
|
|
fontToUse.tabSize = 10;
|
|
faceInfo.tabWidth = 10;
|
|
#if CPP
|
|
fontToUse.faceInfo = faceInfo;
|
|
#else
|
|
typeof(TMP_FontAsset)
|
|
.GetField("m_FaceInfo", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
.SetValue(fontToUse, faceInfo);
|
|
#endif
|
|
|
|
tmpInput.fontAsset = fontToUse;
|
|
mainTextInput.font = fontToUse;
|
|
highlightTextInput.font = fontToUse;
|
|
}
|
|
|
|
#endregion
|
|
|
|
try
|
|
{
|
|
m_codeEditor = new CodeEditor(inputField, mainTextInput, highlightTextInput, linesTextInput,
|
|
mainBgImage, lineHighlightImage, linesBgImage, scrollImage);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ExplorerCore.Log(e);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private class VoidType
|
|
{
|
|
public static readonly VoidType Value = new VoidType();
|
|
private VoidType() { }
|
|
}
|
|
}
|
|
}
|