diff --git a/.gitignore b/.gitignore index 5891524..e645270 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ *.user *.userosscache *.sln.docstates -*/mcs-unity* # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..efb103e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/Il2CppAssemblyUnhollower"] + path = lib/Il2CppAssemblyUnhollower + url = https://github.com/knah/Il2CppAssemblyUnhollower +[submodule "lib/HarmonyX"] + path = lib/HarmonyX + url = https://github.com/BepInEx/HarmonyX +[submodule "lib/mcs-unity"] + path = lib/mcs-unity + url = https://github.com/sinai-dev/mcs-unity diff --git a/README.md b/README.md index 41ed3e7..455a144 100644 --- a/README.md +++ b/README.md @@ -16,61 +16,39 @@ | ----------- | ------ | ---- | | [BepInEx](https://github.com/BepInEx/BepInEx) 6.X | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx6.Mono.zip) | | [BepInEx](https://github.com/BepInEx/BepInEx) 5.X | ✖️ n/a | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx5.Mono.zip) | -| [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) 0.3 | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Mono.zip) | +| [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) 0.3.1 | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Mono.zip) | | Standalone | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Standalone.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Standalone.Mono.zip) | +### Known issues +* UI layouts broken/unusable after changing resolutions: delete the file `data.ini` in the UnityExplorer folder (same place as where you put the DLL). Better fix being worked on. +* Any `MissingMethodException` or `NotSupportedException`: please report the issue and provide a copy of your mod loader log and/or Unity log. +* The C# console may unexpectedly produce a GC Mark Overflow crash when calling certain outside methods. Not clear yet what is causing this, but it's being looked into. +* In IL2CPP, some IEnumerable and IDictionary types may fail enumeration. Waiting for the Unhollower rewrite to address this any further. +* In IL2CPP, the C# console might not suggest deobfuscated (or obfuscated) names. Being looked into. + ## How to install ### BepInEx -1. Install [BepInEx](https://github.com/BepInEx/BepInEx) for your game. For IL2CPP you should use [BepInEx 6 (Bleeding Edge)](https://builds.bepis.io/projects/bepinex_be), for Mono you should use [BepInEx 5](https://github.com/BepInEx/BepInEx/releases) (until Mono support stabilizes in BepInEx 6). +1. Install [BepInEx](https://github.com/BepInEx/BepInEx) for your game. IL2CPP currently requires a [Bleeding Edge](https://builds.bepis.io/projects/bepinex_be) release. 2. Download the UnityExplorer release for BepInEx IL2CPP or Mono above. 3. Take the `UnityExplorer.BIE.___.dll` file and put it in `[GameFolder]\BepInEx\plugins\` 4. In IL2CPP, you will need to download the [Unity libs](https://github.com/LavaGang/Unity-Runtime-Libraries) for the game's Unity version and put them in the `BepInEx\unity-libs\` folder. ### MelonLoader -1. Install [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) 0.3+ for your game. Version 0.3 is currently in pre-release, so you must "Enable ALPHA Releases" in your MelonLoader Installer settings to see the option for it. +1. Install [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) 0.3.1+ for your game. This version can currently be obtained from [here](https://github.com/LavaGang/MelonLoader/actions). 2. Download the UnityExplorer release for MelonLoader IL2CPP or Mono above. 3. Take the `UnityExplorer.ML.___.dll` file and put it in the `[GameFolder]\Mods\` folder. ### Standalone -The standalone release requires you to also load `0Harmony.dll` (HarmonyX, from the `lib\` folder) to function properly, and the IL2CPP also requires `UnhollowerBaseLib.dll` as well. The Mono release should be fairly easy to use with any loader, but the IL2CPP one may be tricky, I'd recommend just using BepInEx or MelonLoader for IL2CPP. +The standalone release can be used with any injector or loader of your choice, but it requires you to load the dependencies manually: HarmonyX, and the IL2CPP version also requires that you set up an [Il2CppAssemblyUnhollower runtime](https://github.com/knah/Il2CppAssemblyUnhollower#required-external-setup). -1. Load the UnityExplorer DLL from your mod or inject it, as well as `0Harmony.dll` and `UnhollowerBaseLib.dll` as required. -2. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();` -3. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish. - -## Issues and contributions - -Both issue reports and PR contributions are welcome in this repository. - -### Issue reporting - -To report an issue with UnityExplorer, please use the following template (as well as any other information / images you can provide). - -Template: - -``` -* Game issue occurs on: -* UnityExplorer version: (eg BIE.IL2CPP v3.3.3, etc) - -* Description of issue: - -* Unity log link: -* Mod Loader log link: -``` - -Please upload your Unity log file to [Pastebin](https://pastebin.com/) (or equivalent service) and provide a link to the paste. - -* The log should be found at `%userprofile%\AppData\LocalLow\[Company]\[Game]\` unless redirected by your Mod Loader. -* The file will be called either `output_log.txt` or `Player.log` - -As well as the Unity log, please upload your Mod Loader's log: - -* BepInEx: `BepInEx\LogOutput.log` -* MelonLoader: `MelonLoader\Latest.log` +1. Load the required libs - HarmonyX, and Il2CppAssemblyUnhollower if IL2CPP +2. Load the UnityExplorer DLL +3. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();` +4. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish ## Features @@ -80,44 +58,31 @@ As well as the Unity log, please upload your Mod Loader's log:

-* Scene Explorer: Simple menu to traverse the Transform heirarchy of the scene. -* GameObject Inspector: Various helpful tools to see and manipulate the GameObject, similar to what you can do in the Editor. -* Reflection Inspector: Inspect Properties and Fields. Can also set primitive values and evaluate primitive methods. -* Search: Search for UnityEngine.Objects with various filters, or use the helpers for static Instances and Classes. -* C# Console: Interactive console for evaluating C# methods on the fly, with some basic helpers. -* Inspect-under-mouse: Hover over an object with a collider and inspect it by clicking on it. There's also a UI mode to inspect UI objects. +### Object Explorer -### C# Console Tips +* Use the Scene Explorer tab to traverse the active scenes, as well as the DontDestroyOnLoad scene and the HideAndDontSave "scene" (assets and hidden objects). +* Use the Object Search tab to search for Unity objects (including GameObjects, Components, etc), C# Singletons or Static Classes. -The C# Console can be used to define temporary classes and methods, or it can be used to evaluate an expression, but you cannot do both at the same time. +### Inspector -For example, you could run this code to define a temporary class (it will be visible within the console until you run `Reset();`). +The inspector is used to see detailed information on GameObjects (GameObject Inspector), C# objects (Reflection Inspector) and C# classes (Static Inspector). -```csharp -public class MyClass -{ - public static void Method() - { - UnityExplorer.ExplorerCore.Log("hello"); - } -} -``` +For the GameObject Inspector, you can edit any of the input fields in the inspector (excluding readonly fields) and press Enter to apply your changes. You can also do this to the GameObject path as a way to change the GameObject's parent. Press the Escape key to cancel your edits. -You could then delete or comment out the class and run the following expression to run that method: +In the Reflection Inspectors, automatic updating is not enabled by default, and you must press Apply for any changes you make to take effect. -```csharp -MyClass.Method(); -``` +### C# Console -However, you cannot define a class and run it both at the same time. You must either define class(es) and run that, or define an expression and run that. +The C# Console uses the `Mono.CSharp.Evaluator` to define temporary classes or run immediate REPL code. -You can also make use of the helper methods in the console to simplify some tasks, which you can see listed when the console has nothing entered for input. These methods are **not** accessible within any temporary classes you define, they can only be used in the expression context. +See the "Help" dropdown in the C# console menu for more detailed information. -### Logging +### Mouse-Inspect -Explorer saves all logs to disk (only keeps the most recent 10 logs). They can be found in a "UnityExplorer" folder in the same place as where you put the DLL file. +The "Mouse Inspect" dropdown on the main UnityExplorer navbar allows you to inspect objects under the mouse. -These logs are also visible in the Debug Console part of the UI. +* World: uses Physics.Raycast to look for Colliders +* UI: uses GraphicRaycasters to find UI objects ### Settings @@ -139,10 +104,9 @@ Building the project should be straight-forward, the references are all inside t ## Acknowledgments -* [ManlyMarco](https://github.com/ManlyMarco) for [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor) \[[license](THIRDPARTY_LICENSES.md#runtimeunityeditor-license)\], snippets from the REPL Console were used for UnityExplorer's C# Console. +* [ManlyMarco](https://github.com/ManlyMarco) for [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor) \[[license](THIRDPARTY_LICENSES.md#runtimeunityeditor-license)\], the ScriptEvaluator from RUE's REPL console was used as the base for UnityExplorer's C# console. * [denikson](https://github.com/denikson) (aka Horse) for [mcs-unity](https://github.com/denikson/mcs-unity) \[no license\], used as the `Mono.CSharp` reference for the C# Console. * [HerpDerpenstine](https://github.com/HerpDerpinstine) for [MelonCoroutines](https://github.com/LavaGang/MelonLoader/blob/6cc958ec23b5e2e8453a73bc2e0d5aa353d4f0d1/MelonLoader.Support.Il2Cpp/MelonCoroutines.cs) \[[license](THIRDPARTY_LICENSES.md#melonloader-license)\], they were included for standalone IL2CPP coroutine support. -* [InGameCodeEditor](https://assetstore.unity.com/packages/tools/gui/ingame-code-editor-144254) \[[license](THIRDPARTY_LICENSES.md#ingamecodeeditor-license)\] was used as the base for the syntax highlighting for UnityExplorer's C# console (`UnityExplorer.UI.Main.CSConsole.Lexer`). ### Disclaimer diff --git a/THIRDPARTY_LICENSES.md b/THIRDPARTY_LICENSES.md index 51f1514..78877a8 100644 --- a/THIRDPARTY_LICENSES.md +++ b/THIRDPARTY_LICENSES.md @@ -1,6 +1,5 @@ * [RuntimeUnityEditor License](#runtimeunityeditor-license) * [MelonLoader License](#melonloader-license) -* [InGameCodeEditor License](#ingamecodeeditor-license) ## RuntimeUnityEditor License @@ -873,195 +872,3 @@ Public License instead of this License. But first, please read See the License for the specific language governing permissions and limitations under the License. -## InGameCodeEditor License - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2020 - 2021 Lava Gang - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/img/preview.png b/img/preview.png index 584ab16..6865e79 100644 Binary files a/img/preview.png and b/img/preview.png differ diff --git a/img/social.png b/img/social.png index 5162087..6a67228 100644 Binary files a/img/social.png and b/img/social.png differ diff --git a/lib/0Harmony.dll b/lib/0Harmony.dll deleted file mode 100644 index eec6135..0000000 Binary files a/lib/0Harmony.dll and /dev/null differ diff --git a/lib/BepInEx.dll b/lib/BepInEx.5/BepInEx.dll similarity index 100% rename from lib/BepInEx.dll rename to lib/BepInEx.5/BepInEx.dll diff --git a/lib/BepInEx.6.IL2CPP/BepInEx.Core.dll b/lib/BepInEx.6.IL2CPP/BepInEx.Core.dll new file mode 100644 index 0000000..c708484 Binary files /dev/null and b/lib/BepInEx.6.IL2CPP/BepInEx.Core.dll differ diff --git a/lib/BepInEx.6.IL2CPP/BepInEx.IL2CPP.dll b/lib/BepInEx.6.IL2CPP/BepInEx.IL2CPP.dll new file mode 100644 index 0000000..f087db6 Binary files /dev/null and b/lib/BepInEx.6.IL2CPP/BepInEx.IL2CPP.dll differ diff --git a/lib/BepInEx.6.Mono/BepInEx.Core.dll b/lib/BepInEx.6.Mono/BepInEx.Core.dll new file mode 100644 index 0000000..35205cd Binary files /dev/null and b/lib/BepInEx.6.Mono/BepInEx.Core.dll differ diff --git a/lib/BepInEx.6.Mono/BepInEx.Unity.dll b/lib/BepInEx.6.Mono/BepInEx.Unity.dll new file mode 100644 index 0000000..1ac65ac Binary files /dev/null and b/lib/BepInEx.6.Mono/BepInEx.Unity.dll differ diff --git a/lib/BepInEx.Core.dll b/lib/BepInEx.Core.dll deleted file mode 100644 index 5121282..0000000 Binary files a/lib/BepInEx.Core.dll and /dev/null differ diff --git a/lib/BepInEx.IL2CPP.dll b/lib/BepInEx.IL2CPP.dll deleted file mode 100644 index b8203de..0000000 Binary files a/lib/BepInEx.IL2CPP.dll and /dev/null differ diff --git a/lib/BepInEx.Unity.dll b/lib/BepInEx.Unity.dll deleted file mode 100644 index c26ad65..0000000 Binary files a/lib/BepInEx.Unity.dll and /dev/null differ diff --git a/lib/HarmonyX b/lib/HarmonyX new file mode 160000 index 0000000..64462b3 --- /dev/null +++ b/lib/HarmonyX @@ -0,0 +1 @@ +Subproject commit 64462b3e31abcbc3839fbfae10b620f2a693de31 diff --git a/lib/INIFileParser.dll b/lib/INIFileParser.dll deleted file mode 100644 index d19e920..0000000 Binary files a/lib/INIFileParser.dll and /dev/null differ diff --git a/lib/Il2CppAssemblyUnhollower b/lib/Il2CppAssemblyUnhollower new file mode 160000 index 0000000..0911fda --- /dev/null +++ b/lib/Il2CppAssemblyUnhollower @@ -0,0 +1 @@ +Subproject commit 0911fdaca645b75727c4cf104003918aa8d8296c diff --git a/lib/MelonLoader.dll b/lib/MelonLoader.dll deleted file mode 100644 index 7ea7b22..0000000 Binary files a/lib/MelonLoader.dll and /dev/null differ diff --git a/lib/MelonLoader/MelonLoader.dll b/lib/MelonLoader/MelonLoader.dll new file mode 100644 index 0000000..133715e Binary files /dev/null and b/lib/MelonLoader/MelonLoader.dll differ diff --git a/lib/UnhollowerBaseLib.dll b/lib/UnhollowerBaseLib.dll deleted file mode 100644 index ab3e40f..0000000 Binary files a/lib/UnhollowerBaseLib.dll and /dev/null differ diff --git a/lib/UnityEngine.UI.dll b/lib/UnityEngine.UI.dll deleted file mode 100644 index 74933d5..0000000 Binary files a/lib/UnityEngine.UI.dll and /dev/null differ diff --git a/lib/UnityEngine.dll b/lib/UnityEngine.dll deleted file mode 100644 index 29d73f6..0000000 Binary files a/lib/UnityEngine.dll and /dev/null differ diff --git a/lib/mcs-unity b/lib/mcs-unity new file mode 160000 index 0000000..0bc7359 --- /dev/null +++ b/lib/mcs-unity @@ -0,0 +1 @@ +Subproject commit 0bc7359dd75049c68920830b980a92c5fa35995b diff --git a/lib/mcs.dll b/lib/mcs.dll deleted file mode 100644 index e83df45..0000000 Binary files a/lib/mcs.dll and /dev/null differ diff --git a/lib/mono/UnityEngine.UI.dll b/lib/mono/UnityEngine.UI.dll new file mode 100644 index 0000000..838d63b Binary files /dev/null and b/lib/mono/UnityEngine.UI.dll differ diff --git a/lib/mono/UnityEngine.dll b/lib/mono/UnityEngine.dll new file mode 100644 index 0000000..606ce38 Binary files /dev/null and b/lib/mono/UnityEngine.dll differ diff --git a/src/Core/CSharp/ScriptInteraction.cs b/src/Core/CSharp/ScriptInteraction.cs deleted file mode 100644 index 0c71220..0000000 --- a/src/Core/CSharp/ScriptInteraction.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using Mono.CSharp; -using System.Collections; -using UnityEngine; -using System.Collections.Generic; -using System.Linq; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.Main.CSConsole; -using UnityExplorer.UI.Main.Home; -using UnityExplorer.UI.Inspectors; - -namespace UnityExplorer.Core.CSharp -{ - public class ScriptInteraction : InteractiveBase - { - public static void Log(object message) - { - ExplorerCore.Log(message); - } - - public static void StartCoroutine(IEnumerator ienumerator) - { - RuntimeProvider.Instance.StartCoroutine(ienumerator); - } - - public static void AddUsing(string directive) - { - CSharpConsole.Instance.AddUsing(directive); - } - - public static void GetUsing() - { - ExplorerCore.Log(CSharpConsole.Instance.Evaluator.GetUsing()); - } - - public static void Reset() - { - CSharpConsole.Instance.ResetConsole(); - } - - public static object CurrentTarget() - { - return InspectorManager.Instance?.m_activeInspector?.Target; - } - - public static object[] AllTargets() - { - int count = InspectorManager.Instance?.m_currentInspectors.Count ?? 0; - object[] ret = new object[count]; - for (int i = 0; i < count; i++) - { - ret[i] = InspectorManager.Instance?.m_currentInspectors[i].Target; - } - return ret; - } - - public static void Inspect(object obj) - { - InspectorManager.Instance.Inspect(obj); - } - - public static void Inspect(Type type) - { - InspectorManager.Instance.Inspect(type); - } - } -} \ No newline at end of file diff --git a/src/Core/CSharp/Suggestion.cs b/src/Core/CSharp/Suggestion.cs deleted file mode 100644 index 2bdd0dc..0000000 --- a/src/Core/CSharp/Suggestion.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -using UnityExplorer.Core; -using UnityExplorer.UI.Main.CSConsole; - -namespace UnityExplorer.Core.CSharp -{ - public struct Suggestion - { - public enum Contexts - { - Namespace, - Keyword, - Other - } - - // ~~~~ Instance ~~~~ - - public readonly string Prefix; - public readonly string Addition; - public readonly Contexts Context; - - public string Full => Prefix + Addition; - - public Color TextColor => GetTextColor(); - - public Suggestion(string addition, string prefix, Contexts type) - { - Addition = addition; - Prefix = prefix; - Context = type; - } - - private Color GetTextColor() - { - switch (Context) - { - case Contexts.Namespace: return Color.grey; - case Contexts.Keyword: return keywordColor; - default: return Color.white; - } - } - - // ~~~~ Static ~~~~ - - public static HashSet Namespaces => m_namespaces ?? GetNamespaces(); - private static HashSet m_namespaces; - - public static HashSet Keywords => m_keywords ?? (m_keywords = new HashSet(CSLexerHighlighter.validKeywordMatcher.Keywords)); - private static HashSet m_keywords; - - private static readonly Color keywordColor = new Color(80f / 255f, 150f / 255f, 215f / 255f); - - private static HashSet GetNamespaces() - { - HashSet set = new HashSet( - AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(GetTypes) - .Where(x => x.IsPublic && !string.IsNullOrEmpty(x.Namespace)) - .Select(x => x.Namespace)); - - return m_namespaces = set; - - IEnumerable GetTypes(Assembly asm) => asm.TryGetTypes(); - } - } -} diff --git a/src/Core/Config/ConfigElement.cs b/src/Core/Config/ConfigElement.cs index b3510cc..d92a465 100644 --- a/src/Core/Config/ConfigElement.cs +++ b/src/Core/Config/ConfigElement.cs @@ -18,6 +18,10 @@ namespace UnityExplorer.Core.Config public object DefaultValue { get; } + public ConfigHandler Handler => IsInternal + ? ConfigManager.InternalHandler + : ConfigManager.Handler; + public T Value { get => m_value; @@ -51,19 +55,19 @@ namespace UnityExplorer.Core.Config m_value = value; - ConfigManager.Handler.SetConfigValue(this, value); + Handler.SetConfigValue(this, value); OnValueChanged?.Invoke(value); OnValueChangedNotify?.Invoke(); - ConfigManager.Handler.OnAnyConfigChanged(); + Handler.OnAnyConfigChanged(); } object IConfigElement.GetLoaderConfigValue() => GetLoaderConfigValue(); public T GetLoaderConfigValue() { - return ConfigManager.Handler.GetConfigValue(this); + return Handler.GetConfigValue(this); } public void RevertToDefaultValue() diff --git a/src/Core/Config/ConfigManager.cs b/src/Core/Config/ConfigManager.cs index 8ab401a..8489f7c 100644 --- a/src/Core/Config/ConfigManager.cs +++ b/src/Core/Config/ConfigManager.cs @@ -6,8 +6,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; using UnityEngine; -using UnityExplorer.UI.Main; -using UnityExplorer.UI.Main.Home; +using UnityExplorer.UI; namespace UnityExplorer.Core.Config { @@ -17,75 +16,89 @@ namespace UnityExplorer.Core.Config // See the UnityExplorer.Loader namespace for the implementations. public static ConfigHandler Handler { get; private set; } - public static ConfigElement Main_Menu_Toggle; + public static ConfigElement Master_Toggle; + public static ConfigElement Main_Navbar_Anchor; public static ConfigElement Force_Unlock_Mouse; - public static ConfigElement Aggressive_Force_Unlock; - public static ConfigElement Default_Tab; - public static ConfigElement Default_Page_Limit; + public static ConfigElement Force_Unlock_Toggle; + public static ConfigElement Aggressive_Mouse_Unlock; public static ConfigElement Default_Output_Path; public static ConfigElement Log_Unity_Debug; public static ConfigElement Hide_On_Startup; public static ConfigElement Startup_Delay_Time; - public static ConfigElement Last_Window_Anchors; - public static ConfigElement Last_Window_Position; - public static ConfigElement Last_DebugConsole_State; - public static ConfigElement Last_SceneExplorer_State; + public static ConfigElement Reflection_Signature_Blacklist; + + // internal configs + internal static InternalConfigHandler InternalHandler { get; private set; } + + public static ConfigElement ObjectExplorerData; + public static ConfigElement InspectorData; + public static ConfigElement CSConsoleData; + public static ConfigElement OptionsPanelData; + public static ConfigElement ConsoleLogData; internal static readonly Dictionary ConfigElements = new Dictionary(); + internal static readonly Dictionary InternalConfigs = new Dictionary(); public static void Init(ConfigHandler configHandler) { Handler = configHandler; Handler.Init(); + InternalHandler = new InternalConfigHandler(); + InternalHandler.Init(); + CreateConfigElements(); Handler.LoadConfig(); + InternalHandler.LoadConfig(); - SceneExplorer.OnToggleShow += SceneExplorer_OnToggleShow; - PanelDragger.OnFinishResize += PanelDragger_OnFinishResize; - PanelDragger.OnFinishDrag += PanelDragger_OnFinishDrag; - DebugConsole.OnToggleShow += DebugConsole_OnToggleShow; - - InitConsoleCallback(); + //InitConsoleCallback(); } internal static void RegisterConfigElement(ConfigElement configElement) { - Handler.RegisterConfigElement(configElement); - ConfigElements.Add(configElement.Name, configElement); + if (!configElement.IsInternal) + { + Handler.RegisterConfigElement(configElement); + ConfigElements.Add(configElement.Name, configElement); + } + else + { + InternalHandler.RegisterConfigElement(configElement); + InternalConfigs.Add(configElement.Name, configElement); + } } private static void CreateConfigElements() { - Main_Menu_Toggle = new ConfigElement("Main Menu Toggle", - "The UnityEngine.KeyCode to toggle the UnityExplorer Menu.", + Master_Toggle = new ConfigElement("UnityExplorer Toggle", + "The key to enable or disable UnityExplorer's menu and features.", KeyCode.F7); + Main_Navbar_Anchor = new ConfigElement("Main Navbar Anchor", + "The vertical anchor of the main UnityExplorer Navbar, in case you want to move it.", + UIManager.VerticalAnchor.Top); + Hide_On_Startup = new ConfigElement("Hide On Startup", "Should UnityExplorer be hidden on startup?", false); - Default_Tab = new ConfigElement("Default Tab", - "The default menu page when starting the game.", - MenuPages.Home); - - Log_Unity_Debug = new ConfigElement("Log Unity Debug", - "Should UnityEngine.Debug.Log messages be printed to UnityExplorer's log?", - false); - Force_Unlock_Mouse = new ConfigElement("Force Unlock Mouse", "Force the Cursor to be unlocked (visible) when the UnityExplorer menu is open.", true); - Aggressive_Force_Unlock = new ConfigElement("Aggressive Mouse Unlock", + Force_Unlock_Toggle = new ConfigElement("Force Unlock Toggle Key", + "The keybind to toggle the 'Force Unlock Mouse' setting. Only usable when UnityExplorer is open.", + KeyCode.None); + + Aggressive_Mouse_Unlock = new ConfigElement("Aggressive Mouse Unlock", "Use WaitForEndOfFrame to aggressively force the Mouse to be unlocked (requires game restart).", false); - Default_Page_Limit = new ConfigElement("Default Page Limit", - "The default maximum number of elements per 'page' in UnityExplorer.", - 25); + Log_Unity_Debug = new ConfigElement("Log Unity Debug", + "Should UnityEngine.Debug.Log messages be printed to UnityExplorer's log?", + false); Default_Output_Path = new ConfigElement("Default Output Path", "The default output path when exporting things from UnityExplorer.", @@ -95,156 +108,18 @@ namespace UnityExplorer.Core.Config "The delay on startup before the UI is created.", 1f); - // Internal configs + Reflection_Signature_Blacklist = new ConfigElement("Reflection Signature Blacklist", + "Use this to blacklist certain member signatures if they are known to cause a crash or other issues." + + "\r\nSeperate signatures with a semicolon ';'.", + "DEFAULT"); - Last_Window_Anchors = new ConfigElement("Last_Window_Anchors", - "For internal use, the last anchors of the UnityExplorer window.", - DEFAULT_WINDOW_ANCHORS, - true); + // Internal configs (panel save data) - Last_Window_Position = new ConfigElement("Last_Window_Position", - "For internal use, the last position of the UnityExplorer window.", - DEFAULT_WINDOW_POSITION, - true); - - Last_DebugConsole_State = new ConfigElement("Last_DebugConsole_State", - "For internal use, the collapsed state of the Debug Console.", - true, - true); - - Last_SceneExplorer_State = new ConfigElement("Last_SceneExplorer_State", - "For internal use, the collapsed state of the Scene Explorer.", - true, - true); + ObjectExplorerData = new ConfigElement("ObjectExplorer", "", "", true); + InspectorData = new ConfigElement("Inspector", "", "", true); + CSConsoleData = new ConfigElement("CSConsole", "", "", true); + OptionsPanelData = new ConfigElement("OptionsPanel", "", "", true); + ConsoleLogData = new ConfigElement("ConsoleLog", "", "", true); } - - // Internal config callback listeners - - private static void PanelDragger_OnFinishResize(RectTransform rect) - { - Last_Window_Anchors.Value = rect.RectAnchorsToString(); - PanelDragger_OnFinishDrag(rect); - } - - private static void PanelDragger_OnFinishDrag(RectTransform rect) - { - Last_Window_Position.Value = rect.RectPositionToString(); - } - - private static void DebugConsole_OnToggleShow(bool showing) - { - Last_DebugConsole_State.Value = showing; - } - - private static void SceneExplorer_OnToggleShow(bool showing) - { - Last_SceneExplorer_State.Value = showing; - } - - #region CONSOLE ONEXIT CALLBACK - - internal static void InitConsoleCallback() - { - handler = new ConsoleEventDelegate(ConsoleEventCallback); - SetConsoleCtrlHandler(handler, true); - } - - static bool ConsoleEventCallback(int eventType) - { - // 2 is Console Quit - if (eventType == 2) - Handler.SaveConfig(); - - return false; - } - - static ConsoleEventDelegate handler; - private delegate bool ConsoleEventDelegate(int eventType); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate callback, bool add); - - #endregion - - #region WINDOW ANCHORS / POSITION HELPERS - - // Window Anchors helpers - - private const string DEFAULT_WINDOW_ANCHORS = "0.25,0.10,0.78,0.95"; - private const string DEFAULT_WINDOW_POSITION = "0,0"; - - internal static CultureInfo _enCulture = new CultureInfo("en-US"); - - internal static string RectAnchorsToString(this RectTransform rect) - { - try - { - return string.Format(_enCulture, "{0},{1},{2},{3}", new object[] - { - rect.anchorMin.x, - rect.anchorMin.y, - rect.anchorMax.x, - rect.anchorMax.y - }); - } - catch - { - return DEFAULT_WINDOW_ANCHORS; - } - } - - internal static void SetAnchorsFromString(this RectTransform panel, string stringAnchors) - { - Vector4 anchors; - try - { - var split = stringAnchors.Split(','); - - if (split.Length != 4) - throw new Exception(); - - anchors.x = float.Parse(split[0], _enCulture); - anchors.y = float.Parse(split[1], _enCulture); - anchors.z = float.Parse(split[2], _enCulture); - anchors.w = float.Parse(split[3], _enCulture); - } - catch - { - anchors = new Vector4(0.25f, 0.1f, 0.78f, 0.95f); - } - - panel.anchorMin = new Vector2(anchors.x, anchors.y); - panel.anchorMax = new Vector2(anchors.z, anchors.w); - } - - internal static string RectPositionToString(this RectTransform rect) - { - return string.Format(_enCulture, "{0},{1}", new object[] - { - rect.localPosition.x, rect.localPosition.y - }); - } - - internal static void SetPositionFromString(this RectTransform rect, string stringPosition) - { - try - { - var split = stringPosition.Split(','); - - if (split.Length != 2) - throw new Exception(); - - Vector3 vector = rect.localPosition; - vector.x = float.Parse(split[0], _enCulture); - vector.y = float.Parse(split[1], _enCulture); - rect.localPosition = vector; - } - catch //(Exception ex) - { - //ExplorerCore.LogWarning("Exception setting window position: " + ex); - } - } - - #endregion } } diff --git a/src/Core/Config/InternalConfigHandler.cs b/src/Core/Config/InternalConfigHandler.cs new file mode 100644 index 0000000..e3ebdf0 --- /dev/null +++ b/src/Core/Config/InternalConfigHandler.cs @@ -0,0 +1,108 @@ +using IniParser.Parser; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityExplorer.UI; + +namespace UnityExplorer.Core.Config +{ + public class InternalConfigHandler : ConfigHandler + { + internal static IniDataParser _parser; + internal static string INI_PATH; + + public override void Init() + { + INI_PATH = Path.Combine(ExplorerCore.Loader.ExplorerFolder, "data.ini"); + _parser = new IniDataParser(); + _parser.Configuration.CommentString = "#"; + } + + public override void LoadConfig() + { + if (!TryLoadConfig()) + SaveConfig(); + } + + public override void RegisterConfigElement(ConfigElement element) + { + // Not necessary + } + + public override void SetConfigValue(ConfigElement element, T value) + { + // Not necessary + } + + public override T GetConfigValue(ConfigElement element) + { + // Not necessary, just return the value. + return element.Value; + } + + public override void OnAnyConfigChanged() + { + SaveConfig(); + } + + public bool TryLoadConfig() + { + try + { + if (!File.Exists(INI_PATH)) + return false; + + string ini = File.ReadAllText(INI_PATH); + + var data = _parser.Parse(ini); + + foreach (var config in data.Sections["Config"]) + { + if (ConfigManager.InternalConfigs.TryGetValue(config.KeyName, out IConfigElement configElement)) + configElement.BoxedValue = StringToConfigValue(config.Value, configElement.ElementType); + } + + return true; + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Error loading internal data: " + ex.ToString()); + return false; + } + } + + public override void SaveConfig() + { + if (UIManager.Initializing) + return; + + var data = new IniParser.Model.IniData(); + + data.Sections.AddSection("Config"); + var sec = data.Sections["Config"]; + + foreach (var entry in ConfigManager.InternalConfigs) + sec.AddKey(entry.Key, entry.Value.BoxedValue.ToString()); + + if (!Directory.Exists(ExplorerCore.Loader.ConfigFolder)) + Directory.CreateDirectory(ExplorerCore.Loader.ConfigFolder); + + File.WriteAllText(INI_PATH, data.ToString()); + } + + public object StringToConfigValue(string value, Type elementType) + { + if (elementType.IsEnum) + return Enum.Parse(elementType, value); + else if (elementType == typeof(bool)) + return bool.Parse(value); + else if (elementType == typeof(int)) + return int.Parse(value); + else + return value; + } + } +} diff --git a/src/Core/ExplorerBehaviour.cs b/src/Core/ExplorerBehaviour.cs new file mode 100644 index 0000000..31e260a --- /dev/null +++ b/src/Core/ExplorerBehaviour.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +#if CPP +using UnhollowerRuntimeLib; +#endif + +namespace UnityExplorer +{ + // Handles all Behaviour update calls for UnityExplorer (Update, FixedUpdate, OnPostRender). + // Basically just a wrapper which calls the corresponding methods in ExplorerCore. + + public class ExplorerBehaviour : MonoBehaviour + { + internal static ExplorerBehaviour Instance { get; private set; } + + internal static void Setup() + { +#if CPP + ClassInjector.RegisterTypeInIl2Cpp(); +#endif + + var obj = new GameObject("ExplorerBehaviour"); + GameObject.DontDestroyOnLoad(obj); + obj.hideFlags |= HideFlags.HideAndDontSave; + Instance = obj.AddComponent(); + } + +#if CPP + public ExplorerBehaviour(IntPtr ptr) : base(ptr) { } +#endif + + internal void Awake() + { +#if CPP + Camera.onPostRender = Camera.onPostRender == null + ? new Action(OnPostRender) + : Il2CppSystem.Delegate.Combine(Camera.onPostRender, (Camera.CameraCallback)new Action(OnPostRender)).Cast(); + +#else + Camera.onPostRender += OnPostRender; +#endif + } + + internal void Update() + { + ExplorerCore.Update(); + } + + internal void FixedUpdate() + { + ExplorerCore.FixedUpdate(); + } + + internal static void OnPostRender(Camera camera) + { + ExplorerCore.OnPostRender(); + } + } +} diff --git a/src/Core/Input/CursorUnlocker.cs b/src/Core/Input/CursorUnlocker.cs index 4a535f0..2e0f558 100644 --- a/src/Core/Input/CursorUnlocker.cs +++ b/src/Core/Input/CursorUnlocker.cs @@ -1,6 +1,5 @@ using System; using UnityEngine; -using UnityExplorer.Core.Unity; using UnityEngine.EventSystems; using UnityExplorer.Core.Input; using BF = System.Reflection.BindingFlags; @@ -8,11 +7,7 @@ using UnityExplorer.Core.Config; using UnityExplorer.Core; using UnityExplorer.UI; using System.Collections; -#if ML -using Harmony; -#else using HarmonyLib; -#endif namespace UnityExplorer.Core.Input { @@ -50,7 +45,7 @@ namespace UnityExplorer.Core.Input Unlock = ConfigManager.Force_Unlock_Mouse.Value; ConfigManager.Force_Unlock_Mouse.OnValueChanged += (bool val) => { Unlock = val; }; - if (ConfigManager.Aggressive_Force_Unlock.Value) + if (ConfigManager.Aggressive_Mouse_Unlock.Value) SetupAggressiveUnlock(); } @@ -167,7 +162,7 @@ namespace UnityExplorer.Core.Input m_lastVisibleState = (bool?)CursorType.GetProperty("visible", BF.Public | BF.Static)?.GetValue(null, null) ?? false; - ExplorerCore.Loader.SetupPatches(); + ExplorerCore.Loader.SetupCursorPatches(); } catch (Exception e) { diff --git a/src/Core/Input/IHandleInput.cs b/src/Core/Input/IHandleInput.cs index 6fe0bfe..5326f1e 100644 --- a/src/Core/Input/IHandleInput.cs +++ b/src/Core/Input/IHandleInput.cs @@ -6,6 +6,7 @@ namespace UnityExplorer.Core.Input public interface IHandleInput { Vector2 MousePosition { get; } + Vector2 MouseScrollDelta { get; } bool GetKeyDown(KeyCode key); bool GetKey(KeyCode key); diff --git a/src/Core/Input/InputManager.cs b/src/Core/Input/InputManager.cs index e905312..624def9 100644 --- a/src/Core/Input/InputManager.cs +++ b/src/Core/Input/InputManager.cs @@ -20,14 +20,27 @@ namespace UnityExplorer.Core.Input public static Vector3 MousePosition => m_inputModule.MousePosition; - public static bool GetKeyDown(KeyCode key) => m_inputModule.GetKeyDown(key); - public static bool GetKey(KeyCode key) => m_inputModule.GetKey(key); + public static bool GetKeyDown(KeyCode key) + { + if (key == KeyCode.None) + return false; + return m_inputModule.GetKeyDown(key); + } + + public static bool GetKey(KeyCode key) + { + if (key == KeyCode.None) + return false; + return m_inputModule.GetKey(key); + } public static bool GetMouseButtonDown(int btn) => m_inputModule.GetMouseButtonDown(btn); public static bool GetMouseButton(int btn) => m_inputModule.GetMouseButton(btn); public static BaseInputModule UIInput => m_inputModule.UIModule; + public static Vector2 MouseScrollDelta => m_inputModule.MouseScrollDelta; + public static void ActivateUIModule() => m_inputModule.ActivateModule(); public static void AddUIModule() @@ -38,25 +51,53 @@ namespace UnityExplorer.Core.Input public static void Init() { - if (InputSystem.TKeyboard != null || (ReflectionUtility.LoadModule("Unity.InputSystem") && InputSystem.TKeyboard != null)) - { - m_inputModule = new InputSystem(); - CurrentType = InputType.InputSystem; - } - else if (LegacyInput.TInput != null || (ReflectionUtility.LoadModule("UnityEngine.InputLegacyModule") && LegacyInput.TInput != null)) - { - m_inputModule = new LegacyInput(); - CurrentType = InputType.Legacy; - } - - if (m_inputModule == null) - { - ExplorerCore.LogWarning("Could not find any Input Module Type!"); - m_inputModule = new NoInput(); - CurrentType = InputType.None; - } + InitHandler(); CursorUnlocker.Init(); } + + private static void InitHandler() + { + // First, just try to use the legacy input, see if its working. + // The InputSystem package may be present but not actually activated, so we can find out this way. + + if (LegacyInput.TInput != null || (ReflectionUtility.LoadModule("UnityEngine.InputLegacyModule") && LegacyInput.TInput != null)) + { + try + { + m_inputModule = new LegacyInput(); + CurrentType = InputType.Legacy; + + // make sure its working + GetKeyDown(KeyCode.F5); + + ExplorerCore.Log("Initialized Legacy Input support"); + return; + } + catch + { + // It's not working, we'll fall back to InputSystem. + } + } + + if (InputSystem.TKeyboard != null || (ReflectionUtility.LoadModule("Unity.InputSystem") && InputSystem.TKeyboard != null)) + { + try + { + m_inputModule = new InputSystem(); + CurrentType = InputType.InputSystem; + ExplorerCore.Log("Initialized new InputSystem support."); + return; + } + catch (Exception ex) + { + ExplorerCore.Log(ex); + } + } + + ExplorerCore.LogWarning("Could not find any Input Module Type!"); + m_inputModule = new NoInput(); + CurrentType = InputType.None; + } } } \ No newline at end of file diff --git a/src/Core/Input/InputSystem.cs b/src/Core/Input/InputSystem.cs index 5fcf2d5..aa9e99f 100644 --- a/src/Core/Input/InputSystem.cs +++ b/src/Core/Input/InputSystem.cs @@ -1,11 +1,9 @@ using System; using System.Reflection; -using UnityExplorer.Core.Unity; using UnityEngine; using UnityEngine.EventSystems; using UnityExplorer.UI; using System.Collections.Generic; -using UnityExplorer.UI.Inspectors; using System.Linq; namespace UnityExplorer.Core.Input @@ -14,8 +12,6 @@ namespace UnityExplorer.Core.Input { public InputSystem() { - ExplorerCore.Log("Initializing new InputSystem support..."); - m_kbCurrentProp = TKeyboard.GetProperty("current"); m_kbIndexer = TKeyboard.GetProperty("Item", new Type[] { TKey }); @@ -26,15 +22,18 @@ namespace UnityExplorer.Core.Input m_mouseCurrentProp = TMouse.GetProperty("current"); m_leftButtonProp = TMouse.GetProperty("leftButton"); m_rightButtonProp = TMouse.GetProperty("rightButton"); + m_scrollDeltaProp = TMouse.GetProperty("scroll"); m_positionProp = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Pointer") .GetProperty("position"); - m_readVector2InputMethod = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputControl`1") + ReadV2ControlMethod = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputControl`1") .MakeGenericType(typeof(Vector2)) .GetMethod("ReadValue"); } + #region reflection cache + public static Type TKeyboard => m_tKeyboard ?? (m_tKeyboard = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Keyboard")); private static Type m_tKeyboard; @@ -64,10 +63,17 @@ namespace UnityExplorer.Core.Input private static object m_rmb; private static PropertyInfo m_rightButtonProp; + private static MethodInfo ReadV2ControlMethod; + private static object MousePositionInfo => m_pos ?? (m_pos = m_positionProp.GetValue(CurrentMouse, null)); private static object m_pos; private static PropertyInfo m_positionProp; - private static MethodInfo m_readVector2InputMethod; + + private static object MouseScrollInfo => m_scrollInfo ?? (m_scrollInfo = m_scrollDeltaProp.GetValue(CurrentMouse, null)); + private static object m_scrollInfo; + private static PropertyInfo m_scrollDeltaProp; + + #endregion public Vector2 MousePosition { @@ -75,12 +81,21 @@ namespace UnityExplorer.Core.Input { try { - return (Vector2)m_readVector2InputMethod.Invoke(MousePositionInfo, new object[0]); + return (Vector2)ReadV2ControlMethod.Invoke(MousePositionInfo, ArgumentUtility.EmptyArgs); } - catch + catch { return Vector2.zero; } + } + } + + public Vector2 MouseScrollDelta + { + get + { + try { - return Vector2.zero; + return (Vector2)ReadV2ControlMethod.Invoke(MouseScrollInfo, ArgumentUtility.EmptyArgs); } + catch { return Vector2.zero; } } } @@ -164,13 +179,13 @@ namespace UnityExplorer.Core.Input var assetType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionAsset"); m_newInputModule = RuntimeProvider.Instance.AddComponent(UIManager.CanvasRoot, TInputSystemUIInputModule); var asset = RuntimeProvider.Instance.CreateScriptable(assetType) - .Cast(assetType); + .TryCast(assetType); inputExtensions = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionSetupExtensions"); var addMap = inputExtensions.GetMethod("AddActionMap", new Type[] { assetType, typeof(string) }); var map = addMap.Invoke(null, new object[] { asset, "UI" }) - .Cast(ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionMap")); + .TryCast(ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionMap")); CreateAction(map, "point", new[] { "/position" }, "point"); CreateAction(map, "click", new[] { "/leftButton" }, "leftClick"); @@ -178,7 +193,7 @@ namespace UnityExplorer.Core.Input CreateAction(map, "scrollWheel", new[] { "/scroll" }, "scrollWheel"); UI_Enable = map.GetType().GetMethod("Enable"); - UI_Enable.Invoke(map, new object[0]); + UI_Enable.Invoke(map, ArgumentUtility.EmptyArgs); UI_ActionMap = map; } @@ -191,28 +206,28 @@ namespace UnityExplorer.Core.Input var inputActionType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputAction"); var addAction = inputExtensions.GetMethod("AddAction"); var action = addAction.Invoke(null, new object[] { map, actionName, default, null, null, null, null, null }) - .Cast(inputActionType); + .TryCast(inputActionType); var addBinding = inputExtensions.GetMethod("AddBinding", new Type[] { inputActionType, typeof(string), typeof(string), typeof(string), typeof(string) }); foreach (string binding in bindings) - addBinding.Invoke(null, new object[] { action.Cast(inputActionType), binding, null, null, null }); + addBinding.Invoke(null, new object[] { action.TryCast(inputActionType), binding, null, null, null }); var refType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionReference"); var inputRef = refType.GetMethod("Create") .Invoke(null, new object[] { action }) - .Cast(refType); + .TryCast(refType); TInputSystemUIInputModule .GetProperty(propertyName) - .SetValue(m_newInputModule.Cast(TInputSystemUIInputModule), inputRef, null); + .SetValue(m_newInputModule.TryCast(TInputSystemUIInputModule), inputRef, null); } public void ActivateModule() { m_newInputModule.ActivateModule(); - UI_Enable.Invoke(UI_ActionMap, new object[0]); + UI_Enable.Invoke(UI_ActionMap, ArgumentUtility.EmptyArgs); } } } \ No newline at end of file diff --git a/src/Core/Input/LegacyInput.cs b/src/Core/Input/LegacyInput.cs index 65d4ad1..c662fa2 100644 --- a/src/Core/Input/LegacyInput.cs +++ b/src/Core/Input/LegacyInput.cs @@ -1,6 +1,5 @@ using System; using System.Reflection; -using UnityExplorer.Core.Unity; using UnityEngine; using UnityEngine.EventSystems; using UnityExplorer.UI; @@ -11,9 +10,8 @@ namespace UnityExplorer.Core.Input { public LegacyInput() { - ExplorerCore.Log("Initializing Legacy Input support..."); - m_mousePositionProp = TInput.GetProperty("mousePosition"); + m_mouseDeltaProp = TInput.GetProperty("mouseScrollDelta"); m_getKeyMethod = TInput.GetMethod("GetKey", new Type[] { typeof(KeyCode) }); m_getKeyDownMethod = TInput.GetMethod("GetKeyDown", new Type[] { typeof(KeyCode) }); m_getMouseButtonMethod = TInput.GetMethod("GetMouseButton", new Type[] { typeof(int) }); @@ -24,6 +22,7 @@ namespace UnityExplorer.Core.Input private static Type m_tInput; private static PropertyInfo m_mousePositionProp; + private static PropertyInfo m_mouseDeltaProp; private static MethodInfo m_getKeyMethod; private static MethodInfo m_getKeyDownMethod; private static MethodInfo m_getMouseButtonMethod; @@ -31,6 +30,8 @@ namespace UnityExplorer.Core.Input public Vector2 MousePosition => (Vector3)m_mousePositionProp.GetValue(null, null); + public Vector2 MouseScrollDelta => (Vector2)m_mouseDeltaProp.GetValue(null, null); + public bool GetKey(KeyCode key) => (bool)m_getKeyMethod.Invoke(null, new object[] { key }); public bool GetKeyDown(KeyCode key) => (bool)m_getKeyDownMethod.Invoke(null, new object[] { key }); diff --git a/src/Core/Input/NoInput.cs b/src/Core/Input/NoInput.cs index 159767e..f56140f 100644 --- a/src/Core/Input/NoInput.cs +++ b/src/Core/Input/NoInput.cs @@ -8,6 +8,7 @@ namespace UnityExplorer.Core.Input public class NoInput : IHandleInput { public Vector2 MousePosition => Vector2.zero; + public Vector2 MouseScrollDelta => Vector2.zero; public bool GetKey(KeyCode key) => false; public bool GetKeyDown(KeyCode key) => false; diff --git a/src/Core/Reflection/Extensions.cs b/src/Core/Reflection/Extensions.cs new file mode 100644 index 0000000..f948fd5 --- /dev/null +++ b/src/Core/Reflection/Extensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace UnityExplorer +{ + public static class ReflectionExtensions + { + // ReflectionUtility extensions + + public static Type GetActualType(this object obj) + => ReflectionUtility.Instance.Internal_GetActualType(obj); + + public static object TryCast(this object obj) + => ReflectionUtility.Instance.Internal_TryCast(obj, ReflectionUtility.Instance.Internal_GetActualType(obj)); + + public static object TryCast(this object obj, Type castTo) + => ReflectionUtility.Instance.Internal_TryCast(obj, castTo); + + public static T TryCast(this object obj) + { + try + { + return (T)ReflectionUtility.Instance.Internal_TryCast(obj, typeof(T)); + } + catch + { + return default; + } + } + + public static HashSet GetImplementationsOf(this Type baseType, bool allowAbstract, bool allowGeneric) + => ReflectionUtility.GetImplementationsOf(baseType, allowAbstract, allowGeneric); + + // ------- Misc extensions -------- + + /// + /// Safely try to get all Types inside an Assembly. + /// + public static IEnumerable TryGetTypes(this Assembly asm) + { + try + { + return asm.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + try + { + return asm.GetExportedTypes(); + } + catch + { + return e.Types.Where(t => t != null); + } + } + catch + { + return Enumerable.Empty(); + } + } + + + /// + /// Check if the two objects are reference-equal, including checking for UnityEngine.Object-equality and Il2CppSystem.Object-equality. + /// + public static bool ReferenceEqual(this object objA, object objB) + { + if (object.ReferenceEquals(objA, objB)) + return true; + + if (objA is UnityEngine.Object unityA && objB is UnityEngine.Object unityB) + { + if (unityA && unityB && unityA.m_CachedPtr == unityB.m_CachedPtr) + return true; + } + +#if CPP + if (objA is Il2CppSystem.Object cppA && objB is Il2CppSystem.Object cppB + && cppA.Pointer == cppB.Pointer) + return true; +#endif + + return false; + } + + /// + /// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception. + /// + public static string ReflectionExToString(this Exception e, bool innerMost = true) + { + if (innerMost) + e = e.GetInnerMostException(); + + return $"{e.GetType()}: {e.Message}"; + } + + public static Exception GetInnerMostException(this Exception e) + { + while (e.InnerException != null) + { +#if CPP + if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException) + break; +#endif + e = e.InnerException; + } + + return e; + } + } +} diff --git a/src/Core/Reflection/Il2CppReflection.cs b/src/Core/Reflection/Il2CppReflection.cs new file mode 100644 index 0000000..d372361 --- /dev/null +++ b/src/Core/Reflection/Il2CppReflection.cs @@ -0,0 +1,893 @@ +#if CPP +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnhollowerBaseLib; +using UnhollowerRuntimeLib; +using System.Runtime.InteropServices; +using System.Reflection; +using System.Collections; +using System.IO; +using System.Diagnostics.CodeAnalysis; +using UnityExplorer.Core; +using CppType = Il2CppSystem.Type; +using BF = System.Reflection.BindingFlags; +using UnityExplorer.Core.Config; + +namespace UnityExplorer +{ + public class Il2CppReflection : ReflectionUtility + { + protected override void Initialize() + { + base.Initialize(); + + TryLoadGameModules(); + + BuildDeobfuscationCache(); + OnTypeLoaded += TryCacheDeobfuscatedType; + } + + #region IL2CPP Extern and pointers + + // Extern C++ methods + [DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern bool il2cpp_class_is_assignable_from(IntPtr klass, IntPtr oklass); + + [DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern IntPtr il2cpp_object_get_class(IntPtr obj); + + public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _); + + public static bool Il2CppTypeNotNull(Type type, out IntPtr il2cppPtr) + { + if (cppClassPointers.TryGetValue(type.AssemblyQualifiedName, out il2cppPtr)) + return il2cppPtr != IntPtr.Zero; + + il2cppPtr = (IntPtr)typeof(Il2CppClassPointerStore<>) + .MakeGenericType(new Type[] { type }) + .GetField("NativeClassPtr", BF.Public | BF.Static) + .GetValue(null); + + cppClassPointers.Add(type.AssemblyQualifiedName, il2cppPtr); + + return il2cppPtr != IntPtr.Zero; + } + + #endregion + + + #region Deobfuscation cache + + private static readonly Dictionary DeobfuscatedTypes = new Dictionary(); + private static readonly Dictionary reverseDeobCache = new Dictionary(); + + private static void BuildDeobfuscationCache() + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + foreach (var type in asm.TryGetTypes()) + TryCacheDeobfuscatedType(type); + } + + if (DeobfuscatedTypes.Count > 0) + ExplorerCore.Log($"Built IL2CPP deobfuscation cache, initial count: {DeobfuscatedTypes.Count}"); + } + + private static void TryCacheDeobfuscatedType(Type type) + { + try + { + // Thanks to Slaynash for this + if (type.CustomAttributes.Any(it => it.AttributeType.Name == "ObfuscatedNameAttribute")) + { + var cppType = Il2CppType.From(type); + + if (!DeobfuscatedTypes.ContainsKey(cppType.FullName)) + { + DeobfuscatedTypes.Add(cppType.FullName, type); + reverseDeobCache.Add(type.FullName, cppType.FullName); + } + } + } + catch { } + } + + internal override string Internal_ProcessTypeInString(string theString, Type type) + { + if (reverseDeobCache.TryGetValue(type.FullName, out string obName)) + return theString.Replace(obName, type.FullName); + + return theString; + } + + #endregion + + + // Get type by name + + internal override Type Internal_GetTypeByName(string fullName) + { + if (DeobfuscatedTypes.TryGetValue(fullName, out Type deob)) + return deob; + + return base.Internal_GetTypeByName(fullName); + } + + #region Get actual type + + internal override Type Internal_GetActualType(object obj) + { + if (obj == null) + return null; + + var type = obj.GetType(); + + try + { + if (IsString(obj)) + return typeof(string); + + if (IsIl2CppPrimitive(type)) + return il2cppPrimitivesToMono[type.FullName]; + + if (obj is Il2CppSystem.Object cppObject) + { + var cppType = cppObject.GetIl2CppType(); + + // check if type is injected + IntPtr classPtr = il2cpp_object_get_class(cppObject.Pointer); + if (RuntimeSpecificsStore.IsInjected(classPtr)) + { + // Note: This will fail on injected subclasses. + // - {Namespace}.{Class}.{Subclass} would be {Namespace}.{Subclass} when injected. + // Not sure on solution yet. + return GetTypeByName(cppType.FullName) ?? type; + } + + if (AllTypes.TryGetValue(cppType.FullName, out Type primitive) && primitive.IsPrimitive) + return primitive; + + return GetUnhollowedType(cppType) ?? type; + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception in IL2CPP GetActualType: " + ex); + } + + return type; + } + + public static Type GetUnhollowedType(CppType cppType) + { + var fullname = cppType.FullName; + + if (DeobfuscatedTypes.TryGetValue(fullname, out Type deob)) + return deob; + + if (fullname.StartsWith("System.")) + fullname = $"Il2Cpp{fullname}"; + + AllTypes.TryGetValue(fullname, out Type monoType); + return monoType; + } + + #endregion + + + #region Casting + + private static readonly Dictionary cppClassPointers = new Dictionary(); + + internal override object Internal_TryCast(object obj, Type castTo) + { + if (obj == null) + return null; + + var type = obj.GetType(); + + if (type == castTo) + return obj; + + // from structs + if (type.IsValueType) + { + // from il2cpp primitive to system primitive + if (IsIl2CppPrimitive(type) && castTo.IsPrimitive) + { + return MakeMonoPrimitive(obj); + } + // from system primitive to il2cpp primitive + else if (IsIl2CppPrimitive(castTo)) + { + return MakeIl2CppPrimitive(castTo, obj); + } + // from other structs to il2cpp object + else if (typeof(Il2CppSystem.Object).IsAssignableFrom(castTo)) + { + return BoxIl2CppObject(obj); + } + else + return obj; + } + + // from string to il2cpp.Object / il2cpp.String + if (obj is string && typeof(Il2CppSystem.Object).IsAssignableFrom(castTo)) + { + return BoxStringToType(obj, castTo); + } + + // from il2cpp objects... + + if (!(obj is Il2CppObjectBase cppObj)) + return obj; + + // from Il2CppSystem.Object to a struct + if (castTo.IsValueType) + return UnboxCppObject(cppObj, castTo); + // or to system string + else if (castTo == typeof(string)) + return UnboxString(obj); + + // Casting from il2cpp object to il2cpp object... + + if (!Il2CppTypeNotNull(castTo, out IntPtr castToPtr)) + return obj; + + IntPtr castFromPtr = il2cpp_object_get_class(cppObj.Pointer); + + if (!il2cpp_class_is_assignable_from(castToPtr, castFromPtr)) + return null; + + if (RuntimeSpecificsStore.IsInjected(castToPtr)) + { + var injectedObj = UnhollowerBaseLib.Runtime.ClassInjectorBase.GetMonoObjectFromIl2CppPointer(cppObj.Pointer); + return injectedObj ?? obj; + } + + try + { + return Activator.CreateInstance(castTo, cppObj.Pointer); + } + catch + { + return obj; + } + } + + #endregion + + + #region Boxing and unboxing ValueTypes + + // cached il2cpp unbox methods + internal static readonly Dictionary unboxMethods = new Dictionary(); + + // Unbox an il2cpp object to a struct or System primitive. + public object UnboxCppObject(Il2CppObjectBase cppObj, Type toType) + { + if (!toType.IsValueType) + return null; + + try + { + if (toType.IsEnum) + return Enum.Parse(toType, cppObj.ToString()); + + var name = toType.AssemblyQualifiedName; + + if (!unboxMethods.ContainsKey(name)) + { + unboxMethods.Add(name, typeof(Il2CppObjectBase) + .GetMethod("Unbox") + .MakeGenericMethod(toType)); + } + + return unboxMethods[name].Invoke(cppObj, ArgumentUtility.EmptyArgs); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception Unboxing Il2Cpp object to struct: " + ex); + return null; + } + } + + private static Il2CppSystem.Object BoxIl2CppObject(object cppStruct, Type structType) + { + return GetMethodInfo(structType, "BoxIl2CppObject", ArgumentUtility.EmptyTypes) + .Invoke(cppStruct, ArgumentUtility.EmptyArgs) + as Il2CppSystem.Object; + } + + public Il2CppSystem.Object BoxIl2CppObject(object value) + { + if (value == null) + return null; + + try + { + var type = value.GetType(); + if (!type.IsValueType) + return null; + + if (type.IsEnum) + return Il2CppSystem.Enum.Parse(Il2CppType.From(type), value.ToString()); + + if (type.IsPrimitive && AllTypes.TryGetValue($"Il2Cpp{type.FullName}", out Type cppType)) + return BoxIl2CppObject(MakeIl2CppPrimitive(cppType, value), cppType); + + return BoxIl2CppObject(value, type); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception in BoxIl2CppObject: " + ex); + return null; + } + } + + // Helpers for Il2Cpp primitive <-> Mono + + internal static readonly Dictionary il2cppPrimitivesToMono = new Dictionary + { + { "Il2CppSystem.Boolean", typeof(bool) }, + { "Il2CppSystem.Byte", typeof(byte) }, + { "Il2CppSystem.SByte", typeof(sbyte) }, + { "Il2CppSystem.Char", typeof(char) }, + { "Il2CppSystem.Double", typeof(double) }, + { "Il2CppSystem.Single", typeof(float) }, + { "Il2CppSystem.Int32", typeof(int) }, + { "Il2CppSystem.UInt32", typeof(uint) }, + { "Il2CppSystem.Int64", typeof(long) }, + { "Il2CppSystem.UInt64", typeof(ulong) }, + { "Il2CppSystem.Int16", typeof(short) }, + { "Il2CppSystem.UInt16", typeof(ushort) }, + { "Il2CppSystem.IntPtr", typeof(IntPtr) }, + { "Il2CppSystem.UIntPtr", typeof(UIntPtr) } + }; + + public static bool IsIl2CppPrimitive(object obj) => IsIl2CppPrimitive(obj.GetType()); + + public static bool IsIl2CppPrimitive(Type type) => il2cppPrimitivesToMono.ContainsKey(type.FullName); + + public object MakeMonoPrimitive(object cppPrimitive) + { + return GetFieldInfo(cppPrimitive.GetType(), "m_value").GetValue(cppPrimitive); + } + + public object MakeIl2CppPrimitive(Type cppType, object monoValue) + { + var cppStruct = Activator.CreateInstance(cppType); + GetFieldInfo(cppType, "m_value").SetValue(cppStruct, monoValue); + return cppStruct; + } + + #endregion + + + #region String boxing/unboxing + + private const string IL2CPP_STRING_FULLNAME = "Il2CppSystem.String"; + private const string STRING_FULLNAME = "System.String"; + + public bool IsString(object obj) + { + if (obj is string || obj is Il2CppSystem.String) + return true; + + if (obj is Il2CppSystem.Object cppObj) + { + var type = cppObj.GetIl2CppType(); + return type.FullName == IL2CPP_STRING_FULLNAME || type.FullName == STRING_FULLNAME; + } + + return false; + } + + public object BoxStringToType(object value, Type castTo) + { + if (castTo == typeof(Il2CppSystem.String)) + return (Il2CppSystem.String)(value as string); + else + return (Il2CppSystem.Object)(value as string); + } + + public string UnboxString(object value) + { + if (value is string s) + return s; + + s = null; + if (value is Il2CppSystem.Object cppObject) + s = cppObject.ToString(); + else if (value is Il2CppSystem.String cppString) + s = cppString; + + return s; + } + + #endregion + + + #region Singleton finder + + internal override void Internal_FindSingleton(string[] possibleNames, Type type, BF flags, List instances) + { + PropertyInfo pi; + foreach (var name in possibleNames) + { + pi = type.GetProperty(name, flags); + if (pi != null) + { + var instance = pi.GetValue(null, null); + if (instance != null) + { + instances.Add(instance); + return; + } + } + } + + base.Internal_FindSingleton(possibleNames, type, flags, instances); + } + + #endregion + + + #region Force-loading game modules + + // Helper for IL2CPP to try to make sure the Unhollowed game assemblies are actually loaded. + + internal override bool Internal_LoadModule(string moduleName) + { + if (!moduleName.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)) + moduleName += ".dll"; +#if ML + var path = Path.Combine("MelonLoader", "Managed", $"{moduleName}"); +#else + var path = Path.Combine("BepInEx", "unhollowed", $"{moduleName}"); +#endif + return DoLoadModule(path); + } + + // Force loading all il2cpp modules + + internal void TryLoadGameModules() + { + string dirpath = + #if ML + Path.Combine("MelonLoader", "Managed"); + #elif BIE + Path.Combine("BepInEx", "unhollowed"); + #else + Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Modules"); + #endif + ; + + if (Directory.Exists(dirpath)) + { + var files = Directory.GetFiles(dirpath); + foreach (var filePath in files) + { + var name = Path.GetFileName(filePath); + if (!name.StartsWith("Unity") && !name.StartsWith("Assembly-CSharp")) + continue; + try + { + DoLoadModule(filePath, true); + } + catch //(Exception ex) + { + //ExplorerCore.LogWarning($"Failed to force-load module '{name}': {ex.ReflectionExToString()}"); + } + } + } + } + + internal bool DoLoadModule(string fullPath, bool suppressWarning = false) + { + if (!File.Exists(fullPath)) + return false; + + try + { + Assembly.Load(File.ReadAllBytes(fullPath)); + return true; + } + catch (Exception e) + { + if (!suppressWarning) + Console.WriteLine($"Failed loading module '{Path.GetFileName(fullPath)}'! {e.ReflectionExToString()}"); + } + + return false; + } + +#endregion + + +#region Il2cpp reflection blacklist + + public override string DefaultReflectionBlacklist => string.Join(";", defaultIl2CppBlacklist); + + // These methods currently cause a crash in most il2cpp games, + // even from doing "GetParameters()" on the MemberInfo. + // Blacklisting until the issue is fixed in Unhollower. + public static HashSet defaultIl2CppBlacklist = new HashSet + { + // These were deprecated a long time ago, still show up in some IL2CPP games for some reason + "UnityEngine.MonoBehaviour.allowPrefabModeInPlayMode", + "UnityEngine.MonoBehaviour.runInEditMode", + "UnityEngine.Component.animation", + "UnityEngine.Component.audio", + "UnityEngine.Component.camera", + "UnityEngine.Component.collider", + "UnityEngine.Component.collider2D", + "UnityEngine.Component.constantForce", + "UnityEngine.Component.hingeJoint", + "UnityEngine.Component.light", + "UnityEngine.Component.networkView", + "UnityEngine.Component.particleSystem", + "UnityEngine.Component.renderer", + "UnityEngine.Component.rigidbody", + "UnityEngine.Component.rigidbody2D", + "UnityEngine.Light.flare", + // These can cause a crash in IL2CPP + "Il2CppSystem.Type.DeclaringMethod", + "Il2CppSystem.RuntimeType.DeclaringMethod", + "Unity.Jobs.LowLevel.Unsafe.JobsUtility.CreateJobReflectionData", + "Unity.Profiling.ProfilerRecorder.CopyTo", + "Unity.Profiling.ProfilerRecorder.StartNew", + "UnityEngine.Analytics.Analytics.RegisterEvent", + "UnityEngine.Analytics.Analytics.SendEvent", + "UnityEngine.Analytics.ContinuousEvent+ConfigureEventDelegate.Invoke", + "UnityEngine.Analytics.ContinuousEvent.ConfigureEvent", + "UnityEngine.Animations.AnimationLayerMixerPlayable.Create", + "UnityEngine.Animations.AnimationLayerMixerPlayable.CreateHandle", + "UnityEngine.Animations.AnimationMixerPlayable.Create", + "UnityEngine.Animations.AnimationMixerPlayable.CreateHandle", + "UnityEngine.AssetBundle.RecompressAssetBundleAsync", + "UnityEngine.Audio.AudioMixerPlayable.Create", + "UnityEngine.BoxcastCommand.ScheduleBatch", + "UnityEngine.Camera.CalculateProjectionMatrixFromPhysicalProperties", + "UnityEngine.CapsulecastCommand.ScheduleBatch", + "UnityEngine.Collider2D.Cast", + "UnityEngine.Collider2D.Raycast", + "UnityEngine.ComputeBuffer+BeginBufferWriteDelegate.Invoke", + "UnityEngine.ComputeBuffer+EndBufferWriteDelegate.Invoke", + "UnityEngine.ComputeBuffer.BeginBufferWrite", + "UnityEngine.ComputeBuffer.EndBufferWrite", + "UnityEngine.Cubemap+SetPixelDataImplArrayDelegate.Invoke", + "UnityEngine.Cubemap+SetPixelDataImplDelegate.Invoke", + "UnityEngine.Cubemap.SetPixelDataImpl", + "UnityEngine.Cubemap.SetPixelDataImplArray", + "UnityEngine.CubemapArray+SetPixelDataImplArrayDelegate.Invoke", + "UnityEngine.CubemapArray+SetPixelDataImplDelegate.Invoke", + "UnityEngine.CubemapArray.SetPixelDataImpl", + "UnityEngine.CubemapArray.SetPixelDataImplArray", + "UnityEngine.Experimental.Playables.MaterialEffectPlayable.Create", + "UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure+AddInstanceDelegate.Invoke", + "UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure+AddInstance_Procedural_InjectedDelegate.Invoke", + "UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure.AddInstance", + "UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure.AddInstance_Procedural", + "UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure.AddInstance_Procedural_Injected", + "UnityEngine.Experimental.Rendering.RayTracingShader+DispatchDelegate.Invoke", + "UnityEngine.Experimental.Rendering.RayTracingShader.Dispatch", + "UnityEngine.Experimental.Rendering.RenderPassAttachment.Clear", + "UnityEngine.GUI.DoButtonGrid", + "UnityEngine.GUI.Slider", + "UnityEngine.GUI.Toolbar", + "UnityEngine.Graphics.DrawMeshInstancedIndirect", + "UnityEngine.Graphics.DrawMeshInstancedProcedural", + "UnityEngine.Graphics.DrawProcedural", + "UnityEngine.Graphics.DrawProceduralIndirect", + "UnityEngine.Graphics.DrawProceduralIndirectNow", + "UnityEngine.Graphics.DrawProceduralNow", + "UnityEngine.LineRenderer+BakeMeshDelegate.Invoke", + "UnityEngine.LineRenderer.BakeMesh", + "UnityEngine.Mesh.GetIndices", + "UnityEngine.Mesh.GetTriangles", + "UnityEngine.Mesh.SetIndices", + "UnityEngine.Mesh.SetTriangles", + "UnityEngine.Physics2D.BoxCast", + "UnityEngine.Physics2D.CapsuleCast", + "UnityEngine.Physics2D.CircleCast", + "UnityEngine.PhysicsScene.BoxCast", + "UnityEngine.PhysicsScene.CapsuleCast", + "UnityEngine.PhysicsScene.OverlapBox", + "UnityEngine.PhysicsScene.OverlapCapsule", + "UnityEngine.PhysicsScene.SphereCast", + "UnityEngine.PhysicsScene2D.BoxCast", + "UnityEngine.PhysicsScene2D.CapsuleCast", + "UnityEngine.PhysicsScene2D.CircleCast", + "UnityEngine.PhysicsScene2D.GetRayIntersection", + "UnityEngine.PhysicsScene2D.Linecast", + "UnityEngine.PhysicsScene2D.OverlapArea", + "UnityEngine.PhysicsScene2D.OverlapBox", + "UnityEngine.PhysicsScene2D.OverlapCapsule", + "UnityEngine.PhysicsScene2D.OverlapCircle", + "UnityEngine.PhysicsScene2D.OverlapCollider", + "UnityEngine.PhysicsScene2D.OverlapPoint", + "UnityEngine.PhysicsScene2D.Raycast", + "UnityEngine.Playables.Playable.Create", + "UnityEngine.Profiling.CustomSampler.Create", + "UnityEngine.RaycastCommand.ScheduleBatch", + "UnityEngine.RemoteConfigSettings+QueueConfigDelegate.Invoke", + "UnityEngine.RemoteConfigSettings.QueueConfig", + "UnityEngine.RenderTexture.GetTemporaryImpl", + "UnityEngine.Rendering.AsyncGPUReadback.Request", + "UnityEngine.Rendering.AttachmentDescriptor.ConfigureClear", + "UnityEngine.Rendering.BatchRendererGroup+AddBatch_InjectedDelegate.Invoke", + "UnityEngine.Rendering.BatchRendererGroup.AddBatch", + "UnityEngine.Rendering.BatchRendererGroup.AddBatch_Injected", + "UnityEngine.Rendering.CommandBuffer+Internal_DispatchRaysDelegate.Invoke", + "UnityEngine.Rendering.CommandBuffer.DispatchRays", + "UnityEngine.Rendering.CommandBuffer.DrawMeshInstancedProcedural", + "UnityEngine.Rendering.CommandBuffer.Internal_DispatchRays", + "UnityEngine.Rendering.CommandBuffer.ResolveAntiAliasedSurface", + "UnityEngine.Rendering.ScriptableRenderContext.BeginRenderPass", + "UnityEngine.Rendering.ScriptableRenderContext.BeginScopedRenderPass", + "UnityEngine.Rendering.ScriptableRenderContext.BeginScopedSubPass", + "UnityEngine.Rendering.ScriptableRenderContext.BeginSubPass", + "UnityEngine.Rendering.ScriptableRenderContext.SetupCameraProperties", + "UnityEngine.Rigidbody2D.Cast", + "UnityEngine.Scripting.GarbageCollector+CollectIncrementalDelegate.Invoke", + "UnityEngine.Scripting.GarbageCollector.CollectIncremental", + "UnityEngine.SpherecastCommand.ScheduleBatch", + "UnityEngine.Texture2D+SetPixelDataImplArrayDelegate.Invoke", + "UnityEngine.Texture2D+SetPixelDataImplDelegate.Invoke", + "UnityEngine.Texture2D.SetPixelDataImpl", + "UnityEngine.Texture2D.SetPixelDataImplArray", + "UnityEngine.Texture2DArray+SetPixelDataImplArrayDelegate.Invoke", + "UnityEngine.Texture2DArray+SetPixelDataImplDelegate.Invoke", + "UnityEngine.Texture2DArray.SetPixelDataImpl", + "UnityEngine.Texture2DArray.SetPixelDataImplArray", + "UnityEngine.Texture3D+SetPixelDataImplArrayDelegate.Invoke", + "UnityEngine.Texture3D+SetPixelDataImplDelegate.Invoke", + "UnityEngine.Texture3D.SetPixelDataImpl", + "UnityEngine.Texture3D.SetPixelDataImplArray", + "UnityEngine.TrailRenderer+BakeMeshDelegate.Invoke", + "UnityEngine.TrailRenderer.BakeMesh", + "UnityEngine.WWW.LoadFromCacheOrDownload", + "UnityEngine.XR.InputDevice.SendHapticImpulse", + }; + +#endregion + + +#region Temp il2cpp list/dictionary fixes + + // Temp fix until Unhollower interface support improves + + internal static readonly Dictionary getEnumeratorMethods = new Dictionary(); + internal static readonly Dictionary enumeratorInfos = new Dictionary(); + internal static readonly HashSet notSupportedTypes = new HashSet(); + + // IEnumerables + + internal static IntPtr cppIEnumerablePointer; + + protected override bool Internal_IsEnumerable(Type type) + { + if (base.Internal_IsEnumerable(type)) + return true; + + try + { + if (cppIEnumerablePointer == IntPtr.Zero) + Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IEnumerable), out cppIEnumerablePointer); + + if (cppIEnumerablePointer != IntPtr.Zero + && Il2CppTypeNotNull(type, out IntPtr assignFromPtr) + && il2cpp_class_is_assignable_from(cppIEnumerablePointer, assignFromPtr)) + { + return true; + } + } + catch { } + + return false; + } + + internal class EnumeratorInfo + { + internal MethodInfo moveNext; + internal PropertyInfo current; + } + + protected override bool Internal_TryGetEnumerator(object list, out IEnumerator enumerator) + { + if (list is IEnumerable) + return base.Internal_TryGetEnumerator(list, out enumerator); + + try + { + PrepareCppEnumerator(list, out object cppEnumerator, out EnumeratorInfo info); + enumerator = EnumerateCppList(info, cppEnumerator); + return true; + } + catch //(Exception ex) + { + //ExplorerCore.LogWarning($"Exception enumerating IEnumerable: {ex.ReflectionExToString()}"); + enumerator = null; + return false; + } + } + + private static void PrepareCppEnumerator(object list, out object cppEnumerator, out EnumeratorInfo info) + { + info = null; + cppEnumerator = null; + if (list == null) + throw new ArgumentNullException("list"); + + // Some ugly reflection to use the il2cpp interface for the instance type + + var type = list.GetType(); + var key = type.AssemblyQualifiedName; + + if (!getEnumeratorMethods.ContainsKey(key)) + { + getEnumeratorMethods.Add(key, type.GetMethod("GetEnumerator")); + + // ensure the enumerator type is supported + try + { + var test = getEnumeratorMethods[key].Invoke(list, null); + test.GetType().GetMethod("MoveNext").Invoke(test, null); + } + catch + { + notSupportedTypes.Add(key); + } + } + + if (notSupportedTypes.Contains(key)) + throw new NotSupportedException($"The IEnumerable type '{type.FullName}' does not support MoveNext."); + + cppEnumerator = getEnumeratorMethods[key].Invoke(list, null); + var enumeratorType = cppEnumerator.GetType(); + + var enumInfoKey = enumeratorType.AssemblyQualifiedName; + + if (!enumeratorInfos.ContainsKey(enumInfoKey)) + { + enumeratorInfos.Add(enumInfoKey, new EnumeratorInfo + { + current = enumeratorType.GetProperty("Current"), + moveNext = enumeratorType.GetMethod("MoveNext"), + }); + } + + info = enumeratorInfos[enumInfoKey]; + } + + internal static IEnumerator EnumerateCppList(EnumeratorInfo info, object enumerator) + { + // Yield and return the actual entries + while ((bool)info.moveNext.Invoke(enumerator, null)) + yield return info.current.GetValue(enumerator); + } + + // IDictionary + + internal static IntPtr cppIDictionaryPointer; + + protected override bool Internal_IsDictionary(Type type) + { + if (base.Internal_IsDictionary(type)) + return true; + + try + { + if (cppIDictionaryPointer == IntPtr.Zero) + if (!Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IDictionary), out cppIDictionaryPointer)) + return false; + + if (Il2CppTypeNotNull(type, out IntPtr classPtr) + && il2cpp_class_is_assignable_from(cppIDictionaryPointer, classPtr)) + return true; + } + catch { } + + return false; + } + + protected override bool Internal_TryGetDictEnumerator(object dictionary, out IEnumerator dictEnumerator) + { + if (dictionary is IDictionary) + return base.Internal_TryGetDictEnumerator(dictionary, out dictEnumerator); + + try + { + var type = dictionary.GetType(); + + if (typeof(Il2CppSystem.Collections.Hashtable).IsAssignableFrom(type)) + { + dictEnumerator = EnumerateCppHashTable(dictionary.TryCast()); + return true; + } + + var keys = type.GetProperty("Keys").GetValue(dictionary, null); + + var keyCollType = keys.GetType(); + var cacheKey = keys.GetType().AssemblyQualifiedName; + if (!getEnumeratorMethods.ContainsKey(cacheKey)) + { + getEnumeratorMethods.Add(cacheKey, keyCollType.GetMethod("GetEnumerator")); + + // test support + try + { + var test = getEnumeratorMethods[cacheKey].Invoke(keys, null); + test.GetType().GetMethod("MoveNext").Invoke(test, null); + } + catch + { + notSupportedTypes.Add(cacheKey); + } + } + + if (notSupportedTypes.Contains(cacheKey)) + throw new Exception($"The IDictionary type '{type.FullName}' does not support MoveNext."); + + var keyEnumerator = getEnumeratorMethods[cacheKey].Invoke(keys, null); + var keyInfo = new EnumeratorInfo + { + current = keyEnumerator.GetType().GetProperty("Current"), + moveNext = keyEnumerator.GetType().GetMethod("MoveNext"), + }; + + var values = type.GetProperty("Values").GetValue(dictionary, null); + var valueEnumerator = values.GetType().GetMethod("GetEnumerator").Invoke(values, null); + var valueInfo = new EnumeratorInfo + { + current = valueEnumerator.GetType().GetProperty("Current"), + moveNext = valueEnumerator.GetType().GetMethod("MoveNext"), + }; + + dictEnumerator = EnumerateCppDict(keyInfo, keyEnumerator, valueInfo, valueEnumerator); + return true; + } + catch //(Exception ex) + { + //ExplorerCore.LogWarning($"Exception enumerating IDictionary: {ex.ReflectionExToString()}"); + dictEnumerator = null; + return false; + } + } + + internal static IEnumerator EnumerateCppDict(EnumeratorInfo keyInfo, object keyEnumerator, + EnumeratorInfo valueInfo, object valueEnumerator) + { + while ((bool)keyInfo.moveNext.Invoke(keyEnumerator, null)) + { + valueInfo.moveNext.Invoke(valueEnumerator, null); + + var key = keyInfo.current.GetValue(keyEnumerator, null); + var value = valueInfo.current.GetValue(valueEnumerator, null); + + yield return new DictionaryEntry(key, value); + } + } + + internal static IEnumerator EnumerateCppHashTable(Il2CppSystem.Collections.Hashtable hashtable) + { + for (int i = 0; i < hashtable.buckets.Count; i++) + { + var bucket = hashtable.buckets[i]; + if (bucket == null || bucket.key == null) + continue; + + yield return new DictionaryEntry(bucket.key, bucket.val); + } + } + +#endregion + + } +} + +#endif \ No newline at end of file diff --git a/src/Core/Reflection/ReflectionUtility.cs b/src/Core/Reflection/ReflectionUtility.cs new file mode 100644 index 0000000..61a4dd3 --- /dev/null +++ b/src/Core/Reflection/ReflectionUtility.cs @@ -0,0 +1,528 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using BF = System.Reflection.BindingFlags; +using UnityExplorer.Core.Runtime; +using System.Text; +using UnityEngine; +using UnityExplorer.Core.Config; + +namespace UnityExplorer +{ + + public class ReflectionUtility + { + public const BF FLAGS = BF.Public | BF.Instance | BF.NonPublic | BF.Static; + + internal static ReflectionUtility Instance; + + public static void Init() + { + Instance = +#if CPP + new Il2CppReflection(); +#else + new ReflectionUtility(); +#endif + Instance.Initialize(); + } + + protected virtual void Initialize() + { + SetupTypeCache(); + + LoadBlacklistString(ConfigManager.Reflection_Signature_Blacklist.Value); + ConfigManager.Reflection_Signature_Blacklist.OnValueChanged += LoadBlacklistString; + } + + #region Type cache + + public static Action OnTypeLoaded; + + /// Key: Type.FullName + public static readonly SortedDictionary AllTypes = new SortedDictionary(StringComparer.OrdinalIgnoreCase); + + public static readonly List AllNamespaces = new List(); + private static readonly HashSet uniqueNamespaces = new HashSet(); + + private static string[] allTypesArray; + public static string[] GetTypeNameArray() + { + if (allTypesArray == null || allTypesArray.Length != AllTypes.Count) + { + allTypesArray = new string[AllTypes.Count]; + int i = 0; + foreach (var name in AllTypes.Keys) + { + allTypesArray[i] = name; + i++; + } + } + return allTypesArray; + } + + private static void SetupTypeCache() + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + CacheTypes(asm); + + AppDomain.CurrentDomain.AssemblyLoad += AssemblyLoaded; + } + + private static void AssemblyLoaded(object sender, AssemblyLoadEventArgs args) + { + if (args.LoadedAssembly == null || args.LoadedAssembly.GetName().Name == "completions") + return; + + CacheTypes(args.LoadedAssembly); + } + + private static void CacheTypes(Assembly asm) + { + foreach (var type in asm.TryGetTypes()) + { + if (!string.IsNullOrEmpty(type.Namespace) && !uniqueNamespaces.Contains(type.Namespace)) + { + uniqueNamespaces.Add(type.Namespace); + int i = 0; + while (i < AllNamespaces.Count) + { + if (type.Namespace.CompareTo(AllNamespaces[i]) < 0) + break; + i++; + } + AllNamespaces.Insert(i, type.Namespace); + } + + if (AllTypes.ContainsKey(type.FullName)) + AllTypes[type.FullName] = type; + else + { + AllTypes.Add(type.FullName, type); + //allTypeNames.Add(type.FullName); + } + + OnTypeLoaded?.Invoke(type); + + foreach (var key in typeInheritance.Keys) + { + try + { + var baseType = AllTypes[key]; + if (baseType.IsAssignableFrom(type) && !typeInheritance[key].Contains(type)) + typeInheritance[key].Add(type); + } + catch { } + } + } + } + + #endregion + + /// + /// Find a in the current AppDomain whose matches the provided . + /// + /// The you want to search for - case sensitive and full matches only. + /// The Type if found, otherwise null. + public static Type GetTypeByName(string fullName) + => Instance.Internal_GetTypeByName(fullName); + + internal virtual Type Internal_GetTypeByName(string fullName) + { + AllTypes.TryGetValue(fullName, out Type type); + return type; + } + + // Getting the actual type of an object + internal virtual Type Internal_GetActualType(object obj) + => obj?.GetType(); + + // Force-casting an object to a type + internal virtual object Internal_TryCast(object obj, Type castTo) + => obj; + + // Processing deobfuscated type names in strings + public static string ProcessTypeInString(Type type, string theString) + => Instance.Internal_ProcessTypeInString(theString, type); + + internal virtual string Internal_ProcessTypeInString(string theString, Type type) + => theString; + + // Force loading modules + public static bool LoadModule(string moduleName) + => Instance.Internal_LoadModule(moduleName); + + internal virtual bool Internal_LoadModule(string moduleName) + => false; + + public static void FindSingleton(string[] possibleNames, Type type, BindingFlags flags, List instances) + => Instance.Internal_FindSingleton(possibleNames, type, flags, instances); + + internal virtual void Internal_FindSingleton(string[] possibleNames, Type type, BindingFlags flags, List instances) + { + // Look for a typical Instance backing field. + FieldInfo fi; + foreach (var name in possibleNames) + { + fi = type.GetField(name, flags); + if (fi != null) + { + var instance = fi.GetValue(null); + if (instance != null) + { + instances.Add(instance); + return; + } + } + } + } + + // Universal helpers + + #region Type inheritance cache + + // cache for GetBaseTypes + internal static readonly Dictionary baseTypes = new Dictionary(); + + /// + /// Get all base types of the provided Type, including itself. + /// + public static Type[] GetAllBaseTypes(object obj) => GetAllBaseTypes(obj?.GetActualType()); + + /// + /// Get all base types of the provided Type, including itself. + /// + public static Type[] GetAllBaseTypes(Type type) + { + if (type == null) + throw new ArgumentNullException("type"); + + var name = type.AssemblyQualifiedName; + + if (baseTypes.TryGetValue(name, out Type[] ret)) + return ret; + + List list = new List(); + + while (type != null) + { + list.Add(type); + type = type.BaseType; + } + + ret = list.ToArray(); + + baseTypes.Add(name, ret); + + return ret; + } + +#endregion + + + #region Type and Generic Parameter implementation cache + + // cache for GetImplementationsOf + internal static readonly Dictionary> typeInheritance = new Dictionary>(); + internal static readonly Dictionary> genericParameterInheritance = new Dictionary>(); + + public static string GetImplementationKey(Type type) + { + if (!type.IsGenericParameter) + return type.FullName; + else + { + var sb = new StringBuilder(); + sb.Append(type.GenericParameterAttributes) + .Append('|'); + foreach (var c in type.GetGenericParameterConstraints()) + sb.Append(c.FullName).Append(','); + return sb.ToString(); + } + } + + /// + /// Get all non-abstract implementations of the provided type (include itself, if not abstract) in the current AppDomain. + /// Also works for generic parameters by analyzing the constraints. + /// + /// The base type, which can optionally be abstract / interface. + /// All implementations of the type in the current AppDomain. + public static HashSet GetImplementationsOf(Type baseType, bool allowAbstract, bool allowGeneric, bool allowRecursive = true) + { + var key = GetImplementationKey(baseType); + + int count = AllTypes.Count; + HashSet ret; + if (!baseType.IsGenericParameter) + ret = GetImplementations(key, baseType, allowAbstract, allowGeneric); + else + ret = GetGenericParameterImplementations(key, baseType, allowAbstract, allowGeneric); + + // types were resolved during the parse, do it again if we're not already rebuilding. + if (allowRecursive && AllTypes.Count != count) + { + ret = GetImplementationsOf(baseType, allowAbstract, allowGeneric, false); + } + + return ret; + } + + private static HashSet GetImplementations(string key, Type baseType, bool allowAbstract, bool allowGeneric) + { + if (!typeInheritance.ContainsKey(key)) + { + var set = new HashSet(); + var names = GetTypeNameArray(); + for (int i = 0; i < names.Length; i++) + { + var name = names[i]; + try + { + var type = AllTypes[name]; + + if (set.Contains(type) + || (type.IsAbstract && type.IsSealed) // ignore static classes + || (!allowAbstract && type.IsAbstract) + || (!allowGeneric && (type.IsGenericType || type.IsGenericTypeDefinition))) + continue; + + if (type.FullName.Contains("PrivateImplementationDetails") + || type.FullName.Contains("DisplayClass") + || type.FullName.Contains('<')) + continue; + + if (baseType.IsAssignableFrom(type) && !set.Contains(type)) + set.Add(type); + } + catch { } + } + + //set. + + typeInheritance.Add(key, set); + } + + return typeInheritance[key]; + } + + private static HashSet GetGenericParameterImplementations(string key, Type baseType, bool allowAbstract, bool allowGeneric) + { + if (!genericParameterInheritance.ContainsKey(key)) + { + var set = new HashSet(); + + var names = GetTypeNameArray(); + for (int i = 0; i < names.Length; i++) + { + var name = names[i]; + try + { + var type = AllTypes[name]; + + if (set.Contains(type) + || (type.IsAbstract && type.IsSealed) // ignore static classes + || (!allowAbstract && type.IsAbstract) + || (!allowGeneric && (type.IsGenericType || type.IsGenericTypeDefinition))) + continue; + + if (type.FullName.Contains("PrivateImplementationDetails") + || type.FullName.Contains("DisplayClass") + || type.FullName.Contains('<')) + continue; + + if (baseType.GenericParameterAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) + && type.IsClass) + continue; + + if (baseType.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint) + && type.IsValueType) + continue; + + if (baseType.GetGenericParameterConstraints().Any(it => !it.IsAssignableFrom(type))) + continue; + + set.Add(type); + } + catch { } + } + + genericParameterInheritance.Add(key, set); + } + + return genericParameterInheritance[key]; + } + +#endregion + + + #region Internal MemberInfo Cache + + internal static Dictionary> fieldInfos = new Dictionary>(); + + public static FieldInfo GetFieldInfo(Type type, string fieldName) + { + if (!fieldInfos.ContainsKey(type)) + fieldInfos.Add(type, new Dictionary()); + + if (!fieldInfos[type].ContainsKey(fieldName)) + fieldInfos[type].Add(fieldName, type.GetField(fieldName, FLAGS)); + + return fieldInfos[type][fieldName]; + } + + internal static Dictionary> propertyInfos = new Dictionary>(); + + public static PropertyInfo GetPropertyInfo(Type type, string propertyName) + { + if (!propertyInfos.ContainsKey(type)) + propertyInfos.Add(type, new Dictionary()); + + if (!propertyInfos[type].ContainsKey(propertyName)) + propertyInfos[type].Add(propertyName, type.GetProperty(propertyName, FLAGS)); + + return propertyInfos[type][propertyName]; + } + + internal static Dictionary> methodInfos = new Dictionary>(); + + public static MethodInfo GetMethodInfo(Type type, string methodName) + => GetMethodInfo(type, methodName, ArgumentUtility.EmptyTypes, false); + + public static MethodInfo GetMethodInfo(Type type, string methodName, Type[] argumentTypes, bool cacheAmbiguous = false) + { + if (!methodInfos.ContainsKey(type)) + methodInfos.Add(type, new Dictionary()); + + if (cacheAmbiguous) + { + methodName += "|"; + foreach (var arg in argumentTypes) + methodName += arg.FullName + ","; + } + + try + { + if (!methodInfos[type].ContainsKey(methodName)) + { + if (argumentTypes != null) + methodInfos[type].Add(methodName, type.GetMethod(methodName, FLAGS, null, argumentTypes, null)); + else + methodInfos[type].Add(methodName, type.GetMethod(methodName, FLAGS)); + } + + return methodInfos[type][methodName]; + } + catch (AmbiguousMatchException) + { + ExplorerCore.LogWarning($"AmbiguousMatchException trying to get method '{methodName}'"); + return null; + } + catch (Exception e) + { + ExplorerCore.LogWarning($"{e.GetType()} trying to get method '{methodName}': {e.Message}\r\n{e.StackTrace}"); + return null; + } + } + + #endregion + + + #region Reflection Blacklist + + public virtual string DefaultReflectionBlacklist => string.Empty; + + public static void LoadBlacklistString(string blacklist) + { + if (string.Equals(blacklist, "DEFAULT", StringComparison.InvariantCultureIgnoreCase)) + { + blacklist = Instance.DefaultReflectionBlacklist; + ConfigManager.Reflection_Signature_Blacklist.Value = blacklist; + ConfigManager.Handler.SaveConfig(); + return; + } + + if (string.IsNullOrEmpty(blacklist)) + return; + + var sigs = blacklist.Split(';'); + foreach (var sig in sigs) + { + var s = sig.Trim(); + if (string.IsNullOrEmpty(s)) + continue; + if (!currentBlacklist.Contains(s)) + currentBlacklist.Add(s); + } + + Mono.CSharp.IL2CPP.Blacklist.SignatureBlacklist = currentBlacklist; + } + + public static bool IsBlacklisted(MemberInfo member) + { + if (string.IsNullOrEmpty(member.DeclaringType?.Namespace)) + return false; + + var sig = $"{member.DeclaringType.FullName}.{member.Name}"; + return currentBlacklist.Contains(sig); + } + + private static readonly HashSet currentBlacklist = new HashSet(); + + #endregion + + + // Temp fix for IL2CPP until interface support improves + + // IsEnumerable + + public static bool IsEnumerable(Type type) => Instance.Internal_IsEnumerable(type); + + protected virtual bool Internal_IsEnumerable(Type type) + { + return typeof(IEnumerable).IsAssignableFrom(type); + } + + // TryGetEnumerator (list) + + public static bool TryGetEnumerator(object list, out IEnumerator enumerator) + => Instance.Internal_TryGetEnumerator(list, out enumerator); + + protected virtual bool Internal_TryGetEnumerator(object list, out IEnumerator enumerator) + { + enumerator = (list as IEnumerable).GetEnumerator(); + return true; + } + + // IsDictionary + + public static bool IsDictionary(Type type) => Instance.Internal_IsDictionary(type); + + protected virtual bool Internal_IsDictionary(Type type) + { + return typeof(IDictionary).IsAssignableFrom(type); + } + + // TryGetEnumerator (dictionary) + + public static bool TryGetDictEnumerator(object dictionary, out IEnumerator dictEnumerator) + => Instance.Internal_TryGetDictEnumerator(dictionary, out dictEnumerator); + + protected virtual bool Internal_TryGetDictEnumerator(object dictionary, out IEnumerator dictEnumerator) + { + dictEnumerator = EnumerateDictionary((IDictionary)dictionary); + return true; + } + + private IEnumerator EnumerateDictionary(IDictionary dict) + { + var enumerator = dict.GetEnumerator(); + while (enumerator.MoveNext()) + { + yield return new DictionaryEntry(enumerator.Key, enumerator.Value); + } + } + } +} diff --git a/src/Core/ReflectionUtility.cs b/src/Core/ReflectionUtility.cs deleted file mode 100644 index b7e8a8f..0000000 --- a/src/Core/ReflectionUtility.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using BF = System.Reflection.BindingFlags; -using UnityExplorer.Core.Runtime; - -namespace UnityExplorer -{ - public static class ReflectionUtility - { - public const BF AllFlags = BF.Public | BF.Instance | BF.NonPublic | BF.Static; - - /// - /// Helper for IL2CPP to get the underlying true Type (Unhollowed) of the object. - /// - /// The object to get the true Type for. - /// The most accurate Type of the object which could be identified. - public static Type GetActualType(this object obj) - { - if (obj == null) - return null; - - return ReflectionProvider.Instance.GetActualType(obj); - } - - /// - /// Cast an object to its underlying Type. - /// - /// The object to cast - /// The object, cast to the underlying Type if possible, otherwise the original object. - public static object Cast(this object obj) - => ReflectionProvider.Instance.Cast(obj, GetActualType(obj)); - - /// - /// Cast an object to a Type, if possible. - /// - /// The object to cast - /// The Type to cast to - /// The object, cast to the Type provided if possible, otherwise the original object. - public static object Cast(this object obj, Type castTo) - => ReflectionProvider.Instance.Cast(obj, castTo); - - public static T TryCast(this object obj) - => ReflectionProvider.Instance.TryCast(obj); - - /// - /// Check if the provided Type is assignable to IEnumerable. - /// - /// The Type to check - /// True if the Type is assignable to IEnumerable, otherwise false. - public static bool IsEnumerable(this Type t) - => ReflectionProvider.Instance.IsAssignableFrom(typeof(IEnumerable), t); - - /// - /// Check if the provided Type is assignable to IDictionary. - /// - /// The Type to check - /// True if the Type is assignable to IDictionary, otherwise false. - public static bool IsDictionary(this Type t) - => ReflectionProvider.Instance.IsAssignableFrom(typeof(IDictionary), t); - - /// - /// [INTERNAL] Used to load Unhollowed DLLs in IL2CPP. - /// - internal static bool LoadModule(string module) - => ReflectionProvider.Instance.LoadModule(module); - - // cache for GetTypeByName - internal static readonly Dictionary s_typesByName = new Dictionary(); - - /// - /// Find a in the current AppDomain whose matches the provided . - /// - /// The you want to search for - case sensitive and full matches only. - /// The Type if found, otherwise null. - public static Type GetTypeByName(string fullName) - { - s_typesByName.TryGetValue(fullName, out Type ret); - - if (ret != null) - return ret; - - foreach (var type in from asm in AppDomain.CurrentDomain.GetAssemblies() - from type in asm.TryGetTypes() - select type) - { - if (type.FullName == fullName) - { - ret = type; - break; - } - } - - if (s_typesByName.ContainsKey(fullName)) - s_typesByName[fullName] = ret; - else - s_typesByName.Add(fullName, ret); - - return ret; - } - - // cache for GetBaseTypes - internal static readonly Dictionary s_cachedTypeInheritance = new Dictionary(); - - /// - /// Get all base types of the provided Type, including itself. - /// - public static Type[] GetAllBaseTypes(this object obj) => GetAllBaseTypes(GetActualType(obj)); - - /// - /// Get all base types of the provided Type, including itself. - /// - public static Type[] GetAllBaseTypes(this Type type) - { - if (type == null) - throw new ArgumentNullException("type"); - - var name = type.AssemblyQualifiedName; - - if (s_cachedTypeInheritance.TryGetValue(name, out Type[] ret)) - return ret; - - List list = new List(); - - while (type != null) - { - list.Add(type); - type = type.BaseType; - } - - ret = list.ToArray(); - - s_cachedTypeInheritance.Add(name, ret); - - return ret; - } - - /// - /// Safely get all valid Types inside an Assembly. - /// - /// The Assembly to find Types in. - /// All possible Types which could be retrieved from the Assembly, or an empty array. - public static IEnumerable TryGetTypes(this Assembly asm) - { - try - { - return asm.GetTypes(); - } - catch (ReflectionTypeLoadException e) - { - try - { - return asm.GetExportedTypes(); - } - catch - { - return e.Types.Where(t => t != null); - } - } - catch - { - return Enumerable.Empty(); - } - } - - internal static Dictionary> s_cachedFieldInfos = new Dictionary>(); - - public static FieldInfo GetFieldInfo(Type type, string fieldName) - { - if (!s_cachedFieldInfos.ContainsKey(type)) - s_cachedFieldInfos.Add(type, new Dictionary()); - - if (!s_cachedFieldInfos[type].ContainsKey(fieldName)) - s_cachedFieldInfos[type].Add(fieldName, type.GetField(fieldName, AllFlags)); - - return s_cachedFieldInfos[type][fieldName]; - } - - internal static Dictionary> s_cachedPropInfos = new Dictionary>(); - - public static PropertyInfo GetPropertyInfo(Type type, string propertyName) - { - if (!s_cachedPropInfos.ContainsKey(type)) - s_cachedPropInfos.Add(type, new Dictionary()); - - if (!s_cachedPropInfos[type].ContainsKey(propertyName)) - s_cachedPropInfos[type].Add(propertyName, type.GetProperty(propertyName, AllFlags)); - - return s_cachedPropInfos[type][propertyName]; - } - - internal static Dictionary> s_cachedMethodInfos = new Dictionary>(); - - public static MethodInfo GetMethodInfo(Type type, string methodName, Type[] argumentTypes) - { - if (!s_cachedMethodInfos.ContainsKey(type)) - s_cachedMethodInfos.Add(type, new Dictionary()); - - var sig = methodName; - - if (argumentTypes != null) - { - sig += "("; - for (int i = 0; i < argumentTypes.Length; i++) - { - if (i > 0) - sig += ","; - sig += argumentTypes[i].FullName; - } - sig += ")"; - } - - try - { - if (!s_cachedMethodInfos[type].ContainsKey(sig)) - { - if (argumentTypes != null) - s_cachedMethodInfos[type].Add(sig, type.GetMethod(methodName, AllFlags, null, argumentTypes, null)); - else - s_cachedMethodInfos[type].Add(sig, type.GetMethod(methodName, AllFlags)); - } - - return s_cachedMethodInfos[type][sig]; - } - catch (AmbiguousMatchException) - { - ExplorerCore.LogWarning($"AmbiguousMatchException trying to get method '{sig}'"); - return null; - } - catch (Exception e) - { - ExplorerCore.LogWarning($"{e.GetType()} trying to get method '{sig}': {e.Message}\r\n{e.StackTrace}"); - return null; - } - } - - /// - /// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception. - /// - /// The Exception to convert to string. - /// Should the inner-most Exception of the stack be used? If false, the Exception you provided will be used directly. - /// The exception to string. - public static string ReflectionExToString(this Exception e, bool innerMost = false) - { - if (innerMost) - { - while (e.InnerException != null) - { - if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException) - break; - - e = e.InnerException; - } - } - - return $"{e.GetType()}: {e.Message}"; - } - } -} diff --git a/src/Core/Runtime/Il2Cpp/AssetBundle.cs b/src/Core/Runtime/Il2Cpp/AssetBundle.cs index 9ad4301..52dbb0f 100644 --- a/src/Core/Runtime/Il2Cpp/AssetBundle.cs +++ b/src/Core/Runtime/Il2Cpp/AssetBundle.cs @@ -19,9 +19,7 @@ namespace UnityExplorer public static AssetBundle LoadFromFile(string path) { var iCall = ICallManager.GetICall("UnityEngine.AssetBundle::LoadFromFile_Internal"); - var ptr = iCall.Invoke(IL2CPP.ManagedStringToIl2Cpp(path), 0u, 0UL); - return new AssetBundle(ptr); } @@ -30,12 +28,20 @@ namespace UnityExplorer public static AssetBundle LoadFromMemory(byte[] binary, uint crc = 0) { var iCall = ICallManager.GetICall("UnityEngine.AssetBundle::LoadFromMemory_Internal"); - var ptr = iCall(((Il2CppStructArray) binary).Pointer, crc); - return new AssetBundle(ptr); } + // static void UnloadAllAssetBundles(bool unloadAllObjects); + + internal delegate void d_UnloadAllAssetBundles(bool unloadAllObjects); + + public static void UnloadAllAssetBundles(bool unloadAllObjects) + { + var iCall = ICallManager.GetICall("UnityEngine.AssetBundle::UnloadAllAssetBundles"); + iCall.Invoke(unloadAllObjects); + } + // ~~~~~~~~~~~~ Instance ~~~~~~~~~~~~ private readonly IntPtr m_bundlePtr = IntPtr.Zero; @@ -71,6 +77,15 @@ namespace UnityExplorer return new UnityEngine.Object(ptr).TryCast(); } + + // public extern void Unload(bool unloadAllLoadedObjects); + internal delegate void d_Unload(IntPtr _this, bool unloadAllLoadedObjects); + + public void Unload(bool unloadAssets = true) + { + var iCall = ICallManager.GetICall("UnityEngine.AssetBundle::Unload"); + iCall.Invoke(this.m_bundlePtr, unloadAssets); + } } } #endif \ No newline at end of file diff --git a/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs b/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs index 8f33ba7..c2ac01f 100644 --- a/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs +++ b/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs @@ -124,8 +124,11 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp if (nextAsEnumerator != null) // il2cpp IEnumerator also handles CustomYieldInstruction next = new Il2CppEnumeratorWrapper(nextAsEnumerator); else - ExplorerCore.LogWarning($"Unknown coroutine yield object of type {il2CppObjectBase} for coroutine {enumerator}"); - break; + ExplorerCore.LogWarning($"Unknown coroutine yield object of type '{il2CppObjectBase}' for coroutine '{enumerator}'"); + return; + default: + ExplorerCore.LogWarning($"Unknown coroutine yield object of type '{next}' for coroutine '{enumerator}'"); + return; } ourCoroutinesStore.Add(new CoroTuple { WaitCondition = next, Coroutine = enumerator }); diff --git a/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs b/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs index 76b1a3a..34f88bc 100644 --- a/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs +++ b/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs @@ -21,7 +21,8 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp { public override void Initialize() { - Reflection = new Il2CppReflection(); + ExplorerCore.Context = RuntimeContext.IL2CPP; + //Reflection = new Il2CppReflection(); TextureUtil = new Il2CppTextureUtil(); } @@ -47,7 +48,7 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp private void Application_logMessageReceived(string condition, string stackTrace, LogType type) { - ExplorerCore.Log(condition, type, true); + ExplorerCore.LogUnity(condition, type); } public override void StartCoroutine(IEnumerator routine) @@ -55,6 +56,11 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp Il2CppCoroutine.Start(routine); } + internal override void ProcessOnPostRender() + { + Il2CppCoroutine.ProcessWaitForEndOfFrame(); + } + public override void Update() { Il2CppCoroutine.Process(); @@ -80,18 +86,6 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp list.AddRange(il2cppList.ToArray()); } - public override bool IsReferenceEqual(object a, object b) - { - if (a.TryCast() is UnityEngine.Object ua) - { - var ub = b.TryCast(); - if (ub && ua.m_CachedPtr == ub.m_CachedPtr) - return true; - } - - return base.IsReferenceEqual(a, b); - } - // LayerMask.LayerToName internal delegate IntPtr d_LayerToName(int layer); @@ -117,9 +111,6 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp return new Il2CppReferenceArray(iCall.Invoke(Il2CppType.From(type).Pointer)); } - public override int GetSceneHandle(Scene scene) - => scene.handle; - // Scene.GetRootGameObjects(); internal delegate void d_GetRootGameObjects(int handle, IntPtr list); @@ -242,34 +233,14 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp ReflectionUtility.GetPropertyInfo(typeof(Selectable), "m_Colors") .SetValue(selectable, _colorBlock, null); - ReflectionUtility.GetMethodInfo(typeof(Selectable), "OnSetProperty", new Type[0]) - .Invoke(selectable, new object[0]); + ReflectionUtility.GetMethodInfo(typeof(Selectable), "OnSetProperty") + .Invoke(selectable, ArgumentUtility.EmptyArgs); } catch (Exception ex) { ExplorerCore.Log(ex); } } - - public override void FindSingleton(string[] possibleNames, Type type, BF flags, List instances) - { - PropertyInfo pi; - foreach (var name in possibleNames) - { - pi = type.GetProperty(name, flags); - if (pi != null) - { - var instance = pi.GetValue(null, null); - if (instance != null) - { - instances.Add(instance); - return; - } - } - } - - base.FindSingleton(possibleNames, type, flags, instances); - } } } @@ -285,6 +256,16 @@ public static class Il2CppExtensions action.AddListener(listener); } + public static void RemoveListener(this UnityEvent action, Action listener) + { + action.RemoveListener(listener); + } + + public static void RemoveListener(this UnityEvent action, Action listener) + { + action.RemoveListener(listener); + } + public static void SetChildControlHeight(this HorizontalOrVerticalLayoutGroup group, bool value) => group.childControlHeight = value; public static void SetChildControlWidth(this HorizontalOrVerticalLayoutGroup group, bool value) => group.childControlWidth = value; } diff --git a/src/Core/Runtime/Il2Cpp/Il2CppReflection.cs b/src/Core/Runtime/Il2Cpp/Il2CppReflection.cs deleted file mode 100644 index 8d10743..0000000 --- a/src/Core/Runtime/Il2Cpp/Il2CppReflection.cs +++ /dev/null @@ -1,508 +0,0 @@ -#if CPP -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using UnhollowerBaseLib; -using UnhollowerRuntimeLib; -using System.Runtime.InteropServices; -using System.Reflection; -using System.Collections; -using System.IO; -using System.Diagnostics.CodeAnalysis; -using UnityExplorer.Core; -using CppType = Il2CppSystem.Type; -using BF = System.Reflection.BindingFlags; - -namespace UnityExplorer.Core.Runtime.Il2Cpp -{ - [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "External methods")] - public class Il2CppReflection : ReflectionProvider - { - public Il2CppReflection() : base() - { - Instance = this; - - TryLoadGameModules(); - } - - public override object Cast(object obj, Type castTo) - { - return Il2CppCast(obj, castTo); - } - - public override T TryCast(object obj) - { - try - { - return (T)Il2CppCast(obj, typeof(T)); - } - catch - { - return default; - } - } - - public override void BoxStringToType(ref object value, Type castTo) - { - if (castTo == typeof(Il2CppSystem.String)) - value = (Il2CppSystem.String)(value as string); - else - value = (Il2CppSystem.Object)(value as string); - } - - public override string UnboxString(object value) - { - if (value is string s) - return s; - - s = null; - // strings boxed as Il2CppSystem.Objects can behave weirdly. - // GetActualType will find they are a string, but if its boxed - // then we need to unbox it like this... - if (value is Il2CppSystem.Object cppObject) - s = cppObject.ToString(); - else if (value is Il2CppSystem.String cppString) - s = cppString; - - return s; - } - - public override string ProcessTypeNameInString(Type type, string theString, ref string typeName) - { - if (!Il2CppTypeNotNull(type)) - return theString; - - var cppType = Il2CppType.From(type); - if (cppType != null && s_deobfuscatedTypeNames.ContainsKey(cppType.FullName)) - { - typeName = s_deobfuscatedTypeNames[cppType.FullName]; - theString = theString.Replace(cppType.FullName, typeName); - } - - return theString; - } - - public override Type GetActualType(object obj) - { - if (obj == null) - return null; - - var type = obj.GetType(); - - if (obj is Il2CppSystem.Object cppObject) - { - // weird specific case - if the object is an Il2CppSystem.Type, then return so manually. - if (cppObject is CppType) - return typeof(CppType); - - if (!string.IsNullOrEmpty(type.Namespace)) - { - // Il2CppSystem-namespace objects should just return GetType, - // because using GetIl2CppType returns the System namespace type instead. - if (type.Namespace.StartsWith("System.") || type.Namespace.StartsWith("Il2CppSystem.")) - return cppObject.GetType(); - } - - var cppType = cppObject.GetIl2CppType(); - - // check if type is injected - IntPtr classPtr = il2cpp_object_get_class(cppObject.Pointer); - if (RuntimeSpecificsStore.IsInjected(classPtr)) - { - var typeByName = ReflectionUtility.GetTypeByName(cppType.FullName); - if (typeByName != null) - return typeByName; - } - - // this should be fine for all other il2cpp objects - var getType = GetMonoType(cppType); - if (getType != null) - return getType; - } - - return type; - } - - // caching for GetMonoType - private static readonly Dictionary Il2CppToMonoType = new Dictionary(); - - // keep deobfuscated type name cache, used to display proper name. - internal static Dictionary s_deobfuscatedTypeNames = new Dictionary(); - - /// - /// Try to get the Mono (Unhollowed) Type representation of the provided . - /// - /// The Cpp Type you want to convert to Mono. - /// The Mono Type if found, otherwise null. - public static Type GetMonoType(CppType cppType) - { - string name = cppType.AssemblyQualifiedName; - - if (Il2CppToMonoType.ContainsKey(name)) - return Il2CppToMonoType[name]; - - Type ret = Type.GetType(name); - - // Thanks to Slaynash for this deobfuscation snippet! - if (ret == null) - { - string baseName = cppType.FullName; - string baseAssembly = cppType.Assembly.GetName().name; - - ret = AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(a - => a.GetName().Name == baseAssembly)? - .TryGetTypes() - .FirstOrDefault(t - => t.CustomAttributes.Any(ca - => ca.AttributeType.Name == "ObfuscatedNameAttribute" - && (string)ca.ConstructorArguments[0].Value == baseName)); - - if (ret != null) - { - // deobfuscated type was found, add to cache. - s_deobfuscatedTypeNames.Add(cppType.FullName, ret.FullName); - } - } - - Il2CppToMonoType.Add(name, ret); - - return ret; - } - - // cached class pointers for Il2CppCast - private static readonly Dictionary s_cppClassPointers = new Dictionary(); - - /// - /// Attempt to cast the object to its underlying type. - /// - /// The object you want to cast. - /// The object, as the underlying type if successful or the input value if not. - public static object Il2CppCast(object obj) => Il2CppCast(obj, Instance.GetActualType(obj)); - - /// - /// Attempt to cast the object to the provided type. - /// - /// The object you want to cast. - /// The Type you want to cast to. - /// The object, as the type (or a normal C# object) if successful or the input value if not. - public static object Il2CppCast(object obj, Type castTo) - { - if (!(obj is Il2CppSystem.Object cppObj)) - return obj; - - if (!Il2CppTypeNotNull(castTo, out IntPtr castToPtr)) - return obj; - - IntPtr castFromPtr = il2cpp_object_get_class(cppObj.Pointer); - - if (!il2cpp_class_is_assignable_from(castToPtr, castFromPtr)) - return null; - - if (RuntimeSpecificsStore.IsInjected(castToPtr)) - return UnhollowerBaseLib.Runtime.ClassInjectorBase.GetMonoObjectFromIl2CppPointer(cppObj.Pointer); - - if (castTo == typeof(string)) - return cppObj.ToString(); - - return Activator.CreateInstance(castTo, cppObj.Pointer); - } - - /// - /// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type. - /// - /// The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for. - /// True if successful, false if not. - public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _); - - /// - /// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type. - /// - /// The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for. - /// The IntPtr for the Il2Cpp class, or IntPtr.Zero if not found. - /// True if successful, false if not. - public static bool Il2CppTypeNotNull(Type type, out IntPtr il2cppPtr) - { - if (s_cppClassPointers.TryGetValue(type.AssemblyQualifiedName, out il2cppPtr)) - return il2cppPtr != IntPtr.Zero; - - il2cppPtr = (IntPtr)typeof(Il2CppClassPointerStore<>) - .MakeGenericType(new Type[] { type }) - .GetField("NativeClassPtr", BF.Public | BF.Static) - .GetValue(null); - - s_cppClassPointers.Add(type.AssemblyQualifiedName, il2cppPtr); - - return il2cppPtr != IntPtr.Zero; - } - - // Extern C++ methods - [DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - public static extern bool il2cpp_class_is_assignable_from(IntPtr klass, IntPtr oklass); - - [DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - public static extern IntPtr il2cpp_object_get_class(IntPtr obj); - - internal static IntPtr s_cppEnumerableClassPtr; - internal static IntPtr s_cppDictionaryClassPtr; - - public override bool IsAssignableFrom(Type toAssignTo, Type toAssignFrom) - { - if (toAssignTo.IsAssignableFrom(toAssignFrom)) - return true; - - if (toAssignTo == typeof(IEnumerable)) - { - try - { - if (s_cppEnumerableClassPtr == IntPtr.Zero) - Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IEnumerable), out s_cppEnumerableClassPtr); - - if (s_cppEnumerableClassPtr != IntPtr.Zero - && Il2CppTypeNotNull(toAssignFrom, out IntPtr assignFromPtr) - && il2cpp_class_is_assignable_from(s_cppEnumerableClassPtr, assignFromPtr)) - { - return true; - } - } - catch { } - } - else if (toAssignTo == typeof(IDictionary)) - { - try - { - if (s_cppDictionaryClassPtr == IntPtr.Zero) - if (!Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IDictionary), out s_cppDictionaryClassPtr)) - return false; - - if (Il2CppTypeNotNull(toAssignFrom, out IntPtr classPtr)) - { - if (il2cpp_class_is_assignable_from(s_cppDictionaryClassPtr, classPtr)) - return true; - } - } - catch { } - } - - return false; - } - - public override bool IsReflectionSupported(Type type) - { - try - { - var gArgs = type.GetGenericArguments(); - if (!gArgs.Any()) - return true; - - foreach (var gType in gArgs) - { - if (!Supported(gType)) - return false; - } - - return true; - - bool Supported(Type t) - { - if (!typeof(Il2CppSystem.Object).IsAssignableFrom(t)) - return true; - - if (!Il2CppTypeNotNull(t, out IntPtr ptr)) - return false; - - return CppType.internal_from_handle(IL2CPP.il2cpp_class_get_type(ptr)) is CppType; - } - } - catch - { - return false; - } - } - - // Helper for IL2CPP to try to make sure the Unhollowed game assemblies are actually loaded. - - internal static void TryLoadGameModules() - { - Instance.LoadModule("Assembly-CSharp"); - Instance.LoadModule("Assembly-CSharp-firstpass"); - } - - public override bool LoadModule(string module) - { -#if ML - var path = Path.Combine("MelonLoader", "Managed", $"{module}.dll"); -#else - var path = Path.Combine("BepInEx", "unhollowed", $"{module}.dll"); -#endif - return LoadModuleInternal(path); - } - - internal static bool LoadModuleInternal(string fullPath) - { - if (!File.Exists(fullPath)) - return false; - - try - { - Assembly.Load(File.ReadAllBytes(fullPath)); - return true; - } - catch (Exception e) - { - Console.WriteLine(e.GetType() + ", " + e.Message); - } - - return false; - } - - internal static readonly Dictionary s_getEnumeratorMethods = new Dictionary(); - - internal static readonly Dictionary s_enumeratorInfos = new Dictionary(); - - internal class EnumeratorInfo - { - internal MethodInfo moveNext; - internal PropertyInfo current; - } - - public override IEnumerable EnumerateEnumerable(object value) - { - if (value == null) - return null; - - var cppEnumerable = (value as Il2CppSystem.Object)?.TryCast(); - if (cppEnumerable != null) - { - var type = value.GetType(); - if (!s_getEnumeratorMethods.ContainsKey(type)) - s_getEnumeratorMethods.Add(type, type.GetMethod("GetEnumerator")); - - var enumerator = s_getEnumeratorMethods[type].Invoke(value, null); - var enumeratorType = enumerator.GetType(); - - if (!s_enumeratorInfos.ContainsKey(enumeratorType)) - { - s_enumeratorInfos.Add(enumeratorType, new EnumeratorInfo - { - current = enumeratorType.GetProperty("Current"), - moveNext = enumeratorType.GetMethod("MoveNext"), - }); - } - var info = s_enumeratorInfos[enumeratorType]; - - // iterate - var list = new List(); - while ((bool)info.moveNext.Invoke(enumerator, null)) - list.Add(info.current.GetValue(enumerator)); - - return list; - } - - return null; - } - - public override IDictionary EnumerateDictionary(object value, Type typeOfKeys, Type typeOfValues) - { - var valueType = ReflectionUtility.GetActualType(value); - - var keyList = new List(); - var valueList = new List(); - - var hashtable = value.Cast(typeof(Il2CppSystem.Collections.Hashtable)) as Il2CppSystem.Collections.Hashtable; - - if (hashtable != null) - { - EnumerateCppHashtable(hashtable, keyList, valueList); - } - else - { - var keys = valueType.GetProperty("Keys").GetValue(value, null); - var values = valueType.GetProperty("Values").GetValue(value, null); - - EnumerateCppIDictionary(keys, keyList); - EnumerateCppIDictionary(values, valueList); - } - - var dict = Activator.CreateInstance(typeof(Dictionary<,>) - .MakeGenericType(typeOfKeys, typeOfValues)) - as IDictionary; - - for (int i = 0; i < keyList.Count; i++) - dict.Add(keyList[i], valueList[i]); - - return dict; - } - - private void EnumerateCppIDictionary(object collection, List list) - { - // invoke GetEnumerator - var enumerator = collection.GetType().GetMethod("GetEnumerator").Invoke(collection, null); - // get the type of it - var enumeratorType = enumerator.GetType(); - // reflect MoveNext and Current - var moveNext = enumeratorType.GetMethod("MoveNext"); - var current = enumeratorType.GetProperty("Current"); - // iterate - while ((bool)moveNext.Invoke(enumerator, null)) - { - list.Add(current.GetValue(enumerator, null)); - } - } - - private void EnumerateCppHashtable(Il2CppSystem.Collections.Hashtable hashtable, List keys, List values) - { - for (int i = 0; i < hashtable.buckets.Count; i++) - { - var bucket = hashtable.buckets[i]; - if (bucket == null || bucket.key == null) - continue; - keys.Add(bucket.key); - values.Add(bucket.val); - } - } - - // ~~~~~~~~~~ not used ~~~~~~~~~~~~ - - // cached il2cpp unbox methods - internal static readonly Dictionary s_unboxMethods = new Dictionary(); - - /// - /// Attempt to unbox the object to the underlying struct type. - /// - /// The object which is a struct underneath. - /// The struct if successful, otherwise null. - public static object Unbox(object obj) => Unbox(obj, Instance.GetActualType(obj)); - - /// - /// Attempt to unbox the object to the struct type. - /// - /// The object which is a struct underneath. - /// The type of the struct you want to unbox to. - /// The struct if successful, otherwise null. - public static object Unbox(object obj, Type type) - { - if (!type.IsValueType) - return null; - - if (!(obj is Il2CppSystem.Object)) - return obj; - - var name = type.AssemblyQualifiedName; - - if (!s_unboxMethods.ContainsKey(name)) - { - s_unboxMethods.Add(name, typeof(Il2CppObjectBase) - .GetMethod("Unbox") - .MakeGenericMethod(type)); - } - - return s_unboxMethods[name].Invoke(obj, new object[0]); - } - } -} - -#endif \ No newline at end of file diff --git a/src/Core/Runtime/Mono/DummyBehaviour.cs b/src/Core/Runtime/Mono/DummyBehaviour.cs deleted file mode 100644 index 3a4888f..0000000 --- a/src/Core/Runtime/Mono/DummyBehaviour.cs +++ /dev/null @@ -1,29 +0,0 @@ -#if MONO -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; - -namespace UnityExplorer.Core.Runtime.Mono -{ - public class DummyBehaviour : MonoBehaviour - { - public static DummyBehaviour Instance; - - public static void Setup() - { - var obj = new GameObject("Explorer_DummyBehaviour"); - DontDestroyOnLoad(obj); - obj.hideFlags |= HideFlags.HideAndDontSave; - - obj.AddComponent(); - } - - internal void Awake() - { - Instance = this; - } - } -} -#endif \ No newline at end of file diff --git a/src/Core/Runtime/Mono/MonoProvider.cs b/src/Core/Runtime/Mono/MonoProvider.cs index 3501634..d6ace86 100644 --- a/src/Core/Runtime/Mono/MonoProvider.cs +++ b/src/Core/Runtime/Mono/MonoProvider.cs @@ -10,8 +10,6 @@ using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.CSharp; namespace UnityExplorer.Core.Runtime.Mono { @@ -19,10 +17,9 @@ namespace UnityExplorer.Core.Runtime.Mono { public override void Initialize() { - Reflection = new MonoReflection(); + ExplorerCore.Context = RuntimeContext.Mono; + //Reflection = new MonoReflection(); TextureUtil = new MonoTextureUtil(); - - DummyBehaviour.Setup(); } public override void SetupEvents() @@ -32,12 +29,12 @@ namespace UnityExplorer.Core.Runtime.Mono private void Application_logMessageReceived(string condition, string stackTrace, LogType type) { - ExplorerCore.Log(condition, type, true); + ExplorerCore.LogUnity(condition, type); } public override void StartCoroutine(IEnumerator routine) { - DummyBehaviour.Instance.StartCoroutine(routine); + ExplorerBehaviour.Instance.StartCoroutine(routine); } public override void Update() @@ -66,12 +63,12 @@ namespace UnityExplorer.Core.Runtime.Mono public override UnityEngine.Object[] FindObjectsOfTypeAll(Type type) => Resources.FindObjectsOfTypeAll(type); - private static readonly FieldInfo fi_Scene_handle = typeof(Scene).GetField("m_Handle", ReflectionUtility.AllFlags); + //private static readonly FieldInfo fi_Scene_handle = typeof(Scene).GetField("m_Handle", ReflectionUtility.AllFlags); - public override int GetSceneHandle(Scene scene) - { - return (int)fi_Scene_handle.GetValue(scene); - } + //public override int GetSceneHandle(Scene scene) + //{ + // return (int)fi_Scene_handle.GetValue(scene); + //} public override GameObject[] GetRootGameObjects(Scene scene) { @@ -125,6 +122,16 @@ public static class MonoExtensions _event.AddListener(new UnityAction(listener)); } + public static void RemoveListener(this UnityEvent _event, Action listener) + { + _event.RemoveListener(new UnityAction(listener)); + } + + public static void RemoveListener(this UnityEvent _event, Action listener) + { + _event.RemoveListener(new UnityAction(listener)); + } + public static void Clear(this StringBuilder sb) { sb.Remove(0, sb.Length); diff --git a/src/Core/Runtime/Mono/MonoReflection.cs b/src/Core/Runtime/Mono/MonoReflection.cs deleted file mode 100644 index 9b96854..0000000 --- a/src/Core/Runtime/Mono/MonoReflection.cs +++ /dev/null @@ -1,48 +0,0 @@ -#if MONO -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer.Core.Runtime.Mono -{ - public class MonoReflection : ReflectionProvider - { - // Mono doesn't need to explicitly cast things. - public override object Cast(object obj, Type castTo) - => obj; - - public override T TryCast(object obj) - { - try - { - return (T)obj; - } - catch - { - return default; - } - } - - // Vanilla GetType is fine for mono - public override Type GetActualType(object obj) - => obj.GetType(); - - public override bool IsAssignableFrom(Type toAssignTo, Type toAssignFrom) - => toAssignTo.IsAssignableFrom(toAssignFrom); - - public override bool IsReflectionSupported(Type type) - => true; - - public override bool LoadModule(string module) - => true; - - public override string ProcessTypeNameInString(Type type, string theString, ref string typeName) - => theString; - - // not necessary - public override void BoxStringToType(ref object _string, Type castTo) { } - } -} - -#endif \ No newline at end of file diff --git a/src/Core/Runtime/Mono/MonoTextureUtil.cs b/src/Core/Runtime/Mono/MonoTextureUtil.cs index 0ae2b83..f04da58 100644 --- a/src/Core/Runtime/Mono/MonoTextureUtil.cs +++ b/src/Core/Runtime/Mono/MonoTextureUtil.cs @@ -46,15 +46,15 @@ namespace UnityExplorer.Core.Runtime.Mono if (method.IsStatic) return (byte[])method.Invoke(null, new object[] { tex }); else - return (byte[])method.Invoke(tex, new object[0]); + return (byte[])method.Invoke(tex, ArgumentUtility.EmptyArgs); } private static MethodInfo GetEncodeToPNGMethod() { if (ReflectionUtility.GetTypeByName("UnityEngine.ImageConversion") is Type imageConversion) - return m_encodeToPNGMethod = imageConversion.GetMethod("EncodeToPNG", ReflectionUtility.AllFlags); + return m_encodeToPNGMethod = imageConversion.GetMethod("EncodeToPNG", ReflectionUtility.FLAGS); - var method = typeof(Texture2D).GetMethod("EncodeToPNG", ReflectionUtility.AllFlags); + var method = typeof(Texture2D).GetMethod("EncodeToPNG", ReflectionUtility.FLAGS); if (method != null) return m_encodeToPNGMethod = method; diff --git a/src/Core/Runtime/ReflectionProvider.cs b/src/Core/Runtime/ReflectionProvider.cs deleted file mode 100644 index 6ddd10b..0000000 --- a/src/Core/Runtime/ReflectionProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer.Core.Runtime -{ - public abstract class ReflectionProvider - { - public static ReflectionProvider Instance; - - public ReflectionProvider() - { - Instance = this; - } - - public abstract Type GetActualType(object obj); - - public abstract object Cast(object obj, Type castTo); - - public abstract T TryCast(object obj); - - public abstract bool IsAssignableFrom(Type toAssignTo, Type toAssignFrom); - - public abstract bool IsReflectionSupported(Type type); - - public abstract string ProcessTypeNameInString(Type type, string theString, ref string typeName); - - public abstract bool LoadModule(string module); - - public abstract void BoxStringToType(ref object _string, Type castTo); - - public virtual string UnboxString(object value) => (string)value; - - public virtual IDictionary EnumerateDictionary(object value, Type typeOfKeys, Type typeOfValues) - => null; - - public virtual IEnumerable EnumerateEnumerable(object value) - => null; - } -} diff --git a/src/Core/Runtime/RuntimeContext.cs b/src/Core/Runtime/RuntimeContext.cs new file mode 100644 index 0000000..1dbd7fe --- /dev/null +++ b/src/Core/Runtime/RuntimeContext.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UnityExplorer.Core.Runtime +{ + public enum RuntimeContext + { + Mono, + IL2CPP + } +} diff --git a/src/Core/Runtime/RuntimeProvider.cs b/src/Core/Runtime/RuntimeProvider.cs index 7715676..615e97b 100644 --- a/src/Core/Runtime/RuntimeProvider.cs +++ b/src/Core/Runtime/RuntimeProvider.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Text; using UnityEngine; using UnityEngine.EventSystems; @@ -17,7 +16,6 @@ namespace UnityExplorer { public static RuntimeProvider Instance; - public ReflectionProvider Reflection; public TextureUtilProvider TextureUtil; public RuntimeProvider() @@ -42,7 +40,7 @@ namespace UnityExplorer public abstract void Update(); - public virtual bool IsReferenceEqual(object a, object b) => ReferenceEquals(a, b); + //public virtual bool IsReferenceEqual(object a, object b) => ReferenceEquals(a, b); // Unity API handlers @@ -56,7 +54,7 @@ namespace UnityExplorer public abstract void GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List list); - public abstract int GetSceneHandle(Scene scene); + //public abstract int GetSceneHandle(Scene scene); public abstract GameObject[] GetRootGameObjects(Scene scene); @@ -67,23 +65,12 @@ namespace UnityExplorer public abstract void SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null, Color? disabled = null); - public virtual void FindSingleton(string[] s_instanceNames, Type type, BindingFlags flags, List instances) + internal virtual void ProcessOnPostRender() + { + } + + internal virtual void ProcessFixedUpdate() { - // Look for a typical Instance backing field. - FieldInfo fi; - foreach (var name in s_instanceNames) - { - fi = type.GetField(name, flags); - if (fi != null) - { - var instance = fi.GetValue(null); - if (instance != null) - { - instances.Add(instance); - return; - } - } - } } } } diff --git a/src/Core/SceneHandler.cs b/src/Core/SceneHandler.cs new file mode 100644 index 0000000..b0d633e --- /dev/null +++ b/src/Core/SceneHandler.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace UnityExplorer.Core +{ + public static class SceneHandler + { + /// + /// The currently inspected Scene. + /// + public static Scene? SelectedScene + { + get => m_selectedScene; + internal set + { + if (m_selectedScene != null && m_selectedScene?.handle == value?.handle) + return; + m_selectedScene = value; + OnInspectedSceneChanged?.Invoke((Scene)m_selectedScene); + } + } + private static Scene? m_selectedScene; + + /// + /// The GameObjects in the currently inspected scene. + /// + public static ReadOnlyCollection CurrentRootObjects => new ReadOnlyCollection(rootObjects); + private static GameObject[] rootObjects = new GameObject[0]; + + /// + /// All currently loaded Scenes. + /// + public static ReadOnlyCollection LoadedScenes => new ReadOnlyCollection(allLoadedScenes); + private static readonly List allLoadedScenes = new List(); + + /// + /// The names of all scenes in the build settings, if they could be retrieved. + /// + public static ReadOnlyCollection AllSceneNames => new ReadOnlyCollection(allScenesInBuild); + private static readonly List allScenesInBuild = new List(); + + /// + /// Whether or not we successfuly retrieved the names of the scenes in the build settings. + /// + public static bool WasAbleToGetScenesInBuild => gotAllScenesInBuild; + private static bool gotAllScenesInBuild = true; + + /// + /// Invoked when the currently inspected Scene changes. The argument is the new scene. + /// + public static event Action OnInspectedSceneChanged; + + /// + /// Invoked whenever the list of currently loaded Scenes changes. The argument contains all loaded scenes after the change. + /// + public static event Action> OnLoadedScenesChanged; + + /// + /// Equivalent to + 2, to include 'DontDestroyOnLoad'. + /// + public static int LoadedSceneCount => SceneManager.sceneCount + 2; + + internal static Scene DontDestroyScene => DontDestroyMe.scene; + internal static int DontDestroyHandle => DontDestroyScene.handle; + + internal static GameObject DontDestroyMe + { + get + { + if (!dontDestroyObject) + { + dontDestroyObject = new GameObject("DontDestroyMe"); + GameObject.DontDestroyOnLoad(dontDestroyObject); + } + return dontDestroyObject; + } + } + private static GameObject dontDestroyObject; + + public static bool InspectingAssetScene => SelectedScene == AssetScene; + + internal static Scene AssetScene => AssetObject.scene; + internal static int AssetHandle => AssetScene.handle; + + internal static GameObject AssetObject + { + get + { + if (!assetObject) + { + assetObject = RuntimeProvider.Instance.FindObjectsOfTypeAll(typeof(GameObject)) + .First(it => !it.TryCast().scene.IsValid()) + .TryCast(); + } + return assetObject; + } + } + private static GameObject assetObject; + + internal static void Init() + { + // Try to get all scenes in the build settings. This may not work. + try + { + Type sceneUtil = ReflectionUtility.GetTypeByName("UnityEngine.SceneManagement.SceneUtility"); + if (sceneUtil == null) + throw new Exception("This version of Unity does not ship with the 'SceneUtility' class, or it was not unstripped."); + + var method = sceneUtil.GetMethod("GetScenePathByBuildIndex", ReflectionUtility.FLAGS); + int sceneCount = SceneManager.sceneCountInBuildSettings; + for (int i = 0; i < sceneCount; i++) + { + var scenePath = (string)method.Invoke(null, new object[] { i }); + allScenesInBuild.Add(scenePath); + } + } + catch (Exception ex) + { + gotAllScenesInBuild = false; + ExplorerCore.Log($"Unable to generate list of all Scenes in the build: {ex}"); + } + } + + internal static void Update() + { + int curHandle = SelectedScene?.handle ?? -1; + // DontDestroyOnLoad always exists, so default to true if our curHandle is that handle. + // otherwise we will check while iterating. + bool inspectedExists = curHandle == DontDestroyHandle || curHandle == AssetHandle; + + // Quick sanity check if the loaded scenes changed + bool anyChange = LoadedSceneCount != allLoadedScenes.Count; + // otherwise keep a lookup table of the previous handles to check if the list changed at all. + HashSet previousHandles = null; + if (!anyChange) + previousHandles = new HashSet(allLoadedScenes.Select(it => it.handle)); + + allLoadedScenes.Clear(); + + for (int i = 0; i < SceneManager.sceneCount; i++) + { + Scene scene = SceneManager.GetSceneAt(i); + if (scene == default || scene.handle == -1 || !scene.isLoaded) + continue; + + // If no changes yet, ensure the previous list contained this handle. + if (!anyChange && !previousHandles.Contains(scene.handle)) + anyChange = true; + + // If we have not yet confirmed inspectedExists, check if this scene is our currently inspected one. + if (curHandle != -1 && !inspectedExists && scene.handle == curHandle) + inspectedExists = true; + + allLoadedScenes.Add(scene); + } + + // Always add the DontDestroyOnLoad scene and the "none" scene. + allLoadedScenes.Add(DontDestroyScene); + allLoadedScenes.Add(AssetScene); + + // Default to first scene if none selected or previous selection no longer exists. + if (!inspectedExists) + { + SelectedScene = allLoadedScenes.First(); + } + + // Notify on the list changing at all + if (anyChange) + { + OnLoadedScenesChanged?.Invoke(LoadedScenes); + } + + // Finally, update the root objects list. + if (SelectedScene != null && ((Scene)SelectedScene).IsValid()) + rootObjects = RuntimeProvider.Instance.GetRootGameObjects((Scene)SelectedScene); + else + { + var allObjects = RuntimeProvider.Instance.FindObjectsOfTypeAll(typeof(GameObject)); + var list = new List(); + foreach (var obj in allObjects) + { + var go = obj.TryCast(); + if (go.transform.parent == null && !go.scene.IsValid()) + list.Add(go); + } + rootObjects = list.ToArray(); + } + } + } +} diff --git a/src/Core/Search/ChildFilter.cs b/src/Core/Search/ChildFilter.cs deleted file mode 100644 index 59b63a3..0000000 --- a/src/Core/Search/ChildFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer.Core.Search -{ - internal enum ChildFilter - { - Any, - RootObject, - HasParent - } -} diff --git a/src/Core/Search/SceneFilter.cs b/src/Core/Search/SceneFilter.cs deleted file mode 100644 index 3373b4e..0000000 --- a/src/Core/Search/SceneFilter.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer.Core.Search -{ - internal enum SceneFilter - { - Any, - Asset, - DontDestroyOnLoad, - Explicit, - } -} diff --git a/src/Core/Search/SearchContext.cs b/src/Core/Search/SearchContext.cs deleted file mode 100644 index 982be2f..0000000 --- a/src/Core/Search/SearchContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer.Core.Search -{ - internal enum SearchContext - { - UnityObject, - GameObject, - Component, - Custom, - Singleton, - StaticClass - } -} diff --git a/src/Core/Search/SearchProvider.cs b/src/Core/Search/SearchProvider.cs deleted file mode 100644 index a21f20a..0000000 --- a/src/Core/Search/SearchProvider.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using UnityEngine; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.Main; -using UnityExplorer.UI.Main.Search; - -namespace UnityExplorer.Core.Search -{ - public static class SearchProvider - { - internal static object[] StaticClassSearch(string input) - { - var list = new List(); - - var nameFilter = ""; - if (!string.IsNullOrEmpty(input)) - nameFilter = input.ToLower(); - - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - foreach (var type in asm.TryGetTypes().Where(it => it.IsSealed && it.IsAbstract)) - { - if (!string.IsNullOrEmpty(nameFilter) && !type.FullName.ToLower().Contains(nameFilter)) - continue; - - list.Add(type); - } - } - - return list.ToArray(); - } - - internal static string[] s_instanceNames = new string[] - { - "m_instance", - "m_Instance", - "s_instance", - "s_Instance", - "_instance", - "_Instance", - "instance", - "Instance", - "k__BackingField", - "k__BackingField", - }; - - internal static object[] SingletonSearch(string input) - { - var instances = new List(); - - var nameFilter = ""; - if (!string.IsNullOrEmpty(input)) - nameFilter = input.ToLower(); - - var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; - - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - // Search all non-static, non-enum classes. - foreach (var type in asm.TryGetTypes().Where(it => !(it.IsSealed && it.IsAbstract) && !it.IsEnum)) - { - try - { - if (!string.IsNullOrEmpty(nameFilter) && !type.FullName.ToLower().Contains(nameFilter)) - continue; - - RuntimeProvider.Instance.FindSingleton(s_instanceNames, type, flags, instances); - } - catch { } - } - } - - return instances.ToArray(); - } - - internal static object[] UnityObjectSearch(string input, string customTypeInput, SearchContext context, - ChildFilter childFilter, SceneFilter sceneFilter) - { - Type searchType = null; - switch (context) - { - case SearchContext.GameObject: - searchType = typeof(GameObject); break; - - case SearchContext.Component: - searchType = typeof(Component); break; - - case SearchContext.Custom: - if (string.IsNullOrEmpty(customTypeInput)) - { - ExplorerCore.LogWarning("Custom Type input must not be empty!"); - return null; - } - if (ReflectionUtility.GetTypeByName(customTypeInput) is Type customType) - if (typeof(UnityEngine.Object).IsAssignableFrom(customType)) - searchType = customType; - else - ExplorerCore.LogWarning($"Custom type '{customType.FullName}' is not assignable from UnityEngine.Object!"); - else - ExplorerCore.LogWarning($"Could not find a type by the name '{customTypeInput}'!"); - break; - - default: - searchType = typeof(UnityEngine.Object); break; - } - - if (searchType == null) - return null; - - var allObjects = RuntimeProvider.Instance.FindObjectsOfTypeAll(searchType); - var results = new List(); - - // perform filter comparers - - string nameFilter = null; - if (!string.IsNullOrEmpty(input)) - nameFilter = input.ToLower(); - - bool canGetGameObject = (sceneFilter != SceneFilter.Any || childFilter != ChildFilter.Any) - && (context == SearchContext.GameObject || typeof(Component).IsAssignableFrom(searchType)); - - string sceneFilterString = null; - if (!canGetGameObject) - { - if (context != SearchContext.UnityObject && (sceneFilter != SceneFilter.Any || childFilter != ChildFilter.Any)) - ExplorerCore.LogWarning($"Type '{searchType}' cannot have Scene or Child filters applied to it"); - } - else - { - if (sceneFilter == SceneFilter.DontDestroyOnLoad) - sceneFilterString = "DontDestroyOnLoad"; - else if (sceneFilter == SceneFilter.Explicit) - sceneFilterString = SearchPage.Instance.m_sceneDropdown.options[SearchPage.Instance.m_sceneDropdown.value].text; - } - - foreach (var obj in allObjects) - { - // name check - if (!string.IsNullOrEmpty(nameFilter) && !obj.name.ToLower().Contains(nameFilter)) - continue; - - if (canGetGameObject) - { - var go = context == SearchContext.GameObject - ? obj.TryCast() - : obj.TryCast().gameObject; - - // scene check - if (sceneFilter != SceneFilter.Any) - { - if (!go) - continue; - - switch (context) - { - case SearchContext.GameObject: - if (go.scene.name != sceneFilterString) - continue; - break; - case SearchContext.Custom: - case SearchContext.Component: - if (go.scene.name != sceneFilterString) - continue; - break; - } - } - - if (childFilter != ChildFilter.Any) - { - if (!go) - continue; - - // root object check (no parent) - if (childFilter == ChildFilter.HasParent && !go.transform.parent) - continue; - else if (childFilter == ChildFilter.RootObject && go.transform.parent) - continue; - } - } - - results.Add(obj); - } - - return results.ToArray(); - } - } -} diff --git a/src/Core/TestClass.cs b/src/Core/TestClass.cs deleted file mode 100644 index b9747c9..0000000 --- a/src/Core/TestClass.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer -{ - public static class TestClass - { - public static UI.Main.PanelDragger.ResizeTypes flags = UI.Main.PanelDragger.ResizeTypes.NONE; - -#if CPP - public static string testStringOne = "Test"; - public static Il2CppSystem.Object testStringTwo = "string boxed as cpp object"; - public static Il2CppSystem.String testStringThree = "string boxed as cpp string"; - public static string nullString = null; - - public static Il2CppSystem.Collections.Hashtable testHashset; - public static Il2CppSystem.Collections.Generic.List testList; - - static TestClass() - { - testHashset = new Il2CppSystem.Collections.Hashtable(); - testHashset.Add("key1", "itemOne"); - testHashset.Add("key2", "itemTwo"); - testHashset.Add("key3", "itemThree"); - - testList = new Il2CppSystem.Collections.Generic.List(3); - testList.Add("One"); - testList.Add("Two"); - testList.Add("Three"); - //testIList = list.TryCast(); - } -#endif - } -} diff --git a/src/Core/Tests/TestClass.cs b/src/Core/Tests/TestClass.cs new file mode 100644 index 0000000..c65948e --- /dev/null +++ b/src/Core/Tests/TestClass.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityExplorer.UI.IValues; +using System.Reflection; +using UnityExplorer.UI; +#if CPP +using UnhollowerRuntimeLib; +using UnhollowerBaseLib; +#endif + +namespace UnityExplorer.Tests +{ + public static class TestClass + { + public static void ATestMethod(string s, float f, Vector3 vector, DateTime date, Quaternion quater, bool b, CameraClearFlags enumvalue) + { + ExplorerCore.Log($"{s}, {f}, {vector.ToString()}, {date}, {quater.eulerAngles.ToString()}, {b}, {enumvalue}"); + } + + public static List AWritableList = new List { 1, 2, 3, 4, 5 }; + public static Dictionary AWritableDict = new Dictionary { { "one", 1 }, { "two", 2 } }; + + public static IEnumerable ANestedList = new List>> + { + new List> + { + new List + { + "one", + "two", + "one", + "two", + "one", + "two", + "one", + "two", + "one", + "two", + "one", + "two", + "one", + "two", + "one", + "two", + }, + new List + { + "three", + "four", + } + }, + new List> + { + new List + { + "five" + } + } + }; + + public static IDictionary ARandomDictionary = new Dictionary + { + { 1, 2 }, + { "one", "two" }, + { true, false }, + { new Vector3(0,1,2), new Vector3(1,2,3) }, + { CameraClearFlags.Depth, CameraClearFlags.Color }, + { "################################################\r\n##########", null }, + { "subdict", new Dictionary { { "key", "value" } } } + }; + + public static Hashtable TestHashtable = new Hashtable + { + { "one", "value" }, + { "two", "value" }, + { "three", "value" }, + }; + + public const int ConstantInt = 5; + + public static Color AColor = Color.magenta; + public static Color32 AColor32 = Color.red; + + public static byte[] ByteArray = new byte[16]; + public static string LongString = new string('#', 10000); + public static List BigList = new List(10000); + + public static List RandomList + { + get + { + var list = new List(); + int count = UnityEngine.Random.Range(0, 100); + for (int i = 0; i < count; i++) + list.Add(GetRandomObject()); + return list; + } + } + + private static void TestGeneric() + { + ExplorerCore.Log("Test1 " + typeof(T).FullName); + } + + private static void TestGenericClass() where T : class + { + ExplorerCore.Log("Test2 " + typeof(T).FullName); + } + + private static void TestComponent() where T : Component + { + ExplorerCore.Log("Test3 " + typeof(T).FullName); + } + + private static void TestStruct() where T : struct + { + ExplorerCore.Log("Test3 " + typeof(T).FullName); + } + + private static object GetRandomObject() + { + object ret = null; + + int ran = UnityEngine.Random.Range(0, 7); + switch (ran) + { + case 0: return null; + case 1: return 123; + case 2: return true; + case 3: return "hello"; + case 4: return 50.5f; + case 5: return UnityEngine.CameraClearFlags.Color; + case 6: return new List { "sub list", "lol" }; + } + + return ret; + } + +#if CPP + + public static Il2CppSystem.Collections.IList IL2CPP_IList; + public static Il2CppSystem.Collections.Generic.List IL2CPP_ListString; + public static Il2CppSystem.Collections.Generic.HashSet IL2CPP_HashSet; + + public static Il2CppSystem.Collections.Generic.Dictionary IL2CPP_Dict; + public static Il2CppSystem.Collections.Hashtable IL2CPP_HashTable; + public static Il2CppSystem.Collections.IDictionary IL2CPP_IDict; + + public static string IL2CPP_systemString = "Test"; + public static Il2CppSystem.Object IL2CPP_objectString = "string boxed as cpp object"; + public static Il2CppSystem.String IL2CPP_il2cppString = "string boxed as cpp string"; + public static string nullString = null; + + public static List IL2CPP_listOfBoxedObjects; + public static Il2CppStructArray IL2CPP_structArray; + public static Il2CppStringArray IL2CPP_stringArray; + public static Il2CppReferenceArray IL2CPP_ReferenceArray; + + public static Il2CppSystem.Object cppBoxedInt; + public static Il2CppSystem.Int32 cppInt; + public static Il2CppSystem.Decimal cppDecimal; + public static Il2CppSystem.Object cppDecimalBoxed; + public static Il2CppSystem.Object cppVector3Boxed; + + public static Il2CppSystem.Object RandomBoxedColor + { + get + { + int ran = UnityEngine.Random.Range(0, 3); + switch (ran) + { + case 1: return new Color32().BoxIl2CppObject(); + case 2: return Color.magenta.BoxIl2CppObject(); + default: + return null; + } + } + } + + public static Il2CppSystem.Collections.Hashtable cppHashset; + + public static Dictionary CppBoxedDict; + +#endif + + static TestClass() + { + for (int i = 0; i < BigList.Capacity; i++) + BigList.Add(i.ToString()); + +#if CPP + IL2CPP_Dict = new Il2CppSystem.Collections.Generic.Dictionary(); + IL2CPP_Dict.Add("key1", "value1"); + IL2CPP_Dict.Add("key2", "value2"); + IL2CPP_Dict.Add("key3", "value3"); + + IL2CPP_HashTable = new Il2CppSystem.Collections.Hashtable(); + IL2CPP_HashTable.Add("key1", "value1"); + IL2CPP_HashTable.Add("key2", "value2"); + IL2CPP_HashTable.Add("key3", "value3"); + + var dict2 = new Il2CppSystem.Collections.Generic.Dictionary(); + dict2.Add("key1", "value1"); + IL2CPP_IDict = dict2.TryCast(); + + var list = new Il2CppSystem.Collections.Generic.List(5); + list.Add("one"); + list.Add("two"); + IL2CPP_IList = list.TryCast(); + + IL2CPP_ListString = new Il2CppSystem.Collections.Generic.List(); + IL2CPP_ListString.Add("hello,"); + IL2CPP_ListString.Add("world!"); + + IL2CPP_HashSet = new Il2CppSystem.Collections.Generic.HashSet(); + IL2CPP_HashSet.Add("one"); + IL2CPP_HashSet.Add("two"); + + CppBoxedDict = new Dictionary(); + CppBoxedDict.Add("1", new Il2CppSystem.Int32 { m_value = 1 }.BoxIl2CppObject()); + CppBoxedDict.Add("2", new Il2CppSystem.Int32 { m_value = 2 }.BoxIl2CppObject()); + CppBoxedDict.Add("3", new Il2CppSystem.Int32 { m_value = 3 }.BoxIl2CppObject()); + CppBoxedDict.Add("4", new Il2CppSystem.Int32 { m_value = 4 }.BoxIl2CppObject()); + + cppDecimal = new Il2CppSystem.Decimal(1f); + cppDecimalBoxed = new Il2CppSystem.Decimal(1f).BoxIl2CppObject(); + cppVector3Boxed = Vector3.down.BoxIl2CppObject(); + + + IL2CPP_listOfBoxedObjects = new List(); + IL2CPP_listOfBoxedObjects.Add((Il2CppSystem.String)"boxedString"); + IL2CPP_listOfBoxedObjects.Add(new Il2CppSystem.Int32 { m_value = 5 }.BoxIl2CppObject()); + IL2CPP_listOfBoxedObjects.Add(Color.red.BoxIl2CppObject()); + + try + { + var cppType = Il2CppType.Of(); + if (cppType != null) + { + var boxedEnum = Il2CppSystem.Enum.Parse(cppType, "Color"); + IL2CPP_listOfBoxedObjects.Add(boxedEnum); + } + + var structBox = Vector3.one.BoxIl2CppObject(); + IL2CPP_listOfBoxedObjects.Add(structBox); + + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Test fail: {ex}"); + } + + IL2CPP_structArray = new UnhollowerBaseLib.Il2CppStructArray(5); + IL2CPP_structArray[0] = 0; + IL2CPP_structArray[1] = 1; + IL2CPP_structArray[2] = 2; + IL2CPP_structArray[3] = 3; + IL2CPP_structArray[4] = 4; + + IL2CPP_stringArray = new UnhollowerBaseLib.Il2CppStringArray(2); + IL2CPP_stringArray[0] = "hello, "; + IL2CPP_stringArray[1] = "world!"; + + IL2CPP_ReferenceArray = new UnhollowerBaseLib.Il2CppReferenceArray(3); + IL2CPP_ReferenceArray[0] = new Il2CppSystem.Int32 { m_value = 5 }.BoxIl2CppObject(); + IL2CPP_ReferenceArray[1] = null; + IL2CPP_ReferenceArray[2] = (Il2CppSystem.String)"whats up"; + + cppBoxedInt = new Il2CppSystem.Int32() { m_value = 5 }.BoxIl2CppObject(); + cppInt = new Il2CppSystem.Int32 { m_value = 420 }; + + cppHashset = new Il2CppSystem.Collections.Hashtable(); + cppHashset.Add("key1", "itemOne"); + cppHashset.Add("key2", "itemTwo"); + cppHashset.Add("key3", "itemThree"); + +#endif + } + } +} diff --git a/src/Core/Utility/ArgumentUtility.cs b/src/Core/Utility/ArgumentUtility.cs new file mode 100644 index 0000000..c7b1634 --- /dev/null +++ b/src/Core/Utility/ArgumentUtility.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UnityExplorer +{ + public static class ArgumentUtility + { + public static readonly Type[] EmptyTypes = new Type[0]; + public static readonly object[] EmptyArgs = new object[0]; + + public static readonly Type[] ParseArgs = new Type[] { typeof(string) }; + } +} diff --git a/src/Core/Utility/IOUtility.cs b/src/Core/Utility/IOUtility.cs new file mode 100644 index 0000000..2904e5a --- /dev/null +++ b/src/Core/Utility/IOUtility.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace UnityExplorer +{ + public static class IOUtility + { + private static readonly char[] invalidDirectoryCharacters = Path.GetInvalidPathChars(); + private static readonly char[] invalidFilenameCharacters = Path.GetInvalidFileNameChars(); + + public static string EnsureValidDirectory(string path) + { + path = string.Concat(path.Split(invalidDirectoryCharacters)); + + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + + return path; + } + + public static string EnsureValidFilename(string filename) + { + return string.Concat(filename.Split(invalidFilenameCharacters)); + } + } +} diff --git a/src/Core/Utility/MiscUtility.cs b/src/Core/Utility/MiscUtility.cs new file mode 100644 index 0000000..94d20d3 --- /dev/null +++ b/src/Core/Utility/MiscUtility.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace UnityExplorer +{ + public static class MiscUtility + { + /// + /// Check if a string contains another string, case-insensitive. + /// + public static bool ContainsIgnoreCase(this string _this, string s) + { + return ParseUtility.en_US.CompareInfo.IndexOf(_this, s, CompareOptions.IgnoreCase) >= 0; + } + + /// + /// Just to allow Enum to do .HasFlag() in NET 3.5 + /// + public static bool HasFlag(this Enum flags, Enum value) + { + ulong flag = Convert.ToUInt64(value); + return (Convert.ToUInt64(flags) & flag) == flag; + } + } +} diff --git a/src/Core/Utility/ParseUtility.cs b/src/Core/Utility/ParseUtility.cs new file mode 100644 index 0000000..da7a18a --- /dev/null +++ b/src/Core/Utility/ParseUtility.cs @@ -0,0 +1,420 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; + +namespace UnityExplorer +{ + public static class ParseUtility + { + public static CultureInfo en_US = new CultureInfo("en-US"); + + private static readonly HashSet nonPrimitiveTypes = new HashSet + { + typeof(string), + typeof(decimal), + typeof(DateTime), + }; + + public const string NUMBER_FORMAT = "0.####"; + + private static readonly Dictionary numSequenceStrings = new Dictionary(); + + // Helper for formatting float/double/decimal numbers to maximum of 4 decimal points. + public static string FormatDecimalSequence(params object[] numbers) + { + if (numbers.Length <= 0) + return null; + + int count = numbers.Length; + var formatString = GetSequenceFormatString(count); + + return string.Format(en_US, formatString, numbers); + } + + public static string GetSequenceFormatString(int count) + { + if (count <= 0) + return null; + + if (numSequenceStrings.ContainsKey(count)) + return numSequenceStrings[count]; + + string[] strings = new string[count]; + + for (int i = 0; i < count; i++) + strings[i] = $"{{{i}:{NUMBER_FORMAT}}}"; + + string s = string.Join(", ", strings); + + numSequenceStrings.Add(count, s); + return s; + } + + public static bool CanParse(Type type) + { + if (string.IsNullOrEmpty(type.FullName)) + return false; + return type.IsPrimitive || type.IsEnum || nonPrimitiveTypes.Contains(type) || customTypes.ContainsKey(type.FullName); + } + + public static bool TryParse(string input, Type type, out object obj, out Exception parseException) + { + obj = null; + parseException = null; + + if (type == null) + return false; + + if (type == typeof(string)) + { + obj = input; + return true; + } + + if (type.IsEnum) + { + try + { + obj = Enum.Parse(type, input); + return true; + } + catch (Exception ex) + { + parseException = ex.GetInnerMostException(); + return false; + } + } + + try + { + if (customTypes.ContainsKey(type.FullName)) + { + obj = customTypes[type.FullName].Invoke(input); + } + else + { + obj = ReflectionUtility.GetMethodInfo(type, "Parse", ArgumentUtility.ParseArgs) + .Invoke(null, new object[] { input }); + } + + return true; + } + catch (Exception ex) + { + ex = ex.GetInnerMostException(); + parseException = ex; + } + + return false; + } + + private static readonly HashSet formattedTypes = new HashSet + { + typeof(float), + typeof(double), + typeof(decimal) + }; + + public static string ToStringForInput(object obj, Type type) + { + if (type == null || obj == null) + return null; + + if (type == typeof(string)) + return obj as string; + + if (type.IsEnum) + { + return Enum.IsDefined(type, obj) + ? Enum.GetName(type, obj) + : obj.ToString(); + } + + try + { + if (customTypes.ContainsKey(type.FullName)) + { + return customTypesToString[type.FullName].Invoke(obj); + } + else if (formattedTypes.Contains(type)) + { + return ReflectionUtility.GetMethodInfo(type, "ToString", new Type[] { typeof(string), typeof(IFormatProvider) }) + .Invoke(obj, new object[] { NUMBER_FORMAT, en_US }) + as string; + } + else + return obj.ToString(); + + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception formatting object for input: {ex}"); + return null; + } + } + + private static readonly Dictionary typeInputExamples = new Dictionary(); + + public static string GetExampleInput(Type type) + { + if (!typeInputExamples.ContainsKey(type.AssemblyQualifiedName)) + { + try + { + if (type.IsEnum) + { + typeInputExamples.Add(type.AssemblyQualifiedName, Enum.GetNames(type).First()); + } + else + { + var instance = Activator.CreateInstance(type); + typeInputExamples.Add(type.AssemblyQualifiedName, ToStringForInput(instance, type)); + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception generating default instance for example input for '" + type.FullName + "'"); + ExplorerCore.Log(ex); + return ""; + } + } + + return typeInputExamples[type.AssemblyQualifiedName]; + } + + #region Custom parse methods + + internal delegate object ParseMethod(string input); + + private static readonly Dictionary customTypes = new Dictionary + { + { typeof(Vector2).FullName, TryParseVector2 }, + { typeof(Vector3).FullName, TryParseVector3 }, + { typeof(Vector4).FullName, TryParseVector4 }, + { typeof(Quaternion).FullName, TryParseQuaternion }, + { typeof(Rect).FullName, TryParseRect }, + { typeof(Color).FullName, TryParseColor }, + { typeof(Color32).FullName, TryParseColor32 }, + { typeof(LayerMask).FullName, TryParseLayerMask }, + }; + + internal delegate string ToStringMethod(object obj); + + private static readonly Dictionary customTypesToString = new Dictionary + { + { typeof(Vector2).FullName, Vector2ToString }, + { typeof(Vector3).FullName, Vector3ToString }, + { typeof(Vector4).FullName, Vector4ToString }, + { typeof(Quaternion).FullName, QuaternionToString }, + { typeof(Rect).FullName, RectToString }, + { typeof(Color).FullName, ColorToString }, + { typeof(Color32).FullName, Color32ToString }, + { typeof(LayerMask).FullName, LayerMaskToString }, + }; + + // Vector2 + + public static object TryParseVector2(string input) + { + Vector2 vector = default; + + var split = input.Split(','); + + vector.x = float.Parse(split[0].Trim(), en_US); + vector.y = float.Parse(split[1].Trim(), en_US); + + return vector; + } + + public static string Vector2ToString(object obj) + { + if (!(obj is Vector2 vector)) + return null; + + return FormatDecimalSequence(vector.x, vector.y); + } + + // Vector3 + + public static object TryParseVector3(string input) + { + Vector3 vector = default; + + var split = input.Split(','); + + vector.x = float.Parse(split[0].Trim(), en_US); + vector.y = float.Parse(split[1].Trim(), en_US); + vector.z = float.Parse(split[2].Trim(), en_US); + + return vector; + } + + public static string Vector3ToString(object obj) + { + if (!(obj is Vector3 vector)) + return null; + + return FormatDecimalSequence(vector.x, vector.y, vector.z); + } + + // Vector4 + + public static object TryParseVector4(string input) + { + Vector4 vector = default; + + var split = input.Split(','); + + vector.x = float.Parse(split[0].Trim(), en_US); + vector.y = float.Parse(split[1].Trim(), en_US); + vector.z = float.Parse(split[2].Trim(), en_US); + vector.w = float.Parse(split[3].Trim(), en_US); + + return vector; + } + + public static string Vector4ToString(object obj) + { + if (!(obj is Vector4 vector)) + return null; + + return FormatDecimalSequence(vector.x, vector.y, vector.z, vector.w); + } + + // Quaternion + + public static object TryParseQuaternion(string input) + { + Vector3 vector = default; + + var split = input.Split(','); + + if (split.Length == 4) + { + Quaternion quat = default; + quat.x = float.Parse(split[0].Trim(), en_US); + quat.y = float.Parse(split[1].Trim(), en_US); + quat.z = float.Parse(split[2].Trim(), en_US); + quat.w = float.Parse(split[3].Trim(), en_US); + return quat; + } + else + { + vector.x = float.Parse(split[0].Trim(), en_US); + vector.y = float.Parse(split[1].Trim(), en_US); + vector.z = float.Parse(split[2].Trim(), en_US); + return Quaternion.Euler(vector); + } + } + + public static string QuaternionToString(object obj) + { + if (!(obj is Quaternion quaternion)) + return null; + + Vector3 vector = quaternion.eulerAngles; + + return FormatDecimalSequence(vector.x, vector.y, vector.z); + } + + // Rect + + public static object TryParseRect(string input) + { + Rect rect = default; + + var split = input.Split(','); + + rect.x = float.Parse(split[0].Trim(), en_US); + rect.y = float.Parse(split[1].Trim(), en_US); + rect.width = float.Parse(split[2].Trim(), en_US); + rect.height = float.Parse(split[3].Trim(), en_US); + + return rect; + } + + public static string RectToString(object obj) + { + if (!(obj is Rect rect)) + return null; + + return FormatDecimalSequence(rect.x, rect.y, rect.width, rect.height); + } + + // Color + + public static object TryParseColor(string input) + { + Color color = default; + + var split = input.Split(','); + + color.r = float.Parse(split[0].Trim(), en_US); + color.g = float.Parse(split[1].Trim(), en_US); + color.b = float.Parse(split[2].Trim(), en_US); + if (split.Length > 3) + color.a = float.Parse(split[3].Trim(), en_US); + else + color.a = 1; + + return color; + } + + public static string ColorToString(object obj) + { + if (!(obj is Color color)) + return null; + + return FormatDecimalSequence(color.r, color.g, color.b, color.a); + } + + // Color32 + + public static object TryParseColor32(string input) + { + Color32 color = default; + + var split = input.Split(','); + + color.r = byte.Parse(split[0].Trim(), en_US); + color.g = byte.Parse(split[1].Trim(), en_US); + color.b = byte.Parse(split[2].Trim(), en_US); + if (split.Length > 3) + color.a = byte.Parse(split[3].Trim(), en_US); + else + color.a = 255; + + return color; + } + + public static string Color32ToString(object obj) + { + if (!(obj is Color32 color)) + return null; + + // ints, this is fine + return $"{color.r}, {color.g}, {color.b}, {color.a}"; + } + + // Layermask (Int32) + + public static object TryParseLayerMask(string input) + { + return (LayerMask)int.Parse(input); + } + + public static string LayerMaskToString(object obj) + { + if (!(obj is LayerMask mask)) + return null; + + return mask.value.ToString(); + } + + #endregion + } +} diff --git a/src/Core/Utility/SignatureHighlighter.cs b/src/Core/Utility/SignatureHighlighter.cs new file mode 100644 index 0000000..8ae4c75 --- /dev/null +++ b/src/Core/Utility/SignatureHighlighter.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; +using UnityExplorer.Core.Runtime; + +namespace UnityExplorer +{ + /// + /// Syntax-highlights a member's signature, by either the Type name or a Type and Member together. + /// + public static class SignatureHighlighter + { + public const string NAMESPACE = "#a8a8a8"; + + public const string CONST = "#92c470"; + + public const string CLASS_STATIC = "#3a8d71"; + public const string CLASS_INSTANCE = "#2df7b2"; + + public const string STRUCT = "#0fba3a"; + public const string INTERFACE = "#9b9b82"; + + public const string FIELD_STATIC = "#8d8dc6"; + public const string FIELD_INSTANCE = "#c266ff"; + + public const string METHOD_STATIC = "#b55b02"; + public const string METHOD_INSTANCE = "#ff8000"; + + public const string PROP_STATIC = "#588075"; + public const string PROP_INSTANCE = "#55a38e"; + + public const string LOCAL_ARG = "#a6e9e9"; + + internal const string ARRAY_TOKEN = "[]"; + internal const string OPEN_COLOR = "').Append(ns).Append(CLOSE_COLOR).Append('.'); + + // Declaring type + + var declaring = type.DeclaringType; + while (declaring != null) + { + syntaxBuilder.Append(HighlightType(declaring)); + syntaxBuilder.Append('.'); + declaring = declaring.DeclaringType; + } + } + + // Highlight the type name + + syntaxBuilder.Append(HighlightType(type)); + + // If memberInfo, highlight the member info + + if (memberInfo != null) + { + syntaxBuilder.Append('.'); + + int start = syntaxBuilder.Length - 1; + syntaxBuilder.Append(OPEN_COLOR) + .Append(GetMemberInfoColor(memberInfo, out bool isStatic)) + .Append('>') + .Append(memberInfo.Name) + .Append(CLOSE_COLOR); + + if (isStatic) + { + syntaxBuilder.Insert(start, OPEN_ITALIC); + syntaxBuilder.Append(CLOSE_ITALIC); + } + + if (memberInfo is MethodInfo method) + { + var args = method.GetGenericArguments(); + if (args.Length > 0) + syntaxBuilder.Append('<').Append(ParseGenericArgs(args, true)).Append('>'); + } + } + + return syntaxBuilder.ToString(); + } + + private static readonly Dictionary typeToRichType = new Dictionary(); + + private static bool EndsWith(this StringBuilder sb, string _string) + { + int len = _string.Length; + + if (sb.Length < len) + return false; + + int stringpos = 0; + for (int i = sb.Length - len; i < sb.Length; i++, stringpos++) + { + if (sb[i] != _string[stringpos]) + return false; + } + return true; + } + + private static string HighlightType(Type type) + { + string key = type.ToString(); + + if (typeToRichType.ContainsKey(key)) + return typeToRichType[key]; + + var sb = new StringBuilder(type.Name); + + bool isArray = false; + if (sb.EndsWith(ARRAY_TOKEN)) + { + isArray = true; + sb.Remove(sb.Length - 2, 2); + type = type.GetElementType(); + } + + if (type.IsGenericParameter || (type.HasElementType && type.GetElementType().IsGenericParameter)) + { + sb.Insert(0, $""); + sb.Append(CLOSE_COLOR); + } + else + { + var args = type.GetGenericArguments(); + + if (args.Length > 0) + { + // remove the `N from the end of the type name + // this could actually be >9 in some cases, so get the length of the length string and use that. + // eg, if it was "List`15", we would remove the ending 3 chars + + int suffixLen = 1 + args.Length.ToString().Length; + + // make sure the typename actually has expected "`N" format. + if (sb[sb.Length - suffixLen] == '`') + sb.Remove(sb.Length - suffixLen, suffixLen); + } + + // highlight the base name itself + // do this after removing the `N suffix, so only the name itself is in the color tags. + sb.Insert(0, $"{OPEN_COLOR}{GetClassColor(type)}>"); + sb.Append(CLOSE_COLOR); + + // parse the generic args, if any + if (args.Length > 0) + { + sb.Append('<').Append(ParseGenericArgs(args)).Append('>'); + } + } + + if (isArray) + sb.Append('[').Append(']'); + + var ret = sb.ToString(); + typeToRichType.Add(key, ret); + + return ret; + } + + public static string ParseGenericArgs(Type[] args, bool isGenericParams = false) + { + if (args.Length < 1) + return string.Empty; + + var sb = new StringBuilder(); + + for (int i = 0; i < args.Length; i++) + { + if (i > 0) + sb.Append(',').Append(' '); + + if (isGenericParams) + { + sb.Append(OPEN_COLOR).Append(CONST).Append('>').Append(args[i].Name).Append(CLOSE_COLOR); + continue; + } + + sb.Append(HighlightType(args[i])); + } + + return sb.ToString(); + } + + public static string GetMemberInfoColor(MemberTypes type) + { + switch (type) + { + case MemberTypes.Method: return METHOD_INSTANCE; + case MemberTypes.Property: return PROP_INSTANCE; + case MemberTypes.Field: return FIELD_INSTANCE; + default: return null; + } + } + + + public static string GetMemberInfoColor(MemberInfo memberInfo, out bool isStatic) + { + isStatic = false; + if (memberInfo is FieldInfo fi) + { + if (fi.IsStatic) + { + isStatic = true; + return FIELD_STATIC; + } + + return FIELD_INSTANCE; + } + else if (memberInfo is MethodInfo mi) + { + if (mi.IsStatic) + { + isStatic = true; + return METHOD_STATIC; + } + + return METHOD_INSTANCE; + } + else if (memberInfo is PropertyInfo pi) + { + if (pi.GetAccessors(true)[0].IsStatic) + { + isStatic = true; + return PROP_STATIC; + } + + return PROP_INSTANCE; + } + //else if (memberInfo is EventInfo ei) + //{ + // if (ei.GetAddMethod().IsStatic) + // { + // isStatic = true; + // return EVENT_STATIC; + // } + + // return EVENT_INSTANCE; + //} + + throw new NotImplementedException(memberInfo.GetType().Name + " is not supported"); + } + } +} diff --git a/src/Core/Utility/ToStringUtility.cs b/src/Core/Utility/ToStringUtility.cs new file mode 100644 index 0000000..971504d --- /dev/null +++ b/src/Core/Utility/ToStringUtility.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityExplorer.Core.Runtime; + +namespace UnityExplorer +{ + public static class ToStringUtility + { + internal static Dictionary toStringMethods = new Dictionary(); + internal static Dictionary toStringFormattedMethods = new Dictionary(); + + private const string nullString = "null"; + private const string nullUnknown = nullString + " (?)"; + private const string destroyedString = "Destroyed"; + private const string untitledString = "untitled"; + + private const string eventSystemNamespace = "UnityEngine.EventSystem"; + + public static string PruneString(string s, int chars = 200, int lines = 5) + { + if (string.IsNullOrEmpty(s)) + return s; + + var sb = new StringBuilder(Math.Max(chars, s.Length)); + int newlines = 0; + for (int i = 0; i < s.Length; i++) + { + if (newlines >= lines || i >= chars) + { + sb.Append("..."); + break; + } + char c = s[i]; + if (c == '\r' || c == '\n') + newlines++; + sb.Append(c); + } + + return sb.ToString(); + } + + public static string ToStringWithType(object value, Type fallbackType, bool includeNamespace = true) + { + if (value.IsNullOrDestroyed() && fallbackType == null) + return nullUnknown; + + Type type = value?.GetActualType() ?? fallbackType; + + string richType = SignatureHighlighter.Parse(type, includeNamespace); + + var sb = new StringBuilder(); + + if (value.IsNullOrDestroyed()) + { + if (value == null) + { + sb.Append(nullString); + AppendRichType(sb, richType); + return sb.ToString(); + } + else // destroyed unity object + { + sb.Append(destroyedString); + AppendRichType(sb, richType); + return sb.ToString(); + } + } + + if (value is UnityEngine.Object obj) + { + if (string.IsNullOrEmpty(obj.name)) + sb.Append(untitledString); + else + { + sb.Append('"'); + sb.Append(PruneString(obj.name, 50, 1)); + sb.Append('"'); + } + + AppendRichType(sb, richType); + } + else if (type.FullName.StartsWith(eventSystemNamespace)) + { + // UnityEngine.EventSystem classes can have some obnoxious ToString results with rich text. + sb.Append(richType); + } + else + { + var toString = ToString(value); + + if (type.IsGenericType + || toString == type.FullName + || toString == $"{type.FullName} {type.FullName}" + || toString == $"Il2Cpp{type.FullName}" || type.FullName == $"Il2Cpp{toString}") + { + sb.Append(richType); + } + else // the ToString contains some actual implementation, use that value. + { + sb.Append(PruneString(toString, 200, 5)); + + AppendRichType(sb, richType); + } + } + + return sb.ToString(); + } + + private static void AppendRichType(StringBuilder sb, string richType) + { + sb.Append(' '); + sb.Append('('); + sb.Append(richType); + sb.Append(')'); + } + + private static string ToString(object value) + { + if (value.IsNullOrDestroyed()) + { + if (value == null) + return nullString; + else // destroyed unity object + return destroyedString; + } + + var type = value.GetActualType(); + + // Find and cache the relevant ToString method for this Type, if haven't already. + + if (!toStringMethods.ContainsKey(type.AssemblyQualifiedName)) + { + try + { + var formatMethod = type.GetMethod("ToString", ArgumentUtility.ParseArgs); + formatMethod.Invoke(value, new object[] { ParseUtility.NUMBER_FORMAT }); + toStringFormattedMethods.Add(type.AssemblyQualifiedName, formatMethod); + toStringMethods.Add(type.AssemblyQualifiedName, null); + } + catch + { + var toStringMethod = type.GetMethod("ToString", ArgumentUtility.EmptyTypes); + toStringMethods.Add(type.AssemblyQualifiedName, toStringMethod); + } + } + + // Invoke the ToString method on the object + + value = value.TryCast(type); + + string toString; + try + { + if (toStringFormattedMethods.TryGetValue(type.AssemblyQualifiedName, out MethodInfo formatMethod)) + toString = (string)formatMethod.Invoke(value, new object[] { ParseUtility.NUMBER_FORMAT }); + else + toString = (string)toStringMethods[type.AssemblyQualifiedName].Invoke(value, ArgumentUtility.EmptyArgs); + } + catch (Exception ex) + { + toString = ex.ReflectionExToString(); + } + + toString = ReflectionUtility.ProcessTypeInString(type, toString); + +#if CPP + if (value is Il2CppSystem.Type cppType) + { + var monoType = Il2CppReflection.GetUnhollowedType(cppType); + if (monoType != null) + toString = ReflectionUtility.ProcessTypeInString(monoType, toString); + } +#endif + + return toString; + } + } +} diff --git a/src/Core/Unity/UnityHelpers.cs b/src/Core/Utility/UnityHelpers.cs similarity index 75% rename from src/Core/Unity/UnityHelpers.cs rename to src/Core/Utility/UnityHelpers.cs index fc401c1..372e32e 100644 --- a/src/Core/Unity/UnityHelpers.cs +++ b/src/Core/Utility/UnityHelpers.cs @@ -4,12 +4,27 @@ using System.Globalization; using System.Linq; using System.Text; using UnityEngine; +using UnityEngine.Events; using Object = UnityEngine.Object; -namespace UnityExplorer.Core.Unity +// Project-wide namespace for accessibility +namespace UnityExplorer { public static class UnityHelpers { + // Time helpers, can't use Time.time since timeScale will affect it. + + // default 10ms (one frame at 100fps) + public static bool OccuredEarlierThanDefault(this float time) + { + return Time.realtimeSinceStartup - 0.01f >= time; + } + + public static bool OccuredEarlierThan(this float time, float secondsAgo) + { + return Time.realtimeSinceStartup - secondsAgo >= time; + } + /// /// Check if an object is null, and if it's a UnityEngine.Object then also check if it was destroyed. /// @@ -36,30 +51,23 @@ namespace UnityExplorer.Core.Unity return false; } - /// - /// Print a nice {X, Y, Z} output of the Vector3, formatted to 3 decimal places. - /// - public static string ToStringPretty(this Vector3 vec) - { - return $"{vec.x:F3}, {vec.y:F3}, {vec.z:F3}"; - } - /// /// Get the full Transform heirarchy path for this provided Transform. /// public static string GetTransformPath(this Transform transform, bool includeSelf = false) { - string path = includeSelf - ? transform.transform.name - : ""; + var sb = new StringBuilder(); + if (includeSelf) + sb.Append(transform.name); while (transform.parent) { transform = transform.parent; - path = $"{transform.name}/{path}"; + sb.Insert(0, '/'); + sb.Insert(0, transform.name); } - return path; + return sb.ToString(); } /// @@ -75,7 +83,7 @@ namespace UnityExplorer.Core.Unity } /// - /// Assumes the string is a 6-digit RGB Hex color code, which it will parse into a UnityEngine.Color. + /// Assumes the string is a 6-digit RGB Hex color code (with optional leading #) which it will parse into a UnityEngine.Color. /// Eg, FF0000 -> RGBA(1,0,0,1) /// public static Color ToColor(this string _string) diff --git a/src/ExplorerCore.cs b/src/ExplorerCore.cs index ee2d8d3..e82a073 100644 --- a/src/ExplorerCore.cs +++ b/src/ExplorerCore.cs @@ -1,53 +1,61 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core; using UnityExplorer.Core.Config; using UnityExplorer.Core.Input; using UnityExplorer.Core.Runtime; +using UnityExplorer.Tests; using UnityExplorer.UI; using UnityExplorer.UI.Inspectors; -using UnityExplorer.UI.Main; +using UnityExplorer.UI.Panels; namespace UnityExplorer { - public class ExplorerCore + public static class ExplorerCore { public const string NAME = "UnityExplorer"; - public const string VERSION = "3.3.15"; + public const string VERSION = "4.0.0"; public const string AUTHOR = "Sinai"; public const string GUID = "com.sinai.unityexplorer"; - public static ExplorerCore Instance { get; private set; } - public static IExplorerLoader Loader { get; private set; } + public static RuntimeContext Context { get; internal set; } - // Prevent using ctor, must use Init method. - private ExplorerCore() { } - + /// + /// Initialize UnityExplorer with the provided Loader implementation. + /// public static void Init(IExplorerLoader loader) { - if (Instance != null) + if (Loader != null) { - Log("An instance of UnityExplorer is already active!"); + LogWarning("UnityExplorer is already loaded!"); return; } - Loader = loader; - Instance = new ExplorerCore(); + + Log($"{NAME} {VERSION} initializing..."); + + ExplorerBehaviour.Setup(); if (!Directory.Exists(Loader.ExplorerFolder)) Directory.CreateDirectory(Loader.ExplorerFolder); ConfigManager.Init(Loader.ConfigHandler); - RuntimeProvider.Init(); + ReflectionUtility.Init(); + RuntimeProvider.Init(); + SceneHandler.Init(); InputManager.Init(); - Log($"{NAME} {VERSION} initialized."); - RuntimeProvider.Instance.StartCoroutine(SetupCoroutine()); + + Log($"Finished core setup, waiting for UI setup..."); } // Do a delayed setup so that objects aren't destroyed instantly. @@ -55,17 +63,28 @@ namespace UnityExplorer // Default delay is 1 second which is usually enough. private static IEnumerator SetupCoroutine() { - float f = Time.realtimeSinceStartup; + yield return null; + + float start = Time.realtimeSinceStartup; float delay = ConfigManager.Startup_Delay_Time.Value; - while (Time.realtimeSinceStartup - f < delay) + + while (delay > 0) + { + float diff = Math.Max(Time.deltaTime, Time.realtimeSinceStartup - start); + delay -= diff; yield return null; + } - Log($"Creating UI, after delay of {delay} second(s)."); - UIManager.Init(); + Log($"Creating UI..."); - //InspectorManager.Instance.Inspect(typeof(TestClass)); + UIManager.InitUI(); + + Log($"{NAME} {VERSION} initialized."); } + /// + /// Should be called once per frame. + /// public static void Update() { RuntimeProvider.Instance.Update(); @@ -73,41 +92,59 @@ namespace UnityExplorer UIManager.Update(); } + public static void FixedUpdate() + { + RuntimeProvider.Instance.ProcessFixedUpdate(); + } + + public static void OnPostRender() + { + RuntimeProvider.Instance.ProcessOnPostRender(); + } + +#region LOGGING + public static void Log(object message) - => Log(message, LogType.Log, false); + => Log(message, LogType.Log); public static void LogWarning(object message) - => Log(message, LogType.Warning, false); + => Log(message, LogType.Warning); public static void LogError(object message) - => Log(message, LogType.Error, false); + => Log(message, LogType.Error); - internal static void Log(object message, LogType logType, bool isFromUnity = false) + public static void LogUnity(object message, LogType logType) { - if (isFromUnity && !ConfigManager.Log_Unity_Debug.Value) + if (!ConfigManager.Log_Unity_Debug.Value) return; + Log($"[Unity] {message}", logType); + } + + private static void Log(object message, LogType logType) + { string log = message?.ToString() ?? ""; + LogPanel.Log(log, logType); + switch (logType) { case LogType.Assert: case LogType.Log: Loader.OnLogMessage(log); - DebugConsole.Log(log, Color.white); break; case LogType.Warning: Loader.OnLogWarning(log); - DebugConsole.Log(log, Color.yellow); break; case LogType.Error: case LogType.Exception: Loader.OnLogError(log); - DebugConsole.Log(log, Color.red); break; } } + +#endregion } } diff --git a/src/ILRepack.targets b/src/ILRepack.targets index c1e668f..944a663 100644 --- a/src/ILRepack.targets +++ b/src/ILRepack.targets @@ -4,14 +4,22 @@ - - + + + + + + + + + + diff --git a/src/Loader/BIE/ExplorerBepInPlugin.cs b/src/Loader/BIE/ExplorerBepInPlugin.cs index 82082b2..79a866d 100644 --- a/src/Loader/BIE/ExplorerBepInPlugin.cs +++ b/src/Loader/BIE/ExplorerBepInPlugin.cs @@ -57,53 +57,23 @@ namespace UnityExplorer { Instance = this; _configHandler = new BepInExConfigHandler(); + ExplorerCore.Init(this); } -#if MONO // Mono-specific +#if MONO // Mono internal void Awake() { UniversalInit(); - ExplorerCore.Init(this); } - internal void Update() - { - ExplorerCore.Update(); - } - -#else // Il2Cpp-specific +#else // Il2Cpp public override void Load() { UniversalInit(); - - ClassInjector.RegisterTypeInIl2Cpp(); - - var obj = new GameObject("ExplorerBehaviour"); - obj.AddComponent(); - obj.hideFlags = HideFlags.HideAndDontSave; - GameObject.DontDestroyOnLoad(obj); - - ExplorerCore.Init(this); - } - - // BepInEx Il2Cpp mod class doesn't have monobehaviour methods yet, so wrap them in a dummy. - public class ExplorerBehaviour : MonoBehaviour - { - public ExplorerBehaviour(IntPtr ptr) : base(ptr) { } - - internal void Awake() - { - Instance.LogSource.LogMessage("ExplorerBehaviour.Awake"); - } - - internal void Update() - { - ExplorerCore.Update(); - } } #endif - public void SetupPatches() + public void SetupCursorPatches() { try { diff --git a/src/Loader/IExplorerLoader.cs b/src/Loader/IExplorerLoader.cs index 78a17b4..ac7520b 100644 --- a/src/Loader/IExplorerLoader.cs +++ b/src/Loader/IExplorerLoader.cs @@ -17,6 +17,6 @@ namespace UnityExplorer Action OnLogWarning { get; } Action OnLogError { get; } - void SetupPatches(); + void SetupCursorPatches(); } } diff --git a/src/Loader/ML/ExplorerMelonMod.cs b/src/Loader/ML/ExplorerMelonMod.cs index f01094c..9f2859e 100644 --- a/src/Loader/ML/ExplorerMelonMod.cs +++ b/src/Loader/ML/ExplorerMelonMod.cs @@ -1,7 +1,6 @@ #if ML using System; using System.IO; -using Harmony; using MelonLoader; using UnityEngine; using UnityEngine.EventSystems; @@ -10,10 +9,12 @@ using UnityExplorer.Core; using UnityExplorer.Core.Config; using UnityExplorer.Core.Input; using UnityExplorer.Loader.ML; +using HarmonyLib; [assembly: MelonInfo(typeof(ExplorerMelonMod), ExplorerCore.NAME, ExplorerCore.VERSION, ExplorerCore.AUTHOR)] [assembly: MelonGame(null, null)] -//[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.UNIVERSAL)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.UNIVERSAL)] +[assembly: MelonColor(ConsoleColor.DarkCyan)] namespace UnityExplorer { @@ -31,8 +32,6 @@ namespace UnityExplorer public Action OnLogWarning => MelonLogger.Warning; public Action OnLogError => MelonLogger.Error; - public Harmony.HarmonyInstance HarmonyInstance => Instance.Harmony; - public override void OnApplicationStart() { Instance = this; @@ -41,12 +40,7 @@ namespace UnityExplorer ExplorerCore.Init(this); } - public override void OnUpdate() - { - ExplorerCore.Update(); - } - - public void SetupPatches() + public void SetupCursorPatches() { try { @@ -73,7 +67,7 @@ namespace UnityExplorer try { var prop = type.GetProperty(property); - this.Harmony.Patch(prop.GetSetMethod(), prefix: prefix); + HarmonyInstance.Patch(prop.GetSetMethod(), prefix: prefix); } catch (Exception e) { diff --git a/src/Loader/ML/MelonLoaderConfigHandler.cs b/src/Loader/ML/MelonLoaderConfigHandler.cs index f99df86..21eeab4 100644 --- a/src/Loader/ML/MelonLoaderConfigHandler.cs +++ b/src/Loader/ML/MelonLoaderConfigHandler.cs @@ -1,129 +1,78 @@ -#if ML -using MelonLoader; -using MelonLoader.Tomlyn.Model; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityExplorer.Core; -using UnityExplorer.Core.Config; - -namespace UnityExplorer.Loader.ML -{ - public class MelonLoaderConfigHandler : ConfigHandler - { - internal const string CTG_NAME = "UnityExplorer"; - - internal MelonPreferences_Category prefCategory; - - public override void Init() - { - prefCategory = MelonPreferences.CreateCategory(CTG_NAME, $"{CTG_NAME} Settings"); - - // temporary until melonloader 0.3.1 released - try { MelonPreferences.Mapper.RegisterMapper(KeycodeReader, KeycodeWriter); } catch { } - try { MelonPreferences.Mapper.RegisterMapper(MenuPagesReader, MenuPagesWriter); } catch { } - } - - public override void LoadConfig() - { - foreach (var entry in ConfigManager.ConfigElements) - { - var key = entry.Key; - if (prefCategory.GetEntry(key) is MelonPreferences_Entry) - { - var config = entry.Value; - config.BoxedValue = config.GetLoaderConfigValue(); - } - } - } - - public override void RegisterConfigElement(ConfigElement config) - { - var entry = prefCategory.CreateEntry(config.Name, config.Value, null, config.IsInternal) as MelonPreferences_Entry; - - entry.OnValueChangedUntyped += () => - { - if ((entry.Value == null && config.Value == null) || config.Value.Equals(entry.Value)) - return; - - config.Value = entry.Value; - }; - } - - public override void SetConfigValue(ConfigElement config, T value) - { - if (prefCategory.GetEntry(config.Name) is MelonPreferences_Entry entry) - { - entry.Value = value; - entry.Save(); - } - } - - public override T GetConfigValue(ConfigElement config) - { - if (prefCategory.GetEntry(config.Name) is MelonPreferences_Entry entry) - return entry.Value; - - return default; - } - - public override void OnAnyConfigChanged() - { - } - - public override void SaveConfig() - { - MelonPreferences.Save(); - } - - // TEMPORARY - JUST REQUIRED UNTIL ML 0.3.1 RELEASED - - public static KeyCode KeycodeReader(TomlObject value) - { - try - { - KeyCode kc = (KeyCode)Enum.Parse(typeof(KeyCode), (value as TomlString).Value); - - if (kc == default) - throw new Exception(); - - return kc; - } - catch - { - return KeyCode.F7; - } - } - - public static TomlObject KeycodeWriter(KeyCode value) - { - return MelonPreferences.Mapper.ToToml(value.ToString()); - } - - public static UI.Main.MenuPages MenuPagesReader(TomlObject value) - { - try - { - var kc = (UI.Main.MenuPages)Enum.Parse(typeof(UI.Main.MenuPages), (value as TomlString).Value); - - if (kc == default) - throw new Exception(); - - return kc; - } - catch - { - return UI.Main.MenuPages.Home; - } - } - - public static TomlObject MenuPagesWriter(UI.Main.MenuPages value) - { - return MelonPreferences.Mapper.ToToml(value.ToString()); - } - } -} - +#if ML +using MelonLoader; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityExplorer.Core; +using UnityExplorer.Core.Config; + +namespace UnityExplorer.Loader.ML +{ + public class MelonLoaderConfigHandler : ConfigHandler + { + internal const string CTG_NAME = "UnityExplorer"; + + internal MelonPreferences_Category prefCategory; + + public override void Init() + { + prefCategory = MelonPreferences.CreateCategory(CTG_NAME, $"{CTG_NAME} Settings", false, true); + } + + public override void LoadConfig() + { + foreach (var entry in ConfigManager.ConfigElements) + { + var key = entry.Key; + if (prefCategory.GetEntry(key) is MelonPreferences_Entry) + { + var config = entry.Value; + config.BoxedValue = config.GetLoaderConfigValue(); + } + } + } + + public override void RegisterConfigElement(ConfigElement config) + { + var entry = prefCategory.CreateEntry(config.Name, config.Value, null, config.Description, config.IsInternal, false); + + entry.OnValueChangedUntyped += () => + { + if ((entry.Value == null && config.Value == null) || config.Value.Equals(entry.Value)) + return; + + config.Value = entry.Value; + }; + } + + public override void SetConfigValue(ConfigElement config, T value) + { + if (prefCategory.GetEntry(config.Name) is MelonPreferences_Entry entry) + { + entry.Value = value; + //entry.Save(); + } + } + + public override T GetConfigValue(ConfigElement config) + { + if (prefCategory.GetEntry(config.Name) is MelonPreferences_Entry entry) + return entry.Value; + + return default; + } + + public override void OnAnyConfigChanged() + { + } + + public override void SaveConfig() + { + MelonPreferences.Save(); + } + } +} + #endif \ No newline at end of file diff --git a/src/Loader/STANDALONE/ExplorerStandalone.cs b/src/Loader/STANDALONE/ExplorerStandalone.cs index 9a2ab59..27a3387 100644 --- a/src/Loader/STANDALONE/ExplorerStandalone.cs +++ b/src/Loader/STANDALONE/ExplorerStandalone.cs @@ -87,30 +87,10 @@ namespace UnityExplorer Instance = this; _configHandler = new StandaloneConfigHandler(); -#if CPP - ClassInjector.RegisterTypeInIl2Cpp(); -#endif - var obj = new GameObject("ExplorerBehaviour"); - obj.AddComponent(); - - GameObject.DontDestroyOnLoad(obj); - obj.hideFlags = HideFlags.HideAndDontSave; - ExplorerCore.Init(this); } - public class ExplorerBehaviour : MonoBehaviour - { -#if CPP - public ExplorerBehaviour(IntPtr ptr) : base(ptr) { } -#endif - internal void Update() - { - ExplorerCore.Update(); - } - } - - public void SetupPatches() + public void SetupCursorPatches() { try { diff --git a/src/UI/CSConsole/CSAutoCompleter.cs b/src/UI/CSConsole/CSAutoCompleter.cs new file mode 100644 index 0000000..697a185 --- /dev/null +++ b/src/UI/CSConsole/CSAutoCompleter.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityExplorer.UI.CSConsole.Lexers; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.CSConsole +{ + public class CSAutoCompleter : ISuggestionProvider + { + public InputFieldRef InputField => ConsoleController.Input; + + public bool AnchorToCaretPosition => true; + + bool ISuggestionProvider.AllowNavigation => true; + + public void OnSuggestionClicked(Suggestion suggestion) + { + ConsoleController.InsertSuggestionAtCaret(suggestion.UnderlyingValue); + AutoCompleteModal.Instance.ReleaseOwnership(this); + } + + // Delimiters for completions, notably does not include '.' + private readonly HashSet delimiters = new HashSet + { + '{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?' + }; + + private readonly List suggestions = new List(); + + public void CheckAutocompletes() + { + if (string.IsNullOrEmpty(InputField.Text)) + { + AutoCompleteModal.Instance.ReleaseOwnership(this); + return; + } + + suggestions.Clear(); + + int caret = Math.Max(0, Math.Min(InputField.Text.Length - 1, InputField.Component.caretPosition - 1)); + int start = caret; + + // If the character at the caret index is whitespace or delimiter, + // or if the next character (if it exists) is not whitespace, + // then we don't want to provide suggestions. + if (char.IsWhiteSpace(InputField.Text[caret]) + || delimiters.Contains(InputField.Text[caret]) + || (InputField.Text.Length > caret + 1 && !char.IsWhiteSpace(InputField.Text[caret + 1]))) + { + AutoCompleteModal.Instance.ReleaseOwnership(this); + return; + } + + // get the current composition string (from caret back to last delimiter) + while (start > 0) + { + start--; + char c = InputField.Text[start]; + if (delimiters.Contains(c)) + { + start++; + break; + } + } + string input = InputField.Text.Substring(start, caret - start + 1); + + // Get MCS completions + + string[] evaluatorCompletions = ConsoleController.Evaluator.GetCompletions(input, out string prefix); + + if (evaluatorCompletions != null && evaluatorCompletions.Any()) + { + suggestions.AddRange(from completion in evaluatorCompletions + select new Suggestion(GetHighlightString(prefix, completion), completion)); + } + + // Get manual namespace completions + + foreach (var ns in ReflectionUtility.AllNamespaces) + { + if (ns.StartsWith(input)) + { + if (!namespaceHighlights.ContainsKey(ns)) + namespaceHighlights.Add(ns, $"{ns}"); + + string completion = ns.Substring(input.Length, ns.Length - input.Length); + suggestions.Add(new Suggestion(namespaceHighlights[ns], completion)); + } + } + + // Get manual keyword completions + + foreach (var kw in KeywordLexer.keywords) + { + 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(keywordHighlights[kw], completion)); + } + } + + if (suggestions.Any()) + { + AutoCompleteModal.Instance.TakeOwnership(this); + AutoCompleteModal.Instance.SetSuggestions(suggestions); + } + else + { + AutoCompleteModal.Instance.ReleaseOwnership(this); + } + } + + + private readonly Dictionary namespaceHighlights = new Dictionary(); + + 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 new file mode 100644 index 0000000..5479fc5 --- /dev/null +++ b/src/UI/CSConsole/ConsoleController.cs @@ -0,0 +1,596 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using UnityExplorer.UI.CSConsole; +using UnityExplorer.Core.Input; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.CSConsole +{ + public static class ConsoleController + { + public static ScriptEvaluator Evaluator; + public static LexerBuilder Lexer; + public static CSAutoCompleter Completer; + + private static HashSet usingDirectives; + private static StringBuilder evaluatorOutput; + + public static CSConsolePanel Panel => UIManager.GetPanel(UIManager.Panels.CSConsole); + public static InputFieldRef Input => Panel.Input; + + public static int LastCaretPosition { get; private set; } + internal static float defaultInputFieldAlpha; + + // Todo save as config? + public static bool EnableCtrlRShortcut { get; private set; } = true; + public static bool EnableAutoIndent { get; private set; } = true; + public static bool EnableSuggestions { get; private set; } = true; + + internal static readonly string[] DefaultUsing = new string[] + { + "System", + "System.Linq", + "System.Text", + "System.Collections.Generic", + "UnityEngine", +#if CPP + "UnhollowerBaseLib", + "UnhollowerRuntimeLib", +#endif + }; + + public static void Init() + { + try + { + ResetConsole(false); + // ensure the compiler is supported (if this fails then SRE is probably stubbed) + Evaluator.Compile("0 == 0"); + } + catch (Exception ex) + { + DisableConsole(ex); + return; + } + + 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 + + private static void OnToggleAutoIndent(bool value) + { + EnableAutoIndent = value; + } + + private static void OnToggleCtrlRShortcut(bool value) + { + EnableCtrlRShortcut = value; + } + + private static void OnToggleSuggestions(bool value) + { + EnableSuggestions = value; + } + + #endregion + + + #region Evaluating + + public static void ResetConsole() => ResetConsole(true); + + public static void ResetConsole(bool logSuccess = true) + { + if (SRENotSupported) + return; + + if (Evaluator != null) + Evaluator.Dispose(); + + evaluatorOutput = new StringBuilder(); + Evaluator = new ScriptEvaluator(new StringWriter(evaluatorOutput)) + { + InteractiveBaseClass = typeof(ScriptInteraction) + }; + + usingDirectives = new HashSet(); + foreach (var use in DefaultUsing) + AddUsing(use); + + if (logSuccess) + ExplorerCore.Log($"C# Console reset. Using directives:\r\n{Evaluator.GetUsing()}"); + } + + public static void AddUsing(string assemblyName) + { + if (!usingDirectives.Contains(assemblyName)) + { + Evaluate($"using {assemblyName};", true); + usingDirectives.Add(assemblyName); + } + } + + public static void Evaluate() + { + if (SRENotSupported) + return; + + Evaluate(Input.Text); + } + + public static void Evaluate(string input, bool supressLog = false) + { + if (SRENotSupported) + return; + + try + { + // 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. + + Evaluator.Run(input); + + string output = ScriptEvaluator._textWriter.ToString(); + var outputSplit = output.Split('\n'); + if (outputSplit.Length >= 2) + output = outputSplit[outputSplit.Length - 2]; + evaluatorOutput.Clear(); + + 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) + { + if (!supressLog) + ExplorerCore.LogWarning(fex.Message); + } + catch (Exception ex) + { + if (!supressLog) + ExplorerCore.LogWarning(ex); + } + } + + #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) + { + if (SRENotSupported) + return; + + // 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(); + + UpdateCaret(out _); + } + + public static void Update() + { + if (SRENotSupported) + return; + + UpdateCaret(out bool caretMoved); + + if (!settingCaretCoroutine && EnableSuggestions && AutoCompleteModal.CheckEscape(Completer)) + { + OnAutocompleteEscaped(); + return; + } + + if (!settingCaretCoroutine && EnableSuggestions && caretMoved) + { + AutoCompleteModal.Instance.ReleaseOwnership(Completer); + //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; + Input.Component.readOnly = 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; + + Input.Component.readOnly = false; + settingCaretCoroutine = false; + } + + + #region Lexer Highlighting + + private static void HighlightVisibleInput() + { + int startIdx = 0; + int endIdx = Input.Text.Length - 1; + int topLine = 0; + + // Calculate visible text if necessary + if (Input.Rect.rect.height > Panel.InputScroll.ViewportRect.rect.height) + { + topLine = -1; + int bottomLine = -1; + + // the top and bottom position of the viewport in relation to the text height + // they need the half-height adjustment to normalize against the 'line.topY' value. + var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); + var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; + + for (int i = 0; i < Input.TextGenerator.lineCount; i++) + { + var line = Input.TextGenerator.lines[i]; + // if not set the top line yet, and top of line is below the viewport top + if (topLine == -1 && line.topY <= viewportMin) + topLine = i; + // if bottom of line is below the viewport bottom + if ((line.topY - line.height) >= viewportMax) + bottomLine = i; + } + + topLine = Math.Max(0, topLine - 1); + bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1); + + startIdx = Input.TextGenerator.lines[topLine].startCharIdx; + endIdx = (bottomLine >= Input.TextGenerator.lineCount - 1) + ? Input.Text.Length - 1 + : (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1); + } + + // Highlight the visible text with the LexerBuilder + Panel.HighlightText.text = Lexer.BuildHighlightedString(Input.Text, startIdx, endIdx, topLine); + } + + #endregion + + + #region Autocompletes + + public static void InsertSuggestionAtCaret(string suggestion) + { + settingCaretCoroutine = true; + Input.Text = Input.Text.Insert(LastCaretPosition, suggestion); + + SetCaretPosition(LastCaretPosition + suggestion.Length); + LastCaretPosition = Input.Component.caretPosition; + } + + private static void OnAutocompleteEnter() + { + // Remove the new line + int lastIdx = Input.Component.caretPosition - 1; + Input.Text = Input.Text.Remove(lastIdx, 1); + + // Use the selected suggestion + Input.Component.caretPosition = LastCaretPosition; + Completer.OnSuggestionClicked(AutoCompleteModal.SelectedSuggestion); + } + + private static void OnAutocompleteEscaped() + { + AutoCompleteModal.Instance.ReleaseOwnership(Completer); + SetCaretPosition(LastCaretPosition); + } + + + #endregion + + + #region Auto indenting + + private static int prevContentLen = 0; + + private static void DoAutoIndent() + { + if (Input.Text.Length > prevContentLen) + { + int inc = Input.Text.Length - prevContentLen; + + if (inc == 1) + { + int caret = Input.Component.caretPosition; + Input.Text = Lexer.IndentCharacter(Input.Text, ref caret); + Input.Component.caretPosition = caret; + LastCaretPosition = caret; + } + else + { + // todo indenting for copy+pasted content + + //ExplorerCore.Log("Content increased by " + inc); + //var comp = Input.Text.Substring(PreviousCaretPosition, inc); + //ExplorerCore.Log("composition string: " + comp); + } + } + + prevContentLen = Input.Text.Length; + } + + #endregion + + + #region "Help" interaction + + private static bool SRENotSupported; + + private static void DisableConsole(Exception ex) + { + SRENotSupported = true; + Input.Component.readOnly = true; + Input.Component.textComponent.color = "5d8556".ToColor(); + + if (ex is NotSupportedException) + { + Input.Text = $@"The C# Console has been disabled because System.Reflection.Emit threw an exception: {ex.ReflectionExToString()} + +If the game was built with Unity's stubbed netstandard 2.0 runtime, you can fix this with UnityDoorstop: + * Download the Unity Editor version that the game uses + * Navigate to the folder: + - Editor\Data\PlaybackEngines\windowsstandalonesupport\Variations\mono\Managed + - or, Editor\Data\MonoBleedingEdge\lib\mono\4.5 + * Copy the mscorlib.dll and System.Reflection.Emit DLLs from the folder + * Make a subfolder in the folder that contains doorstop_config.ini + * Put the DLLs inside the subfolder + * Set the 'dllSearchPathOverride' in doorstop_config.ini to the subfolder name"; + } + else + { + Input.Text = $@"The C# Console has been disabled because of an unknown error. +{ex}"; + } + } + + 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 = @"// You can add a using directive to any namespace, but you must compile for it to take effect. +// It will remain in effect until you Reset the console. +using UnityEngine.UI; + +// To see your current usings, use the ""GetUsing();"" helper. +// Note: You cannot add usings and evaluate REPL at the same time."; + + 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: + * 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 + * Log(obj); - prints a message to the console log + * CurrentTarget; - System.Object, the target of the active Inspector tab + * AllTargets; - System.Object[], the targets of all Inspector tabs + * Inspect(obj); - inspect the object with the Inspector + * Inspect(someType); - inspect a Type with static reflection + * Start(enumerator); - starts the IEnumerator as a Coroutine + * help; - the default REPL help command, contains additional helpers. +*/"; + + 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. + +// In REPL, use the ""GetClasses();"" helper to see the classes you have defined since the last Reset."; + + internal const string HELP_COROUTINES = @"// To start a Coroutine directly, use ""Start(SomeCoroutine());"" in REPL mode. + +// To declare a coroutine, you will need to compile it separately. For example: +public class MyCoro +{ + public static IEnumerator Main() + { + yield return null; + UnityExplorer.ExplorerCore.Log(""Hello, world after one frame!""); + } +} +// To run this Coroutine in REPL, it would look like ""Start(MyCoro.Main());"""; + + #endregion + } +} diff --git a/src/UI/CSConsole/LexerBuilder.cs b/src/UI/CSConsole/LexerBuilder.cs new file mode 100644 index 0000000..e8eb5e0 --- /dev/null +++ b/src/UI/CSConsole/LexerBuilder.cs @@ -0,0 +1,343 @@ +using Mono.CSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CSConsole.Lexers; + +namespace UnityExplorer.UI.CSConsole +{ + public struct MatchInfo + { + public int startIndex; + public int endIndex; + public string htmlColorTag; + } + + public class LexerBuilder + { + #region Core and initialization + + public const char WHITESPACE = ' '; + public readonly HashSet IndentOpenChars = new HashSet { '{', '(' }; + public readonly HashSet IndentCloseChars = new HashSet { '}', ')' }; + + private readonly Lexer[] lexers; + private readonly HashSet delimiters = new HashSet(); + + private readonly StringLexer stringLexer = new StringLexer(); + private readonly CommentLexer commentLexer = new CommentLexer(); + + public LexerBuilder() + { + lexers = new Lexer[] + { + commentLexer, + stringLexer, + new SymbolLexer(), + new NumberLexer(), + new KeywordLexer(), + }; + + foreach (var matcher in lexers) + { + foreach (char c in matcher.Delimiters) + { + if (!delimiters.Contains(c)) + delimiters.Add(c); + } + } + } + + #endregion + + /// The last committed index for a match or no-match. Starts at -1 for a new parse. + public int CommittedIndex { get; private set; } + /// The index of the character we are currently parsing, at minimum it will be CommittedIndex + 1. + public int CurrentIndex { get; private set; } + + /// The current character we are parsing, determined by CurrentIndex. + public char Current => !EndOfInput ? currentInput[CurrentIndex] : WHITESPACE; + /// The previous character (CurrentIndex - 1), or whitespace if no previous character. + public char Previous => CurrentIndex >= 1 ? currentInput[CurrentIndex - 1] : WHITESPACE; + + /// Returns true if CurrentIndex is >= the current input length. + public bool EndOfInput => CurrentIndex > currentEndIdx; + /// Returns true if EndOfInput or current character is a new line. + public bool EndOrNewLine => EndOfInput || IsNewLine(Current); + + public static bool IsNewLine(char c) => c == '\n' || c == '\r'; + + private string currentInput; + private int currentStartIdx; + private int currentEndIdx; + + /// + /// Parse the range of the string with the Lexer and build a RichText-highlighted representation of it. + /// + /// The entire input string which you want to parse a section (or all) of + /// The first character you want to highlight + /// The last character you want to highlight + /// The amount of leading empty lines you want before the first character in the return string. + /// A string which contains the amount of leading lines specified, as well as the rich-text highlighted section. + public string BuildHighlightedString(string input, int startIdx, int endIdx, int leadingLines) + { + if (string.IsNullOrEmpty(input) || endIdx <= startIdx) + return input; + + currentInput = input; + currentStartIdx = startIdx; + currentEndIdx = endIdx; + + var sb = new StringBuilder(); + + for (int i = 0; i < leadingLines; i++) + sb.Append('\n'); + + int lastUnhighlighted = startIdx; + foreach (var match in GetMatches()) + { + // append non-highlighted text between last match and this + for (int i = lastUnhighlighted; i < match.startIndex; i++) + sb.Append(input[i]); + + // append the highlighted match + sb.Append(match.htmlColorTag); + + for (int i = match.startIndex; i <= match.endIndex && i <= currentEndIdx; i++) + sb.Append(input[i]); + + sb.Append(SignatureHighlighter.CLOSE_COLOR); + + // update the last unhighlighted start index + lastUnhighlighted = match.endIndex + 1; + } + + // Append trailing unhighlighted input + while (lastUnhighlighted <= endIdx) + { + sb.Append(input[lastUnhighlighted]); + lastUnhighlighted++; + } + + return sb.ToString(); + } + + + // Match builder, iterates through each Lexer and returns all matches found. + + public IEnumerable GetMatches() + { + CommittedIndex = currentStartIdx - 1; + Rollback(); + + while (!EndOfInput) + { + SkipWhitespace(); + bool anyMatch = false; + int startIndex = CommittedIndex + 1; + + foreach (var lexer in lexers) + { + if (lexer.TryMatchCurrent(this)) + { + anyMatch = true; + + yield return new MatchInfo + { + startIndex = startIndex, + endIndex = CommittedIndex, + htmlColorTag = lexer.ColorTag, + }; + break; + } + else + Rollback(); + } + + if (!anyMatch) + { + CurrentIndex = CommittedIndex + 1; + Commit(); + } + } + } + + // Methods used by the Lexers for interfacing with the current parse process + + public char PeekNext(int amount = 1) + { + CurrentIndex += amount; + return Current; + } + + public void Commit() + { + CommittedIndex = Math.Min(currentEndIdx, CurrentIndex); + } + + public void Rollback() + { + CurrentIndex = CommittedIndex + 1; + } + + public void RollbackBy(int amount) + { + CurrentIndex = Math.Max(CommittedIndex + 1, CurrentIndex - amount); + } + + public bool IsDelimiter(char character, bool orWhitespace = false, bool orLetterOrDigit = false) + { + return delimiters.Contains(character) + || (orWhitespace && char.IsWhiteSpace(character)) + || (orLetterOrDigit && char.IsLetterOrDigit(character)); + } + + private void SkipWhitespace() + { + // peek and commit as long as there is whitespace + while (!EndOfInput && char.IsWhiteSpace(Current)) + { + Commit(); + PeekNext(); + } + + if (!char.IsWhiteSpace(Current)) + Rollback(); + } + + #region Auto Indenting + + // Using the Lexer for indenting as it already has what we need to tokenize strings and comments. + // At the moment this only handles when a single newline or close-delimiter is composed. + // Does not handle copy+paste or any other characters yet. + + public string IndentCharacter(string input, ref int caretIndex) + { + int lastCharIndex = caretIndex - 1; + char c = input[lastCharIndex]; + + // we only want to indent for new lines and close indents + if (!IsNewLine(c) && !IndentCloseChars.Contains(c)) + return input; + + // perform a light parse up to the caret to determine indent level + currentInput = input; + currentStartIdx = 0; + currentEndIdx = lastCharIndex; + CommittedIndex = -1; + Rollback(); + + int indent = 0; + + while (!EndOfInput) + { + if (CurrentIndex >= lastCharIndex) + { + // reached the caret index + if (indent <= 0) + break; + + if (IsNewLine(c)) + input = IndentNewLine(input, indent, ref caretIndex); + else // closing indent + input = IndentCloseDelimiter(input, indent, lastCharIndex, ref caretIndex); + + break; + } + + // Try match strings and comments (Lexer will commit to the end of the match) + if (stringLexer.TryMatchCurrent(this) || commentLexer.TryMatchCurrent(this)) + { + PeekNext(); + continue; + } + + // Still parsing, check indent + + if (IndentOpenChars.Contains(Current)) + indent++; + else if (IndentCloseChars.Contains(Current)) + indent--; + + Commit(); + PeekNext(); + } + + return input; + } + + private string IndentNewLine(string input, int indent, ref int caretIndex) + { + // continue until the end of line or next non-whitespace character. + // if there's a close-indent on this line, reduce the indent level. + while (CurrentIndex < input.Length - 1) + { + CurrentIndex++; + char next = input[CurrentIndex]; + if (IsNewLine(next)) + break; + if (char.IsWhiteSpace(next)) + continue; + else if (IndentCloseChars.Contains(next)) + indent--; + + break; + } + + if (indent > 0) + { + input = input.Insert(caretIndex, new string('\t', indent)); + caretIndex += indent; + } + + return input; + } + + private string IndentCloseDelimiter(string input, int indent, int lastCharIndex, ref int caretIndex) + { + if (CurrentIndex > lastCharIndex) + { + return input; + } + + // lower the indent level by one as we would not have accounted for this closing symbol + indent--; + + // go back from the caret to the start of the line, calculate how much indent we need to adjust. + while (CurrentIndex > 0) + { + CurrentIndex--; + char prev = input[CurrentIndex]; + if (IsNewLine(prev)) + break; + if (!char.IsWhiteSpace(prev)) + { + // the line containing the closing bracket has non-whitespace characters before it. do not indent. + indent = 0; + break; + } + else if (prev == '\t') + indent--; + } + + if (indent > 0) + { + input = input.Insert(caretIndex, new string('\t', indent)); + caretIndex += indent; + } + else if (indent < 0) + { + // line is overly indented + input = input.Remove(lastCharIndex - 1, -indent); + caretIndex += indent; + } + + return input; + } + + #endregion + } +} diff --git a/src/UI/CSConsole/Lexers/CommentLexer.cs b/src/UI/CSConsole/Lexers/CommentLexer.cs new file mode 100644 index 0000000..e4c541e --- /dev/null +++ b/src/UI/CSConsole/Lexers/CommentLexer.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace UnityExplorer.UI.CSConsole.Lexers +{ + public class CommentLexer : Lexer + { + private enum CommentType + { + Line, + Block + } + + // forest green + protected override Color HighlightColor => new Color(0.34f, 0.65f, 0.29f, 1.0f); + + public override bool TryMatchCurrent(LexerBuilder lexer) + { + if (lexer.Current == '/') + { + lexer.PeekNext(); + if (lexer.Current == '/') + { + // line comment. read to end of line or file. + do + { + lexer.Commit(); + lexer.PeekNext(); + } + while (!lexer.EndOrNewLine); + + return true; + } + else if (lexer.Current == '*') + { + // block comment, read until end of file or closing '*/' + lexer.PeekNext(); + do + { + lexer.PeekNext(); + lexer.Commit(); + } + while (!lexer.EndOfInput && !(lexer.Current == '/' && lexer.Previous == '*')); + + return true; + } + } + + return false; + } + } +} diff --git a/src/UI/CSConsole/Lexers/KeywordLexer.cs b/src/UI/CSConsole/Lexers/KeywordLexer.cs new file mode 100644 index 0000000..376c793 --- /dev/null +++ b/src/UI/CSConsole/Lexers/KeywordLexer.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.UI.CSConsole.Lexers +{ + public class KeywordLexer : Lexer + { + // system blue + protected override Color HighlightColor => new Color(0.33f, 0.61f, 0.83f, 1.0f); + + public static readonly HashSet keywords = new HashSet + { +// reserved keywords +"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", +"decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally", +"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", +"long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public", +"readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", +"this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "void", +"volatile", "while", +// contextual keywords +"add", "and", "alias", "ascending", "async", "await", "by", "descending", "dynamic", "equals", "from", "get", +"global", "group", "init", "into", "join", "let", "managed", "nameof", "not", "notnull", "on", +"or", "orderby", "partial", "record", "remove", "select", "set", "unmanaged", "value", "var", "when", "where", +"where", "with", "yield", "nint", "nuint" + }; + + public override bool TryMatchCurrent(LexerBuilder lexer) + { + var prev = lexer.Previous; + var first = lexer.Current; + + // check for keywords + if (lexer.IsDelimiter(prev, true) && char.IsLetter(first)) + { + // can be a keyword... + + var sb = new StringBuilder(); + sb.Append(lexer.Current); + while (!lexer.EndOfInput && char.IsLetter(lexer.PeekNext())) + sb.Append(lexer.Current); + + if (keywords.Contains(sb.ToString())) + { + if (!lexer.EndOfInput) + lexer.RollbackBy(1); + lexer.Commit(); + return true; + } + + return false; + + } + else + return false; + } + } +} diff --git a/src/UI/CSConsole/Lexers/Lexer.cs b/src/UI/CSConsole/Lexers/Lexer.cs new file mode 100644 index 0000000..a7adf58 --- /dev/null +++ b/src/UI/CSConsole/Lexers/Lexer.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using UnityEngine; +using System.Linq; + +namespace UnityExplorer.UI.CSConsole.Lexers +{ + public abstract class Lexer + { + public virtual IEnumerable Delimiters => Enumerable.Empty(); + + protected abstract Color HighlightColor { get; } + + public string ColorTag => colorTag ?? (colorTag = ""); + private string colorTag; + + public abstract bool TryMatchCurrent(LexerBuilder lexer); + } +} diff --git a/src/UI/CSConsole/Lexers/NumberLexer.cs b/src/UI/CSConsole/Lexers/NumberLexer.cs new file mode 100644 index 0000000..98c0c17 --- /dev/null +++ b/src/UI/CSConsole/Lexers/NumberLexer.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace UnityExplorer.UI.CSConsole.Lexers +{ + public class NumberLexer : Lexer + { + // Maroon + protected override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f); + + private bool IsNumeric(char c) => char.IsNumber(c) || c == '.'; + + public override bool TryMatchCurrent(LexerBuilder lexer) + { + // previous character must be whitespace or delimiter + if (!lexer.IsDelimiter(lexer.Previous, true)) + return false; + + if (!IsNumeric(lexer.Current)) + return false; + + while (!lexer.EndOfInput) + { + lexer.Commit(); + if (!IsNumeric(lexer.PeekNext())) + break; + } + + return true; + } + } + +} diff --git a/src/UI/CSConsole/Lexers/StringLexer.cs b/src/UI/CSConsole/Lexers/StringLexer.cs new file mode 100644 index 0000000..e7c5e02 --- /dev/null +++ b/src/UI/CSConsole/Lexers/StringLexer.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace UnityExplorer.UI.CSConsole.Lexers +{ + public class StringLexer : Lexer + { + public override IEnumerable Delimiters => new[] { '"', '\'', }; + + // orange + protected override Color HighlightColor => new Color(0.79f, 0.52f, 0.32f, 1.0f); + + public override bool TryMatchCurrent(LexerBuilder lexer) + { + if (lexer.Current == '"') + { + if (lexer.Previous == '@') + { + // verbatim string, continue until un-escaped quote. + while (!lexer.EndOfInput) + { + lexer.Commit(); + if (lexer.PeekNext() == '"') + { + lexer.Commit(); + // possibly the end, check for escaped quotes. + // commit the character and flip the escape bool for each quote. + bool escaped = false; + while (lexer.PeekNext() == '"') + { + lexer.Commit(); + escaped = !escaped; + } + // if the last quote wasnt escaped, that was the end of the string. + if (!escaped) + break; + } + } + } + else + { + // normal string + // continue until a quote which is not escaped, or end of input + + while (!lexer.EndOfInput) + { + lexer.Commit(); + lexer.PeekNext(); + if ((lexer.Current == '"') && lexer.Previous != '\\') + { + lexer.Commit(); + break; + } + } + } + + return true; + } + else if (lexer.Current == '\'') + { + // char + + while (!lexer.EndOfInput) + { + lexer.Commit(); + lexer.PeekNext(); + if ((lexer.Current == '\'') && lexer.Previous != '\\') + { + lexer.Commit(); + break; + } + } + + return true; + } + else + return false; + } + } +} diff --git a/src/UI/CSConsole/Lexers/SymbolLexer.cs b/src/UI/CSConsole/Lexers/SymbolLexer.cs new file mode 100644 index 0000000..2562d0a --- /dev/null +++ b/src/UI/CSConsole/Lexers/SymbolLexer.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.UI.CSConsole.Lexers +{ + public class SymbolLexer : Lexer + { + // silver + protected override Color HighlightColor => new Color(0.6f, 0.6f, 0.6f); + + // all symbols are delimiters + public override IEnumerable Delimiters => symbols; + + public static bool IsSymbol(char c) => symbols.Contains(c); + + public static readonly HashSet symbols = new HashSet + { + '[', '{', '(', // open + ']', '}', ')', // close + '.', ',', ';', ':', '?', '@', // special + + // operators + '+', '-', '*', '/', '%', '&', '|', '^', '~', '=', '<', '>', '!', + }; + + public override bool TryMatchCurrent(LexerBuilder lexer) + { + // previous character must be delimiter, whitespace, or alphanumeric. + if (!lexer.IsDelimiter(lexer.Previous, true, true)) + return false; + + if (IsSymbol(lexer.Current)) + { + do + { + lexer.Commit(); + lexer.PeekNext(); + } + while (IsSymbol(lexer.Current)); + + return true; + } + + return false; + } + } +} diff --git a/src/Core/CSharp/ScriptEvaluator.cs b/src/UI/CSConsole/ScriptEvaluator.cs similarity index 83% rename from src/Core/CSharp/ScriptEvaluator.cs rename to src/UI/CSConsole/ScriptEvaluator.cs index 39f5ac4..f5f6b51 100644 --- a/src/Core/CSharp/ScriptEvaluator.cs +++ b/src/UI/CSConsole/ScriptEvaluator.cs @@ -2,11 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Text; using Mono.CSharp; -// Thanks to ManlyMarco for most of this +// Thanks to ManlyMarco for this -namespace UnityExplorer.Core.CSharp +namespace UnityExplorer.UI.CSConsole { public class ScriptEvaluator : Evaluator, IDisposable { @@ -22,7 +23,7 @@ namespace UnityExplorer.Core.CSharp { _textWriter = tw; - ImportAppdomainAssemblies(ReferenceAssembly); + ImportAppdomainAssemblies(Reference); AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad; } @@ -39,7 +40,15 @@ namespace UnityExplorer.Core.CSharp if (StdLib.Contains(name)) return; - ReferenceAssembly(args.LoadedAssembly); + Reference(args.LoadedAssembly); + } + + private void Reference(Assembly asm) + { + var name = asm.GetName().Name; + if (name == "completions") + return; + ReferenceAssembly(asm); } private static CompilerContext BuildContext(TextWriter tw) @@ -65,9 +74,7 @@ namespace UnityExplorer.Core.CSharp { string name = assembly.GetName().Name; if (StdLib.Contains(name)) - { continue; - } import(assembly); } diff --git a/src/UI/CSConsole/ScriptInteraction.cs b/src/UI/CSConsole/ScriptInteraction.cs new file mode 100644 index 0000000..340a955 --- /dev/null +++ b/src/UI/CSConsole/ScriptInteraction.cs @@ -0,0 +1,79 @@ +using System; +using Mono.CSharp; +using System.Collections; +using UnityEngine; +using System.Collections.Generic; +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 + { + public static void Log(object message) + { + ExplorerCore.Log(message); + } + + public static object CurrentTarget => InspectorManager.ActiveInspector?.Target; + + public static object[] AllTargets => InspectorManager.Inspectors.Select(it => it.Target).ToArray(); + + public static void Inspect(object obj) + { + InspectorManager.Inspect(obj); + } + + public static void Inspect(Type type) + { + InspectorManager.Inspect(type); + } + + public static void Start(IEnumerator ienumerator) + { + RuntimeProvider.Instance.StartCoroutine(ienumerator); + } + + public static void GetUsing() + { + Log(Evaluator.GetUsing()); + } + + public static void 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() + { + if (ReflectionUtility.GetFieldInfo(typeof(Evaluator), "source_file") + .GetValue(Evaluator) is CompilationSourceFile sourceFile + && sourceFile.Containers.Any()) + { + var sb = new StringBuilder(); + sb.Append($"There are {sourceFile.Containers.Count} defined classes:"); + foreach (TypeDefinition type in sourceFile.Containers.Where(it => it is TypeDefinition)) + { + sb.Append($"\n\n{type.MemberName.Name}:"); + foreach (var member in type.Members) + sb.Append($"\n\t- {member.AttributeTargets}: \"{member.MemberName.Name}\" ({member.ModFlags})"); + } + Log(sb.ToString()); + } + else + ExplorerCore.LogWarning("No classes seem to be defined."); + + } + } +} \ No newline at end of file diff --git a/src/UI/CacheObject/CacheConfigEntry.cs b/src/UI/CacheObject/CacheConfigEntry.cs index ca27b2e..fd2dcd4 100644 --- a/src/UI/CacheObject/CacheConfigEntry.cs +++ b/src/UI/CacheObject/CacheConfigEntry.cs @@ -2,101 +2,48 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using UnityEngine; -using UnityEngine.UI; using UnityExplorer.Core.Config; -using UnityExplorer.UI.InteractiveValues; +using UnityExplorer.UI.CacheObject.Views; namespace UnityExplorer.UI.CacheObject { public class CacheConfigEntry : CacheObjectBase { - public IConfigElement RefConfig { get; } + public CacheConfigEntry(IConfigElement configElement) + { + this.RefConfigElement = configElement; - public override Type FallbackType => RefConfig.ElementType; + this.NameLabelText = $"{configElement.Name}" + + $"\r\n{configElement.Description}"; - public override bool HasEvaluated => true; - public override bool HasParameters => false; - public override bool IsMember => false; + this.FallbackType = configElement.ElementType; + + configElement.OnValueChangedNotify += UpdateValueFromSource; + } + + public IConfigElement RefConfigElement; + + public override bool ShouldAutoEvaluate => true; + public override bool HasArguments => false; public override bool CanWrite => true; - public CacheConfigEntry(IConfigElement config, GameObject parent) + public void UpdateValueFromSource() { - RefConfig = config; + //if (RefConfigElement.BoxedValue.Equals(this.Value)) + // return; - m_parentContent = parent; + SetValueFromSource(RefConfigElement.BoxedValue); - config.OnValueChangedNotify += () => { UpdateValue(); }; - - CreateIValue(config.BoxedValue, config.ElementType); + if (this.CellView != null) + this.SetDataToCell(CellView); } - public override void CreateIValue(object value, Type fallbackType) + public override void TrySetUserValue(object value) { - IValue = InteractiveValue.Create(value, fallbackType); - IValue.Owner = this; - IValue.m_mainContentParent = m_mainGroup; - IValue.m_subContentParent = this.m_subContent; + this.Value = value; + RefConfigElement.BoxedValue = value; } - public override void UpdateValue() - { - IValue.Value = RefConfig.BoxedValue; - - base.UpdateValue(); - } - - public override void SetValue() - { - RefConfig.BoxedValue = IValue.Value; - } - - internal GameObject m_mainGroup; - - internal override void ConstructUI() - { - base.ConstructUI(); - - m_mainGroup = UIFactory.CreateVerticalGroup(m_mainContent, "ConfigHolder", true, false, true, true, 5, new Vector4(2, 2, 2, 2)); - - var horiGroup = UIFactory.CreateHorizontalGroup(m_mainGroup, "ConfigEntryHolder", false, false, true, true, childAlignment: TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(horiGroup, minHeight: 30, flexibleHeight: 0); - - // config entry label - - var configLabel = UIFactory.CreateLabel(horiGroup, "ConfigLabel", this.RefConfig.Name, TextAnchor.MiddleLeft); - var leftRect = configLabel.GetComponent(); - leftRect.anchorMin = Vector2.zero; - leftRect.anchorMax = Vector2.one; - leftRect.offsetMin = Vector2.zero; - leftRect.offsetMax = Vector2.zero; - leftRect.sizeDelta = Vector2.zero; - UIFactory.SetLayoutElement(configLabel.gameObject, minWidth: 250, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); - - // Default button - - var defaultButton = UIFactory.CreateButton(horiGroup, - "RevertDefaultButton", - "Default", - () => { RefConfig.RevertToDefaultValue(); }, - new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(defaultButton.gameObject, minWidth: 80, minHeight: 22, flexibleWidth: 0); - - // Description label - - var desc = UIFactory.CreateLabel(m_mainGroup, "Description", $"{RefConfig.Description}", TextAnchor.MiddleLeft, Color.grey); - UIFactory.SetLayoutElement(desc.gameObject, minWidth: 250, minHeight: 20, flexibleWidth: 9999, flexibleHeight: 0); - - // IValue - - if (IValue != null) - { - IValue.m_mainContentParent = m_mainGroup; - IValue.m_subContentParent = this.m_subContent; - } - - // makes the subcontent look nicer - m_subContent.transform.SetParent(m_mainGroup.transform, false); - } + protected override bool SetCellEvaluateState(CacheObjectCell cell) => false; } } diff --git a/src/UI/CacheObject/CacheEnumerated.cs b/src/UI/CacheObject/CacheEnumerated.cs deleted file mode 100644 index 97fae07..0000000 --- a/src/UI/CacheObject/CacheEnumerated.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityExplorer.UI; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.UI.InteractiveValues; - -namespace UnityExplorer.UI.CacheObject -{ - public class CacheEnumerated : CacheObjectBase - { - public override Type FallbackType => ParentEnumeration.m_baseEntryType; - public override bool CanWrite => RefIList != null && ParentEnumeration.Owner.CanWrite; - - public int Index { get; set; } - public IList RefIList { get; set; } - public InteractiveEnumerable ParentEnumeration { get; set; } - - public CacheEnumerated(int index, InteractiveEnumerable parentEnumeration, IList refIList, GameObject parentContent) - { - this.ParentEnumeration = parentEnumeration; - this.Index = index; - this.RefIList = refIList; - this.m_parentContent = parentContent; - } - - public override void CreateIValue(object value, Type fallbackType) - { - IValue = InteractiveValue.Create(value, fallbackType); - IValue.Owner = this; - } - - public override void SetValue() - { - RefIList[Index] = IValue.Value; - ParentEnumeration.Value = RefIList; - - ParentEnumeration.Owner.SetValue(); - } - - internal override void ConstructUI() - { - base.ConstructUI(); - - var rowObj = UIFactory.CreateHorizontalGroup(m_mainContent, "CacheEnumeratedGroup", false, true, true, true, 0, new Vector4(0,0,5,2), - new Color(1, 1, 1, 0)); - - var indexLabel = UIFactory.CreateLabel(rowObj, "IndexLabel", $"{this.Index}:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(indexLabel.gameObject, minWidth: 20, flexibleWidth: 30, minHeight: 25); - - IValue.m_mainContentParent = rowObj; - } - } -} diff --git a/src/UI/CacheObject/CacheField.cs b/src/UI/CacheObject/CacheField.cs index d3f5160..6a7375e 100644 --- a/src/UI/CacheObject/CacheField.cs +++ b/src/UI/CacheObject/CacheField.cs @@ -1,40 +1,54 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Reflection; -using UnityExplorer.UI; -using UnityEngine; +using System.Text; +using UnityExplorer.UI.Inspectors; namespace UnityExplorer.UI.CacheObject { public class CacheField : CacheMember { - public override bool IsStatic => (MemInfo as FieldInfo).IsStatic; + public FieldInfo FieldInfo { get; internal set; } + public override Type DeclaringType => FieldInfo.DeclaringType; + public override bool IsStatic => FieldInfo.IsStatic; + public override bool CanWrite => m_canWrite ?? (bool)(m_canWrite = !(FieldInfo.IsLiteral && !FieldInfo.IsInitOnly)); + private bool? m_canWrite; - public override Type FallbackType => (MemInfo as FieldInfo).FieldType; + public override bool ShouldAutoEvaluate => true; - public CacheField(FieldInfo fieldInfo, object declaringInstance, GameObject parent) : base(fieldInfo, declaringInstance, parent) + public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member) { - CreateIValue(null, fieldInfo.FieldType); + base.SetInspectorOwner(inspector, member); } - public override void UpdateReflection() + protected override object TryEvaluate() { - var fi = MemInfo as FieldInfo; - IValue.Value = fi.GetValue(fi.IsStatic ? null : DeclaringInstance); - - m_evaluated = true; - ReflectionException = null; + try + { + var ret = FieldInfo.GetValue(DeclaringInstance); + HadException = false; + LastException = null; + return ret; + } + catch (Exception ex) + { + HadException = true; + LastException = ex; + return null; + } } - public override void SetValue() + protected override void TrySetValue(object value) { - var fi = MemInfo as FieldInfo; - fi.SetValue(fi.IsStatic ? null : DeclaringInstance, IValue.Value); - - if (this.ParentInspector?.ParentMember != null) - this.ParentInspector.ParentMember.SetValue(); + try + { + FieldInfo.SetValue(DeclaringInstance, value); + } + catch (Exception ex) + { + ExplorerCore.LogWarning(ex); + } } } } diff --git a/src/UI/CacheObject/CacheKeyValuePair.cs b/src/UI/CacheObject/CacheKeyValuePair.cs new file mode 100644 index 0000000..e50a872 --- /dev/null +++ b/src/UI/CacheObject/CacheKeyValuePair.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.IValues; +using UnityExplorer.UI.Utility; + +namespace UnityExplorer.UI.CacheObject +{ + public class CacheKeyValuePair : CacheObjectBase + { + //public InteractiveList CurrentList { get; set; } + + public int DictIndex; + public object DictKey; + public object DisplayedKey; + + public bool KeyInputWanted; + public bool InspectWanted; + public string KeyLabelText; + public string KeyInputText; + public string KeyInputTypeText; + + public float DesiredKeyWidth; + public float DesiredValueWidth; + + public override bool ShouldAutoEvaluate => true; + public override bool HasArguments => false; + public override bool CanWrite => Owner.CanWrite; + + public void SetDictOwner(InteractiveDictionary dict, int index) + { + this.Owner = dict; + this.DictIndex = index; + } + + public void SetKey(object key) + { + this.DictKey = key; + this.DisplayedKey = key.TryCast(); + + var type = DisplayedKey.GetType(); + if (ParseUtility.CanParse(type)) + { + KeyInputWanted = true; + KeyInputText = ParseUtility.ToStringForInput(DisplayedKey, type); + KeyInputTypeText = SignatureHighlighter.Parse(type, false); + } + else + { + KeyInputWanted = false; + InspectWanted = type != typeof(bool) && !type.IsEnum; + KeyLabelText = ToStringUtility.ToStringWithType(DisplayedKey, type, true); + } + } + + public override void SetDataToCell(CacheObjectCell cell) + { + base.SetDataToCell(cell); + + var kvpCell = cell as CacheKeyValuePairCell; + + kvpCell.NameLabel.text = $"{DictIndex}:"; + kvpCell.Image.color = DictIndex % 2 == 0 ? CacheListEntryCell.EvenColor : CacheListEntryCell.OddColor; + + if (KeyInputWanted) + { + kvpCell.KeyInputField.UIRoot.SetActive(true); + kvpCell.KeyInputTypeLabel.gameObject.SetActive(true); + kvpCell.KeyLabel.gameObject.SetActive(false); + kvpCell.KeyInspectButton.Component.gameObject.SetActive(false); + + kvpCell.KeyInputField.Text = KeyInputText; + kvpCell.KeyInputTypeLabel.text = KeyInputTypeText; + } + else + { + kvpCell.KeyInputField.UIRoot.SetActive(false); + kvpCell.KeyInputTypeLabel.gameObject.SetActive(false); + kvpCell.KeyLabel.gameObject.SetActive(true); + kvpCell.KeyInspectButton.Component.gameObject.SetActive(InspectWanted); + + kvpCell.KeyLabel.text = KeyLabelText; + } + } + + public override void TrySetUserValue(object value) + { + (Owner as InteractiveDictionary).TrySetValueToKey(DictKey, value, DictIndex); + } + + + protected override bool SetCellEvaluateState(CacheObjectCell cell) + { + // not needed + return false; + } + } +} diff --git a/src/UI/CacheObject/CacheListEntry.cs b/src/UI/CacheObject/CacheListEntry.cs new file mode 100644 index 0000000..be9c736 --- /dev/null +++ b/src/UI/CacheObject/CacheListEntry.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.IValues; + +namespace UnityExplorer.UI.CacheObject +{ + public class CacheListEntry : CacheObjectBase + { + public int ListIndex; + + public override bool ShouldAutoEvaluate => true; + public override bool HasArguments => false; + public override bool CanWrite => Owner.CanWrite; + + public void SetListOwner(InteractiveList list, int listIndex) + { + this.Owner = list; + this.ListIndex = listIndex; + } + + public override void SetDataToCell(CacheObjectCell cell) + { + base.SetDataToCell(cell); + + var listCell = cell as CacheListEntryCell; + + listCell.NameLabel.text = $"{ListIndex}:"; + listCell.Image.color = ListIndex % 2 == 0 ? CacheListEntryCell.EvenColor : CacheListEntryCell.OddColor; + } + + public override void TrySetUserValue(object value) + { + (Owner as InteractiveList).TrySetValueToIndex(value, this.ListIndex); + } + + + protected override bool SetCellEvaluateState(CacheObjectCell cell) + { + // not needed + return false; + } + } +} diff --git a/src/UI/CacheObject/CacheMember.cs b/src/UI/CacheObject/CacheMember.cs index df5cbe8..c2a05d6 100644 --- a/src/UI/CacheObject/CacheMember.cs +++ b/src/UI/CacheObject/CacheMember.cs @@ -2,60 +2,175 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.UI; -using UnityExplorer.Core.Unity; using UnityExplorer.Core.Runtime; -using UnityExplorer.Core; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.ObjectPool; using UnityExplorer.UI.Utility; -using UnityExplorer.UI.InteractiveValues; -using UnityExplorer.UI.Inspectors.Reflection; namespace UnityExplorer.UI.CacheObject { public abstract class CacheMember : CacheObjectBase { - public override bool IsMember => true; + //public ReflectionInspector ParentInspector { get; internal set; } + //public bool AutoUpdateWanted { get; internal set; } + + public abstract Type DeclaringType { get; } + public string NameForFiltering { get; protected set; } + public object DeclaringInstance => IsStatic ? null : (m_declaringInstance ?? (m_declaringInstance = Owner.Target.TryCast(DeclaringType))); + private object m_declaringInstance; - public override Type FallbackType { get; } - - public ReflectionInspector ParentInspector { get; set; } - public MemberInfo MemInfo { get; set; } - public Type DeclaringType { get; set; } - public object DeclaringInstance { get; set; } - public virtual bool IsStatic { get; private set; } - - public string ReflectionException { get; set; } - - public override bool CanWrite => m_canWrite ?? GetCanWrite(); - private bool? m_canWrite; - - public override bool HasParameters => ParamCount > 0; - public virtual int ParamCount => m_arguments.Length; - public override bool HasEvaluated => m_evaluated; - public bool m_evaluated = false; - public bool m_isEvaluating; - public ParameterInfo[] m_arguments = new ParameterInfo[0]; - public string[] m_argumentInput = new string[0]; - - public string NameForFiltering => m_nameForFilter ?? (m_nameForFilter = $"{MemInfo.DeclaringType.Name}.{MemInfo.Name}".ToLower()); - private string m_nameForFilter; - - public string RichTextName => m_richTextName ?? GetRichTextName(); - private string m_richTextName; - - public CacheMember(MemberInfo memberInfo, object declaringInstance, GameObject parentContent) + public abstract bool IsStatic { get; } + public override bool HasArguments => Arguments?.Length > 0 || GenericArguments.Length > 0; + public ParameterInfo[] Arguments { get; protected set; } = new ParameterInfo[0]; + public Type[] GenericArguments { get; protected set; } = ArgumentUtility.EmptyTypes; + public EvaluateWidget Evaluator { get; protected set; } + public bool Evaluating => Evaluator != null && Evaluator.UIRoot.activeSelf; + + public virtual void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member) { - MemInfo = memberInfo; - DeclaringType = memberInfo.DeclaringType; - DeclaringInstance = declaringInstance; - this.m_parentContent = parentContent; - - DeclaringInstance = ReflectionProvider.Instance.Cast(declaringInstance, DeclaringType); + this.Owner = inspector; + this.NameLabelText = SignatureHighlighter.Parse(member.DeclaringType, false, member); + this.NameForFiltering = $"{member.DeclaringType.Name}.{member.Name}"; } - public static bool CanProcessArgs(ParameterInfo[] parameters) + public override void ReleasePooledObjects() + { + base.ReleasePooledObjects(); + + if (this.Evaluator != null) + { + this.Evaluator.OnReturnToPool(); + Pool.Return(this.Evaluator); + this.Evaluator = null; + } + } + + public override void UnlinkFromView() + { + if (this.Evaluator != null) + this.Evaluator.UIRoot.transform.SetParent(Pool.Instance.InactiveHolder.transform, false); + + base.UnlinkFromView(); + } + + protected abstract object TryEvaluate(); + + protected abstract void TrySetValue(object value); + + public void EvaluateAndSetCell() + { + Evaluate(); + if (CellView != null) + SetDataToCell(CellView); + } + + /// + /// Evaluate when first shown (if ShouldAutoEvaluate), or else when Evaluate button is clicked, or auto-updated. + /// + public void Evaluate() + { + SetValueFromSource(TryEvaluate()); + } + + public override void TrySetUserValue(object value) + { + TrySetValue(value); + + Evaluate(); + } + + protected override void SetValueState(CacheObjectCell cell, ValueStateArgs args) + { + base.SetValueState(cell, args); + + //var memCell = cell as CacheMemberCell; + //memCell.UpdateToggle.gameObject.SetActive(ShouldAutoEvaluate); + } + + private static readonly Color evalEnabledColor = new Color(0.15f, 0.25f, 0.15f); + private static readonly Color evalDisabledColor = new Color(0.15f, 0.15f, 0.15f); + + protected override bool SetCellEvaluateState(CacheObjectCell objectcell) + { + var cell = objectcell as CacheMemberCell; + + cell.EvaluateHolder.SetActive(!ShouldAutoEvaluate); + if (!ShouldAutoEvaluate) + { + //cell.UpdateToggle.gameObject.SetActive(false); + cell.EvaluateButton.Component.gameObject.SetActive(true); + if (HasArguments) + { + if (!Evaluating) + cell.EvaluateButton.ButtonText.text = $"Evaluate ({Arguments.Length + GenericArguments.Length})"; + else + { + cell.EvaluateButton.ButtonText.text = "Hide"; + Evaluator.UIRoot.transform.SetParent(cell.EvaluateHolder.transform, false); + RuntimeProvider.Instance.SetColorBlock(cell.EvaluateButton.Component, evalEnabledColor, evalEnabledColor * 1.3f); + } + } + else + cell.EvaluateButton.ButtonText.text = "Evaluate"; + + if (!Evaluating) + RuntimeProvider.Instance.SetColorBlock(cell.EvaluateButton.Component, evalDisabledColor, evalDisabledColor * 1.3f); + } + //else + //{ + // cell.UpdateToggle.gameObject.SetActive(true); + // cell.UpdateToggle.isOn = AutoUpdateWanted; + //} + + if (State == ValueState.NotEvaluated && !ShouldAutoEvaluate) + { + SetValueState(cell, ValueStateArgs.Default); + cell.RefreshSubcontentButton(); + + return true; + } + + if (State == ValueState.NotEvaluated) + Evaluate(); + + return false; + } + + + public void OnEvaluateClicked() + { + if (!HasArguments) + { + EvaluateAndSetCell(); + } + else + { + if (Evaluator == null) + { + this.Evaluator = Pool.Borrow(); + Evaluator.OnBorrowedFromPool(this); + Evaluator.UIRoot.transform.SetParent((CellView as CacheMemberCell).EvaluateHolder.transform, false); + SetCellEvaluateState(CellView); + } + else + { + if (Evaluator.UIRoot.activeSelf) + Evaluator.UIRoot.SetActive(false); + else + Evaluator.UIRoot.SetActive(true); + + SetCellEvaluateState(CellView); + } + } + } + + + #region Cache Member Util + + public static bool CanParseArgs(ParameterInfo[] parameters) { foreach (var param in parameters) { @@ -64,7 +179,7 @@ namespace UnityExplorer.UI.CacheObject if (pType.IsByRef && pType.HasElementType) pType = pType.GetElementType(); - if (pType != null && (pType.IsPrimitive || pType == typeof(string))) + if (pType != null && ParseUtility.CanParse(pType)) continue; else return false; @@ -72,312 +187,164 @@ namespace UnityExplorer.UI.CacheObject return true; } - public override void CreateIValue(object value, Type fallbackType) + public static List GetCacheMembers(object inspectorTarget, Type _type, ReflectionInspector _inspector) { - IValue = InteractiveValue.Create(value, fallbackType); - IValue.Owner = this; - IValue.m_mainContentParent = this.m_rightGroup; - IValue.m_subContentParent = this.m_subContent; - } + var list = new List(); + var cachedSigs = new HashSet(); - public override void UpdateValue() - { - if (!HasParameters || m_isEvaluating) + var types = ReflectionUtility.GetAllBaseTypes(_type); + + var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static; + if (!_inspector.StaticOnly) + flags |= BindingFlags.Instance; + + var infos = new List(); + + foreach (var declaringType in types) { - try + var target = inspectorTarget; + if (!_inspector.StaticOnly) + target = target.TryCast(declaringType); + + infos.Clear(); + infos.AddRange(declaringType.GetProperties(flags)); + infos.AddRange(declaringType.GetFields(flags)); + infos.AddRange(declaringType.GetMethods(flags)); + + foreach (var member in infos) { - Type baseType = ReflectionUtility.GetActualType(IValue.Value) ?? FallbackType; - - if (!ReflectionProvider.Instance.IsReflectionSupported(baseType)) - throw new Exception("Type not supported with reflection"); - - UpdateReflection(); - - if (IValue.Value != null) - IValue.Value = IValue.Value.Cast(ReflectionUtility.GetActualType(IValue.Value)); - } - catch (Exception e) - { - ReflectionException = e.ReflectionExToString(true); - } - } - - base.UpdateValue(); - } - - public abstract void UpdateReflection(); - - public override void SetValue() - { - // no implementation for base class - } - - public object[] ParseArguments() - { - if (m_arguments.Length < 1) - return new object[0]; - - var parsedArgs = new List(); - for (int i = 0; i < m_arguments.Length; i++) - { - var input = m_argumentInput[i]; - var type = m_arguments[i].ParameterType; - - if (type.IsByRef) - type = type.GetElementType(); - - if (!string.IsNullOrEmpty(input)) - { - if (type == typeof(string)) - { - parsedArgs.Add(input); + if (member.DeclaringType != declaringType) continue; - } - else - { - try + TryCacheMember(member, list, cachedSigs, declaringType, _inspector); + } + } + + var typeList = types.ToList(); + + var sorted = new List(); + sorted.AddRange(list.Where(it => it is CacheProperty) + .OrderBy(it => typeList.IndexOf(it.DeclaringType)) + .ThenBy(it => it.NameForFiltering)); + sorted.AddRange(list.Where(it => it is CacheField) + .OrderBy(it => typeList.IndexOf(it.DeclaringType)) + .ThenBy(it => it.NameForFiltering)); + sorted.AddRange(list.Where(it => it is CacheMethod) + .OrderBy(it => typeList.IndexOf(it.DeclaringType)) + .ThenBy(it => it.NameForFiltering)); + + return sorted; + } + + private static void TryCacheMember(MemberInfo member, List list, HashSet cachedSigs, + Type declaringType, ReflectionInspector _inspector, bool ignorePropertyMethodInfos = true) + { + try + { + if (ReflectionUtility.IsBlacklisted(member)) + return; + + var sig = GetSig(member); + + //ExplorerCore.Log($"Trying to cache member {sig}..."); + //ExplorerCore.Log(member.DeclaringType.FullName + "." + member.Name); + + CacheMember cached; + Type returnType; + switch (member.MemberType) + { + case MemberTypes.Method: { - var arg = type.GetMethod("Parse", new Type[] { typeof(string) }) - .Invoke(null, new object[] { input }); + var mi = member as MethodInfo; + if (ignorePropertyMethodInfos + && (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_"))) + return; - parsedArgs.Add(arg); - continue; + var args = mi.GetParameters(); + if (!CanParseArgs(args)) + return; + + sig += GetArgumentString(args); + if (cachedSigs.Contains(sig)) + return; + + cached = new CacheMethod() { MethodInfo = mi }; + returnType = mi.ReturnType; + break; } - catch + + case MemberTypes.Property: { - ExplorerCore.Log($"Could not parse input '{input}' for argument #{i} '{m_arguments[i].Name}' ({type.FullName})"); + var pi = member as PropertyInfo; + + var args = pi.GetIndexParameters(); + if (!CanParseArgs(args)) + return; + + if (!pi.CanRead && pi.CanWrite) + { + // write-only property, cache the set method instead. + var setMethod = pi.GetSetMethod(true); + if (setMethod != null) + TryCacheMember(setMethod, list, cachedSigs, declaringType, _inspector, false); + return; + } + + sig += GetArgumentString(args); + if (cachedSigs.Contains(sig)) + return; + + cached = new CacheProperty() { PropertyInfo = pi }; + returnType = pi.PropertyType; + break; } - } + + case MemberTypes.Field: + { + var fi = member as FieldInfo; + cached = new CacheField() { FieldInfo = fi }; + returnType = fi.FieldType; + break; + } + + default: return; } - // No input, see if there is a default value. - if (m_arguments[i].IsOptional) - { - parsedArgs.Add(m_arguments[i].DefaultValue); - continue; - } + cachedSigs.Add(sig); - // Try add a null arg I guess - parsedArgs.Add(null); + //cached.Initialize(_inspector, declaringType, member, returnType); + cached.SetFallbackType(returnType); + cached.SetInspectorOwner(_inspector, member); + + list.Add(cached); } - - return parsedArgs.ToArray(); - } - - private bool GetCanWrite() - { - if (MemInfo is FieldInfo fi) - m_canWrite = !(fi.IsLiteral && !fi.IsInitOnly); - else if (MemInfo is PropertyInfo pi) - m_canWrite = pi.CanWrite; - else - m_canWrite = false; - - return (bool)m_canWrite; - } - - private string GetRichTextName() - { - return m_richTextName = SignatureHighlighter.ParseFullSyntax(MemInfo.DeclaringType, false, MemInfo); - } - - #region UI - - internal float GetMemberLabelWidth(RectTransform scrollRect) - { - var textGenSettings = m_memLabelText.GetGenerationSettings(m_topRowRect.rect.size); - textGenSettings.scaleFactor = InputFieldScroller.canvasScaler.scaleFactor; - - var textGen = m_memLabelText.cachedTextGeneratorForLayout; - float preferredWidth = textGen.GetPreferredWidth(RichTextName, textGenSettings); - - float max = scrollRect.rect.width * 0.4f; - - if (preferredWidth > max) preferredWidth = max; - - return preferredWidth < 125f ? 125f : preferredWidth; - } - - internal void SetWidths(float labelWidth, float valueWidth) - { - m_leftLayout.preferredWidth = labelWidth; - m_rightLayout.preferredWidth = valueWidth; - } - - internal RectTransform m_topRowRect; - internal Text m_memLabelText; - internal GameObject m_leftGroup; - internal LayoutElement m_leftLayout; - internal GameObject m_rightGroup; - internal LayoutElement m_rightLayout; - - internal override void ConstructUI() - { - base.ConstructUI(); - - var topGroupObj = UIFactory.CreateHorizontalGroup(m_mainContent, "CacheMemberGroup", false, false, true, true, 10, new Vector4(0, 0, 3, 3), - new Color(1, 1, 1, 0)); - - m_topRowRect = topGroupObj.GetComponent(); - - UIFactory.SetLayoutElement(topGroupObj, minHeight: 25, flexibleHeight: 0, minWidth: 300, flexibleWidth: 5000); - - // left group - - m_leftGroup = UIFactory.CreateHorizontalGroup(topGroupObj, "LeftGroup", false, true, true, true, 4, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(m_leftGroup, minHeight: 25, flexibleHeight: 0, minWidth: 125, flexibleWidth: 200); - - // member label - - m_memLabelText = UIFactory.CreateLabel(m_leftGroup, "MemLabelText", RichTextName, TextAnchor.MiddleLeft); - m_memLabelText.horizontalOverflow = HorizontalWrapMode.Wrap; - var leftRect = m_memLabelText.GetComponent(); - leftRect.anchorMin = Vector2.zero; - leftRect.anchorMax = Vector2.one; - leftRect.offsetMin = Vector2.zero; - leftRect.offsetMax = Vector2.zero; - leftRect.sizeDelta = Vector2.zero; - m_leftLayout = m_memLabelText.gameObject.AddComponent(); - m_leftLayout.preferredWidth = 125; - m_leftLayout.minHeight = 25; - m_leftLayout.flexibleHeight = 100; - var labelFitter = m_memLabelText.gameObject.AddComponent(); - labelFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; - labelFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; - - // right group - - m_rightGroup = UIFactory.CreateVerticalGroup(topGroupObj, "RightGroup", false, true, true, true, 2, new Vector4(4,2,0,0), - new Color(1, 1, 1, 0)); - - m_rightLayout = m_rightGroup.AddComponent(); - m_rightLayout.minHeight = 25; - m_rightLayout.flexibleHeight = 480; - m_rightLayout.minWidth = 125; - m_rightLayout.flexibleWidth = 5000; - - ConstructArgInput(out GameObject argsHolder); - - ConstructEvaluateButtons(argsHolder); - - IValue.m_mainContentParent = m_rightGroup; - } - - internal void ConstructArgInput(out GameObject argsHolder) - { - argsHolder = null; - - if (HasParameters) + catch (Exception e) { - argsHolder = UIFactory.CreateVerticalGroup(m_rightGroup, "ArgsHolder", true, false, true, true, 4, new Color(1, 1, 1, 0)); - - if (this is CacheMethod cm && cm.GenericArgs.Length > 0) - cm.ConstructGenericArgInput(argsHolder); - - if (m_arguments.Length > 0) - { - UIFactory.CreateLabel(argsHolder, "ArgumentsLabel", "Arguments:", TextAnchor.MiddleLeft); - - for (int i = 0; i < m_arguments.Length; i++) - AddArgRow(i, argsHolder); - } - - argsHolder.SetActive(false); + ExplorerCore.LogWarning($"Exception caching member {member.DeclaringType.FullName}.{member.Name}!"); + ExplorerCore.Log(e.ToString()); } } - internal void AddArgRow(int i, GameObject parent) + internal static string GetSig(MemberInfo member) => $"{member.DeclaringType.Name}.{member.Name}"; + + internal static string GetArgumentString(ParameterInfo[] args) { - var arg = m_arguments[i]; - - var rowObj = UIFactory.CreateHorizontalGroup(parent, "ArgRow", true, false, true, true, 4, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleWidth: 5000); - - var argTypeTxt = SignatureHighlighter.ParseFullSyntax(arg.ParameterType, false); - var argLabel = UIFactory.CreateLabel(rowObj, "ArgLabel", $"{argTypeTxt} {arg.Name}", - TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(argLabel.gameObject, minHeight: 25); - - var argInputObj = UIFactory.CreateInputField(rowObj, "ArgInput", "...", 14, (int)TextAnchor.MiddleLeft, 1); - UIFactory.SetLayoutElement(argInputObj, flexibleWidth: 1200, preferredWidth: 150, minWidth: 20, minHeight: 25, flexibleHeight: 0); - - var argInput = argInputObj.GetComponent(); - argInput.onValueChanged.AddListener((string val) => { m_argumentInput[i] = val; }); - - if (arg.IsOptional) + var sb = new StringBuilder(); + sb.Append(' '); + sb.Append('('); + foreach (var param in args) { - var phInput = argInput.placeholder.GetComponent(); - phInput.text = " = " + arg.DefaultValue?.ToString() ?? "null"; - } - } - - internal void ConstructEvaluateButtons(GameObject argsHolder) - { - if (HasParameters) - { - var evalGroupObj = UIFactory.CreateHorizontalGroup(m_rightGroup, "EvalGroup", false, false, true, true, 5, - default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(evalGroupObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 5000); - - var evalButton = UIFactory.CreateButton(evalGroupObj, - "EvalButton", - $"Evaluate ({ParamCount})", - null); - - RuntimeProvider.Instance.SetColorBlock(evalButton, new Color(0.4f, 0.4f, 0.4f), - new Color(0.4f, 0.7f, 0.4f), new Color(0.3f, 0.3f, 0.3f)); - - UIFactory.SetLayoutElement(evalButton.gameObject, minWidth: 100, minHeight: 22, flexibleWidth: 0); - - var evalText = evalButton.GetComponentInChildren(); - - var cancelButton = UIFactory.CreateButton(evalGroupObj, "CancelButton", "Close", null, new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(cancelButton.gameObject, minWidth: 100, minHeight: 22, flexibleWidth: 0); - - cancelButton.gameObject.SetActive(false); - - evalButton.onClick.AddListener(() => - { - if (!m_isEvaluating) - { - argsHolder.SetActive(true); - m_isEvaluating = true; - evalText.text = "Evaluate"; - RuntimeProvider.Instance.SetColorBlock(evalButton, new Color(0.3f, 0.6f, 0.3f)); - - cancelButton.gameObject.SetActive(true); - } - else - { - if (this is CacheMethod cm) - cm.Evaluate(); - else - UpdateValue(); - } - }); - - cancelButton.onClick.AddListener(() => - { - cancelButton.gameObject.SetActive(false); - argsHolder.SetActive(false); - m_isEvaluating = false; - - evalText.text = $"Evaluate ({ParamCount})"; - RuntimeProvider.Instance.SetColorBlock(evalButton, new Color(0.4f, 0.4f, 0.4f)); - }); - } - else if (this is CacheMethod) - { - // simple method evaluate button - - var evalButton = UIFactory.CreateButton(m_rightGroup, "EvalButton", "Evaluate", () => { (this as CacheMethod).Evaluate(); }); - RuntimeProvider.Instance.SetColorBlock(evalButton, new Color(0.4f, 0.4f, 0.4f), - new Color(0.4f, 0.7f, 0.4f), new Color(0.3f, 0.3f, 0.3f)); - - UIFactory.SetLayoutElement(evalButton.gameObject, minWidth: 100, minHeight: 22, flexibleWidth: 0); + sb.Append(param.ParameterType.Name); + sb.Append(' '); + sb.Append(param.Name); + sb.Append(','); + sb.Append(' '); } + sb.Append(')'); + return sb.ToString(); } #endregion + + } } diff --git a/src/UI/CacheObject/CacheMethod.cs b/src/UI/CacheObject/CacheMethod.cs index e34d3f6..28771dd 100644 --- a/src/UI/CacheObject/CacheMethod.cs +++ b/src/UI/CacheObject/CacheMethod.cs @@ -2,172 +2,55 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.Core; -using UnityExplorer.UI.Utility; +using System.Text; +using UnityExplorer.UI.Inspectors; namespace UnityExplorer.UI.CacheObject { public class CacheMethod : CacheMember { - //private CacheObjectBase m_cachedReturnValue; + public MethodInfo MethodInfo { get; internal set; } + public override Type DeclaringType => MethodInfo.DeclaringType; + public override bool CanWrite => false; + public override bool IsStatic => MethodInfo.IsStatic; - public override Type FallbackType => (MemInfo as MethodInfo).ReturnType; + public override bool ShouldAutoEvaluate => false; - public override bool HasParameters => base.HasParameters || GenericArgs.Length > 0; - - public override bool IsStatic => (MemInfo as MethodInfo).IsStatic; - - public override int ParamCount => base.ParamCount + m_genericArgInput.Length; - - public Type[] GenericArgs { get; private set; } - public Type[][] GenericConstraints { get; private set; } - - public string[] m_genericArgInput = new string[0]; - - public CacheMethod(MethodInfo methodInfo, object declaringInstance, GameObject parent) : base(methodInfo, declaringInstance, parent) + public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member) { - GenericArgs = methodInfo.GetGenericArguments(); + base.SetInspectorOwner(inspector, member); - GenericConstraints = GenericArgs.Select(x => x.GetGenericParameterConstraints()) - .Where(x => x != null) - .ToArray(); - - m_genericArgInput = new string[GenericArgs.Length]; - - m_arguments = methodInfo.GetParameters(); - m_argumentInput = new string[m_arguments.Length]; - - CreateIValue(null, methodInfo.ReturnType); + Arguments = MethodInfo.GetParameters(); + if (MethodInfo.IsGenericMethod) + GenericArguments = MethodInfo.GetGenericArguments(); } - public override void UpdateReflection() + protected override object TryEvaluate() { - // CacheMethod cannot UpdateValue directly. Need to Evaluate. - } - - public void Evaluate() - { - MethodInfo mi; - if (GenericArgs.Length > 0) - { - mi = MakeGenericMethodFromInput(); - if (mi == null) return; - } - else - { - mi = MemInfo as MethodInfo; - } - - object ret = null; - try { - ret = mi.Invoke(mi.IsStatic ? null : DeclaringInstance, ParseArguments()); - m_evaluated = true; - m_isEvaluating = false; - ReflectionException = null; + var methodInfo = MethodInfo; + + if (methodInfo.IsGenericMethod) + methodInfo = MethodInfo.MakeGenericMethod(Evaluator.TryParseGenericArguments()); + + if (Arguments.Length > 0) + return methodInfo.Invoke(DeclaringInstance, Evaluator.TryParseArguments()); + + var ret = methodInfo.Invoke(DeclaringInstance, ArgumentUtility.EmptyArgs); + + HadException = false; + LastException = null; + return ret; } - catch (Exception e) + catch (Exception ex) { - while (e.InnerException != null) - e = e.InnerException; - - ExplorerCore.LogWarning($"Exception evaluating: {e.GetType()}, {e.Message}"); - ReflectionException = ReflectionUtility.ReflectionExToString(e); + HadException = true; + LastException = ex; + return null; } - - IValue.Value = ret; - UpdateValue(); } - private MethodInfo MakeGenericMethodFromInput() - { - var mi = MemInfo as MethodInfo; - - var list = new List(); - for (int i = 0; i < GenericArgs.Length; i++) - { - var input = m_genericArgInput[i]; - if (ReflectionUtility.GetTypeByName(input) is Type t) - { - if (GenericConstraints[i].Length == 0) - { - list.Add(t); - } - else - { - foreach (var constraint in GenericConstraints[i].Where(x => x != null)) - { - if (!constraint.IsAssignableFrom(t)) - { - ExplorerCore.LogWarning($"Generic argument #{i}, '{input}' is not assignable from the constraint '{constraint}'!"); - return null; - } - } - - list.Add(t); - } - } - else - { - ExplorerCore.LogWarning($"Generic argument #{i}, could not get any type by the name of '{input}'!" + - $" Make sure you use the full name including the namespace."); - return null; - } - } - - // make into a generic with type list - mi = mi.MakeGenericMethod(list.ToArray()); - - return mi; - } - - #region UI CONSTRUCTION - - internal void ConstructGenericArgInput(GameObject parent) - { - UIFactory.CreateLabel(parent, "GenericArgLabel", "Generic Arguments:", TextAnchor.MiddleLeft); - - for (int i = 0; i < GenericArgs.Length; i++) - AddGenericArgRow(i, parent); - } - - internal void AddGenericArgRow(int i, GameObject parent) - { - var arg = GenericArgs[i]; - - string constrainTxt = ""; - if (this.GenericConstraints[i].Length > 0) - { - foreach (var constraint in this.GenericConstraints[i]) - { - if (constrainTxt != "") - constrainTxt += ", "; - - constrainTxt += $"{SignatureHighlighter.ParseFullSyntax(constraint, false)}"; - } - } - else - constrainTxt = $"Any"; - - var rowObj = UIFactory.CreateHorizontalGroup(parent, "ArgRowObj", false, true, true, true, 4, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleWidth: 5000); - - var argLabelObj = UIFactory.CreateLabel(rowObj, "ArgLabelObj", $"{constrainTxt} {arg.Name}", - TextAnchor.MiddleLeft); - - var argInputObj = UIFactory.CreateInputField(rowObj, "ArgInput", "...", 14, (int)TextAnchor.MiddleLeft, 1); - UIFactory.SetLayoutElement(argInputObj, flexibleWidth: 1200); - - var argInput = argInputObj.GetComponent(); - argInput.onValueChanged.AddListener((string val) => { m_genericArgInput[i] = val; }); - - } - - #endregion + protected override void TrySetValue(object value) => throw new NotImplementedException("You can't set a method"); } } diff --git a/src/UI/CacheObject/CacheObjectBase.cs b/src/UI/CacheObject/CacheObjectBase.cs index b9516c7..ad623ac 100644 --- a/src/UI/CacheObject/CacheObjectBase.cs +++ b/src/UI/CacheObject/CacheObjectBase.cs @@ -1,110 +1,478 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; using UnityEngine; -using UnityExplorer.UI; -using UnityExplorer.Core.Unity; using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.UI.InteractiveValues; +using UnityExplorer.Core.Runtime; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.IValues; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Utility; namespace UnityExplorer.UI.CacheObject { + public enum ValueState + { + NotEvaluated, + Exception, + Boolean, + Number, + String, + Enum, + Collection, + Dictionary, + ValueStruct, + Color, + Unsupported + } + public abstract class CacheObjectBase { - public InteractiveValue IValue; + public ICacheObjectController Owner { get; set; } - public virtual bool CanWrite => false; - public virtual bool HasParameters => false; - public virtual bool IsMember => false; - public virtual bool HasEvaluated => true; + public CacheObjectCell CellView { get; internal set; } - public abstract Type FallbackType { get; } + public object Value { get; protected set; } + public Type FallbackType { get; protected set; } + public bool LastValueWasNull { get; private set; } - public abstract void CreateIValue(object value, Type fallbackType); + public ValueState State = ValueState.NotEvaluated; + public Type LastValueType; - public virtual void Enable() + public InteractiveValue IValue { get; private set; } + public Type CurrentIValueType { get; private set; } + public bool SubContentShowWanted { get; private set; } + + public string NameLabelText { get; protected set; } + public string ValueLabelText { get; protected set; } + + public abstract bool ShouldAutoEvaluate { get; } + public abstract bool HasArguments { get; } + public abstract bool CanWrite { get; } + public bool HadException { get; protected set; } + public Exception LastException { get; protected set; } + + public virtual void SetFallbackType(Type fallbackType) { - if (!m_constructedUI) + this.FallbackType = fallbackType; + this.ValueLabelText = GetValueLabel(); + } + + protected const string NOT_YET_EVAL = "Not yet evaluated"; + + public virtual void ReleasePooledObjects() + { + if (this.IValue != null) + ReleaseIValue(); + + if (this.CellView != null) + UnlinkFromView(); + } + + public virtual void SetView(CacheObjectCell cellView) + { + this.CellView = cellView; + cellView.Occupant = this; + } + + public virtual void UnlinkFromView() + { + if (this.CellView == null) + return; + + this.CellView.Occupant = null; + this.CellView = null; + + if (this.IValue != null) + this.IValue.UIRoot.transform.SetParent(InactiveIValueHolder.transform, false); + } + + // Updating and applying values + + public void SetUserValue(object value) + { + value = value.TryCast(FallbackType); + + TrySetUserValue(value); + + if (CellView != null) + SetDataToCell(CellView); + + // If the owner's parent CacheObject is set, we are setting the value of an inspected struct. + // Set the inspector target as the value back to that parent cacheobject. + if (Owner.ParentCacheObject != null) + Owner.ParentCacheObject.SetUserValue(Owner.Target); + } + + public abstract void TrySetUserValue(object value); + + // The only method which sets the CacheObjectBase.Value + public virtual void SetValueFromSource(object value) + { + this.Value = value; + + if (!Value.IsNullOrDestroyed()) + Value = Value.TryCast(); + + ProcessOnEvaluate(); + + if (this.IValue != null) { - ConstructUI(); - UpdateValue(); + if (SubContentShowWanted) + this.IValue.SetValue(Value); + else + IValue.PendingValueWanted = true; + } + } + + protected virtual void ProcessOnEvaluate() + { + var prevState = State; + + if (HadException) + { + LastValueWasNull = true; + LastValueType = FallbackType; + State = ValueState.Exception; + } + else if (Value.IsNullOrDestroyed()) + { + LastValueWasNull = true; + State = GetStateForType(FallbackType); + } + else + { + LastValueWasNull = false; + State = GetStateForType(Value.GetActualType()); } - m_mainContent.SetActive(true); - m_mainContent.transform.SetAsLastSibling(); - } - - public virtual void Disable() - { - if (m_mainContent) - m_mainContent.SetActive(false); - } - - public void Destroy() - { - if (this.m_mainContent) - GameObject.Destroy(this.m_mainContent); - } - - public virtual void UpdateValue() - { - var value = IValue.Value; - - // if the type has changed fundamentally, make a new interactivevalue for it - var type = value == null - ? FallbackType - : ReflectionUtility.GetActualType(value); - - var ivalueType = InteractiveValue.GetIValueForType(type); - - if (ivalueType != IValue.GetType()) + if (IValue != null) { - IValue.OnDestroy(); - CreateIValue(value, FallbackType); - m_subContent.SetActive(false); + // If we changed states (always needs IValue change) + // or if the value is null, and the fallback type isnt string (we always want to edit strings). + if (State != prevState || (State != ValueState.String && Value.IsNullOrDestroyed())) + { + // need to return IValue + ReleaseIValue(); + SubContentShowWanted = false; + } } - IValue.OnValueUpdated(); - - IValue.RefreshElementsAfterUpdate(); + // Set label text + this.ValueLabelText = GetValueLabel(); } - public virtual void SetValue() => throw new NotImplementedException(); - - #region UI CONSTRUCTION - - internal bool m_constructedUI; - internal GameObject m_parentContent; - internal RectTransform m_mainRect; - internal GameObject m_mainContent; - internal GameObject m_subContent; - - // Make base UI holder for CacheObject, this doesnt actually display anything. - internal virtual void ConstructUI() + public ValueState GetStateForType(Type type) { - m_constructedUI = true; + if (LastValueType == type) + return State; - m_mainContent = UIFactory.CreateVerticalGroup(m_parentContent, "CacheObjectBase.MainContent", true, true, true, true, 0, default, - new Color(0.1f, 0.1f, 0.1f)); - m_mainRect = m_mainContent.GetComponent(); - m_mainRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 25); - - UIFactory.SetLayoutElement(m_mainContent, minHeight: 25, flexibleHeight: 9999, minWidth: 200, flexibleWidth: 5000); - - // subcontent - - m_subContent = UIFactory.CreateVerticalGroup(m_mainContent, "CacheObjectBase.SubContent", true, false, true, true, 0, default, - new Color(0.085f, 0.085f, 0.085f)); - UIFactory.SetLayoutElement(m_subContent, minHeight: 30, flexibleHeight: 9999, minWidth: 125, flexibleWidth: 9000); - - m_subContent.SetActive(false); - - IValue.m_subContentParent = m_subContent; + LastValueType = type; + if (type == typeof(bool)) + return ValueState.Boolean; + else if (type.IsPrimitive || type == typeof(decimal)) + return ValueState.Number; + else if (type == typeof(string)) + return ValueState.String; + else if (type.IsEnum) + return ValueState.Enum; + else if (type == typeof(Color) || type == typeof(Color32)) + return ValueState.Color; + else if (InteractiveValueStruct.SupportsType(type)) + return ValueState.ValueStruct; + else if (ReflectionUtility.IsDictionary(type)) + return ValueState.Dictionary; + else if (!typeof(Transform).IsAssignableFrom(type) && ReflectionUtility.IsEnumerable(type)) + return ValueState.Collection; + else + return ValueState.Unsupported; } - #endregion + protected string GetValueLabel() + { + string label = ""; + switch (State) + { + case ValueState.NotEvaluated: + return $"{NOT_YET_EVAL} ({SignatureHighlighter.Parse(FallbackType, true)})"; + + case ValueState.Exception: + return $"{LastException.ReflectionExToString()}"; + + // bool and number dont want the label for the value at all + case ValueState.Boolean: + case ValueState.Number: + return null; + + // and valuestruct also doesnt want it if we can parse it + case ValueState.ValueStruct: + if (ParseUtility.CanParse(LastValueType)) + return null; + break; + + // string wants it trimmed to max 200 chars + case ValueState.String: + if (!LastValueWasNull) + { + string s = Value as string; + return $"\"{ToStringUtility.PruneString(s, 200, 5)}\""; + } + break; + + // try to prefix the count of the collection for lists and dicts + case ValueState.Collection: + if (!LastValueWasNull) + { + if (Value is IList iList) + label = $"[{iList.Count}] "; + else if (Value is ICollection iCol) + label = $"[{iCol.Count}] "; + else + label = "[?] "; + } + break; + + case ValueState.Dictionary: + if (!LastValueWasNull) + { + if (Value is IDictionary iDict) + label = $"[{iDict.Count}] "; + else + label = "[?] "; + } + break; + } + + // Cases which dont return will append to ToStringWithType + + return label += ToStringUtility.ToStringWithType(Value, FallbackType, true); + } + + // Setting cell state from our model + + /// Return true if SetCell should abort, false if it should continue. + protected abstract bool SetCellEvaluateState(CacheObjectCell cell); + + public virtual void SetDataToCell(CacheObjectCell cell) + { + cell.NameLabel.text = NameLabelText; + cell.ValueLabel.gameObject.SetActive(true); + + cell.SubContentHolder.gameObject.SetActive(SubContentShowWanted); + if (IValue != null) + { + IValue.UIRoot.transform.SetParent(cell.SubContentHolder.transform, false); + IValue.SetLayout(); + } + + if (SetCellEvaluateState(cell)) + return; + + switch (State) + { + case ValueState.Exception: + SetValueState(cell, ValueStateArgs.Default); + break; + case ValueState.Boolean: + SetValueState(cell, new ValueStateArgs(false, toggleActive: true, applyActive: CanWrite)); + break; + case ValueState.Number: + SetValueState(cell, new ValueStateArgs(false, typeLabelActive: true, inputActive: true, applyActive: CanWrite)); + break; + case ValueState.String: + if (LastValueWasNull) + SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: true)); + else + SetValueState(cell, new ValueStateArgs(true, false, SignatureHighlighter.StringOrange, subContentButtonActive: true)); + break; + case ValueState.Enum: + SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: CanWrite)); + break; + case ValueState.Color: + case ValueState.ValueStruct: + if (ParseUtility.CanParse(LastValueType)) + SetValueState(cell, new ValueStateArgs(false, false, null, true, false, true, CanWrite, true, true)); + else + SetValueState(cell, new ValueStateArgs(true, inspectActive: true, subContentButtonActive: true)); + break; + case ValueState.Collection: + case ValueState.Dictionary: + SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull, subContentButtonActive: !LastValueWasNull)); + break; + case ValueState.Unsupported: + SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull)); + break; + } + + cell.RefreshSubcontentButton(); + } + + protected virtual void SetValueState(CacheObjectCell cell, ValueStateArgs args) + { + // main value label + if (args.valueActive) + { + cell.ValueLabel.text = ValueLabelText; + cell.ValueLabel.supportRichText = args.valueRichText; + cell.ValueLabel.color = args.valueColor; + } + else + cell.ValueLabel.text = ""; + + // Type label (for primitives) + cell.TypeLabel.gameObject.SetActive(args.typeLabelActive); + if (args.typeLabelActive) + cell.TypeLabel.text = SignatureHighlighter.Parse(LastValueType, false); + + // toggle for bools + cell.Toggle.gameObject.SetActive(args.toggleActive); + if (args.toggleActive) + { + cell.Toggle.interactable = CanWrite; + cell.Toggle.isOn = (bool)Value; + cell.ToggleText.text = Value.ToString(); + } + + // inputfield for numbers + cell.InputField.UIRoot.SetActive(args.inputActive); + if (args.inputActive) + { + cell.InputField.Text = ParseUtility.ToStringForInput(Value, LastValueType); + cell.InputField.Component.readOnly = !CanWrite; + } + + // apply for bool and numbers + cell.ApplyButton.Component.gameObject.SetActive(args.applyActive); + + // Inspect button only if last value not null. + if (cell.InspectButton != null) + cell.InspectButton.Component.gameObject.SetActive(args.inspectActive && !LastValueWasNull); + + // allow IValue for null strings though + cell.SubContentButton.Component.gameObject.SetActive(args.subContentButtonActive && (!LastValueWasNull || State == ValueState.String)); + } + + // CacheObjectCell Apply + + public virtual void OnCellApplyClicked() + { + if (State == ValueState.Boolean) + SetUserValue(this.CellView.Toggle.isOn); + else + { + if (ParseUtility.TryParse(CellView.InputField.Text, LastValueType, out object value, out Exception ex)) + { + SetUserValue(value); + } + else + { + ExplorerCore.LogWarning("Unable to parse input!"); + if (ex != null) + ExplorerCore.Log(ex.ReflectionExToString()); + } + } + + SetDataToCell(this.CellView); + } + + // IValues + + public virtual void OnCellSubContentToggle() + { + if (this.IValue == null) + { + var ivalueType = InteractiveValue.GetIValueTypeForState(State); + + if (ivalueType == null) + return; + + IValue = (InteractiveValue)Pool.Borrow(ivalueType); + CurrentIValueType = ivalueType; + + IValue.OnBorrowed(this); + IValue.SetValue(this.Value); + IValue.UIRoot.transform.SetParent(CellView.SubContentHolder.transform, false); + CellView.SubContentHolder.SetActive(true); + SubContentShowWanted = true; + + // update our cell after creating the ivalue (the value may have updated, make sure its consistent) + this.ProcessOnEvaluate(); + this.SetDataToCell(this.CellView); + } + else + { + SubContentShowWanted = !SubContentShowWanted; + CellView.SubContentHolder.SetActive(SubContentShowWanted); + + if (SubContentShowWanted && IValue.PendingValueWanted) + { + IValue.PendingValueWanted = false; + this.ProcessOnEvaluate(); + this.SetDataToCell(this.CellView); + IValue.SetValue(this.Value); + } + } + + CellView.RefreshSubcontentButton(); + } + + public virtual void ReleaseIValue() + { + if (IValue == null) + return; + + IValue.ReleaseFromOwner(); + Pool.Return(CurrentIValueType, IValue); + + IValue = null; + } + + internal static GameObject InactiveIValueHolder + { + get + { + if (!inactiveIValueHolder) + { + inactiveIValueHolder = new GameObject("Temp_IValue_Holder"); + GameObject.DontDestroyOnLoad(inactiveIValueHolder); + inactiveIValueHolder.transform.parent = UIManager.PoolHolder.transform; + inactiveIValueHolder.SetActive(false); + } + return inactiveIValueHolder; + } + } + private static GameObject inactiveIValueHolder; + + // Value state args helper + + public struct ValueStateArgs + { + public ValueStateArgs(bool valueActive = true, bool valueRichText = true, Color? valueColor = null, + bool typeLabelActive = false, bool toggleActive = false, bool inputActive = false, bool applyActive = false, + bool inspectActive = false, bool subContentButtonActive = false) + { + this.valueActive = valueActive; + this.valueRichText = valueRichText; + this.valueColor = valueColor == null ? Color.white : (Color)valueColor; + this.typeLabelActive = typeLabelActive; + this.toggleActive = toggleActive; + this.inputActive = inputActive; + this.applyActive = applyActive; + this.inspectActive = inspectActive; + this.subContentButtonActive = subContentButtonActive; + } + + public static ValueStateArgs Default => _default; + private static ValueStateArgs _default = new ValueStateArgs(true); + + public bool valueActive, valueRichText, typeLabelActive, toggleActive, + inputActive, applyActive, inspectActive, subContentButtonActive; + + public Color valueColor; + } } } diff --git a/src/UI/CacheObject/CachePaired.cs b/src/UI/CacheObject/CachePaired.cs deleted file mode 100644 index ec508df..0000000 --- a/src/UI/CacheObject/CachePaired.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityExplorer.UI; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.UI.InteractiveValues; - -namespace UnityExplorer.UI.CacheObject -{ - public enum PairTypes - { - Key, - Value - } - - public class CachePaired : CacheObjectBase - { - public override Type FallbackType => PairType == PairTypes.Key - ? ParentDictionary.m_typeOfKeys - : ParentDictionary.m_typeofValues; - - public override bool CanWrite => false; // todo? - - public PairTypes PairType; - public int Index { get; private set; } - public InteractiveDictionary ParentDictionary { get; private set; } - internal IDictionary RefIDict; - - public CachePaired(int index, InteractiveDictionary parentDict, IDictionary refIDict, PairTypes pairType, GameObject parentContent) - { - Index = index; - ParentDictionary = parentDict; - RefIDict = refIDict; - this.PairType = pairType; - this.m_parentContent = parentContent; - } - - public override void CreateIValue(object value, Type fallbackType) - { - IValue = InteractiveValue.Create(value, fallbackType); - IValue.Owner = this; - } - - #region UI CONSTRUCTION - - internal override void ConstructUI() - { - base.ConstructUI(); - - Color bgColor = this.PairType == PairTypes.Key - ? new Color(0.07f, 0.07f, 0.07f) - : new Color(0.1f, 0.1f, 0.1f); - - var rowObj = UIFactory.CreateHorizontalGroup(m_mainContent, "PairedGroup", false, false, true, true, 0, new Vector4(0,0,5,2), - bgColor); - - string lbl = $"{this.PairType}"; - if (this.PairType == PairTypes.Key) - lbl = $"[{Index}] {lbl}"; - - var indexLabel = UIFactory.CreateLabel(rowObj, "IndexLabel", lbl, TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(indexLabel.gameObject, minWidth: 80, flexibleWidth: 30, minHeight: 25); - - IValue.m_mainContentParent = rowObj; - } - - #endregion - } -} diff --git a/src/UI/CacheObject/CacheProperty.cs b/src/UI/CacheObject/CacheProperty.cs index 5e3620b..6dfdeee 100644 --- a/src/UI/CacheObject/CacheProperty.cs +++ b/src/UI/CacheObject/CacheProperty.cs @@ -1,71 +1,67 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Reflection; -using UnityExplorer.UI; -using UnityExplorer.Core.Unity; -using UnityEngine; +using System.Text; +using UnityExplorer.UI.Inspectors; namespace UnityExplorer.UI.CacheObject { public class CacheProperty : CacheMember { - public override Type FallbackType => (MemInfo as PropertyInfo).PropertyType; + public PropertyInfo PropertyInfo { get; internal set; } + public override Type DeclaringType => PropertyInfo.DeclaringType; + public override bool CanWrite => PropertyInfo.CanWrite; + public override bool IsStatic => m_isStatic ?? (bool)(m_isStatic = PropertyInfo.GetAccessors(true)[0].IsStatic); + private bool? m_isStatic; - public override bool IsStatic => (MemInfo as PropertyInfo).GetAccessors(true)[0].IsStatic; + public override bool ShouldAutoEvaluate => !HasArguments; - public CacheProperty(PropertyInfo propertyInfo, object declaringInstance, GameObject parent) : base(propertyInfo, declaringInstance, parent) + public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member) { - this.m_arguments = propertyInfo.GetIndexParameters(); - this.m_argumentInput = new string[m_arguments.Length]; + base.SetInspectorOwner(inspector, member); - CreateIValue(null, propertyInfo.PropertyType); + Arguments = PropertyInfo.GetIndexParameters(); } - public override void UpdateReflection() + protected override object TryEvaluate() { - if (HasParameters && !m_isEvaluating) + try { - // Need to enter parameters first. + if (HasArguments) + return PropertyInfo.GetValue(DeclaringInstance, this.Evaluator.TryParseArguments()); + + var ret = PropertyInfo.GetValue(DeclaringInstance, null); + HadException = false; + LastException = null; + return ret; + } + catch (Exception ex) + { + HadException = true; + LastException = ex; + return null; + } + } + + protected override void TrySetValue(object value) + { + if (!CanWrite) return; - } - var pi = MemInfo as PropertyInfo; - - if (pi.CanRead) + try { - var target = pi.GetAccessors(true)[0].IsStatic ? null : DeclaringInstance; + bool _static = PropertyInfo.GetAccessors(true)[0].IsStatic; - IValue.Value = pi.GetValue(target, ParseArguments()); - - m_evaluated = true; - ReflectionException = null; + if (HasArguments) + PropertyInfo.SetValue(DeclaringInstance, value, Evaluator.TryParseArguments()); + else + PropertyInfo.SetValue(DeclaringInstance, value, null); } - else + catch (Exception ex) { - if (FallbackType == typeof(string)) - { - IValue.Value = ""; - } - else if (FallbackType.IsPrimitive) - { - IValue.Value = Activator.CreateInstance(FallbackType); - } - m_evaluated = true; - ReflectionException = null; + ExplorerCore.LogWarning(ex); } } - - public override void SetValue() - { - var pi = MemInfo as PropertyInfo; - var target = pi.GetAccessors()[0].IsStatic ? null : DeclaringInstance; - - pi.SetValue(target, IValue.Value, ParseArguments()); - - if (this.ParentInspector?.ParentMember != null) - this.ParentInspector.ParentMember.SetValue(); - } } } diff --git a/src/UI/CacheObject/ICacheObjectController.cs b/src/UI/CacheObject/ICacheObjectController.cs new file mode 100644 index 0000000..0e5126f --- /dev/null +++ b/src/UI/CacheObject/ICacheObjectController.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.CacheObject.Views; + +namespace UnityExplorer.UI.CacheObject +{ + public interface ICacheObjectController + { + CacheObjectBase ParentCacheObject { get; } + + object Target { get; } + Type TargetType { get; } + + bool CanWrite { get; } + } + + public static class CacheObjectControllerHelper + { + // Helper so that this doesn't need to be copy+pasted between each implementation of the interface + + public static void SetCell(CacheObjectCell cell, int index, IList cachedEntries, Action onDataSetToCell) + { + if (index < 0 || index >= cachedEntries.Count) + { + if (cell.Occupant != null) + cell.Occupant.UnlinkFromView(); + + cell.Disable(); + return; + } + + var entry = (CacheObjectBase)cachedEntries[index]; + + if (entry.CellView != null && entry.CellView != cell) + entry.UnlinkFromView(); + + if (cell.Occupant != null && cell.Occupant != entry) + cell.Occupant.UnlinkFromView(); + + if (entry.CellView != cell) + entry.SetView(cell); + + entry.SetDataToCell(cell); + + onDataSetToCell?.Invoke(cell); + } + } +} diff --git a/src/UI/CacheObject/Views/CacheConfigCell.cs b/src/UI/CacheObject/Views/CacheConfigCell.cs new file mode 100644 index 0000000..cf1c30e --- /dev/null +++ b/src/UI/CacheObject/Views/CacheConfigCell.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; + +namespace UnityExplorer.UI.CacheObject.Views +{ + public class ConfigEntryCell : CacheObjectCell + { + public override GameObject CreateContent(GameObject parent) + { + // Main layout + + UIRoot = UIFactory.CreateUIObject(this.GetType().Name, parent, new Vector2(100, 30)); + Rect = UIRoot.GetComponent(); + UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, 4, 4, 4, 4, 4, childAlignment: TextAnchor.UpperLeft); + UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600); + UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // Left label + + NameLabel = UIFactory.CreateLabel(UIRoot, "NameLabel", "", TextAnchor.MiddleLeft); + NameLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + UIFactory.SetLayoutElement(NameLabel.gameObject, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 300); + NameLayout = NameLabel.GetComponent(); + + // horizontal group + + var horiGroup = UIFactory.CreateUIObject("RightHoriGroup", UIRoot); + UIFactory.SetLayoutGroup(horiGroup, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft); + UIFactory.SetLayoutElement(horiGroup, minHeight: 25, minWidth: 200, flexibleWidth: 9999, flexibleHeight: 800); + + SubContentButton = UIFactory.CreateButton(horiGroup, "SubContentButton", "▲", subInactiveColor); + UIFactory.SetLayoutElement(SubContentButton.Component.gameObject, minWidth: 25, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); + SubContentButton.OnClick += SubContentClicked; + + // Type label + + TypeLabel = UIFactory.CreateLabel(horiGroup, "TypeLabel", "", TextAnchor.MiddleLeft); + TypeLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + UIFactory.SetLayoutElement(TypeLabel.gameObject, minHeight: 25, flexibleHeight: 150, minWidth: 60, flexibleWidth: 0); + + // Bool and number value interaction + + var toggleObj = UIFactory.CreateToggle(horiGroup, "Toggle", out Toggle, out ToggleText); + UIFactory.SetLayoutElement(toggleObj, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); + ToggleText.color = SignatureHighlighter.KeywordBlue; + Toggle.onValueChanged.AddListener(ToggleClicked); + + InputField = UIFactory.CreateInputField(horiGroup, "InputField", "..."); + UIFactory.SetLayoutElement(InputField.UIRoot, minWidth: 150, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); + + // Apply + + ApplyButton = UIFactory.CreateButton(horiGroup, "ApplyButton", "Apply", new Color(0.15f, 0.19f, 0.15f)); + UIFactory.SetLayoutElement(ApplyButton.Component.gameObject, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); + ApplyButton.OnClick += ApplyClicked; + + // Main value label + + ValueLabel = UIFactory.CreateLabel(horiGroup, "ValueLabel", "Value goes here", TextAnchor.MiddleLeft); + ValueLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + UIFactory.SetLayoutElement(ValueLabel.gameObject, minHeight: 25, flexibleHeight: 150, flexibleWidth: 9999); + + // Subcontent + + SubContentHolder = UIFactory.CreateUIObject("SubContent", UIRoot); + UIFactory.SetLayoutElement(SubContentHolder.gameObject, minHeight: 30, flexibleHeight: 600, minWidth: 100, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(SubContentHolder, true, true, true, true, 2, childAlignment: TextAnchor.UpperLeft); + //SubContentHolder.AddComponent().verticalFit = ContentSizeFitter.FitMode.MinSize; + SubContentHolder.SetActive(false); + + // Bottom separator + var separator = UIFactory.CreateUIObject("BottomSeperator", UIRoot); + UIFactory.SetLayoutElement(separator, minHeight: 1, flexibleHeight: 0, flexibleWidth: 9999); + separator.AddComponent().color = Color.black; + + return UIRoot; + } + + protected override void ConstructEvaluateHolder(GameObject parent) { } + } +} diff --git a/src/UI/CacheObject/Views/CacheKeyValuePairCell.cs b/src/UI/CacheObject/Views/CacheKeyValuePairCell.cs new file mode 100644 index 0000000..3b1b8af --- /dev/null +++ b/src/UI/CacheObject/Views/CacheKeyValuePairCell.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.IValues; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.CacheObject.Views +{ + public class CacheKeyValuePairCell : CacheObjectCell + { + public Image Image { get; private set; } + public InteractiveDictionary DictOwner => Occupant.Owner as InteractiveDictionary; + + public LayoutElement KeyGroupLayout; + public Text KeyLabel; + public ButtonRef KeyInspectButton; + public InputFieldRef KeyInputField; + public Text KeyInputTypeLabel; + + public static Color EvenColor = new Color(0.07f, 0.07f, 0.07f); + public static Color OddColor = new Color(0.063f, 0.063f, 0.063f); + + public int AdjustedWidth => (int)Rect.rect.width - 70; + + //public int HalfWidth => (int)(0.5f * Rect.rect.width) - 75; + //public int AdjustedKeyWidth => HalfWidth - 50; + //public int AdjustedRightWidth => HalfWidth; + + private void KeyInspectClicked() + { + InspectorManager.Inspect((Occupant as CacheKeyValuePair).DictKey, this.Occupant); + } + + public override GameObject CreateContent(GameObject parent) + { + var root = base.CreateContent(parent); + + Image = root.AddComponent(); + + this.NameLayout.minWidth = 70; + this.NameLayout.flexibleWidth = 0; + this.NameLayout.minHeight = 30; + this.NameLayout.flexibleHeight = 0; + this.NameLabel.alignment = TextAnchor.MiddleRight; + + this.RightGroupLayout.minWidth = AdjustedWidth * 0.55f; + + // Key area + var keyGroup = UIFactory.CreateUIObject("KeyHolder", root.transform.Find("HoriGroup").gameObject); + UIFactory.SetLayoutGroup(keyGroup, false, false, true, true, 2, 0, 0, 4, 4, childAlignment: TextAnchor.MiddleLeft); + KeyGroupLayout = UIFactory.SetLayoutElement(keyGroup, minHeight: 30, minWidth: (int)(AdjustedWidth * 0.44f), flexibleWidth: 0); + + // set to be after the NameLabel (our index label), and before the main horizontal group. + keyGroup.transform.SetSiblingIndex(1); + + // key Inspect + + KeyInspectButton = UIFactory.CreateButton(keyGroup, "KeyInspectButton", "Inspect", new Color(0.15f, 0.15f, 0.15f)); + UIFactory.SetLayoutElement(KeyInspectButton.Component.gameObject, minWidth: 60, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); + KeyInspectButton.OnClick += KeyInspectClicked; + + // label + + KeyLabel = UIFactory.CreateLabel(keyGroup, "KeyLabel", "empty", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(KeyLabel.gameObject, minWidth: 50, flexibleWidth: 999, minHeight: 25); + + // Type label for input field + + KeyInputTypeLabel = UIFactory.CreateLabel(keyGroup, "InputTypeLabel", "null", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(KeyInputTypeLabel.gameObject, minWidth: 55, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); + + // input field + + KeyInputField = UIFactory.CreateInputField(keyGroup, "KeyInput", "empty"); + UIFactory.SetLayoutElement(KeyInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 0, preferredWidth: 200); + //KeyInputField.lineType = InputField.LineType.MultiLineNewline; + KeyInputField.Component.readOnly = true; + + return root; + } + + protected override void ConstructEvaluateHolder(GameObject parent) + { + // not used + } + } +} diff --git a/src/UI/CacheObject/Views/CacheListEntryCell.cs b/src/UI/CacheObject/Views/CacheListEntryCell.cs new file mode 100644 index 0000000..16341fe --- /dev/null +++ b/src/UI/CacheObject/Views/CacheListEntryCell.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.IValues; + +namespace UnityExplorer.UI.CacheObject.Views +{ + public class CacheListEntryCell : CacheObjectCell + { + public Image Image { get; private set; } + public InteractiveList ListOwner => Occupant.Owner as InteractiveList; + + public static Color EvenColor = new Color(0.12f, 0.12f, 0.12f); + public static Color OddColor = new Color(0.1f, 0.1f, 0.1f); + + public override GameObject CreateContent(GameObject parent) + { + var root = base.CreateContent(parent); + + Image = root.AddComponent(); + + this.NameLayout.minWidth = 40; + this.NameLayout.flexibleWidth = 50; + this.NameLayout.minHeight = 25; + this.NameLayout.flexibleHeight = 0; + this.NameLabel.alignment = TextAnchor.MiddleRight; + + return root; + } + + protected override void ConstructEvaluateHolder(GameObject parent) + { + // not used + } + + //protected override void ConstructUpdateToggle(GameObject parent) + //{ + // // not used + //} + } +} diff --git a/src/UI/CacheObject/Views/CacheMemberCell.cs b/src/UI/CacheObject/Views/CacheMemberCell.cs new file mode 100644 index 0000000..5c3e9b1 --- /dev/null +++ b/src/UI/CacheObject/Views/CacheMemberCell.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.CacheObject.Views +{ + public class CacheMemberCell : CacheObjectCell + { + //public ReflectionInspector Owner { get; set; } + + public CacheMember MemberOccupant => Occupant as CacheMember; + + public GameObject EvaluateHolder; + public ButtonRef EvaluateButton; + + //public Toggle UpdateToggle; + + protected virtual void EvaluateClicked() + { + this.MemberOccupant.OnEvaluateClicked(); + } + + protected override void ConstructEvaluateHolder(GameObject parent) + { + // Evaluate vert group + + EvaluateHolder = UIFactory.CreateUIObject("EvalGroup", parent); + UIFactory.SetLayoutGroup(EvaluateHolder, false, false, true, true, 3); + UIFactory.SetLayoutElement(EvaluateHolder, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 775); + + EvaluateButton = UIFactory.CreateButton(EvaluateHolder, "EvaluateButton", "Evaluate", new Color(0.15f, 0.15f, 0.15f)); + UIFactory.SetLayoutElement(EvaluateButton.Component.gameObject, minWidth: 100, minHeight: 25); + EvaluateButton.OnClick += EvaluateClicked; + } + + //protected override void ConstructUpdateToggle(GameObject parent) + //{ + // // Auto-update toggle + // + // var updateToggle = UIFactory.CreateToggle(parent, "AutoUpdate", out UpdateToggle, out Text autoText); + // UIFactory.SetLayoutElement(updateToggle, minHeight: 25, minWidth: 30, flexibleWidth: 0, flexibleHeight: 0); + // GameObject.Destroy(autoText); + // UpdateToggle.isOn = false; + // UpdateToggle.onValueChanged.AddListener((bool val) => { MemberOccupant.AutoUpdateWanted = val; }); + //} + } +} diff --git a/src/UI/CacheObject/Views/CacheObjectCell.cs b/src/UI/CacheObject/Views/CacheObjectCell.cs new file mode 100644 index 0000000..afb029c --- /dev/null +++ b/src/UI/CacheObject/Views/CacheObjectCell.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.IValues; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.CacheObject.Views +{ + public abstract class CacheObjectCell : ICell + { + #region ICell + + public float DefaultHeight => 30f; + + public GameObject UIRoot { get; set; } + + public bool Enabled => m_enabled; + private bool m_enabled; + + public RectTransform Rect { get; set; } + + public void Disable() + { + m_enabled = false; + UIRoot.SetActive(false); + } + + public void Enable() + { + m_enabled = true; + UIRoot.SetActive(true); + } + + #endregion + + public CacheObjectBase Occupant { get; set; } + public bool SubContentActive => SubContentHolder.activeSelf; + + public LayoutElement NameLayout; + public LayoutElement RightGroupLayout; + + public Text NameLabel; + public Text TypeLabel; + public Text ValueLabel; + public Toggle Toggle; + public Text ToggleText; + public InputFieldRef InputField; + + public ButtonRef InspectButton; + public ButtonRef SubContentButton; + public ButtonRef ApplyButton; + + public GameObject SubContentHolder; + + protected virtual void ApplyClicked() + { + Occupant.OnCellApplyClicked(); + } + + protected virtual void InspectClicked() + { + InspectorManager.Inspect(Occupant.Value, this.Occupant); + } + + protected virtual void ToggleClicked(bool value) + { + ToggleText.text = value.ToString(); + } + + protected virtual void SubContentClicked() + { + this.Occupant.OnCellSubContentToggle(); + } + + public readonly Color subInactiveColor = new Color(0.23f, 0.23f, 0.23f); + public readonly Color subActiveColor = new Color(0.23f, 0.33f, 0.23f); + + public void RefreshSubcontentButton() + { + if (!this.SubContentHolder.activeSelf) + { + this.SubContentButton.ButtonText.text = "▲"; + RuntimeProvider.Instance.SetColorBlock(SubContentButton.Component, subInactiveColor, subInactiveColor * 1.3f); + } + else + { + this.SubContentButton.ButtonText.text = "▼"; + RuntimeProvider.Instance.SetColorBlock(SubContentButton.Component, subActiveColor, subActiveColor * 1.3f); + } + } + + protected abstract void ConstructEvaluateHolder(GameObject parent); + + + public virtual GameObject CreateContent(GameObject parent) + { + // Main layout + + UIRoot = UIFactory.CreateUIObject(this.GetType().Name, parent, new Vector2(100, 30)); + Rect = UIRoot.GetComponent(); + UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, childAlignment: TextAnchor.UpperLeft); + UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600); + UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + var horiRow = UIFactory.CreateUIObject("HoriGroup", UIRoot); + UIFactory.SetLayoutElement(horiRow, minHeight: 29, flexibleHeight: 150, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(horiRow, false, false, true, true, 5, 2, childAlignment: TextAnchor.UpperLeft); + horiRow.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // Left name label + + NameLabel = UIFactory.CreateLabel(horiRow, "NameLabel", "", TextAnchor.MiddleLeft); + NameLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + UIFactory.SetLayoutElement(NameLabel.gameObject, minHeight: 25, minWidth: 20, flexibleHeight: 300, flexibleWidth: 0); + NameLayout = NameLabel.GetComponent(); + + // Right vertical group + + var rightGroupHolder = UIFactory.CreateUIObject("RightGroup", horiRow); + UIFactory.SetLayoutGroup(rightGroupHolder, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft); + UIFactory.SetLayoutElement(rightGroupHolder, minHeight: 25, minWidth: 200, flexibleWidth: 9999, flexibleHeight: 800); + RightGroupLayout = rightGroupHolder.GetComponent(); + + ConstructEvaluateHolder(rightGroupHolder); + + // Right horizontal group + + var rightHoriGroup = UIFactory.CreateUIObject("RightHoriGroup", rightGroupHolder); + UIFactory.SetLayoutGroup(rightHoriGroup, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft); + UIFactory.SetLayoutElement(rightHoriGroup, minHeight: 25, minWidth: 200, flexibleWidth: 9999, flexibleHeight: 800); + + SubContentButton = UIFactory.CreateButton(rightHoriGroup, "SubContentButton", "▲", subInactiveColor); + UIFactory.SetLayoutElement(SubContentButton.Component.gameObject, minWidth: 25, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); + SubContentButton.OnClick += SubContentClicked; + + // Type label + + TypeLabel = UIFactory.CreateLabel(rightHoriGroup, "ReturnLabel", "", TextAnchor.MiddleLeft); + TypeLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + UIFactory.SetLayoutElement(TypeLabel.gameObject, minHeight: 25, flexibleHeight: 150, minWidth: 60, flexibleWidth: 0); + + // Bool and number value interaction + + var toggleObj = UIFactory.CreateToggle(rightHoriGroup, "Toggle", out Toggle, out ToggleText); + UIFactory.SetLayoutElement(toggleObj, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); + ToggleText.color = SignatureHighlighter.KeywordBlue; + Toggle.onValueChanged.AddListener(ToggleClicked); + + InputField = UIFactory.CreateInputField(rightHoriGroup, "InputField", "..."); + UIFactory.SetLayoutElement(InputField.UIRoot, minWidth: 150, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); + + // Apply + + ApplyButton = UIFactory.CreateButton(rightHoriGroup, "ApplyButton", "Apply", new Color(0.15f, 0.19f, 0.15f)); + UIFactory.SetLayoutElement(ApplyButton.Component.gameObject, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); + ApplyButton.OnClick += ApplyClicked; + + // Inspect + + InspectButton = UIFactory.CreateButton(rightHoriGroup, "InspectButton", "Inspect", new Color(0.15f, 0.15f, 0.15f)); + UIFactory.SetLayoutElement(InspectButton.Component.gameObject, minWidth: 70, flexibleWidth: 0, minHeight: 25); + InspectButton.OnClick += InspectClicked; + + // Main value label + + ValueLabel = UIFactory.CreateLabel(rightHoriGroup, "ValueLabel", "Value goes here", TextAnchor.MiddleLeft); + ValueLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + UIFactory.SetLayoutElement(ValueLabel.gameObject, minHeight: 25, flexibleHeight: 150, flexibleWidth: 9999); + + // Subcontent + + SubContentHolder = UIFactory.CreateUIObject("SubContent", UIRoot); + UIFactory.SetLayoutElement(SubContentHolder.gameObject, minHeight: 30, flexibleHeight: 600, minWidth: 100, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(SubContentHolder, true, true, true, true, 2, childAlignment: TextAnchor.UpperLeft); + //SubContentHolder.AddComponent().verticalFit = ContentSizeFitter.FitMode.MinSize; + SubContentHolder.SetActive(false); + + // Bottom separator + var separator = UIFactory.CreateUIObject("BottomSeperator", UIRoot); + UIFactory.SetLayoutElement(separator, minHeight: 1, flexibleHeight: 0, flexibleWidth: 9999); + separator.AddComponent().color = Color.black; + + return UIRoot; + } + } +} diff --git a/src/UI/CacheObject/Views/EvaluateWidget.cs b/src/UI/CacheObject/Views/EvaluateWidget.cs new file mode 100644 index 0000000..46d8cf9 --- /dev/null +++ b/src/UI/CacheObject/Views/EvaluateWidget.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.CacheObject.Views +{ + public class EvaluateWidget : IPooledObject + { + public CacheMember Owner { get; set; } + + public GameObject UIRoot { get; set; } + public float DefaultHeight => -1f; + + private ParameterInfo[] arguments; + private string[] argumentInput; + + private GameObject argHolder; + private readonly List argRows = new List(); + private readonly List argLabels = new List(); + + private Type[] genericArguments; + private string[] genericInput; + + private GameObject genericArgHolder; + private readonly List genericArgRows = new List(); + private readonly List genericArgLabels = new List(); + private readonly List genericAutocompleters = new List(); + + //private readonly List inputFields = new List(); + private readonly List argInputFields = new List(); + private readonly List genericInputFields = new List(); + + public void OnBorrowedFromPool(CacheMember owner) + { + this.Owner = owner; + + arguments = owner.Arguments; + argumentInput = new string[arguments.Length]; + + genericArguments = owner.GenericArguments; + genericInput = new string[genericArguments.Length]; + + SetArgRows(); + + this.UIRoot.SetActive(true); + } + + public void OnReturnToPool() + { + foreach (var input in argInputFields) + input.Text = ""; + foreach (var input in genericInputFields) + input.Text = ""; + + this.Owner = null; + } + + public Type[] TryParseGenericArguments() + { + Type[] outArgs = new Type[genericArguments.Length]; + + for (int i = 0; i < genericArguments.Length; i++) + { + outArgs[i] = ReflectionUtility.GetTypeByName(genericInput[i]) + ?? throw new Exception($"Could not find any type by name '{genericInput[i]}'!"); + } + + return outArgs; + } + + public object[] TryParseArguments() + { + object[] outArgs = new object[arguments.Length]; + + for (int i = 0; i < arguments.Length; i++) + { + var arg = arguments[i]; + var input = argumentInput[i]; + + var type = arg.ParameterType; + if (type.IsByRef) + type = type.GetElementType(); + + if (type == typeof(string)) + { + outArgs[i] = input; + continue; + } + + if (string.IsNullOrEmpty(input)) + { + if (arg.IsOptional) + outArgs[i] = arg.DefaultValue; + else + outArgs[i] = null; + continue; + } + + if (!ParseUtility.TryParse(input, type, out outArgs[i], out Exception ex)) + { + outArgs[i] = null; + ExplorerCore.LogWarning($"Cannot parse argument '{arg.Name}' ({arg.ParameterType.Name})" + + $"{(ex == null ? "" : $", {ex.GetType().Name}: {ex.Message}")}"); + } + } + + return outArgs; + } + + private void SetArgRows() + { + if (genericArguments.Any()) + { + genericArgHolder.SetActive(true); + SetGenericRows(); + } + else + genericArgHolder.SetActive(false); + + if (arguments.Any()) + { + argHolder.SetActive(true); + SetNormalArgRows(); + } + else + argHolder.SetActive(false); + } + + private void SetGenericRows() + { + for (int i = 0; i < genericArguments.Length || i < genericArgRows.Count; i++) + { + if (i >= genericArguments.Length) + { + if (i >= genericArgRows.Count) + break; + else + // exceeded actual args, but still iterating so there must be views left, disable them + genericArgRows[i].SetActive(false); + continue; + } + + var arg = genericArguments[i]; + + if (i >= genericArgRows.Count) + AddArgRow(i, true); + + genericArgRows[i].SetActive(true); + + var autoCompleter = genericAutocompleters[i]; + autoCompleter.BaseType = arg; + autoCompleter.CacheTypes(); + + var constraints = arg.GetGenericParameterConstraints(); + autoCompleter.GenericConstraints = constraints; + + var sb = new StringBuilder($"{arg.Name}"); + + for (int j = 0; j < constraints.Length; j++) + { + if (j == 0) sb.Append(' ').Append('('); + else sb.Append(',').Append(' '); + + sb.Append(SignatureHighlighter.Parse(constraints[j], false)); + + if (j + 1 == constraints.Length) + sb.Append(')'); + } + + genericArgLabels[i].text = sb.ToString(); + } + } + + private void SetNormalArgRows() + { + for (int i = 0; i < arguments.Length || i < argRows.Count; i++) + { + if (i >= arguments.Length) + { + if (i >= argRows.Count) + break; + else + // exceeded actual args, but still iterating so there must be views left, disable them + argRows[i].SetActive(false); + continue; + } + + var arg = arguments[i]; + + + if (i >= argRows.Count) + AddArgRow(i, false); + + argRows[i].SetActive(true); + argLabels[i].text = $"{SignatureHighlighter.Parse(arg.ParameterType, false)} {arg.Name}"; + if (arg.ParameterType == typeof(string)) + argInputFields[i].PlaceholderText.text = ""; + else + { + var elemType = arg.ParameterType; + if (elemType.IsByRef) + elemType = elemType.GetElementType(); + argInputFields[i].PlaceholderText.text = $"eg. {ParseUtility.GetExampleInput(elemType)}"; + } + } + } + + private void AddArgRow(int index, bool generic) + { + if (!generic) + AddArgRow(index, argHolder, argRows, argLabels, argumentInput, false); + else + AddArgRow(index, genericArgHolder, genericArgRows, genericArgLabels, genericInput, true); + } + + private void AddArgRow(int index, GameObject parent, List objectList, List labelList, string[] inputArray, bool generic) + { + var horiGroup = UIFactory.CreateUIObject("ArgRow_" + index, parent); + UIFactory.SetLayoutElement(horiGroup, minHeight: 25, flexibleHeight: 50, minWidth: 50, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(horiGroup, false, false, true, true, 5); + horiGroup.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + objectList.Add(horiGroup); + + var label = UIFactory.CreateLabel(horiGroup, "ArgLabel", "not set", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(label.gameObject, minWidth: 40, flexibleWidth: 90, minHeight: 25, flexibleHeight: 50); + labelList.Add(label); + label.horizontalOverflow = HorizontalWrapMode.Wrap; + + var inputField = UIFactory.CreateInputField(horiGroup, "InputField", "..."); + UIFactory.SetLayoutElement(inputField.UIRoot, minHeight: 25, flexibleHeight: 50, minWidth: 100, flexibleWidth: 1000); + inputField.Component.lineType = InputField.LineType.MultiLineNewline; + inputField.UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + inputField.OnValueChanged += (string val) => { inputArray[index] = val; }; + + if (!generic) + argInputFields.Add(inputField); + else + genericInputFields.Add(inputField); + + if (generic) + genericAutocompleters.Add(new TypeCompleter(null, inputField)); + } + + public GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "EvaluateWidget", false, false, true, true, 3, new Vector4(2, 2, 2, 2), + new Color(0.15f, 0.15f, 0.15f)); + UIFactory.SetLayoutElement(UIRoot, minWidth: 50, flexibleWidth: 9999, minHeight: 50, flexibleHeight: 800); + //UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // generic args + this.genericArgHolder = UIFactory.CreateUIObject("GenericHolder", UIRoot); + UIFactory.SetLayoutElement(genericArgHolder, flexibleWidth: 1000); + var genericsTitle = UIFactory.CreateLabel(genericArgHolder, "GenericsTitle", "Generic Arguments", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(genericsTitle.gameObject, minHeight: 25, flexibleWidth: 1000); + UIFactory.SetLayoutGroup(genericArgHolder, false, false, true, true, 3); + UIFactory.SetLayoutElement(genericArgHolder, minHeight: 25, flexibleHeight: 750, minWidth: 50, flexibleWidth: 9999); + //genericArgHolder.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // args + this.argHolder = UIFactory.CreateUIObject("ArgHolder", UIRoot); + UIFactory.SetLayoutElement(argHolder, flexibleWidth: 1000); + var argsTitle = UIFactory.CreateLabel(argHolder, "ArgsTitle", "Arguments", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(argsTitle.gameObject, minHeight: 25, flexibleWidth: 1000); + UIFactory.SetLayoutGroup(argHolder, false, false, true, true, 3); + UIFactory.SetLayoutElement(argHolder, minHeight: 25, flexibleHeight: 750, minWidth: 50, flexibleWidth: 9999); + //argHolder.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // evaluate button + var evalButton = UIFactory.CreateButton(UIRoot, "EvaluateButton", "Evaluate", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(evalButton.Component.gameObject, minHeight: 25, minWidth: 150, flexibleWidth: 0); + evalButton.OnClick += () => + { + Owner.EvaluateAndSetCell(); + }; + + return UIRoot; + } + } +} diff --git a/src/UI/IValues/InteractiveColor.cs b/src/UI/IValues/InteractiveColor.cs new file mode 100644 index 0000000..3778b49 --- /dev/null +++ b/src/UI/IValues/InteractiveColor.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CacheObject; + +namespace UnityExplorer.UI.IValues +{ + public class InteractiveColor : InteractiveValue + { + public bool IsValueColor32; + + public Color EditedColor; + + private Image m_colorImage; + private readonly InputFieldRef[] m_inputs = new InputFieldRef[4]; + private readonly Slider[] m_sliders = new Slider[4]; + + private ButtonRef m_applyButton; + + private static readonly string[] fieldNames = new[] { "R", "G", "B", "A" }; + + public override void OnBorrowed(CacheObjectBase owner) + { + base.OnBorrowed(owner); + + m_applyButton.Component.gameObject.SetActive(owner.CanWrite); + + foreach (var slider in m_sliders) + slider.interactable = owner.CanWrite; + foreach (var input in m_inputs) + input.Component.readOnly = !owner.CanWrite; + } + + // owner setting value to this + public override void SetValue(object value) + { + OnOwnerSetValue(value); + } + + private void OnOwnerSetValue(object value) + { + if (value is Color32 c32) + { + IsValueColor32 = true; + EditedColor = c32; + m_inputs[0].Text = c32.r.ToString(); + m_inputs[1].Text = c32.g.ToString(); + m_inputs[2].Text = c32.b.ToString(); + m_inputs[3].Text = c32.a.ToString(); + foreach (var slider in m_sliders) + slider.maxValue = 255; + } + else + { + IsValueColor32 = false; + EditedColor = (Color)value; + m_inputs[0].Text = EditedColor.r.ToString(); + m_inputs[1].Text = EditedColor.g.ToString(); + m_inputs[2].Text = EditedColor.b.ToString(); + m_inputs[3].Text = EditedColor.a.ToString(); + foreach (var slider in m_sliders) + slider.maxValue = 1; + } + + if (m_colorImage) + m_colorImage.color = EditedColor; + } + + // setting value to owner + + public void SetValueToOwner() + { + if (IsValueColor32) + CurrentOwner.SetUserValue((Color32)EditedColor); + else + CurrentOwner.SetUserValue(EditedColor); + } + + private void SetColorField(float val, int fieldIndex) + { + switch (fieldIndex) + { + case 0: EditedColor.r = val; break; + case 1: EditedColor.g = val; break; + case 2: EditedColor.b = val; break; + case 3: EditedColor.a = val; break; + } + + if (m_colorImage) + m_colorImage.color = EditedColor; + } + + private void OnInputChanged(string val, int fieldIndex) + { + try + { + float f; + if (IsValueColor32) + { + byte value = byte.Parse(val); + m_sliders[fieldIndex].value = value; + f = (float)((decimal)value / 255); + } + else + { + f = float.Parse(val); + m_sliders[fieldIndex].value = f; + } + + SetColorField(f, fieldIndex); + } + catch (ArgumentException) { } // ignore bad user input + catch (FormatException) { } + catch (OverflowException) { } + catch (Exception ex) + { + ExplorerCore.LogWarning("InteractiveColor OnInput: " + ex.ToString()); + } + } + + private void OnSliderValueChanged(float val, int fieldIndex) + { + try + { + if (IsValueColor32) + { + m_inputs[fieldIndex].Text = ((byte)val).ToString(); + val /= 255f; + } + else + { + m_inputs[fieldIndex].Text = val.ToString(); + } + + SetColorField(val, fieldIndex); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("InteractiveColor OnSlider: " + ex.ToString()); + } + } + + // UI Construction + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveColor", false, false, true, true, 3, new Vector4(4, 4, 4, 4), + new Color(0.06f, 0.06f, 0.06f)); + + // hori group + + var horiGroup = UIFactory.CreateHorizontalGroup(UIRoot, "ColorEditor", false, false, true, true, 5, + default, new Color(1, 1, 1, 0), TextAnchor.MiddleLeft); + + // sliders / inputs + + var grid = UIFactory.CreateGridGroup(horiGroup, "Grid", new Vector2(140, 25), new Vector2(2, 2), new Color(1, 1, 1, 0)); + UIFactory.SetLayoutElement(grid, minWidth: 580, minHeight: 25, flexibleWidth: 0); + + for (int i = 0; i < 4; i++) + AddEditorRow(i, grid); + + // apply button + + m_applyButton = UIFactory.CreateButton(horiGroup, "ApplyButton", "Apply", new Color(0.2f, 0.26f, 0.2f)); + UIFactory.SetLayoutElement(m_applyButton.Component.gameObject, minHeight: 25, minWidth: 90); + m_applyButton.OnClick += SetValueToOwner; + + // image of color + + var imgObj = UIFactory.CreateUIObject("ColorImageHelper", horiGroup); + UIFactory.SetLayoutElement(imgObj, minHeight: 25, minWidth: 50, flexibleWidth: 50); + m_colorImage = imgObj.AddComponent(); + + return UIRoot; + } + + internal void AddEditorRow(int index, GameObject groupObj) + { + var row = UIFactory.CreateHorizontalGroup(groupObj, "EditorRow_" + fieldNames[index], + false, true, true, true, 5, default, new Color(1, 1, 1, 0)); + + var label = UIFactory.CreateLabel(row, "RowLabel", $"{fieldNames[index]}:", TextAnchor.MiddleRight, Color.cyan); + UIFactory.SetLayoutElement(label.gameObject, minWidth: 17, flexibleWidth: 0, minHeight: 25); + + var input = UIFactory.CreateInputField(row, "Input", "..."); + UIFactory.SetLayoutElement(input.UIRoot, minWidth: 40, minHeight: 25, flexibleHeight: 0); + m_inputs[index] = input; + input.OnValueChanged += (string val) => { OnInputChanged(val, index); }; + + var sliderObj = UIFactory.CreateSlider(row, "Slider", out Slider slider); + m_sliders[index] = slider; + UIFactory.SetLayoutElement(sliderObj, minHeight: 25, minWidth: 70, flexibleWidth: 999, flexibleHeight: 0); + slider.minValue = 0; + slider.maxValue = 1; + slider.onValueChanged.AddListener((float val) => { OnSliderValueChanged(val, index); }); + } + } +} diff --git a/src/UI/IValues/InteractiveDictionary.cs b/src/UI/IValues/InteractiveDictionary.cs new file mode 100644 index 0000000..f17c447 --- /dev/null +++ b/src/UI/IValues/InteractiveDictionary.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.IValues +{ + public class InteractiveDictionary : InteractiveValue, ICellPoolDataSource, ICacheObjectController + { + CacheObjectBase ICacheObjectController.ParentCacheObject => this.CurrentOwner; + object ICacheObjectController.Target => this.CurrentOwner.Value; + public Type TargetType { get; private set; } + + public override bool CanWrite => base.CanWrite && RefIDictionary != null && !RefIDictionary.IsReadOnly; + + public Type KeyType; + public Type ValueType; + public IDictionary RefIDictionary; + + public int ItemCount => cachedEntries.Count; + private readonly List cachedEntries = new List(); + + public ScrollPool DictScrollPool { get; private set; } + + private Text NotSupportedLabel; + + public Text TopLabel; + + public LayoutElement KeyTitleLayout; + public LayoutElement ValueTitleLayout; + + public override void OnBorrowed(CacheObjectBase owner) + { + base.OnBorrowed(owner); + + DictScrollPool.Refresh(true, true); + } + + public override void ReleaseFromOwner() + { + base.ReleaseFromOwner(); + + ClearAndRelease(); + } + + private void ClearAndRelease() + { + RefIDictionary = null; + + foreach (var entry in cachedEntries) + { + entry.UnlinkFromView(); + entry.ReleasePooledObjects(); + } + + cachedEntries.Clear(); + } + + public override void SetValue(object value) + { + if (value == null) + { + // should never be null + ClearAndRelease(); + return; + } + else + { + var type = value.GetActualType(); + if (type.IsGenericType && type.GetGenericArguments().Length == 2) + { + KeyType = type.GetGenericArguments()[0]; + ValueType = type.GetGenericArguments()[1]; + } + else + { + KeyType = typeof(object); + ValueType = typeof(object); + } + + CacheEntries(value); + + TopLabel.text = $"[{cachedEntries.Count}] {SignatureHighlighter.Parse(type, false)}"; + } + + + this.DictScrollPool.Refresh(true, false); + } + + private void CacheEntries(object value) + { + RefIDictionary = value as IDictionary; + + if (ReflectionUtility.TryGetDictEnumerator(value, out IEnumerator dictEnumerator)) + { + NotSupportedLabel.gameObject.SetActive(false); + + int idx = 0; + while (dictEnumerator.MoveNext()) + { + CacheKeyValuePair cache; + if (idx >= cachedEntries.Count) + { + cache = new CacheKeyValuePair(); + cache.SetDictOwner(this, idx); + cachedEntries.Add(cache); + } + else + cache = cachedEntries[idx]; + + cache.SetFallbackType(ValueType); + cache.SetKey(dictEnumerator.Current.Key); + cache.SetValueFromSource(dictEnumerator.Current.Value); + + idx++; + } + + // Remove excess cached entries if dict count decreased + if (cachedEntries.Count > idx) + { + for (int i = cachedEntries.Count - 1; i >= idx; i--) + { + var cache = cachedEntries[i]; + if (cache.CellView != null) + cache.UnlinkFromView(); + + cache.ReleasePooledObjects(); + cachedEntries.RemoveAt(i); + } + } + } + else + { + NotSupportedLabel.gameObject.SetActive(true); + } + } + + // Setting value to dictionary + + public void TrySetValueToKey(object key, object value, int keyIndex) + { + try + { + if (!RefIDictionary.Contains(key)) + { + ExplorerCore.LogWarning("Unable to set key! Key may have been boxed to/from Il2Cpp Object."); + return; + } + + RefIDictionary[key] = value; + + var entry = cachedEntries[keyIndex]; + entry.SetValueFromSource(value); + if (entry.CellView != null) + entry.SetDataToCell(entry.CellView); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception setting IDictionary key! {ex}"); + } + } + + // KVP entry scroll pool + + public void OnCellBorrowed(CacheKeyValuePairCell cell) { } + + public void SetCell(CacheKeyValuePairCell cell, int index) + { + CacheObjectControllerHelper.SetCell(cell, index, cachedEntries, SetCellLayout); + } + + public int AdjustedWidth => (int)UIRect.rect.width - 80; + //public int AdjustedKeyWidth => HalfWidth - 50; + + public override void SetLayout() + { + var minHeight = 5f; + + KeyTitleLayout.minWidth = AdjustedWidth * 0.44f; + ValueTitleLayout.minWidth = AdjustedWidth * 0.55f; + + foreach (var cell in DictScrollPool.CellPool) + { + SetCellLayout(cell); + if (cell.Enabled) + minHeight += cell.Rect.rect.height; + } + + this.scrollLayout.minHeight = Math.Min(InspectorPanel.CurrentPanelHeight - 400f, minHeight); + } + + private void SetCellLayout(CacheObjectCell objcell) + { + var cell = objcell as CacheKeyValuePairCell; + cell.KeyGroupLayout.minWidth = cell.AdjustedWidth * 0.44f; + cell.RightGroupLayout.minWidth = cell.AdjustedWidth * 0.55f; + + if (cell.Occupant?.IValue != null) + cell.Occupant.IValue.SetLayout(); + } + + private LayoutElement scrollLayout; + private RectTransform UIRect; + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveDict", true, true, true, true, 6, new Vector4(10, 3, 15, 4), + new Color(0.05f, 0.05f, 0.05f)); + UIFactory.SetLayoutElement(UIRoot, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 475); + UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + UIRect = UIRoot.GetComponent(); + + // Entries label + + TopLabel = UIFactory.CreateLabel(UIRoot, "EntryLabel", "not set", TextAnchor.MiddleLeft, fontSize: 16); + TopLabel.horizontalOverflow = HorizontalWrapMode.Overflow; + + // key / value titles + + var titleGroup = UIFactory.CreateUIObject("TitleGroup", UIRoot); + UIFactory.SetLayoutElement(titleGroup, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 0); + UIFactory.SetLayoutGroup(titleGroup, false, true, true, true, padLeft: 65, padRight: 0, childAlignment: TextAnchor.LowerLeft); + + var keyTitle = UIFactory.CreateLabel(titleGroup, "KeyTitle", "Keys", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(keyTitle.gameObject, minWidth: 100, flexibleWidth: 0); + KeyTitleLayout = keyTitle.GetComponent(); + + var valueTitle = UIFactory.CreateLabel(titleGroup, "ValueTitle", "Values", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(valueTitle.gameObject, minWidth: 100, flexibleWidth: 0); + ValueTitleLayout = valueTitle.GetComponent(); + + // entry scroll pool + + DictScrollPool = UIFactory.CreateScrollPool(UIRoot, "EntryList", out GameObject scrollObj, + out GameObject _, new Color(0.09f, 0.09f, 0.09f)); + UIFactory.SetLayoutElement(scrollObj, minHeight: 150, flexibleHeight: 0); + DictScrollPool.Initialize(this, SetLayout); + scrollLayout = scrollObj.GetComponent(); + + NotSupportedLabel = UIFactory.CreateLabel(DictScrollPool.Content.gameObject, "NotSupportedMessage", + "The IDictionary failed to enumerate. This is likely due to an issue with Unhollowed interfaces.", + TextAnchor.MiddleLeft, Color.red); + + UIFactory.SetLayoutElement(NotSupportedLabel.gameObject, minHeight: 25, flexibleWidth: 9999); + NotSupportedLabel.gameObject.SetActive(false); + + return UIRoot; + } + } +} \ No newline at end of file diff --git a/src/UI/IValues/InteractiveEnum.cs b/src/UI/IValues/InteractiveEnum.cs new file mode 100644 index 0000000..bcf1fab --- /dev/null +++ b/src/UI/IValues/InteractiveEnum.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CacheObject; + +namespace UnityExplorer.UI.IValues +{ + public class InteractiveEnum : InteractiveValue + { + public bool IsFlags; + public Type EnumType; + + private Type lastType; + + public OrderedDictionary CurrentValues; + + public CachedEnumValue ValueAtIdx(int idx) => (CachedEnumValue)CurrentValues[idx]; + public CachedEnumValue ValueAtKey(object key) => (CachedEnumValue)CurrentValues[key]; + + private Dropdown enumDropdown; + private GameObject toggleHolder; + private readonly List flagToggles = new List(); + private readonly List flagTexts = new List(); + + // Setting value from owner + public override void SetValue(object value) + { + EnumType = value.GetType(); + + if (lastType != EnumType) + { + CurrentValues = GetEnumValues(EnumType, out IsFlags); + + if (IsFlags) + SetupTogglesForEnumType(); + else + SetupDropdownForEnumType(); + + lastType = EnumType; + } + + // setup ui for changes + if (IsFlags) + SetTogglesForValue(value); + else + SetDropdownForValue(value); + } + + // Setting value to owner + + private void OnApplyClicked() + { + if (IsFlags) + SetValueFromFlags(); + else + SetValueFromDropdown(); + } + + private void SetValueFromDropdown() + { + try + { + CurrentOwner.SetUserValue(ValueAtIdx(enumDropdown.value).ActualValue); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception setting from dropdown: " + ex); + } + } + + private void SetValueFromFlags() + { + try + { + List values = new List(); + for (int i = 0; i < CurrentValues.Count; i++) + { + if (flagToggles[i].isOn) + values.Add(ValueAtIdx(i).Name); + } + + CurrentOwner.SetUserValue(Enum.Parse(EnumType, string.Join(", ", values.ToArray()))); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception setting from flag toggles: " + ex); + } + } + + // setting UI state for value + + private void SetDropdownForValue(object value) + { + if (CurrentValues.Contains(value)) + { + var cached = ValueAtKey(value); + enumDropdown.value = cached.EnumIndex; + enumDropdown.RefreshShownValue(); + } + else + ExplorerCore.LogWarning("CurrentValues does not contain key '" + value?.ToString() ?? "" + "'"); + } + + private void SetTogglesForValue(object value) + { + try + { + var split = value.ToString().Split(','); + var set = new HashSet(); + foreach (var s in split) + set.Add(s.Trim()); + + for (int i = 0; i < CurrentValues.Count; i++) + flagToggles[i].isOn = set.Contains(ValueAtIdx(i).Name); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception setting flag toggles: " + ex); + } + } + + // Setting up the UI for the enum type when it changes or is first set + + private void SetupDropdownForEnumType() + { + toggleHolder.SetActive(false); + enumDropdown.gameObject.SetActive(true); + + // create dropdown entries + enumDropdown.options.Clear(); + + foreach (CachedEnumValue entry in CurrentValues.Values) + enumDropdown.options.Add(new Dropdown.OptionData(entry.Name)); + + enumDropdown.value = 0; + enumDropdown.RefreshShownValue(); + } + + private void SetupTogglesForEnumType() + { + toggleHolder.SetActive(true); + enumDropdown.gameObject.SetActive(false); + + // create / set / hide toggles + for (int i = 0; i < CurrentValues.Count || i < flagToggles.Count; i++) + { + if (i >= CurrentValues.Count) + { + if (i >= flagToggles.Count) + break; + + flagToggles[i].gameObject.SetActive(false); + continue; + } + + if (i >= flagToggles.Count) + AddToggleRow(); + + flagToggles[i].isOn = false; + flagTexts[i].text = ValueAtIdx(i).Name; + } + } + + private void AddToggleRow() + { + var row = UIFactory.CreateUIObject("ToggleRow", toggleHolder); + UIFactory.SetLayoutGroup(row, false, false, true, true, 2); + UIFactory.SetLayoutElement(row, minHeight: 25, flexibleWidth: 9999); + + var toggleObj = UIFactory.CreateToggle(row, "ToggleObj", out Toggle toggle, out Text toggleText); + UIFactory.SetLayoutElement(toggleObj, minHeight: 25, flexibleWidth: 9999); + + flagToggles.Add(toggle); + flagTexts.Add(toggleText); + } + + // UI Construction + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveEnum", false, false, true, true, 3, new Vector4(4, 4, 4, 4), + new Color(0.06f, 0.06f, 0.06f)); + UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleHeight: 9999, flexibleWidth: 9999); + + var hori = UIFactory.CreateUIObject("Hori", UIRoot); + UIFactory.SetLayoutElement(hori, minHeight: 25, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(hori, false, false, true, true, 2); + + var applyButton = UIFactory.CreateButton(hori, "ApplyButton", "Apply", new Color(0.2f, 0.27f, 0.2f)); + UIFactory.SetLayoutElement(applyButton.Component.gameObject, minHeight: 25, minWidth: 100); + applyButton.OnClick += OnApplyClicked; + + var dropdownObj = UIFactory.CreateDropdown(hori, out enumDropdown, "not set", 14, null); + UIFactory.SetLayoutElement(dropdownObj, minHeight: 25, flexibleWidth: 600); + + toggleHolder = UIFactory.CreateUIObject("ToggleHolder", UIRoot); + UIFactory.SetLayoutGroup(toggleHolder, false, false, true, true, 4); + UIFactory.SetLayoutElement(toggleHolder, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 9999); + + return UIRoot; + } + + + #region Enum cache + + public struct CachedEnumValue + { + public CachedEnumValue(object value, int index, string name) + { + EnumIndex = index; + Name = name; + ActualValue = value; + } + + public readonly object ActualValue; + public int EnumIndex; + public readonly string Name; + } + + internal static readonly Dictionary enumCache = new Dictionary(); + + internal static OrderedDictionary GetEnumValues(Type enumType, out bool isFlags) + { + isFlags = enumType.GetCustomAttributes(typeof(FlagsAttribute), true) is object[] fa && fa.Any(); + + if (!enumCache.ContainsKey(enumType.AssemblyQualifiedName)) + { + var dict = new OrderedDictionary(); + var addedNames = new HashSet(); + + int i = 0; + foreach (var value in Enum.GetValues(enumType)) + { + var name = value.ToString(); + if (addedNames.Contains(name)) + continue; + addedNames.Add(name); + + dict.Add(value, new CachedEnumValue(value, i, name)); + i++; + } + + enumCache.Add(enumType.AssemblyQualifiedName, dict); + } + + return enumCache[enumType.AssemblyQualifiedName]; + } + + #endregion + } +} diff --git a/src/UI/IValues/InteractiveList.cs b/src/UI/IValues/InteractiveList.cs new file mode 100644 index 0000000..c861653 --- /dev/null +++ b/src/UI/IValues/InteractiveList.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.IValues +{ + public class InteractiveList : InteractiveValue, ICellPoolDataSource, ICacheObjectController + { + CacheObjectBase ICacheObjectController.ParentCacheObject => this.CurrentOwner; + object ICacheObjectController.Target => this.CurrentOwner.Value; + public Type TargetType { get; private set; } + + public override bool CanWrite => base.CanWrite && RefIList != null && !RefIList.IsReadOnly; + + public Type EntryType; + public IList RefIList; + + public int ItemCount => values.Count; + private readonly List values = new List(); + private readonly List cachedEntries = new List(); + + public ScrollPool ListScrollPool { get; private set; } + + public Text TopLabel; + + public override void OnBorrowed(CacheObjectBase owner) + { + base.OnBorrowed(owner); + + ListScrollPool.Refresh(true, true); + } + + public override void ReleaseFromOwner() + { + base.ReleaseFromOwner(); + + ClearAndRelease(); + } + + private void ClearAndRelease() + { + RefIList = null; + values.Clear(); + + foreach (var entry in cachedEntries) + { + entry.UnlinkFromView(); + entry.ReleasePooledObjects(); + } + + cachedEntries.Clear(); + } + + // Setting the List value itself to this model + public override void SetValue(object value) + { + if (value == null) + { + // should never be null + if (values.Any()) + ClearAndRelease(); + } + else + { + var type = value.GetActualType(); + if (type.IsGenericType) + EntryType = type.GetGenericArguments()[0]; + else if (type.HasElementType) + EntryType = type.GetElementType(); + else + EntryType = typeof(object); + + CacheEntries(value); + + TopLabel.text = $"[{cachedEntries.Count}] {SignatureHighlighter.Parse(type, false)}"; + } + + //this.ScrollPoolLayout.minHeight = Math.Min(400f, 35f * values.Count); + this.ListScrollPool.Refresh(true, false); + } + + private void CacheEntries(object value) + { + RefIList = value as IList; + + values.Clear(); + int idx = 0; + + if (ReflectionUtility.TryGetEnumerator(value, out IEnumerator enumerator)) + { + NotSupportedLabel.gameObject.SetActive(false); + + while (enumerator.MoveNext()) + { + var entry = enumerator.Current; + + values.Add(entry); + + // If list count increased, create new cache entries + CacheListEntry cache; + if (idx >= cachedEntries.Count) + { + cache = new CacheListEntry(); + cache.SetListOwner(this, idx); + cachedEntries.Add(cache); + } + else + cache = cachedEntries[idx]; + + cache.SetFallbackType(this.EntryType); + cache.SetValueFromSource(entry); + idx++; + } + + // Remove excess cached entries if list count decreased + if (cachedEntries.Count > values.Count) + { + for (int i = cachedEntries.Count - 1; i >= values.Count; i--) + { + var cache = cachedEntries[i]; + if (cache.CellView != null) + cache.UnlinkFromView(); + + cache.ReleasePooledObjects(); + cachedEntries.RemoveAt(i); + } + } + } + else + { + NotSupportedLabel.gameObject.SetActive(true); + } + } + + // Setting the value of an index to the list + + public void TrySetValueToIndex(object value, int index) + { + try + { + //value = value.TryCast(this.EntryType); + RefIList[index] = value; + + var entry = cachedEntries[index]; + entry.SetValueFromSource(value); + + if (entry.CellView != null) + entry.SetDataToCell(entry.CellView); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception setting IList value: {ex}"); + } + } + + // List entry scroll pool + + public override void SetLayout() + { + var minHeight = 5f; + + foreach (var cell in ListScrollPool.CellPool) + { + if (cell.Enabled) + minHeight += cell.Rect.rect.height; + } + + this.scrollLayout.minHeight = Math.Min(InspectorPanel.CurrentPanelHeight - 400f, minHeight); + } + + public void OnCellBorrowed(CacheListEntryCell cell) { } // not needed + + public void SetCell(CacheListEntryCell cell, int index) + { + CacheObjectControllerHelper.SetCell(cell, index, cachedEntries, null); + } + + private LayoutElement scrollLayout; + + private Text NotSupportedLabel; + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveList", true, true, true, true, 6, new Vector4(10, 3, 15, 4), + new Color(0.05f, 0.05f, 0.05f)); + UIFactory.SetLayoutElement(UIRoot, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 600); + UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // Entries label + + TopLabel = UIFactory.CreateLabel(UIRoot, "EntryLabel", "not set", TextAnchor.MiddleLeft, fontSize: 16); + TopLabel.horizontalOverflow = HorizontalWrapMode.Overflow; + + // entry scroll pool + + ListScrollPool = UIFactory.CreateScrollPool(UIRoot, "EntryList", out GameObject scrollObj, + out GameObject _, new Color(0.09f, 0.09f, 0.09f)); + UIFactory.SetLayoutElement(scrollObj, minHeight: 400, flexibleHeight: 0); + ListScrollPool.Initialize(this, SetLayout); + scrollLayout = scrollObj.GetComponent(); + + NotSupportedLabel = UIFactory.CreateLabel(ListScrollPool.Content.gameObject, "NotSupportedMessage", + "The IEnumerable failed to enumerate. This is likely due to an issue with Unhollowed interfaces.", + TextAnchor.MiddleLeft, Color.red); + + UIFactory.SetLayoutElement(NotSupportedLabel.gameObject, minHeight: 25, flexibleWidth: 9999); + NotSupportedLabel.gameObject.SetActive(false); + + return UIRoot; + } + } +} \ No newline at end of file diff --git a/src/UI/IValues/InteractiveString.cs b/src/UI/IValues/InteractiveString.cs new file mode 100644 index 0000000..1a0c8a7 --- /dev/null +++ b/src/UI/IValues/InteractiveString.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.IValues +{ + public class InteractiveString : InteractiveValue + { + private string RealValue; + public string EditedValue = ""; + + public InputFieldRef inputField; + public ButtonRef ApplyButton; + + public GameObject SaveFileRow; + public InputFieldRef SaveFilePath; + + public override void OnBorrowed(CacheObjectBase owner) + { + base.OnBorrowed(owner); + + inputField.Component.readOnly = !owner.CanWrite; + ApplyButton.Component.gameObject.SetActive(owner.CanWrite); + + SaveFilePath.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, "untitled.txt"); + } + + private bool IsStringTooLong(string s) + { + if (s == null) + return false; + + return s.Length >= UIManager.MAX_INPUTFIELD_CHARS; + } + + public override void SetValue(object value) + { + RealValue = value as string; + SaveFileRow.SetActive(IsStringTooLong(RealValue)); + + if (value == null) + { + inputField.Text = ""; + EditedValue = ""; + } + else + { + EditedValue = (string)value; + inputField.Text = EditedValue; + } + } + + private void OnApplyClicked() + { + CurrentOwner.SetUserValue(EditedValue); + } + + private void OnInputChanged(string input) + { + EditedValue = input; + SaveFileRow.SetActive(IsStringTooLong(EditedValue)); + } + + private void OnSaveFileClicked() + { + if (RealValue == null) + return; + + if (string.IsNullOrEmpty(SaveFilePath.Text)) + { + ExplorerCore.LogWarning("Cannot save an empty file path!"); + return; + } + + var path = IOUtility.EnsureValidDirectory(SaveFilePath.Text); + + if (File.Exists(path)) + File.Delete(path); + + File.WriteAllText(path, RealValue); + } + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveString", false, false, true, true, 3, new Vector4(4, 4, 4, 4), + new Color(0.06f, 0.06f, 0.06f)); + + // Save to file helper + + SaveFileRow = UIFactory.CreateUIObject("SaveFileRow", UIRoot); + UIFactory.SetLayoutElement(SaveFileRow, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(SaveFileRow, false, true, true, true, 3); + + UIFactory.CreateLabel(SaveFileRow, "Info", "String is too long! Save to file if you want to see the full string.", + TextAnchor.MiddleLeft); + + var horizRow = UIFactory.CreateUIObject("Horiz", SaveFileRow); + UIFactory.SetLayoutGroup(horizRow, false, false, true, true, 4); + + var saveButton = UIFactory.CreateButton(horizRow, "SaveButton", "Save file"); + UIFactory.SetLayoutElement(saveButton.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); + saveButton.OnClick += OnSaveFileClicked; + + SaveFilePath = UIFactory.CreateInputField(horizRow, "SaveInput", "..."); + UIFactory.SetLayoutElement(SaveFilePath.UIRoot, minHeight: 25, flexibleWidth: 9999); + + // Main Input / apply + + ApplyButton = UIFactory.CreateButton(UIRoot, "ApplyButton", "Apply", new Color(0.2f, 0.27f, 0.2f)); + UIFactory.SetLayoutElement(ApplyButton.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); + ApplyButton.OnClick += OnApplyClicked; + + inputField = UIFactory.CreateInputField(UIRoot, "InputField", "empty"); + inputField.UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + UIFactory.SetLayoutElement(inputField.UIRoot, minHeight: 25, flexibleHeight: 500, flexibleWidth: 9999); + inputField.Component.lineType = InputField.LineType.MultiLineNewline; + inputField.OnValueChanged += OnInputChanged; + + return UIRoot; + } + + } +} diff --git a/src/UI/IValues/InteractiveValue.cs b/src/UI/IValues/InteractiveValue.cs new file mode 100644 index 0000000..80f5000 --- /dev/null +++ b/src/UI/IValues/InteractiveValue.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.ObjectPool; + +namespace UnityExplorer.UI.IValues +{ + public abstract class InteractiveValue : IPooledObject + { + public static Type GetIValueTypeForState(ValueState state) + { + switch (state) + { + case ValueState.String: + return typeof(InteractiveString); + case ValueState.Enum: + return typeof(InteractiveEnum); + case ValueState.Collection: + return typeof(InteractiveList); + case ValueState.Dictionary: + return typeof(InteractiveDictionary); + case ValueState.ValueStruct: + return typeof(InteractiveValueStruct); + case ValueState.Color: + return typeof(InteractiveColor); + default: return null; + } + } + + public GameObject UIRoot { get; set; } + public float DefaultHeight => -1f; + + public virtual bool CanWrite => this.CurrentOwner.CanWrite; + + public CacheObjectBase CurrentOwner => m_owner; + private CacheObjectBase m_owner; + + public bool PendingValueWanted; + + public virtual void OnBorrowed(CacheObjectBase owner) + { + if (this.m_owner != null) + { + ExplorerCore.LogWarning("Setting an IValue's owner but there is already one set. Maybe it wasn't cleaned up?"); + ReleaseFromOwner(); + } + + this.m_owner = owner; + } + + public virtual void ReleaseFromOwner() + { + if (this.m_owner == null) + return; + + this.m_owner = null; + } + + public abstract void SetValue(object value); + + public virtual void SetLayout() { } + + public abstract GameObject CreateContent(GameObject parent); + } +} diff --git a/src/UI/IValues/InteractiveValueStruct.cs b/src/UI/IValues/InteractiveValueStruct.cs new file mode 100644 index 0000000..adbc56f --- /dev/null +++ b/src/UI/IValues/InteractiveValueStruct.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.Utility; + +namespace UnityExplorer.UI.IValues +{ + public class InteractiveValueStruct : InteractiveValue + { + #region Struct cache / wrapper + + public class StructInfo + { + public bool IsSupported; + public FieldInfo[] Fields; + + public StructInfo(bool isSupported, FieldInfo[] fields) + { + IsSupported = isSupported; + Fields = fields; + } + + public void SetValue(object instance, string input, int fieldIndex) + { + var field = Fields[fieldIndex]; + + object val; + if (field.FieldType == typeof(string)) + val = input; + else + { + if (!ParseUtility.TryParse(input, field.FieldType, out val, out Exception ex)) + { + ExplorerCore.LogWarning("Unable to parse input!"); + if (ex != null) ExplorerCore.Log(ex.ReflectionExToString()); + return; + } + } + + field.SetValue(instance, val); + } + + public string GetValue(object instance, int fieldIndex) + { + var field = Fields[fieldIndex]; + var value = field.GetValue(instance); + return ParseUtility.ToStringForInput(value, field.FieldType); + } + } + + private static readonly Dictionary typeSupportCache = new Dictionary(); + + private const BindingFlags INSTANCE_FLAGS = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; + private const string SYSTEM_VOID = "System.Void"; + + public static bool SupportsType(Type type) + { + if (!type.IsValueType || string.IsNullOrEmpty(type.AssemblyQualifiedName) || type.FullName == SYSTEM_VOID) + return false; + + if (typeSupportCache.TryGetValue(type.AssemblyQualifiedName, out var info)) + return info.IsSupported; + + var supported = false; + + var fields = type.GetFields(INSTANCE_FLAGS); + if (fields.Length > 0) + { + if (fields.Any(it => !ParseUtility.CanParse(it.FieldType))) + { + supported = false; + info = new StructInfo(supported, null); + } + else + { + supported = true; + info = new StructInfo(supported, fields); + } + } + + typeSupportCache.Add(type.AssemblyQualifiedName, info); + + return supported; + } + + #endregion + + public object RefInstance; + + public StructInfo CurrentInfo; + private Type lastStructType; + + private ButtonRef applyButton; + private readonly List fieldRows = new List(); + private readonly List inputFields = new List(); + private readonly List labels = new List(); + + public override void OnBorrowed(CacheObjectBase owner) + { + base.OnBorrowed(owner); + + applyButton.Component.gameObject.SetActive(owner.CanWrite); + } + + // Setting value from owner to this + + public override void SetValue(object value) + { + RefInstance = value; + + var type = RefInstance.GetType(); + + if (type != lastStructType) + { + CurrentInfo = typeSupportCache[type.AssemblyQualifiedName]; + SetupUIForType(); + lastStructType = type; + } + + for (int i = 0; i < CurrentInfo.Fields.Length; i++) + { + inputFields[i].Text = CurrentInfo.GetValue(RefInstance, i); + } + } + + private void OnApplyClicked() + { + try + { + for (int i = 0; i < CurrentInfo.Fields.Length; i++) + { + CurrentInfo.SetValue(RefInstance, inputFields[i].Text, i); + } + + CurrentOwner.SetUserValue(RefInstance); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Exception setting value: " + ex); + } + } + + // UI Setup for type + + private void SetupUIForType() + { + for (int i = 0; i < CurrentInfo.Fields.Length || i <= inputFields.Count; i++) + { + if (i >= CurrentInfo.Fields.Length) + { + if (i >= inputFields.Count) + break; + + fieldRows[i].SetActive(false); + continue; + } + + if (i >= inputFields.Count) + AddEditorRow(); + + fieldRows[i].SetActive(true); + + string label = SignatureHighlighter.Parse(CurrentInfo.Fields[i].FieldType, false); + label += $" {CurrentInfo.Fields[i].Name}:"; + labels[i].text = label; + } + } + + private void AddEditorRow() + { + var row = UIFactory.CreateUIObject("HoriGroup", UIRoot); + //row.AddComponent().horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + UIFactory.SetLayoutElement(row, minHeight: 25, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(row, false, false, true, true, 8, childAlignment: TextAnchor.MiddleLeft); + + fieldRows.Add(row); + + var label = UIFactory.CreateLabel(row, "Label", "notset", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(label.gameObject, minHeight: 25, minWidth: 50, flexibleWidth: 0); + label.horizontalOverflow = HorizontalWrapMode.Wrap; + labels.Add(label); + + var input = UIFactory.CreateInputField(row, "InputField", "..."); + UIFactory.SetLayoutElement(input.UIRoot, minHeight: 25, minWidth: 200); + var fitter = input.UIRoot.AddComponent(); + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + input.Component.lineType = InputField.LineType.MultiLineNewline; + inputFields.Add(input); + } + + // UI Construction + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveValueStruct", false, false, true, true, 3, new Vector4(4, 4, 4, 4), + new Color(0.06f, 0.06f, 0.06f), TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleWidth: 9999); + + applyButton = UIFactory.CreateButton(UIRoot, "ApplyButton", "Apply", new Color(0.2f, 0.27f, 0.2f)); + UIFactory.SetLayoutElement(applyButton.Component.gameObject, minHeight: 25, minWidth: 175); + applyButton.OnClick += OnApplyClicked; + + return UIRoot; + } + } +} diff --git a/src/UI/Inspectors/GameObjectInspector.cs b/src/UI/Inspectors/GameObjectInspector.cs new file mode 100644 index 0000000..b9f836b --- /dev/null +++ b/src/UI/Inspectors/GameObjectInspector.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Input; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.Inspectors +{ + public class GameObjectInspector : InspectorBase + { + public GameObject GOTarget => Target as GameObject; + + public GameObject Content; + + public GameObjectControls GOControls; + + public TransformTree TransformTree; + private ScrollPool transformScroll; + private readonly List cachedChildren = new List(); + + public ComponentList ComponentList; + private ScrollPool componentScroll; + + private InputFieldRef addChildInput; + private InputFieldRef addCompInput; + + public override void OnBorrowedFromPool(object target) + { + base.OnBorrowedFromPool(target); + + Target = target as GameObject; + + GOControls.UpdateGameObjectInfo(true, true); + GOControls.UpdateTransformControlValues(true); + + RuntimeProvider.Instance.StartCoroutine(InitCoroutine()); + } + + private IEnumerator InitCoroutine() + { + yield return null; + + LayoutRebuilder.ForceRebuildLayoutImmediate(InspectorPanel.Instance.ContentRect); + + TransformTree.Rebuild(); + + ComponentList.ScrollPool.Refresh(true, true); + UpdateComponents(); + } + + public override void OnReturnToPool() + { + base.OnReturnToPool(); + + addChildInput.Text = ""; + addCompInput.Text = ""; + + TransformTree.Clear(); + ComponentList.Clear(); + } + + public override void CloseInspector() + { + InspectorManager.ReleaseInspector(this); + } + + public void ChangeTarget(GameObject newTarget) + { + this.Target = newTarget; + GOControls.UpdateGameObjectInfo(true, true); + GOControls.UpdateTransformControlValues(true); + TransformTree.RefreshData(true, false); + UpdateComponents(); + } + + private float timeOfLastUpdate; + + public override void Update() + { + if (!this.IsActive) + return; + + if (Target.IsNullOrDestroyed(false)) + { + InspectorManager.ReleaseInspector(this); + return; + } + + GOControls.UpdateVectorSlider(); + GOControls.UpdateTransformControlValues(false); + + // Slow update + if (timeOfLastUpdate.OccuredEarlierThan(1)) + { + timeOfLastUpdate = Time.realtimeSinceStartup; + + GOControls.UpdateGameObjectInfo(false, false); + + TransformTree.RefreshData(true, false); + UpdateComponents(); + } + } + + // Child and Component Lists + + private IEnumerable GetTransformEntries() + { + cachedChildren.Clear(); + for (int i = 0; i < GOTarget.transform.childCount; i++) + cachedChildren.Add(GOTarget.transform.GetChild(i).gameObject); + return cachedChildren; + } + + private readonly List componentEntries = new List(); + private readonly HashSet compInstanceIDs = new HashSet(); + private readonly List behaviourEntries = new List(); + private readonly List behaviourEnabledStates = new List(); + + // ComponentList.GetRootEntriesMethod + private List GetComponentEntries() => componentEntries; + + public void UpdateComponents() + { + // Check if we actually need to refresh the component cells or not. + var comps = GOTarget.GetComponents(); + var behaviours = GOTarget.GetComponents(); + + bool needRefresh = false; + if (comps.Length != componentEntries.Count || behaviours.Length != behaviourEntries.Count) + { + needRefresh = true; + } + else + { + foreach (var comp in comps) + { + if (!compInstanceIDs.Contains(comp.GetInstanceID())) + { + needRefresh = true; + break; + } + } + + if (!needRefresh) + { + for (int i = 0; i < behaviours.Length; i++) + { + var behaviour = behaviours[i]; + if (behaviour.enabled != behaviourEnabledStates[i]) + { + needRefresh = true; + break; + } + } + } + } + + if (!needRefresh) + return; + + componentEntries.Clear(); + compInstanceIDs.Clear(); + + foreach (var comp in comps) + { + componentEntries.Add(comp); + compInstanceIDs.Add(comp.GetInstanceID()); + } + + behaviourEntries.Clear(); + behaviourEnabledStates.Clear(); + foreach (var behaviour in behaviours) + { + behaviourEntries.Add(behaviour); + behaviourEnabledStates.Add(behaviour.enabled); + } + + ComponentList.RefreshData(); + ComponentList.ScrollPool.Refresh(true); + } + + + private void OnAddChildClicked(string input) + { + var newObject = new GameObject(input); + newObject.transform.parent = GOTarget.transform; + + TransformTree.RefreshData(true, false); + } + + private void OnAddComponentClicked(string input) + { + if (ReflectionUtility.AllTypes.TryGetValue(input, out Type type)) + { + try + { + RuntimeProvider.Instance.AddComponent(GOTarget, type); + UpdateComponents(); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception adding component: {ex.ReflectionExToString()}"); + } + } + else + { + ExplorerCore.LogWarning($"Could not find any Type by the name '{input}'!"); + } + } + + + #region UI Construction + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(Pool.Instance.InactiveHolder, + "GameObjectInspector", true, false, true, true, 5, new Vector4(4, 4, 4, 4), new Color(0.065f, 0.065f, 0.065f)); + + var scrollObj = UIFactory.CreateScrollView(UIRoot, "GameObjectInspector", out Content, out var scrollbar, + new Color(0.065f, 0.065f, 0.065f)); + UIFactory.SetLayoutElement(scrollObj, minHeight: 300, flexibleWidth: 9999, flexibleHeight: 1); + + UIFactory.SetLayoutGroup(Content, spacing: 3, padTop: 2, padBottom: 2, padLeft: 2, padRight: 2); + + // Construct GO Controls + GOControls = new GameObjectControls(this); + + ConstructLists(); + + return UIRoot; + } + + // Child and Comp Lists + + private void ConstructLists() + { + var listHolder = UIFactory.CreateUIObject("ListHolders", UIRoot); + UIFactory.SetLayoutGroup(listHolder, false, true, true, true, 8, 2, 2, 2, 2); + UIFactory.SetLayoutElement(listHolder, minHeight: 350, flexibleWidth: 9999, flexibleHeight: 9999); + //var listRect = listHolder.GetComponent(); + //listRect.anchorMin = new Vector2(0, 1); + //listRect.anchorMax = new Vector2(1, 1); + + // Left group (Children) + + var leftGroup = UIFactory.CreateUIObject("ChildrenGroup", listHolder); + UIFactory.SetLayoutElement(leftGroup, flexibleWidth: 9999, flexibleHeight: 9999); + UIFactory.SetLayoutGroup(leftGroup, false, false, true, true, 2); + + var childrenLabel = UIFactory.CreateLabel(leftGroup, "ChildListTitle", "Children", TextAnchor.MiddleCenter, default, false, 16); + UIFactory.SetLayoutElement(childrenLabel.gameObject, flexibleWidth: 9999); + + // Add Child + var addChildRow = UIFactory.CreateUIObject("AddChildRow", leftGroup); + UIFactory.SetLayoutGroup(addChildRow, false, false, true, true, 2); + + addChildInput = UIFactory.CreateInputField(addChildRow, "AddChildInput", "Enter a name..."); + UIFactory.SetLayoutElement(addChildInput.Component.gameObject, minHeight: 25, preferredWidth: 9999); + + var addChildButton = UIFactory.CreateButton(addChildRow, "AddChildButton", "Add Child"); + UIFactory.SetLayoutElement(addChildButton.Component.gameObject, minHeight: 25, minWidth: 80); + addChildButton.OnClick += () => { OnAddChildClicked(addChildInput.Text); }; + + // TransformTree + + transformScroll = UIFactory.CreateScrollPool(leftGroup, "TransformTree", out GameObject transformObj, + out GameObject transformContent, new Color(0.11f, 0.11f, 0.11f)); + UIFactory.SetLayoutElement(transformObj, flexibleHeight: 9999); + UIFactory.SetLayoutElement(transformContent, flexibleHeight: 9999); + + TransformTree = new TransformTree(transformScroll, GetTransformEntries); + TransformTree.Init(); + TransformTree.OnClickOverrideHandler = ChangeTarget; + + // Right group (Components) + + var rightGroup = UIFactory.CreateUIObject("ComponentGroup", listHolder); + UIFactory.SetLayoutElement(rightGroup, flexibleWidth: 9999, flexibleHeight: 9999); + UIFactory.SetLayoutGroup(rightGroup, false, false, true, true, 2); + + var compLabel = UIFactory.CreateLabel(rightGroup, "CompListTitle", "Components", TextAnchor.MiddleCenter, default, false, 16); + UIFactory.SetLayoutElement(compLabel.gameObject, flexibleWidth: 9999); + + // Add Comp + var addCompRow = UIFactory.CreateUIObject("AddCompRow", rightGroup); + UIFactory.SetLayoutGroup(addCompRow, false, false, true, true, 2); + + addCompInput = UIFactory.CreateInputField(addCompRow, "AddCompInput", "Enter a Component type..."); + UIFactory.SetLayoutElement(addCompInput.Component.gameObject, minHeight: 25, preferredWidth: 9999); + + var addCompButton = UIFactory.CreateButton(addCompRow, "AddCompButton", "Add Comp"); + UIFactory.SetLayoutElement(addCompButton.Component.gameObject, minHeight: 25, minWidth: 80); + addCompButton.OnClick += () => { OnAddComponentClicked(addCompInput.Text); }; + + // comp autocompleter + new TypeCompleter(typeof(Component), addCompInput); + + // Component List + + componentScroll = UIFactory.CreateScrollPool(rightGroup, "ComponentList", out GameObject compObj, + out GameObject compContent, new Color(0.11f, 0.11f, 0.11f)); + UIFactory.SetLayoutElement(compObj, flexibleHeight: 9999); + UIFactory.SetLayoutElement(compContent, flexibleHeight: 9999); + + ComponentList = new ComponentList(componentScroll, GetComponentEntries); + ComponentList.Parent = this; + componentScroll.Initialize(ComponentList); + } + + + #endregion + } +} diff --git a/src/UI/Inspectors/GameObjectWidgets/ComponentCell.cs b/src/UI/Inspectors/GameObjectWidgets/ComponentCell.cs new file mode 100644 index 0000000..59ef68d --- /dev/null +++ b/src/UI/Inspectors/GameObjectWidgets/ComponentCell.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Inspectors +{ + public class ComponentCell : ButtonCell + { + public Toggle BehaviourToggle; + public ButtonRef DestroyButton; + + public Action OnBehaviourToggled; + public Action OnDestroyClicked; + + private void BehaviourToggled(bool val) + { + OnBehaviourToggled?.Invoke(val, CurrentDataIndex); + } + + private void DestroyClicked() + { + OnDestroyClicked?.Invoke(CurrentDataIndex); + } + + public override GameObject CreateContent(GameObject parent) + { + var root = base.CreateContent(parent); + + // Add mask to button so text doesnt overlap on Close button + this.Button.Component.gameObject.AddComponent().showMaskGraphic = true; + + // Behaviour toggle + + var toggleObj = UIFactory.CreateToggle(UIRoot, "BehaviourToggle", out BehaviourToggle, out var behavText); + UIFactory.SetLayoutElement(toggleObj, minHeight: 25, minWidth: 25); + BehaviourToggle.onValueChanged.AddListener(BehaviourToggled); + // put at first object + toggleObj.transform.SetSiblingIndex(0); + + // Destroy button + + DestroyButton = UIFactory.CreateButton(UIRoot, "DestroyButton", "X", new Color(0.3f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(DestroyButton.Component.gameObject, minHeight: 21, minWidth: 25); + DestroyButton.OnClick += DestroyClicked; + + return root; + } + } +} diff --git a/src/UI/Inspectors/GameObjectWidgets/ComponentList.cs b/src/UI/Inspectors/GameObjectWidgets/ComponentList.cs new file mode 100644 index 0000000..5f4338e --- /dev/null +++ b/src/UI/Inspectors/GameObjectWidgets/ComponentList.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Inspectors +{ + public class ComponentList : ButtonListHandler + { + public GameObjectInspector Parent; + + public ComponentList(ScrollPool scrollPool, Func> getEntriesMethod) + : base(scrollPool, getEntriesMethod, null, null, null) + { + base.SetICell = SetComponentCell; + base.ShouldDisplay = CheckShouldDisplay; + base.OnCellClicked = OnComponentClicked; + } + + public void Clear() + { + this.currentEntries.Clear(); + } + + private bool CheckShouldDisplay(Component _, string __) => true; + + public override void OnCellBorrowed(ComponentCell cell) + { + base.OnCellBorrowed(cell); + + cell.OnBehaviourToggled += OnBehaviourToggled; + cell.OnDestroyClicked += OnDestroyClicked; + } + + public override void SetCell(ComponentCell cell, int index) + { + base.SetCell(cell, index); + } + + private void OnComponentClicked(int index) + { + var entries = GetEntries(); + + if (index < 0 || index >= entries.Count) + return; + + var comp = entries[index]; + if (comp) + InspectorManager.Inspect(comp); + } + + private void OnBehaviourToggled(bool value, int index) + { + try + { + var entries = GetEntries(); + var comp = entries[index]; + + if (comp.TryCast() is Behaviour behaviour) + behaviour.enabled = value; + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception toggling Behaviour.enabled: {ex.ReflectionExToString()}"); + } + } + + private void OnDestroyClicked(int index) + { + try + { + var entries = GetEntries(); + var comp = entries[index]; + + GameObject.DestroyImmediate(comp); + + Parent.UpdateComponents(); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception destroying Component: {ex.ReflectionExToString()}"); + } + } + + private static readonly Dictionary compToStringCache = new Dictionary(); + + // Called from ButtonListHandler.SetCell, will be valid + private void SetComponentCell(ComponentCell cell, int index) + { + var entries = GetEntries(); + cell.Enable(); + + var comp = entries[index]; + var type = comp.GetActualType(); + + if (!compToStringCache.ContainsKey(type.AssemblyQualifiedName)) + compToStringCache.Add(type.AssemblyQualifiedName, SignatureHighlighter.Parse(type, true)); + + cell.Button.ButtonText.text = compToStringCache[type.AssemblyQualifiedName]; + + if (typeof(Behaviour).IsAssignableFrom(type)) + { + cell.BehaviourToggle.interactable = true; + cell.BehaviourToggle.Set(comp.TryCast().enabled, false); + cell.BehaviourToggle.graphic.color = new Color(0.8f, 1, 0.8f, 0.3f); + } + else + { + cell.BehaviourToggle.interactable = false; + cell.BehaviourToggle.Set(true, false); + //RuntimeProvider.Instance.SetColorBlock(cell.BehaviourToggle,) + cell.BehaviourToggle.graphic.color = new Color(0.2f, 0.2f, 0.2f); + } + + // if component is the first index it must be the transform, dont show Destroy button for it. + if (index == 0 && cell.DestroyButton.Component.gameObject.activeSelf) + cell.DestroyButton.Component.gameObject.SetActive(false); + else if (index > 0 && !cell.DestroyButton.Component.gameObject.activeSelf) + cell.DestroyButton.Component.gameObject.SetActive(true); + } + } +} diff --git a/src/UI/Inspectors/GameObjectWidgets/GameObjectControls.cs b/src/UI/Inspectors/GameObjectWidgets/GameObjectControls.cs new file mode 100644 index 0000000..f4cb64c --- /dev/null +++ b/src/UI/Inspectors/GameObjectWidgets/GameObjectControls.cs @@ -0,0 +1,669 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Input; + +namespace UnityExplorer.UI.Inspectors +{ + public class GameObjectControls + { + public GameObjectInspector Parent; + private GameObject GOTarget => Parent.GOTarget; + + // Top info + + private ButtonRef ViewParentButton; + private InputFieldRef PathInput; + + private InputFieldRef NameInput; + private Toggle ActiveSelfToggle; + private Text ActiveSelfText; + private Toggle IsStaticToggle; + + private InputFieldRef SceneInput; + private InputFieldRef InstanceIDInput; + private InputFieldRef TagInput; + + private Dropdown LayerDropdown; + private Dropdown FlagsDropdown; + + // transform controls + + private TransformControl PositionControl; + private TransformControl LocalPositionControl; + private TransformControl RotationControl; + private TransformControl ScaleControl; + + private VectorSlider currentSlidingVectorControl; + private float currentVectorValue; + + public GameObjectControls(GameObjectInspector parent) + { + this.Parent = parent; + + ConstructTopInfo(); + ConstructTransformControls(); + } + + #region GO Controls + + private string lastGoName; + private string lastPath; + private bool lastParentState; + private int lastSceneHandle; + private string lastTag; + private int lastLayer; + private int lastFlags; + + public void UpdateGameObjectInfo(bool firstUpdate, bool force) + { + if (firstUpdate) + { + InstanceIDInput.Text = GOTarget.GetInstanceID().ToString(); + } + + if (force || (!NameInput.Component.isFocused && GOTarget.name != lastGoName)) + { + lastGoName = GOTarget.name; + Parent.Tab.TabText.text = $"[G] {GOTarget.name}"; + NameInput.Text = GOTarget.name; + } + + if (force || !PathInput.Component.isFocused) + { + string path = GOTarget.transform.GetTransformPath(); + if (path != lastPath) + { + lastPath = path; + PathInput.Text = path; + } + } + + if (force || GOTarget.transform.parent != lastParentState) + { + lastParentState = GOTarget.transform.parent; + ViewParentButton.Component.interactable = lastParentState; + if (lastParentState) + { + ViewParentButton.ButtonText.color = Color.white; + ViewParentButton.ButtonText.text = "◄ View Parent"; + } + else + { + ViewParentButton.ButtonText.color = Color.grey; + ViewParentButton.ButtonText.text = "No parent"; + } + } + + if (force || GOTarget.activeSelf != ActiveSelfToggle.isOn) + { + ActiveSelfToggle.Set(GOTarget.activeSelf, false); + ActiveSelfText.color = ActiveSelfToggle.isOn ? Color.green : Color.red; + } + + if (force || GOTarget.isStatic != IsStaticToggle.isOn) + { + IsStaticToggle.Set(GOTarget.isStatic, false); + } + + if (force || GOTarget.scene.handle != lastSceneHandle) + { + lastSceneHandle = GOTarget.scene.handle; + SceneInput.Text = GOTarget.scene.IsValid() ? GOTarget.scene.name : "None (Asset/Resource)"; + } + + if (force || (!TagInput.Component.isFocused && GOTarget.tag != lastTag)) + { + lastTag = GOTarget.tag; + TagInput.Text = lastTag; + } + + if (force || (GOTarget.layer != lastLayer)) + { + lastLayer = GOTarget.layer; + LayerDropdown.value = GOTarget.layer; + } + + if (force || ((int)GOTarget.hideFlags != lastFlags)) + { + lastFlags = (int)GOTarget.hideFlags; + FlagsDropdown.captionText.text = GOTarget.hideFlags.ToString(); + } + } + + private void OnViewParentClicked() + { + if (this.GOTarget && this.GOTarget.transform.parent) + { + Parent.ChangeTarget(this.GOTarget.transform.parent.gameObject); + } + } + + private void OnPathEndEdit(string input) + { + lastPath = input; + + if (string.IsNullOrEmpty(input)) + { + DoSetParent(null); + } + else + { + Transform parentToSet = null; + + if (input.EndsWith("/")) + input = input.Remove(input.Length - 1); + + // try the easy way + if (GameObject.Find(input) is GameObject found) + { + parentToSet = found.transform; + } + else + { + // look for inactive objects + var name = input.Split('/').Last(); + var allObjects = RuntimeProvider.Instance.FindObjectsOfTypeAll(typeof(GameObject)); + var shortList = new List(); + foreach (var obj in allObjects) + if (obj.name == name) shortList.Add(obj.TryCast()); + foreach (var go in shortList) + { + var path = go.transform.GetTransformPath(true); + if (path.EndsWith("/")) + path = path.Remove(path.Length - 1); + if (path == input) + { + parentToSet = go.transform; + break; + } + } + } + + if (parentToSet) + DoSetParent(parentToSet); + else + { + ExplorerCore.LogWarning($"Could not find any GameObject name or path '{input}'!"); + UpdateGameObjectInfo(false, true); + } + } + + } + + private void DoSetParent(Transform transform) + { + ExplorerCore.Log($"Setting target's transform parent to: {(transform == null ? "null" : $"'{transform.name}'")}"); + + if (GOTarget.GetComponent()) + GOTarget.transform.SetParent(transform, false); + else + GOTarget.transform.parent = transform; + + UpdateGameObjectInfo(false, false); + UpdateTransformControlValues(false); + } + + private void OnNameEndEdit(string value) + { + GOTarget.name = value; + UpdateGameObjectInfo(false, true); + } + + private void OnActiveSelfToggled(bool value) + { + GOTarget.SetActive(value); + UpdateGameObjectInfo(false, true); + } + + private void OnTagEndEdit(string value) + { + try + { + GOTarget.tag = value; + UpdateGameObjectInfo(false, true); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception setting tag! {ex.ReflectionExToString()}"); + } + } + + private void OnLayerDropdownChanged(int value) + { + GOTarget.layer = value; + UpdateGameObjectInfo(false, true); + } + + private void OnFlagsDropdownChanged(int value) + { + try + { + var enumVal = hideFlagsValues[FlagsDropdown.options[value].text]; + GOTarget.hideFlags = enumVal; + + UpdateGameObjectInfo(false, true); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception setting hideFlags: {ex}"); + } + } + + private void OnDestroyClicked() + { + GameObject.Destroy(this.GOTarget); + InspectorManager.ReleaseInspector(Parent); + } + + private void OnInstantiateClicked() + { + var clone = GameObject.Instantiate(this.GOTarget); + InspectorManager.Inspect(clone); + } + + #endregion + + + #region Transform Controls + + private enum TransformType { Position, LocalPosition, Rotation, Scale } + + private class TransformControl + { + public TransformType Type; + public InputFieldRef Input; + + public TransformControl(TransformType type, InputFieldRef input) + { + this.Type = type; + this.Input = input; + } + } + + private class VectorSlider + { + public int axis; + public Slider slider; + public TransformControl parentControl; + + public VectorSlider(int axis, Slider slider, TransformControl parentControl) + { + this.axis = axis; + this.slider = slider; + this.parentControl = parentControl; + } + } + + private Vector3 lastPosValue; + private Vector3 lastLocalValue; + private Quaternion lastRotValue; + private Vector3 lastScaleValue; + + public void UpdateTransformControlValues(bool force) + { + var transform = GOTarget.transform; + if (force || (!PositionControl.Input.Component.isFocused && lastPosValue != transform.position)) + { + PositionControl.Input.Text = ParseUtility.ToStringForInput(transform.position, typeof(Vector3)); + lastPosValue = transform.position; + } + if (force || (!LocalPositionControl.Input.Component.isFocused && lastLocalValue != transform.localPosition)) + { + LocalPositionControl.Input.Text = ParseUtility.ToStringForInput(transform.localPosition, typeof(Vector3)); + lastLocalValue = transform.localPosition; + } + if (force || (!RotationControl.Input.Component.isFocused && lastRotValue != transform.localRotation)) + { + RotationControl.Input.Text = ParseUtility.ToStringForInput(transform.localRotation, typeof(Quaternion)); + lastRotValue = transform.localRotation; + } + if (force || (!ScaleControl.Input.Component.isFocused && lastScaleValue != transform.localScale)) + { + ScaleControl.Input.Text = ParseUtility.ToStringForInput(transform.localScale, typeof(Vector3)); + lastScaleValue = transform.localScale; + } + } + + private void OnTransformInputEndEdit(TransformType type, string input) + { + switch (type) + { + case TransformType.Position: + { + if (ParseUtility.TryParse(input, typeof(Vector3), out object boxed, out _)) + GOTarget.transform.position = (Vector3)boxed; + } + break; + case TransformType.LocalPosition: + { + if (ParseUtility.TryParse(input, typeof(Vector3), out object boxed, out _)) + GOTarget.transform.localPosition = (Vector3)boxed; + } + break; + case TransformType.Rotation: + { + if (ParseUtility.TryParse(input, typeof(Quaternion), out object boxed, out _)) + GOTarget.transform.localRotation = (Quaternion)boxed; + } + break; + case TransformType.Scale: + { + if (ParseUtility.TryParse(input, typeof(Vector3), out object boxed, out _)) + GOTarget.transform.localScale = (Vector3)boxed; + } + break; + } + + UpdateTransformControlValues(true); + } + + private void OnVectorSliderChanged(VectorSlider slider, float value) + { + if (value == 0f) + { + currentSlidingVectorControl = null; + } + else + { + currentSlidingVectorControl = slider; + currentVectorValue = value; + } + } + + public void UpdateVectorSlider() + { + if (currentSlidingVectorControl == null) + return; + + if (!InputManager.GetMouseButton(0)) + { + currentSlidingVectorControl.slider.value = 0f; + currentSlidingVectorControl = null; + currentVectorValue = 0f; + return; + } + + var transform = GOTarget.transform; + + Vector3 vector = Vector2.zero; + switch (currentSlidingVectorControl.parentControl.Type) + { + case TransformType.Position: + vector = transform.position; break; + case TransformType.LocalPosition: + vector = transform.localPosition; break; + case TransformType.Rotation: + vector = transform.eulerAngles; break; + case TransformType.Scale: + vector = transform.localScale; break; + } + + // apply vector value change + switch (currentSlidingVectorControl.axis) + { + case 0: + vector.x += currentVectorValue; break; + case 1: + vector.y += currentVectorValue; break; + case 2: + vector.z += currentVectorValue; break; + } + + // set vector back to transform + switch (currentSlidingVectorControl.parentControl.Type) + { + case TransformType.Position: + transform.position = vector; break; + case TransformType.LocalPosition: + transform.localPosition = vector; break; + case TransformType.Rotation: + transform.eulerAngles = vector; break; + case TransformType.Scale: + transform.localScale = vector; break; + } + + UpdateTransformControlValues(false); + } + + #endregion + + + #region GO Controls UI Construction + + private void ConstructTopInfo() + { + var topInfoHolder = UIFactory.CreateVerticalGroup(Parent.Content, "TopInfoHolder", false, false, true, true, 3, + new Vector4(3, 3, 3, 3), new Color(0.1f, 0.1f, 0.1f), TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(topInfoHolder, minHeight: 100, flexibleWidth: 9999); + topInfoHolder.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // first row (parent, path) + + var firstRow = UIFactory.CreateUIObject("ParentRow", topInfoHolder); + UIFactory.SetLayoutGroup(firstRow, false, false, true, true, 5, 0, 0, 0, 0, default); + UIFactory.SetLayoutElement(firstRow, minHeight: 25, flexibleWidth: 9999); + + ViewParentButton = UIFactory.CreateButton(firstRow, "ViewParentButton", "◄ View Parent", new Color(0.2f, 0.2f, 0.2f)); + ViewParentButton.ButtonText.fontSize = 13; + UIFactory.SetLayoutElement(ViewParentButton.Component.gameObject, minHeight: 25, minWidth: 100); + ViewParentButton.OnClick += OnViewParentClicked; + + this.PathInput = UIFactory.CreateInputField(firstRow, "PathInput", "..."); + PathInput.Component.textComponent.color = Color.grey; + PathInput.Component.textComponent.fontSize = 14; + UIFactory.SetLayoutElement(PathInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 9999); + PathInput.Component.lineType = InputField.LineType.MultiLineSubmit; + + //var pathApplyBtn = UIFactory.CreateButton(firstRow, "PathButton", "Set Parent Path", new Color(0.2f, 0.2f, 0.2f)); + //UIFactory.SetLayoutElement(pathApplyBtn.Component.gameObject, minHeight: 25, minWidth: 120); + //pathApplyBtn.OnClick += () => { OnPathEndEdit(PathInput.Text); }; + + PathInput.Component.onEndEdit.AddListener((string val) => { OnPathEndEdit(val); }); + + // Title and update row + + var titleRow = UIFactory.CreateUIObject("TitleRow", topInfoHolder); + UIFactory.SetLayoutGroup(titleRow, false, false, true, true, 5); + + var titleLabel = UIFactory.CreateLabel(titleRow, "Title", SignatureHighlighter.Parse(typeof(GameObject), false), + TextAnchor.MiddleLeft, fontSize: 17); + UIFactory.SetLayoutElement(titleLabel.gameObject, minHeight: 30, minWidth: 100); + + // name + + NameInput = UIFactory.CreateInputField(titleRow, "NameInput", "untitled"); + UIFactory.SetLayoutElement(NameInput.Component.gameObject, minHeight: 30, minWidth: 100, flexibleWidth: 9999); + NameInput.Component.textComponent.fontSize = 15; + NameInput.Component.onEndEdit.AddListener((string val) => { OnNameEndEdit(val); }); + + // second row (toggles, instanceID, tag, buttons) + + var secondRow = UIFactory.CreateUIObject("ParentRow", topInfoHolder); + UIFactory.SetLayoutGroup(secondRow, false, false, true, true, 5, 0, 0, 0, 0, default); + UIFactory.SetLayoutElement(secondRow, minHeight: 25, flexibleWidth: 9999); + + // activeSelf + var activeToggleObj = UIFactory.CreateToggle(secondRow, "ActiveSelf", out ActiveSelfToggle, out ActiveSelfText); + UIFactory.SetLayoutElement(activeToggleObj, minHeight: 25, minWidth: 100); + ActiveSelfText.text = "ActiveSelf"; + ActiveSelfToggle.onValueChanged.AddListener(OnActiveSelfToggled); + + // isStatic + var isStaticObj = UIFactory.CreateToggle(secondRow, "IsStatic", out IsStaticToggle, out Text staticText); + UIFactory.SetLayoutElement(isStaticObj, minHeight: 25, minWidth: 80); + staticText.text = "IsStatic"; + staticText.color = Color.grey; + IsStaticToggle.interactable = false; + + // InstanceID + var instanceIdLabel = UIFactory.CreateLabel(secondRow, "InstanceIDLabel", "Instance ID:", TextAnchor.MiddleRight, Color.grey); + UIFactory.SetLayoutElement(instanceIdLabel.gameObject, minHeight: 25, minWidth: 90); + + InstanceIDInput = UIFactory.CreateInputField(secondRow, "InstanceIDInput", "error"); + UIFactory.SetLayoutElement(InstanceIDInput.Component.gameObject, minHeight: 25, minWidth: 110); + InstanceIDInput.Component.textComponent.color = Color.grey; + InstanceIDInput.Component.readOnly = true; + + //Tag + var tagLabel = UIFactory.CreateLabel(secondRow, "TagLabel", "Tag:", TextAnchor.MiddleRight, Color.grey); + UIFactory.SetLayoutElement(tagLabel.gameObject, minHeight: 25, minWidth: 40); + + TagInput = UIFactory.CreateInputField(secondRow, "TagInput", "none"); + UIFactory.SetLayoutElement(TagInput.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 999); + TagInput.Component.textComponent.color = Color.white; + TagInput.Component.onEndEdit.AddListener((string val) => { OnTagEndEdit(val); }); + + // Instantiate + var instantiateBtn = UIFactory.CreateButton(secondRow, "InstantiateBtn", "Instantiate", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(instantiateBtn.Component.gameObject, minHeight: 25, minWidth: 120); + instantiateBtn.OnClick += OnInstantiateClicked; + + // Destroy + var destroyBtn = UIFactory.CreateButton(secondRow, "DestroyBtn", "Destroy", new Color(0.3f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(destroyBtn.Component.gameObject, minHeight: 25, minWidth: 80); + destroyBtn.OnClick += OnDestroyClicked; + + // third row (scene, layer, flags) + + var thirdrow = UIFactory.CreateUIObject("ParentRow", topInfoHolder); + UIFactory.SetLayoutGroup(thirdrow, false, false, true, true, 5, 0, 0, 0, 0, default); + UIFactory.SetLayoutElement(thirdrow, minHeight: 25, flexibleWidth: 9999); + + // Scene + var sceneLabel = UIFactory.CreateLabel(thirdrow, "SceneLabel", "Scene:", TextAnchor.MiddleLeft, Color.grey); + UIFactory.SetLayoutElement(sceneLabel.gameObject, minHeight: 25, minWidth: 50); + + SceneInput = UIFactory.CreateInputField(thirdrow, "SceneInput", "untitled"); + UIFactory.SetLayoutElement(SceneInput.Component.gameObject, minHeight: 25, minWidth: 120, flexibleWidth: 999); + SceneInput.Component.readOnly = true; + SceneInput.Component.textComponent.color = new Color(0.7f, 0.7f, 0.7f); + + // Layer + var layerLabel = UIFactory.CreateLabel(thirdrow, "LayerLabel", "Layer:", TextAnchor.MiddleLeft, Color.grey); + UIFactory.SetLayoutElement(layerLabel.gameObject, minHeight: 25, minWidth: 50); + + var layerDrop = UIFactory.CreateDropdown(thirdrow, out LayerDropdown, "0", 14, OnLayerDropdownChanged); + UIFactory.SetLayoutElement(layerDrop, minHeight: 25, minWidth: 120, flexibleWidth: 999); + LayerDropdown.captionText.color = SignatureHighlighter.EnumGreen; + if (layerToNames == null) + GetLayerNames(); + foreach (var name in layerToNames) + LayerDropdown.options.Add(new Dropdown.OptionData(name)); + LayerDropdown.value = 0; + LayerDropdown.RefreshShownValue(); + + // Flags + var flagsLabel = UIFactory.CreateLabel(thirdrow, "FlagsLabel", "Flags:", TextAnchor.MiddleRight, Color.grey); + UIFactory.SetLayoutElement(flagsLabel.gameObject, minHeight: 25, minWidth: 50); + + var flagsDrop = UIFactory.CreateDropdown(thirdrow, out FlagsDropdown, "None", 14, OnFlagsDropdownChanged); + FlagsDropdown.captionText.color = SignatureHighlighter.EnumGreen; + UIFactory.SetLayoutElement(flagsDrop, minHeight: 25, minWidth: 135, flexibleWidth: 999); + if (hideFlagsValues == null) + GetHideFlagNames(); + foreach (var name in hideFlagsValues.Keys) + FlagsDropdown.options.Add(new Dropdown.OptionData(name)); + FlagsDropdown.value = 0; + FlagsDropdown.RefreshShownValue(); + } + + private static List layerToNames; + + private static void GetLayerNames() + { + layerToNames = new List(); + for (int i = 0; i < 32; i++) + { + var name = RuntimeProvider.Instance.LayerToName(i); + if (string.IsNullOrEmpty(name)) + name = i.ToString(); + layerToNames.Add(name); + } + } + + private static Dictionary hideFlagsValues; + + private static void GetHideFlagNames() + { + hideFlagsValues = new Dictionary(); + + var names = Enum.GetValues(typeof(HideFlags)); + foreach (HideFlags value in names) + { + hideFlagsValues.Add(value.ToString(), value); + } + } + + #endregion + + + #region Transform Controls UI Construction + + private void ConstructTransformControls() + { + var transformGroup = UIFactory.CreateVerticalGroup(Parent.Content, "TransformControls", false, false, true, true, 2, + new Vector4(2, 2, 0, 0), new Color(0.1f, 0.1f, 0.1f)); + UIFactory.SetLayoutElement(transformGroup, minHeight: 100, flexibleWidth: 9999); + //transformGroup.SetActive(false); + //var groupRect = transformGroup.GetComponent(); + //groupRect.anchorMin = new Vector2(0, 1); + //groupRect.anchorMax = new Vector2(1, 1); + //groupRect.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 100); + + PositionControl = AddTransformRow(transformGroup, "Position:", TransformType.Position); + LocalPositionControl = AddTransformRow(transformGroup, "Local Position:", TransformType.LocalPosition); + RotationControl = AddTransformRow(transformGroup, "Rotation:", TransformType.Rotation); + ScaleControl = AddTransformRow(transformGroup, "Scale:", TransformType.Scale); + } + + private TransformControl AddTransformRow(GameObject transformGroup, string title, TransformType type) + { + var rowObj = UIFactory.CreateUIObject("Row_" + title, transformGroup); + UIFactory.SetLayoutGroup(rowObj, false, false, true, true, 5, 0, 0, 0, 0, default); + UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleWidth: 9999); + + var titleLabel = UIFactory.CreateLabel(rowObj, "PositionLabel", title, TextAnchor.MiddleRight, Color.grey); + UIFactory.SetLayoutElement(titleLabel.gameObject, minHeight: 25, minWidth: 110); + + var inputField = UIFactory.CreateInputField(rowObj, "InputField", "..."); + UIFactory.SetLayoutElement(inputField.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 999); + + inputField.Component.onEndEdit.AddListener((string value) => { OnTransformInputEndEdit(type, value); }); + + var control = new TransformControl(type, inputField); + + AddVectorAxisSlider(rowObj, "X", 0, control); + AddVectorAxisSlider(rowObj, "Y", 1, control); + AddVectorAxisSlider(rowObj, "Z", 2, control); + + return control; + } + + private VectorSlider AddVectorAxisSlider(GameObject parent, string title, int axis, TransformControl control) + { + var label = UIFactory.CreateLabel(parent, "Label_" + title, title + ":", TextAnchor.MiddleRight, Color.grey); + UIFactory.SetLayoutElement(label.gameObject, minHeight: 25, minWidth: 30); + + var sliderObj = UIFactory.CreateSlider(parent, "Slider_" + title, out var slider); + UIFactory.SetLayoutElement(sliderObj, minHeight: 25, minWidth: 120, flexibleWidth: 0); + slider.m_FillImage.color = Color.clear; + + slider.minValue = -1; + slider.maxValue = 1; + var sliderControl = new VectorSlider(axis, slider, control); + + slider.onValueChanged.AddListener((float val) => + { + OnVectorSliderChanged(sliderControl, val); + }); + + return sliderControl; + } + + #endregion + } +} diff --git a/src/UI/Inspectors/GameObjects/ChildList.cs b/src/UI/Inspectors/GameObjects/ChildList.cs deleted file mode 100644 index 28b7276..0000000 --- a/src/UI/Inspectors/GameObjects/ChildList.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.Inspectors.GameObjects -{ - public class ChildList - { - internal static ChildList Instance; - - public ChildList() - { - Instance = this; - } - - public static PageHandler s_childListPageHandler; - private static GameObject s_childListContent; - - private static GameObject[] s_allChildren = new GameObject[0]; - private static readonly List s_childrenShortlist = new List(); - private static int s_lastChildCount; - - private static readonly List s_childListTexts = new List(); - private static readonly List s_childListToggles = new List(); - - internal void RefreshChildObjectList() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - - s_allChildren = new GameObject[go.transform.childCount]; - for (int i = 0; i < go.transform.childCount; i++) - { - var child = go.transform.GetChild(i); - s_allChildren[i] = child.gameObject; - } - - var objects = s_allChildren; - s_childListPageHandler.ListCount = objects.Length; - - int newCount = 0; - - foreach (var itemIndex in s_childListPageHandler) - { - newCount++; - - // normalized index starting from 0 - var i = itemIndex - s_childListPageHandler.StartIndex; - - if (itemIndex >= objects.Length) - { - if (i > s_lastChildCount || i >= s_childListTexts.Count) - break; - - GameObject label = s_childListTexts[i].transform.parent.parent.gameObject; - if (label.activeSelf) - label.SetActive(false); - } - else - { - GameObject obj = objects[itemIndex]; - - if (!obj) - continue; - - if (i >= s_childrenShortlist.Count) - { - s_childrenShortlist.Add(obj); - AddChildListButton(); - } - else - { - s_childrenShortlist[i] = obj; - } - - var text = s_childListTexts[i]; - - var name = obj.name; - - if (obj.transform.childCount > 0) - name = $"[{obj.transform.childCount}] {name}"; - - text.text = name; - text.color = obj.activeSelf ? Color.green : Color.red; - - var tog = s_childListToggles[i]; - tog.isOn = obj.activeSelf; - - var label = text.transform.parent.parent.gameObject; - if (!label.activeSelf) - { - label.SetActive(true); - } - } - } - - s_lastChildCount = newCount; - } - - internal static void OnChildListObjectClicked(int index) - { - if (GameObjectInspector.ActiveInstance == null) - return; - - if (index >= s_childrenShortlist.Count || !s_childrenShortlist[index]) - return; - - GameObjectInspector.ActiveInstance.ChangeInspectorTarget(s_childrenShortlist[index]); - GameObjectInspector.ActiveInstance.Update(); - } - - internal static void OnChildListPageTurn() - { - if (Instance == null) - return; - - Instance.RefreshChildObjectList(); - } - - internal static void OnToggleClicked(int index, bool newVal) - { - if (GameObjectInspector.ActiveInstance == null) - return; - - if (index >= s_childrenShortlist.Count || !s_childrenShortlist[index]) - return; - - var obj = s_childrenShortlist[index]; - obj.SetActive(newVal); - } - - #region UI CONSTRUCTION - - internal void ConstructChildList(GameObject parent) - { - var vertGroupObj = UIFactory.CreateVerticalGroup(parent, "ChildListGroup", false, true, true, true, 5, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(vertGroupObj, minWidth: 120, flexibleWidth: 25000, minHeight: 200, flexibleHeight: 5000); - - var childTitle = UIFactory.CreateLabel(vertGroupObj, "ChildListTitle", "Children:", TextAnchor.MiddleLeft, Color.grey, true, 14); - UIFactory.SetLayoutElement(childTitle.gameObject, minHeight: 30); - - var childrenScrollObj = UIFactory.CreateScrollView(vertGroupObj, "ChildListScrollView", out s_childListContent, - out SliderScrollbar scroller, new Color(0.07f, 0.07f, 0.07f)); - UIFactory.SetLayoutElement(childrenScrollObj, minHeight: 50); - - s_childListPageHandler = new PageHandler(scroller); - s_childListPageHandler.ConstructUI(vertGroupObj); - s_childListPageHandler.OnPageChanged += OnChildListPageTurn; - } - - internal void AddChildListButton() - { - int thisIndex = s_childListTexts.Count; - - var btnGroupObj = UIFactory.CreateHorizontalGroup(s_childListContent, "ChildButtonGroup", true, false, true, true, - 0, default, new Color(0.07f, 0.07f, 0.07f)); - UIFactory.SetLayoutElement(btnGroupObj, flexibleWidth: 320, minHeight: 25, flexibleHeight: 0); - btnGroupObj.AddComponent(); - - var toggleObj = UIFactory.CreateToggle(btnGroupObj, "Toggle", out Toggle toggle, out Text toggleText, new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(toggleObj, minHeight: 25, minWidth: 25); - toggleText.text = ""; - toggle.isOn = false; - s_childListToggles.Add(toggle); - toggle.onValueChanged.AddListener((bool val) => { OnToggleClicked(thisIndex, val); }); - - var mainBtn = UIFactory.CreateButton(btnGroupObj, - "MainButton", - "", - () => { OnChildListObjectClicked(thisIndex); }); - - RuntimeProvider.Instance.SetColorBlock(mainBtn, new Color(0.07f, 0.07f, 0.07f), - new Color(0.2f, 0.2f, 0.2f, 1), new Color(0.05f, 0.05f, 0.05f)); - - UIFactory.SetLayoutElement(mainBtn.gameObject, minHeight: 25, flexibleHeight: 0, minWidth: 25, flexibleWidth: 9999); - - Text mainText = mainBtn.GetComponentInChildren(); - mainText.alignment = TextAnchor.MiddleLeft; - mainText.horizontalOverflow = HorizontalWrapMode.Overflow; - mainText.resizeTextForBestFit = true; - mainText.resizeTextMaxSize = 14; - mainText.resizeTextMinSize = 10; - - s_childListTexts.Add(mainText); - } - - #endregion - } -} diff --git a/src/UI/Inspectors/GameObjects/ComponentList.cs b/src/UI/Inspectors/GameObjects/ComponentList.cs deleted file mode 100644 index 869a66a..0000000 --- a/src/UI/Inspectors/GameObjects/ComponentList.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.Inspectors.GameObjects -{ - public class ComponentList - { - internal static ComponentList Instance; - - public ComponentList() - { - Instance = this; - } - - public static PageHandler s_compListPageHandler; - private static Component[] s_allComps = new Component[0]; - private static readonly List s_compShortlist = new List(); - private static GameObject s_compListContent; - private static readonly List s_compListTexts = new List(); - private static int s_lastCompCount; - public static readonly List s_compToggles = new List(); - - internal void RefreshComponentList() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - - s_allComps = go.GetComponents().ToArray(); - - var components = s_allComps; - s_compListPageHandler.ListCount = components.Length; - - //int startIndex = m_sceneListPageHandler.StartIndex; - - int newCount = 0; - - foreach (var itemIndex in s_compListPageHandler) - { - newCount++; - - // normalized index starting from 0 - var i = itemIndex - s_compListPageHandler.StartIndex; - - if (itemIndex >= components.Length) - { - if (i > s_lastCompCount || i >= s_compListTexts.Count) - break; - - GameObject label = s_compListTexts[i].transform.parent.parent.gameObject; - if (label.activeSelf) - label.SetActive(false); - } - else - { - Component comp = components[itemIndex]; - - if (!comp) - continue; - - if (i >= s_compShortlist.Count) - { - s_compShortlist.Add(comp); - AddCompListButton(); - } - else - { - s_compShortlist[i] = comp; - } - - var text = s_compListTexts[i]; - - text.text = SignatureHighlighter.ParseFullSyntax(ReflectionUtility.GetActualType(comp), true); - - var toggle = s_compToggles[i]; - if (comp.TryCast() is Behaviour behaviour) - { - if (!toggle.gameObject.activeSelf) - toggle.gameObject.SetActive(true); - - toggle.isOn = behaviour.enabled; - } - else - { - if (toggle.gameObject.activeSelf) - toggle.gameObject.SetActive(false); - } - - var label = text.transform.parent.parent.gameObject; - if (!label.activeSelf) - { - label.SetActive(true); - } - } - } - - s_lastCompCount = newCount; - } - - internal static void OnCompToggleClicked(int index, bool value) - { - var comp = s_compShortlist[index]; - comp.TryCast().enabled = value; - } - - internal static void OnCompListObjectClicked(int index) - { - if (index >= s_compShortlist.Count || !s_compShortlist[index]) - return; - - InspectorManager.Instance.Inspect(s_compShortlist[index]); - } - - internal static void OnCompListPageTurn() - { - if (Instance == null) - return; - - Instance.RefreshComponentList(); - } - - - #region UI CONSTRUCTION - - internal void ConstructCompList(GameObject parent) - { - var vertGroupObj = UIFactory.CreateVerticalGroup(parent, "ComponentList", false, true, true, true, 5, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(vertGroupObj, minWidth: 120, flexibleWidth: 25000, minHeight: 200, flexibleHeight: 5000); - - var compTitle = UIFactory.CreateLabel(vertGroupObj, "ComponentsTitle", "Components:", TextAnchor.MiddleLeft, Color.grey); - UIFactory.SetLayoutElement(compTitle.gameObject, minHeight: 30); - - var compScrollObj = UIFactory.CreateScrollView(vertGroupObj, "ComponentListScrollView", out s_compListContent, - out SliderScrollbar scroller, new Color(0.07f, 0.07f, 0.07f)); - - UIFactory.SetLayoutElement(compScrollObj, minHeight: 50, flexibleHeight: 5000); - - s_compListPageHandler = new PageHandler(scroller); - s_compListPageHandler.ConstructUI(vertGroupObj); - s_compListPageHandler.OnPageChanged += OnCompListPageTurn; - } - - internal void AddCompListButton() - { - int thisIndex = s_compListTexts.Count; - - GameObject groupObj = UIFactory.CreateHorizontalGroup(s_compListContent, "CompListButton", true, false, true, true, 0, default, - new Color(0.07f, 0.07f, 0.07f), TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(groupObj, minWidth: 25, flexibleWidth: 999, minHeight: 25, flexibleHeight: 0); - groupObj.AddComponent(); - - // Behaviour enabled toggle - - var toggleObj = UIFactory.CreateToggle(groupObj, "EnabledToggle", out Toggle toggle, out Text toggleText, new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(toggleObj, minWidth: 25, minHeight: 25); - toggleText.text = ""; - toggle.isOn = true; - s_compToggles.Add(toggle); - toggle.onValueChanged.AddListener((bool val) => { OnCompToggleClicked(thisIndex, val); }); - - // Main component button - - var mainBtn = UIFactory.CreateButton(groupObj, - "MainButton", - "", - () => { OnCompListObjectClicked(thisIndex); }); - - RuntimeProvider.Instance.SetColorBlock(mainBtn, new Color(0.07f, 0.07f, 0.07f), - new Color(0.2f, 0.2f, 0.2f, 1), new Color(0.05f, 0.05f, 0.05f)); - - UIFactory.SetLayoutElement(mainBtn.gameObject, minHeight: 25, flexibleHeight: 0, minWidth: 25, flexibleWidth: 999); - - // Component button text - - Text mainText = mainBtn.GetComponentInChildren(); - mainText.alignment = TextAnchor.MiddleLeft; - mainText.horizontalOverflow = HorizontalWrapMode.Overflow; - mainText.resizeTextForBestFit = true; - mainText.resizeTextMaxSize = 14; - mainText.resizeTextMinSize = 8; - - s_compListTexts.Add(mainText); - } - - #endregion - } -} diff --git a/src/UI/Inspectors/GameObjects/GameObjectControls.cs b/src/UI/Inspectors/GameObjects/GameObjectControls.cs deleted file mode 100644 index bd9bc66..0000000 --- a/src/UI/Inspectors/GameObjects/GameObjectControls.cs +++ /dev/null @@ -1,468 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Input; -using UnityExplorer.Core.Runtime; -using UnityExplorer.Core.Unity; - -namespace UnityExplorer.UI.Inspectors.GameObjects -{ - public class GameObjectControls - { - internal static GameObjectControls Instance; - - public GameObjectControls() - { - Instance = this; - } - - internal static bool Showing; - - internal static void ToggleVisibility() => SetVisibility(!Showing); - - internal static void SetVisibility(bool show) - { - if (show == Showing) - return; - - Showing = show; - - m_hideShowLabel.text = show ? "Hide" : "Show"; - m_contentObj.SetActive(show); - } - - internal static GameObject m_contentObj; - internal static Text m_hideShowLabel; - - private static InputField s_setParentInput; - - private static ControlEditor s_positionControl; - private static ControlEditor s_localPosControl; - private static ControlEditor s_rotationControl; - private static ControlEditor s_scaleControl; - - // Transform Vector editors - - internal struct ControlEditor - { - public InputField fullValue; - public Slider[] sliders; - public InputField[] inputs; - public Text[] values; - } - - internal static bool s_sliderChangedWanted; - private static Slider s_currentSlider; - private static ControlType s_currentSliderType; - private static VectorValue s_currentSliderValueType; - private static float s_currentSliderValue; - - internal enum ControlType - { - position, - localPosition, - eulerAngles, - localScale - } - - internal enum VectorValue - { - x, y, z - }; - - internal void RefreshControls() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - - s_positionControl.fullValue.text = go.transform.position.ToStringPretty(); - s_positionControl.values[0].text = go.transform.position.x.ToString("F3"); - s_positionControl.values[1].text = go.transform.position.y.ToString("F3"); - s_positionControl.values[2].text = go.transform.position.z.ToString("F3"); - - s_localPosControl.fullValue.text = go.transform.localPosition.ToStringPretty(); - s_localPosControl.values[0].text = go.transform.localPosition.x.ToString("F3"); - s_localPosControl.values[1].text = go.transform.localPosition.y.ToString("F3"); - s_localPosControl.values[2].text = go.transform.localPosition.z.ToString("F3"); - - s_rotationControl.fullValue.text = go.transform.eulerAngles.ToStringPretty(); - s_rotationControl.values[0].text = go.transform.eulerAngles.x.ToString("F3"); - s_rotationControl.values[1].text = go.transform.eulerAngles.y.ToString("F3"); - s_rotationControl.values[2].text = go.transform.eulerAngles.z.ToString("F3"); - - s_scaleControl.fullValue.text = go.transform.localScale.ToStringPretty(); - s_scaleControl.values[0].text = go.transform.localScale.x.ToString("F3"); - s_scaleControl.values[1].text = go.transform.localScale.y.ToString("F3"); - s_scaleControl.values[2].text = go.transform.localScale.z.ToString("F3"); - - } - - internal static void OnSetParentClicked() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - - if (!go) - return; - - var input = s_setParentInput.text; - - if (string.IsNullOrEmpty(input)) - { - go.transform.parent = null; - } - else - { - if (GameObject.Find(input) is GameObject newParent) - { - go.transform.parent = newParent.transform; - } - else - { - ExplorerCore.Log($"Could not find any GameObject from name or path '{input}'! Note: The target must be enabled."); - } - } - } - - internal static void OnSliderControlChanged(float value, Slider slider, ControlType controlType, VectorValue vectorValue) - { - if (value == 0) - s_sliderChangedWanted = false; - else - { - if (!s_sliderChangedWanted) - { - s_sliderChangedWanted = true; - s_currentSlider = slider; - s_currentSliderType = controlType; - s_currentSliderValueType = vectorValue; - } - - s_currentSliderValue = value; - } - } - - internal static void UpdateSliderControl() - { - if (!InputManager.GetMouseButton(0)) - { - s_sliderChangedWanted = false; - s_currentSlider.value = 0; - - return; - } - - if (GameObjectInspector.ActiveInstance == null) return; - - var transform = GameObjectInspector.ActiveInstance.TargetGO.transform; - - // get the current vector for the control type - Vector3 vector = Vector2.zero; - switch (s_currentSliderType) - { - case ControlType.position: - vector = transform.position; break; - case ControlType.localPosition: - vector = transform.localPosition; break; - case ControlType.eulerAngles: - vector = transform.eulerAngles; break; - case ControlType.localScale: - vector = transform.localScale; break; - } - - // apply vector value change - switch (s_currentSliderValueType) - { - case VectorValue.x: - vector.x += s_currentSliderValue; break; - case VectorValue.y: - vector.y += s_currentSliderValue; break; - case VectorValue.z: - vector.z += s_currentSliderValue; break; - } - - // set vector to transform member - switch (s_currentSliderType) - { - case ControlType.position: - transform.position = vector; break; - case ControlType.localPosition: - transform.localPosition = vector; break; - case ControlType.eulerAngles: - transform.eulerAngles = vector; break; - case ControlType.localScale: - transform.localScale = vector; break; - } - } - - internal static void OnVectorControlInputApplied(ControlType controlType, VectorValue vectorValue) - { - if (!(InspectorManager.Instance.m_activeInspector is GameObjectInspector instance)) return; - - // get relevant input for controltype + value - - InputField[] inputs = null; - switch (controlType) - { - case ControlType.position: - inputs = s_positionControl.inputs; break; - case ControlType.localPosition: - inputs = s_localPosControl.inputs; break; - case ControlType.eulerAngles: - inputs = s_rotationControl.inputs; break; - case ControlType.localScale: - inputs = s_scaleControl.inputs; break; - } - InputField input = inputs[(int)vectorValue]; - - float val = float.Parse(input.text); - - // apply transform value - - Vector3 vector = Vector3.zero; - var transform = instance.TargetGO.transform; - switch (controlType) - { - case ControlType.position: - vector = transform.position; break; - case ControlType.localPosition: - vector = transform.localPosition; break; - case ControlType.eulerAngles: - vector = transform.eulerAngles; break; - case ControlType.localScale: - vector = transform.localScale; break; - } - - switch (vectorValue) - { - case VectorValue.x: - vector.x = val; break; - case VectorValue.y: - vector.y = val; break; - case VectorValue.z: - vector.z = val; break; - } - - // set back to transform - switch (controlType) - { - case ControlType.position: - transform.position = vector; break; - case ControlType.localPosition: - transform.localPosition = vector; break; - case ControlType.eulerAngles: - transform.eulerAngles = vector; break; - case ControlType.localScale: - transform.localScale = vector; break; - } - } - - #region UI CONSTRUCTION - - internal void ConstructControls(GameObject parent) - { - var mainGroup = UIFactory.CreateVerticalGroup(parent, "ControlsGroup", false, false, true, true, 5, new Vector4(4,4,4,4), - new Color(0.07f, 0.07f, 0.07f)); - - // ~~~~~~ Top row ~~~~~~ - - var topRow = UIFactory.CreateHorizontalGroup(mainGroup, "TopRow", false, false, true, true, 5, default, new Color(1, 1, 1, 0)); - - var hideButton = UIFactory.CreateButton(topRow, "ToggleShowButton", "Show", ToggleVisibility, new Color(0.16f, 0.16f, 0.16f)); - UIFactory.SetLayoutElement(hideButton.gameObject, minWidth: 40, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); - m_hideShowLabel = hideButton.GetComponentInChildren(); - - var topTitle = UIFactory.CreateLabel(topRow, "ControlsLabel", "Controls", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(topTitle.gameObject, minWidth: 100, flexibleWidth: 9500, minHeight: 25); - - //// ~~~~~~~~ Content ~~~~~~~~ // - - m_contentObj = UIFactory.CreateVerticalGroup(mainGroup, "ContentGroup", true, false, true, true, 5, default, new Color(1, 1, 1, 0)); - - // transform controls - ConstructVector3Editor(m_contentObj, "Position", ControlType.position, out s_positionControl); - ConstructVector3Editor(m_contentObj, "Local Position", ControlType.localPosition, out s_localPosControl); - ConstructVector3Editor(m_contentObj, "Rotation", ControlType.eulerAngles, out s_rotationControl); - ConstructVector3Editor(m_contentObj, "Scale", ControlType.localScale, out s_scaleControl); - - // set parent - ConstructSetParent(m_contentObj); - - // bottom row buttons - ConstructBottomButtons(m_contentObj); - - // set controls content inactive now that content is made (otherwise TMP font size goes way too big?) - m_contentObj.SetActive(false); - } - - internal void ConstructSetParent(GameObject contentObj) - { - var setParentGroupObj = UIFactory.CreateHorizontalGroup(contentObj, "SetParentRow", false, false, true, true, 5, default, - new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(setParentGroupObj, minHeight: 25, flexibleHeight: 0); - - var title = UIFactory.CreateLabel(setParentGroupObj, "SetParentLabel", "Set Parent:", TextAnchor.MiddleLeft, Color.grey); - UIFactory.SetLayoutElement(title.gameObject, minWidth: 110, minHeight: 25, flexibleHeight: 0); - - var inputFieldObj = UIFactory.CreateInputField(setParentGroupObj, "SetParentInputField", "Enter a GameObject name or path..."); - s_setParentInput = inputFieldObj.GetComponent(); - UIFactory.SetLayoutElement(inputFieldObj, minHeight: 25, preferredWidth: 400, flexibleWidth: 9999); - - var applyButton = UIFactory.CreateButton(setParentGroupObj, "SetParentButton", "Apply", OnSetParentClicked); - UIFactory.SetLayoutElement(applyButton.gameObject, minWidth: 55, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); - } - - internal void ConstructVector3Editor(GameObject parent, string titleText, ControlType type, out ControlEditor editor) - { - editor = new ControlEditor(); - - var topBarObj = UIFactory.CreateHorizontalGroup(parent, "Vector3Editor", false, false, true, true, 5, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(topBarObj, minHeight: 25, flexibleHeight: 0); - - var title = UIFactory.CreateLabel(topBarObj, "Title", titleText, TextAnchor.MiddleLeft, Color.grey); - UIFactory.SetLayoutElement(title.gameObject, minWidth: 110, flexibleWidth: 0, minHeight: 25); - - // expand button - var expandButton = UIFactory.CreateButton(topBarObj, "ExpandArrow", "▼"); - var expandText = expandButton.GetComponentInChildren(); - expandText.fontSize = 12; - UIFactory.SetLayoutElement(expandButton.gameObject, minWidth: 35, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); - - // readonly value input - - var valueInputObj = UIFactory.CreateInputField(topBarObj, "ValueInput", "..."); - var valueInput = valueInputObj.GetComponent(); - valueInput.readOnly = true; - UIFactory.SetLayoutElement(valueInputObj, minHeight: 25, flexibleHeight: 0, preferredWidth: 400, flexibleWidth: 9999); - - editor.fullValue = valueInput; - - editor.sliders = new Slider[3]; - editor.inputs = new InputField[3]; - editor.values = new Text[3]; - - var xRow = ConstructEditorRow(parent, editor, type, VectorValue.x); - xRow.SetActive(false); - var yRow = ConstructEditorRow(parent, editor, type, VectorValue.y); - yRow.SetActive(false); - var zRow = ConstructEditorRow(parent, editor, type, VectorValue.z); - zRow.SetActive(false); - - // add expand callback now that we have group reference - expandButton.onClick.AddListener(ToggleExpand); - void ToggleExpand() - { - if (xRow.activeSelf) - { - xRow.SetActive(false); - yRow.SetActive(false); - zRow.SetActive(false); - expandText.text = "▼"; - } - else - { - xRow.SetActive(true); - yRow.SetActive(true); - zRow.SetActive(true); - expandText.text = "▲"; - } - } - } - - internal GameObject ConstructEditorRow(GameObject parent, ControlEditor editor, ControlType type, VectorValue vectorValue) - { - var rowObject = UIFactory.CreateHorizontalGroup(parent, "EditorRow", false, false, true, true, 5, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(rowObject, minHeight: 25, flexibleHeight: 0, minWidth: 100); - - // Value labels - - var valueTitle = UIFactory.CreateLabel(rowObject, "ValueTitle", $"{vectorValue.ToString().ToUpper()}:", TextAnchor.MiddleLeft, Color.cyan); - UIFactory.SetLayoutElement(valueTitle.gameObject, minHeight: 25, flexibleHeight: 0, minWidth: 25, flexibleWidth: 0); - - // actual value label - var valueLabel = UIFactory.CreateLabel(rowObject, "ValueLabel", "", TextAnchor.MiddleLeft); - editor.values[(int)vectorValue] = valueLabel; - UIFactory.SetLayoutElement(valueLabel.gameObject, minWidth: 85, flexibleWidth: 0, minHeight: 25); - - // input field - - var inputHolder = UIFactory.CreateVerticalGroup(rowObject, "InputFieldGroup", false, false, true, true, 0, default, new Color(1, 1, 1, 0)); - - var inputObj = UIFactory.CreateInputField(inputHolder, "InputField", "..."); - var input = inputObj.GetComponent(); - //input.characterValidation = InputField.CharacterValidation.Decimal; - - UIFactory.SetLayoutElement(inputObj, minHeight: 25, flexibleHeight: 0, minWidth: 90, flexibleWidth: 50); - - editor.inputs[(int)vectorValue] = input; - - // apply button - - var applyBtn = UIFactory.CreateButton(rowObject, "ApplyButton", "Apply", () => { OnVectorControlInputApplied(type, vectorValue); }); - UIFactory.SetLayoutElement(applyBtn.gameObject, minWidth: 60, minHeight: 25); - - - // Slider - - var sliderObj = UIFactory.CreateSlider(rowObject, "VectorSlider", out Slider slider); - UIFactory.SetLayoutElement(sliderObj, minHeight: 20, flexibleHeight: 0, minWidth: 200, flexibleWidth: 9000); - sliderObj.transform.Find("Fill Area").gameObject.SetActive(false); - RuntimeProvider.Instance.SetColorBlock(slider, new Color(0.65f, 0.65f, 0.65f)); - slider.minValue = -2; - slider.maxValue = 2; - slider.value = 0; - slider.onValueChanged.AddListener((float val) => { OnSliderControlChanged(val, slider, type, vectorValue); }); - editor.sliders[(int)vectorValue] = slider; - - return rowObject; - } - - internal void ConstructBottomButtons(GameObject contentObj) - { - var bottomRow = UIFactory.CreateHorizontalGroup(contentObj, "BottomButtons", true, true, false, false, 4, default, new Color(1, 1, 1, 0)); - - var instantiateBtn = UIFactory.CreateButton(bottomRow, "InstantiateBtn", "Instantiate", InstantiateBtn, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(instantiateBtn.gameObject, minWidth: 150); - - void InstantiateBtn() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - if (!go) - return; - - var clone = GameObject.Instantiate(go); - InspectorManager.Instance.Inspect(clone); - } - - var dontDestroyBtn = UIFactory.CreateButton(bottomRow, "DontDestroyButton", "Set DontDestroyOnLoad", DontDestroyOnLoadBtn, - new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(dontDestroyBtn.gameObject, flexibleWidth: 5000); - - void DontDestroyOnLoadBtn() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - if (!go) - return; - - GameObject.DontDestroyOnLoad(go); - } - - var destroyBtn = UIFactory.CreateButton(bottomRow, "DestroyButton", "Destroy", DestroyBtn, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(destroyBtn.gameObject, minWidth: 150); - var destroyText = destroyBtn.GetComponentInChildren(); - destroyText.color = Color.red; - - void DestroyBtn() - { - var go = GameObjectInspector.ActiveInstance.TargetGO; - if (!go) - return; - - GameObject.Destroy(go); - } - } - - #endregion - } -} diff --git a/src/UI/Inspectors/GameObjects/GameObjectInspector.cs b/src/UI/Inspectors/GameObjects/GameObjectInspector.cs deleted file mode 100644 index df01178..0000000 --- a/src/UI/Inspectors/GameObjects/GameObjectInspector.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Runtime; -using UnityExplorer.Core.Unity; - -namespace UnityExplorer.UI.Inspectors.GameObjects -{ - public class GameObjectInspector : InspectorBase - { - public override string TabLabel => $" [G] {TargetGO?.name}"; - - public static GameObjectInspector ActiveInstance { get; private set; } - - public GameObject TargetGO; - - // sub modules - internal static ChildList s_childList; - internal static ComponentList s_compList; - internal static GameObjectControls s_controls; - - internal static bool m_UIConstructed; - - public GameObjectInspector(GameObject target) : base(target) - { - ActiveInstance = this; - - TargetGO = target; - - if (!TargetGO) - { - ExplorerCore.LogWarning("Target GameObject is null!"); - return; - } - - // one UI is used for all gameobject inspectors. no point recreating it. - if (!m_UIConstructed) - { - m_UIConstructed = true; - - s_childList = new ChildList(); - s_compList = new ComponentList(); - s_controls = new GameObjectControls(); - - ConstructUI(); - } - } - - public override void SetActive() - { - base.SetActive(); - ActiveInstance = this; - } - - public override void SetInactive() - { - base.SetInactive(); - ActiveInstance = null; - } - - internal void ChangeInspectorTarget(GameObject newTarget) - { - if (!newTarget) - return; - - this.Target = this.TargetGO = newTarget; - } - - // Update - - public override void Update() - { - base.Update(); - - if (m_pendingDestroy || !this.IsActive) - return; - - RefreshTopInfo(); - - s_childList.RefreshChildObjectList(); - - s_compList.RefreshComponentList(); - - s_controls.RefreshControls(); - - if (GameObjectControls.s_sliderChangedWanted) - GameObjectControls.UpdateSliderControl(); - } - - private static GameObject s_content; - public override GameObject Content - { - get => s_content; - set => s_content = value; - } - - private static string m_lastName; - public static InputField m_nameInput; - - private static string m_lastPath; - public static InputField m_pathInput; - private static RectTransform m_pathInputRect; - private static GameObject m_pathGroupObj; - private static Text m_hiddenPathText; - private static RectTransform m_hiddenPathRect; - - private static Toggle m_enabledToggle; - private static Text m_enabledText; - private static bool? m_lastEnabledState; - - private static Dropdown m_layerDropdown; - private static int m_lastLayer = -1; - - private static Text m_sceneText; - private static string m_lastScene; - - internal void RefreshTopInfo() - { - var target = TargetGO; - string name = target.name; - - if (m_lastName != name) - { - m_lastName = name; - m_nameInput.text = m_lastName; - } - - if (target.transform.parent) - { - if (!m_pathGroupObj.activeSelf) - m_pathGroupObj.SetActive(true); - - var path = target.transform.GetTransformPath(true); - if (m_lastPath != path) - { - m_lastPath = path; - - m_pathInput.text = path; - m_hiddenPathText.text = path; - - LayoutRebuilder.ForceRebuildLayoutImmediate(m_pathInputRect); - LayoutRebuilder.ForceRebuildLayoutImmediate(m_hiddenPathRect); - } - } - else if (m_pathGroupObj.activeSelf) - m_pathGroupObj.SetActive(false); - - if (m_lastEnabledState != target.activeSelf) - { - m_lastEnabledState = target.activeSelf; - - m_enabledToggle.isOn = target.activeSelf; - m_enabledText.text = target.activeSelf ? "Enabled" : "Disabled"; - m_enabledText.color = target.activeSelf ? Color.green : Color.red; - } - - if (m_lastLayer != target.layer) - { - m_lastLayer = target.layer; - m_layerDropdown.value = target.layer; - } - - if (string.IsNullOrEmpty(m_lastScene) || m_lastScene != target.scene.name) - { - m_lastScene = target.scene.name; - - if (!string.IsNullOrEmpty(target.scene.name)) - m_sceneText.text = m_lastScene; - else - m_sceneText.text = "None (Asset/Resource)"; - } - } - - // UI Callbacks - - private static void OnApplyNameClicked() - { - if (ActiveInstance == null) - return; - - ActiveInstance.TargetGO.name = m_nameInput.text; - } - - private static void OnEnableToggled(bool enabled) - { - if (ActiveInstance == null) - return; - - ActiveInstance.TargetGO.SetActive(enabled); - } - - private static void OnLayerSelected(int layer) - { - if (ActiveInstance == null) - return; - - ActiveInstance.TargetGO.layer = layer; - } - - internal static void OnBackButtonClicked() - { - if (ActiveInstance == null) - return; - - ActiveInstance.ChangeInspectorTarget(ActiveInstance.TargetGO.transform.parent.gameObject); - } - - #region UI CONSTRUCTION - - internal void ConstructUI() - { - var parent = InspectorManager.m_inspectorContent; - - s_content = UIFactory.CreateScrollView(parent, "GameObjectInspector_Content", out GameObject scrollContent, out _, - new Color(0.1f, 0.1f, 0.1f)); - - UIFactory.SetLayoutGroup(scrollContent.transform.parent.gameObject, true, true, true, true); - - UIFactory.SetLayoutGroup(scrollContent, true, true, true, true, 5); - var contentFitter = scrollContent.GetComponent(); - contentFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained; - - ConstructTopArea(scrollContent); - - s_controls.ConstructControls(scrollContent); - - var midGroupObj = ConstructMidGroup(scrollContent); - - s_childList.ConstructChildList(midGroupObj); - s_compList.ConstructCompList(midGroupObj); - - LayoutRebuilder.ForceRebuildLayoutImmediate(s_content.GetComponent()); - Canvas.ForceUpdateCanvases(); - } - - private void ConstructTopArea(GameObject scrollContent) - { - // path row - - m_pathGroupObj = UIFactory.CreateHorizontalGroup(scrollContent, "TopArea", false, false, true, true, 5, default, new Color(0.1f, 0.1f, 0.1f)); - - var pathRect = m_pathGroupObj.GetComponent(); - pathRect.sizeDelta = new Vector2(pathRect.sizeDelta.x, 20); - UIFactory.SetLayoutElement(m_pathGroupObj, minHeight: 20, flexibleHeight: 75); - - // Back button - - var backButton = UIFactory.CreateButton(m_pathGroupObj, "BackButton", "◄", OnBackButtonClicked, new Color(0.15f, 0.15f, 0.15f)); - UIFactory.SetLayoutElement(backButton.gameObject, minWidth: 55, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); - - m_hiddenPathText = UIFactory.CreateLabel(m_pathGroupObj, "HiddenPathText", "", TextAnchor.MiddleLeft); - m_hiddenPathText.color = Color.clear; - m_hiddenPathText.fontSize = 14; - m_hiddenPathText.raycastTarget = false; - - var hiddenFitter = m_hiddenPathText.gameObject.AddComponent(); - hiddenFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; - - UIFactory.SetLayoutElement(m_hiddenPathText.gameObject, minHeight: 25, flexibleHeight: 125, minWidth: 250, flexibleWidth: 9000); - UIFactory.SetLayoutGroup(m_hiddenPathText.gameObject, true, true, true, true); - - // Path input - - var pathInputObj = UIFactory.CreateInputField(m_hiddenPathText.gameObject, "PathInputField", "..."); - UIFactory.SetLayoutElement(pathInputObj, minHeight: 25, flexibleHeight: 75, preferredWidth: 400, flexibleWidth: 9999); - var pathInputRect = pathInputObj.GetComponent(); - pathInputRect.sizeDelta = new Vector2(pathInputRect.sizeDelta.x, 25); - - m_pathInput = pathInputObj.GetComponent(); - m_pathInput.text = ActiveInstance.TargetGO.transform.GetTransformPath(); - m_pathInput.readOnly = true; - m_pathInput.lineType = InputField.LineType.MultiLineNewline; - m_pathInput.textComponent.color = new Color(0.75f, 0.75f, 0.75f); - - var textRect = m_pathInput.textComponent.GetComponent(); - textRect.offsetMin = new Vector2(3, 3); - textRect.offsetMax = new Vector2(3, 3); - - m_pathInputRect = m_pathInput.GetComponent(); - m_hiddenPathRect = m_hiddenPathText.GetComponent(); - - // name and enabled row - - var nameRowObj = UIFactory.CreateHorizontalGroup(scrollContent, "NameGroup", false, false, true, true, 5, default, new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(nameRowObj, minHeight: 25, flexibleHeight: 0); - var nameRect = nameRowObj.GetComponent(); - nameRect.sizeDelta = new Vector2(nameRect.sizeDelta.x, 25); - - var nameLabel = UIFactory.CreateLabel(nameRowObj, "NameLabel", "Name:", TextAnchor.MiddleCenter, Color.grey, true, 14); - UIFactory.SetLayoutElement(nameLabel.gameObject, minHeight: 25, flexibleHeight: 0, minWidth: 55, flexibleWidth: 0); - - var nameInputObj = UIFactory.CreateInputField(nameRowObj, "NameInput", "..."); - var nameInputRect = nameInputObj.GetComponent(); - nameInputRect.sizeDelta = new Vector2(nameInputRect.sizeDelta.x, 25); - m_nameInput = nameInputObj.GetComponent(); - m_nameInput.text = ActiveInstance.TargetGO.name; - - var applyNameBtn = UIFactory.CreateButton(nameRowObj, "ApplyNameButton", "Apply", OnApplyNameClicked); - UIFactory.SetLayoutElement(applyNameBtn.gameObject, minWidth: 65, minHeight: 25, flexibleHeight: 0); - var applyNameRect = applyNameBtn.GetComponent(); - applyNameRect.sizeDelta = new Vector2(applyNameRect.sizeDelta.x, 25); - - var activeLabel = UIFactory.CreateLabel(nameRowObj, "ActiveLabel", "Active:", TextAnchor.MiddleCenter, Color.grey, true, 14); - UIFactory.SetLayoutElement(activeLabel.gameObject, minWidth: 55, minHeight: 25); - - var enabledToggleObj = UIFactory.CreateToggle(nameRowObj, "EnabledToggle", out m_enabledToggle, out m_enabledText); - UIFactory.SetLayoutElement(enabledToggleObj, minHeight: 25, minWidth: 100, flexibleWidth: 0); - m_enabledText.text = "Enabled"; - m_enabledText.color = Color.green; - m_enabledToggle.onValueChanged.AddListener(OnEnableToggled); - - // layer and scene row - - var sceneLayerRow = UIFactory.CreateHorizontalGroup(scrollContent, "SceneLayerRow", false, true, true, true, 5, default, new Color(0.1f, 0.1f, 0.1f)); - - // layer - - var layerLabel = UIFactory.CreateLabel(sceneLayerRow, "LayerLabel", "Layer:", TextAnchor.MiddleCenter, Color.grey, true, 14); - UIFactory.SetLayoutElement(layerLabel.gameObject, minWidth: 55, flexibleWidth: 0); - - var layerDropdownObj = UIFactory.CreateDropdown(sceneLayerRow, out m_layerDropdown, "", 14, OnLayerSelected); - m_layerDropdown.options.Clear(); - for (int i = 0; i < 32; i++) - { - var layer = RuntimeProvider.Instance.LayerToName(i); - m_layerDropdown.options.Add(new Dropdown.OptionData { text = $"{i}: {layer}" }); - } - UIFactory.SetLayoutElement(layerDropdownObj, minWidth: 120, flexibleWidth: 2000, minHeight: 25); - - // scene - - var sceneLabel = UIFactory.CreateLabel(sceneLayerRow, "SceneLabel", "Scene:", TextAnchor.MiddleCenter, Color.grey, true, 14); - UIFactory.SetLayoutElement(sceneLabel.gameObject, minWidth: 55, flexibleWidth: 0); - - m_sceneText = UIFactory.CreateLabel(sceneLayerRow, "SceneText", "", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(m_sceneText.gameObject, minWidth: 120, flexibleWidth: 2000); - } - - private GameObject ConstructMidGroup(GameObject parent) - { - var midGroupObj = UIFactory.CreateHorizontalGroup(parent, "MidGroup", true, true, true, true, 5, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(midGroupObj, minHeight: 300, flexibleHeight: 3000); - return midGroupObj; - } - #endregion - } -} diff --git a/src/UI/Inspectors/InspectUnderMouse.cs b/src/UI/Inspectors/InspectUnderMouse.cs index ae837c6..19bf893 100644 --- a/src/UI/Inspectors/InspectUnderMouse.cs +++ b/src/UI/Inspectors/InspectUnderMouse.cs @@ -6,70 +6,97 @@ using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using UnityExplorer.Core; -using UnityExplorer.Core.Unity; using UnityExplorer.Core.Input; using UnityExplorer.Core.Runtime; using UnityExplorer.UI; -using UnityExplorer.UI.Main; -using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.Panels; -namespace UnityExplorer.UI.Main.Home +namespace UnityExplorer.UI.Inspectors { - public class InspectUnderMouse + public enum MouseInspectMode { - public enum MouseInspectMode + World, + UI + } + + public class InspectUnderMouse : UIPanel + { + public static InspectUnderMouse Instance { get; private set; } + + public InspectUnderMouse() { Instance = this; } + + public static void OnDropdownSelect(int index) { - World, - UI + switch (index) + { + case 0: return; + case 1: Instance.StartInspect(MouseInspectMode.World); break; + case 2: Instance.StartInspect(MouseInspectMode.UI); break; + } + UIManager.MouseInspectDropdown.value = 0; } - public static bool Inspecting { get; set; } + // UIPanel + public override string Name => "Inspect Under Mouse"; + public override UIManager.Panels PanelType => UIManager.Panels.MouseInspector; + public override int MinWidth => -1; + public override int MinHeight => -1; + public override bool CanDragAndResize => false; + public override bool NavButtonWanted => false; + public override bool ShouldSaveActiveState => false; + public override bool ShowByDefault => false; + internal static Text objNameLabel; + internal static Text objPathLabel; + internal static Text mousePosLabel; + + // Mouse Inspector + public static bool Inspecting { get; set; } public static MouseInspectMode Mode { get; set; } - private static GameObject s_lastHit; - private static Vector3 s_lastMousePos; + private static GameObject lastHitObject; + private static Vector3 lastMousePos; - private static readonly List _wasDisabledGraphics = new List(); - private static readonly List _wasDisabledCanvasGroups = new List(); - private static readonly List _objectsAddedCastersTo = new List(); + private static readonly List wasDisabledGraphics = new List(); + private static readonly List wasDisabledCanvasGroups = new List(); + private static readonly List objectsAddedCastersTo = new List(); internal static Camera MainCamera; internal static GraphicRaycaster[] graphicRaycasters; - public static void Init() - { - ConstructUI(); - } - public static void StartInspect(MouseInspectMode mode) + public void StartInspect(MouseInspectMode mode) { MainCamera = Camera.main; if (!MainCamera) return; + PanelDragger.ForceEnd(); + Mode = mode; Inspecting = true; - MainMenu.Instance.MainPanel.SetActive(false); + UIManager.NavBarRect.gameObject.SetActive(false); + UIManager.PanelHolder.SetActive(false); - s_UIContent.SetActive(true); + UIRoot.SetActive(true); if (mode == MouseInspectMode.UI) SetupUIRaycast(); } - internal static void ClearHitData() + internal void ClearHitData() { - s_lastHit = null; - s_objNameLabel.text = "No hits..."; - s_objPathLabel.text = ""; + lastHitObject = null; + objNameLabel.text = "No hits..."; + objPathLabel.text = ""; } - public static void StopInspect() + public void StopInspect() { Inspecting = false; - MainMenu.Instance.MainPanel.SetActive(true); - s_UIContent.SetActive(false); + UIManager.NavBarRect.gameObject.SetActive(true); + UIManager.PanelHolder.SetActive(true); + UIRoot.SetActive(false); if (Mode == MouseInspectMode.UI) StopUIInspect(); @@ -77,7 +104,9 @@ namespace UnityExplorer.UI.Main.Home ClearHitData(); } - public static void UpdateInspect() + private static float timeOfLastRaycast; + + public void UpdateInspect() { if (InputManager.GetKeyDown(KeyCode.Escape)) { @@ -85,11 +114,24 @@ namespace UnityExplorer.UI.Main.Home return; } + if (lastHitObject && InputManager.GetMouseButtonDown(0)) + { + var target = lastHitObject; + StopInspect(); + InspectorManager.Inspect(target); + return; + } + var mousePos = InputManager.MousePosition; - if (mousePos != s_lastMousePos) + if (mousePos != lastMousePos) UpdatePosition(mousePos); + if (!timeOfLastRaycast.OccuredEarlierThan(0.1f)) + return; + + timeOfLastRaycast = Time.realtimeSinceStartup; + // actual inspect raycast switch (Mode) @@ -101,37 +143,42 @@ namespace UnityExplorer.UI.Main.Home } } - internal static void UpdatePosition(Vector2 mousePos) + internal void UpdatePosition(Vector2 mousePos) { - s_lastMousePos = mousePos; + lastMousePos = mousePos; + // use the raw mouse pos for the label + mousePosLabel.text = $"Mouse Position: {mousePos.ToString()}"; + + // constrain the mouse pos we use within certain bounds + if (mousePos.x < 350) + mousePos.x = 350; + if (mousePos.x > Screen.width - 350) + mousePos.x = Screen.width - 350; + if (mousePos.y < mainPanelRect.rect.height) + mousePos.y += mainPanelRect.rect.height + 10; + else + mousePos.y -= 10; + + // calculate and set our UI position var inversePos = UIManager.CanvasRoot.transform.InverseTransformPoint(mousePos); - s_mousePosLabel.text = $"Mouse Position: {mousePos.ToString()}"; - - float yFix = mousePos.y < 120 ? 80 : -80; - s_UIContent.transform.localPosition = new Vector3(inversePos.x, inversePos.y + yFix, 0); + UIRoot.transform.localPosition = new Vector3(inversePos.x, inversePos.y, 0); } - internal static void OnHitGameObject(GameObject obj) + internal void OnHitGameObject(GameObject obj) { - if (obj != s_lastHit) + if (obj != lastHitObject) { - s_lastHit = obj; - s_objNameLabel.text = $"Click to Inspect: {obj.name}"; - s_objPathLabel.text = $"Path: {obj.transform.GetTransformPath(true)}"; - } - - if (InputManager.GetMouseButtonDown(0)) - { - StopInspect(); - InspectorManager.Instance.Inspect(obj); + lastHitObject = obj; + objNameLabel.text = $"Click to Inspect: {obj.name}"; + objPathLabel.text = $"Path: {obj.transform.GetTransformPath(true)}"; } } // Collider raycasting - internal static void RaycastWorld(Vector2 mousePos) + internal void RaycastWorld(Vector2 mousePos) { var ray = MainCamera.ScreenPointToRay(mousePos); Physics.Raycast(ray, out RaycastHit hit, 1000f); @@ -143,7 +190,7 @@ namespace UnityExplorer.UI.Main.Home } else { - if (s_lastHit) + if (lastHitObject) ClearHitData(); } } @@ -154,14 +201,14 @@ namespace UnityExplorer.UI.Main.Home { foreach (var obj in RuntimeProvider.Instance.FindObjectsOfTypeAll(typeof(Canvas))) { - var canvas = obj.Cast(typeof(Canvas)) as Canvas; + var canvas = obj.TryCast(); if (!canvas || !canvas.enabled || !canvas.gameObject.activeInHierarchy) continue; if (!canvas.GetComponent()) { canvas.gameObject.AddComponent(); //ExplorerCore.Log("Added raycaster to " + canvas.name); - _objectsAddedCastersTo.Add(canvas.gameObject); + objectsAddedCastersTo.Add(canvas.gameObject); } } @@ -170,33 +217,33 @@ namespace UnityExplorer.UI.Main.Home graphicRaycasters = new GraphicRaycaster[casters.Length]; for (int i = 0; i < casters.Length; i++) { - graphicRaycasters[i] = casters[i].Cast(typeof(GraphicRaycaster)) as GraphicRaycaster; + graphicRaycasters[i] = casters[i].TryCast(); } // enable raycastTarget on Graphics foreach (var obj in RuntimeProvider.Instance.FindObjectsOfTypeAll(typeof(Graphic))) { - var graphic = obj.Cast(typeof(Graphic)) as Graphic; + var graphic = obj.TryCast(); if (!graphic || !graphic.enabled || graphic.raycastTarget || !graphic.gameObject.activeInHierarchy) continue; graphic.raycastTarget = true; //ExplorerCore.Log("Enabled raycastTarget on " + graphic.name); - _wasDisabledGraphics.Add(graphic); + wasDisabledGraphics.Add(graphic); } // enable blocksRaycasts on CanvasGroups foreach (var obj in RuntimeProvider.Instance.FindObjectsOfTypeAll(typeof(CanvasGroup))) { - var canvas = obj.Cast(typeof(CanvasGroup)) as CanvasGroup; + var canvas = obj.TryCast(); if (!canvas || !canvas.gameObject.activeInHierarchy || canvas.blocksRaycasts) continue; canvas.blocksRaycasts = true; //ExplorerCore.Log("Enabled raycasts on " + canvas.name); - _wasDisabledCanvasGroups.Add(canvas); + wasDisabledCanvasGroups.Add(canvas); } } - internal static void RaycastUI(Vector2 mousePos) + internal void RaycastUI(Vector2 mousePos) { var ped = new PointerEventData(null) { @@ -210,17 +257,18 @@ namespace UnityExplorer.UI.Main.Home int highestDepth = int.MinValue; foreach (var gr in graphicRaycasters) { + if (!gr || !gr.canvas) + continue; + var list = new List(); RuntimeProvider.Instance.GraphicRaycast(gr, ped, list); - //gr.Raycast(ped, list); - if (list.Count > 0) { foreach (var hit in list) { - // Manual trying to determine which object is "on top". - // Not perfect, but not terrible. + // Manualy trying to determine which object is "on top". + // Could be improved, but seems to work pretty well and isn't as laggy as you would expect. if (!hit.gameObject) continue; @@ -258,7 +306,7 @@ namespace UnityExplorer.UI.Main.Home } else { - if (s_lastHit) + if (lastHitObject) ClearHitData(); } } @@ -271,58 +319,64 @@ namespace UnityExplorer.UI.Main.Home private static void StopUIInspect() { - foreach (var obj in _objectsAddedCastersTo) + foreach (var obj in objectsAddedCastersTo) { if (obj.GetComponent() is GraphicRaycaster raycaster) GameObject.Destroy(raycaster); } - foreach (var graphic in _wasDisabledGraphics) + foreach (var graphic in wasDisabledGraphics) graphic.raycastTarget = false; - foreach (var canvas in _wasDisabledCanvasGroups) + foreach (var canvas in wasDisabledCanvasGroups) canvas.blocksRaycasts = false; - _objectsAddedCastersTo.Clear(); - _wasDisabledCanvasGroups.Clear(); - _wasDisabledGraphics.Clear(); + objectsAddedCastersTo.Clear(); + wasDisabledCanvasGroups.Clear(); + wasDisabledGraphics.Clear(); } - internal static Text s_objNameLabel; - internal static Text s_objPathLabel; - internal static Text s_mousePosLabel; - internal static GameObject s_UIContent; - internal static void ConstructUI() + // UI Construction + + protected internal override void DoSetDefaultPosAndAnchors() { - s_UIContent = UIFactory.CreatePanel("InspectUnderMouse_UI", out GameObject content); + mainPanelRect.anchorMin = Vector2.zero; + mainPanelRect.anchorMax = Vector2.zero; + mainPanelRect.pivot = new Vector2(0.5f, 1); + mainPanelRect.sizeDelta = new Vector2(700, 150); + } - var baseRect = s_UIContent.GetComponent(); - var half = new Vector2(0.5f, 0.5f); - baseRect.anchorMin = half; - baseRect.anchorMax = half; - baseRect.pivot = half; - baseRect.sizeDelta = new Vector2(700, 150); + public override void ConstructPanelContent() + { + // hide title bar + this.titleBar.SetActive(false); + this.UIRoot.transform.SetParent(UIManager.CanvasRoot.transform, false); - var group = content.GetComponent(); - group.childForceExpandHeight = true; + var inspectContent = UIFactory.CreateVerticalGroup(this.content, "InspectContent", true, true, true, true, 3, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(inspectContent, flexibleWidth: 9999, flexibleHeight: 9999); // Title text - UIFactory.CreateLabel(content, "InspectLabel", "Mouse Inspector (press ESC to cancel)", TextAnchor.MiddleCenter); + var title = UIFactory.CreateLabel(inspectContent, "InspectLabel", "Mouse Inspector (press ESC to cancel)", TextAnchor.MiddleCenter); + UIFactory.SetLayoutElement(title.gameObject, flexibleWidth: 9999); - s_mousePosLabel = UIFactory.CreateLabel(content, "MousePosLabel", "Mouse Position:", TextAnchor.MiddleCenter); + mousePosLabel = UIFactory.CreateLabel(inspectContent, "MousePosLabel", "Mouse Position:", TextAnchor.MiddleCenter); - s_objNameLabel = UIFactory.CreateLabel(content, "HitLabelObj", "No hits...", TextAnchor.MiddleLeft); - s_objNameLabel.horizontalOverflow = HorizontalWrapMode.Overflow; + objNameLabel = UIFactory.CreateLabel(inspectContent, "HitLabelObj", "No hits...", TextAnchor.MiddleLeft); + objNameLabel.horizontalOverflow = HorizontalWrapMode.Overflow; - s_objPathLabel = UIFactory.CreateLabel(content, "PathLabel", "", TextAnchor.MiddleLeft); - s_objPathLabel.fontStyle = FontStyle.Italic; - s_objPathLabel.horizontalOverflow = HorizontalWrapMode.Wrap; + objPathLabel = UIFactory.CreateLabel(inspectContent, "PathLabel", "", TextAnchor.MiddleLeft); + objPathLabel.fontStyle = FontStyle.Italic; + objPathLabel.horizontalOverflow = HorizontalWrapMode.Wrap; - UIFactory.SetLayoutElement(s_objPathLabel.gameObject, minHeight: 75); + UIFactory.SetLayoutElement(objPathLabel.gameObject, minHeight: 75); - s_UIContent.SetActive(false); + UIRoot.SetActive(false); } + + public override void DoSaveToConfigElement() { } + + public override string GetSaveDataFromConfigManager() => null; } } diff --git a/src/UI/Inspectors/InspectorBase.cs b/src/UI/Inspectors/InspectorBase.cs index 2e167c0..0634bac 100644 --- a/src/UI/Inspectors/InspectorBase.cs +++ b/src/UI/Inspectors/InspectorBase.cs @@ -1,120 +1,68 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; using UnityEngine; using UnityEngine.UI; -using UnityExplorer.Core.Unity; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Panels; namespace UnityExplorer.UI.Inspectors { - public abstract class InspectorBase + public abstract class InspectorBase : IPooledObject { - public object Target; - public bool IsActive { get; private set; } + public bool IsActive { get; internal set; } + public object Target { get; set; } - public abstract string TabLabel { get; } + public InspectorTab Tab { get; internal set; } - internal bool m_pendingDestroy; + public GameObject UIRoot { get; set; } - public InspectorBase(object target) + public float DefaultHeight => -1f; + public abstract GameObject CreateContent(GameObject parent); + + public abstract void Update(); + + public abstract void CloseInspector(); + + public virtual void OnBorrowedFromPool(object target) { - Target = target; + this.Target = target; + Tab = Pool.Borrow(); + Tab.UIRoot.transform.SetParent(InspectorPanel.Instance.NavbarHolder.transform, false); - if (Target.IsNullOrDestroyed(false)) - { - Destroy(); - return; - } - - AddInspectorTab(this); + Tab.TabButton.OnClick += OnTabButtonClicked; + Tab.CloseButton.OnClick += CloseInspector; } - public virtual void SetActive() + public virtual void OnReturnToPool() { - this.IsActive = true; - Content?.SetActive(true); + Pool.Return(Tab); + + this.Target = null; + + Tab.TabButton.OnClick -= OnTabButtonClicked; + Tab.CloseButton.OnClick -= CloseInspector; } - public virtual void SetInactive() + public virtual void OnSetActive() { - this.IsActive = false; - Content?.SetActive(false); + Tab.SetTabColor(true); + UIRoot.SetActive(true); + IsActive = true; + LayoutRebuilder.ForceRebuildLayoutImmediate(UIRoot.GetComponent()); } - public virtual void Update() + public virtual void OnSetInactive() { - if (Target.IsNullOrDestroyed(false)) - { - Destroy(); - return; - } - - m_tabText.text = TabLabel; + Tab.SetTabColor(false); + UIRoot.SetActive(false); + IsActive = false; } - public virtual void Destroy() + private void OnTabButtonClicked() { - m_pendingDestroy = true; - - GameObject tabGroup = m_tabButton?.transform.parent.gameObject; - - if (tabGroup) - GameObject.Destroy(tabGroup); - - int thisIndex = -1; - if (InspectorManager.Instance.m_currentInspectors.Contains(this)) - { - thisIndex = InspectorManager.Instance.m_currentInspectors.IndexOf(this); - InspectorManager.Instance.m_currentInspectors.Remove(this); - } - - if (ReferenceEquals(InspectorManager.Instance.m_activeInspector, this)) - { - InspectorManager.Instance.UnsetInspectorTab(); - - if (InspectorManager.Instance.m_currentInspectors.Count > 0) - { - var prevTab = InspectorManager.Instance.m_currentInspectors[thisIndex > 0 ? thisIndex - 1 : 0]; - InspectorManager.Instance.SetInspectorTab(prevTab); - } - } + InspectorManager.SetInspectorActive(this); } - - #region UI - - public abstract GameObject Content { get; set; } - public Button m_tabButton; - public Text m_tabText; - - public void AddInspectorTab(InspectorBase parent) - { - var tabContent = InspectorManager.m_tabBarContent; - - var tabGroupObj = UIFactory.CreateHorizontalGroup(tabContent, "TabObject", true, true, true, true); - UIFactory.SetLayoutElement(tabGroupObj, minWidth: 185, flexibleWidth: 0); - tabGroupObj.AddComponent(); - - m_tabButton = UIFactory.CreateButton(tabGroupObj, - "TabButton", - "", - () => { InspectorManager.Instance.SetInspectorTab(parent); }); - - UIFactory.SetLayoutElement(m_tabButton.gameObject, minWidth: 165, flexibleWidth: 0); - - m_tabText = m_tabButton.GetComponentInChildren(); - m_tabText.horizontalOverflow = HorizontalWrapMode.Overflow; - m_tabText.alignment = TextAnchor.MiddleLeft; - - var closeBtn = UIFactory.CreateButton(tabGroupObj, - "CloseButton", - "X", - parent.Destroy, - new Color(0.2f, 0.2f, 0.2f, 1)); - - UIFactory.SetLayoutElement(closeBtn.gameObject, minWidth: 20, flexibleWidth: 0); - - var closeBtnText = closeBtn.GetComponentInChildren(); - closeBtnText.color = new Color(1, 0, 0, 1); - } - - #endregion } } diff --git a/src/UI/Inspectors/InspectorManager.cs b/src/UI/Inspectors/InspectorManager.cs index 739a193..c1dffe0 100644 --- a/src/UI/Inspectors/InspectorManager.cs +++ b/src/UI/Inspectors/InspectorManager.cs @@ -1,204 +1,166 @@ using System; using System.Collections.Generic; using System.Linq; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using UnityExplorer.UI.Main; +using System.Text; using UnityEngine; -using UnityEngine.SceneManagement; using UnityEngine.UI; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.Main.Home; +using UnityExplorer.UI; using UnityExplorer.UI.CacheObject; -using UnityExplorer.UI.Inspectors.GameObjects; -using UnityExplorer.UI.Inspectors.Reflection; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Panels; -namespace UnityExplorer.UI.Inspectors +namespace UnityExplorer { - public class InspectorManager + public static class InspectorManager { - public static InspectorManager Instance { get; private set; } + public static readonly List Inspectors = new List(); - public InspectorManager() + public static InspectorBase ActiveInspector { get; private set; } + private static InspectorBase lastActiveInspector; + + public static float PanelWidth; + + internal static void CloseAllTabs() { - Instance = this; - - ConstructInspectorPane(); - } - - public InspectorBase m_activeInspector; - public readonly List m_currentInspectors = new List(); - - public void Update() - { - for (int i = 0; i < m_currentInspectors.Count; i++) + if (Inspectors.Any()) { - if (i >= m_currentInspectors.Count) - break; + for (int i = Inspectors.Count - 1; i >= 0; i--) + Inspectors[i].CloseInspector(); - m_currentInspectors[i].Update(); + Inspectors.Clear(); } + + UIManager.SetPanelActive(UIManager.Panels.Inspector, false); } - public void Inspect(object obj, CacheObjectBase parentMember = null) + public static void Inspect(object obj, CacheObjectBase sourceCache = null) { - obj = ReflectionProvider.Instance.Cast(obj, ReflectionProvider.Instance.GetActualType(obj)); - - UnityEngine.Object unityObj = obj as UnityEngine.Object; - - if (obj.IsNullOrDestroyed(false)) - { + if (obj.IsNullOrDestroyed()) return; - } - // check if currently inspecting this object - foreach (InspectorBase tab in m_currentInspectors) - { - if (RuntimeProvider.Instance.IsReferenceEqual(obj, tab.Target)) - { - SetInspectorTab(tab); - return; - } - } + obj = obj.TryCast(); - InspectorBase inspector; - if (obj is GameObject go) - inspector = new GameObjectInspector(go); + if (TryFocusActiveInspector(obj)) + return; + + if (obj is GameObject) + CreateInspector(obj); else - inspector = new InstanceInspector(obj); - - if (inspector is ReflectionInspector ri) - ri.ParentMember = parentMember; - - m_currentInspectors.Add(inspector); - SetInspectorTab(inspector); + CreateInspector(obj, false, sourceCache); } - public void Inspect(Type type) + private static bool TryFocusActiveInspector(object target) { - if (type == null) + foreach (var inspector in Inspectors) { - ExplorerCore.LogWarning("The provided type was null!"); - return; - } - - foreach (var tab in m_currentInspectors.Where(x => x is StaticInspector)) - { - if (ReferenceEquals(tab.Target as Type, type)) + if (inspector.Target.ReferenceEqual(target)) { - SetInspectorTab(tab); - return; + UIManager.SetPanelActive(UIManager.Panels.Inspector, true); + SetInspectorActive(inspector); + return true; } } - - var inspector = new StaticInspector(type); - - m_currentInspectors.Add(inspector); - SetInspectorTab(inspector); + return false; } - public void SetInspectorTab(InspectorBase inspector) + public static void Inspect(Type type) { - MainMenu.Instance.SetPage(HomePage.Instance); - - if (m_activeInspector == inspector) - return; - - UnsetInspectorTab(); - - m_activeInspector = inspector; - inspector.SetActive(); - - OnSetInspectorTab(inspector); + CreateInspector(type, true); } - public void UnsetInspectorTab() + public static void SetInspectorActive(InspectorBase inspector) { - if (m_activeInspector == null) - return; + UnsetActiveInspector(); - m_activeInspector.SetInactive(); - - OnUnsetInspectorTab(); - - m_activeInspector = null; + ActiveInspector = inspector; + inspector.OnSetActive(); } - public static GameObject m_tabBarContent; - public static GameObject m_inspectorContent; - - public void OnSetInspectorTab(InspectorBase inspector) + public static void UnsetActiveInspector() { - Color activeColor = new Color(0, 0.25f, 0, 1); - RuntimeProvider.Instance.SetColorBlock(inspector.m_tabButton, activeColor, activeColor); + if (ActiveInspector != null) + { + lastActiveInspector = ActiveInspector; + ActiveInspector.OnSetInactive(); + ActiveInspector = null; + } } - public void OnUnsetInspectorTab() + private static void CreateInspector(object target, bool staticReflection = false, + CacheObjectBase sourceCache = null) where T : InspectorBase { - RuntimeProvider.Instance.SetColorBlock(m_activeInspector.m_tabButton, - new Color(0.2f, 0.2f, 0.2f, 1), new Color(0.1f, 0.3f, 0.1f, 1)); + var inspector = Pool.Borrow(); + Inspectors.Add(inspector); + inspector.Target = target; + + if (sourceCache != null && sourceCache.CanWrite) + { + // only set parent cache object if we are inspecting a struct, otherwise there is no point. + if (target.GetType().IsValueType && inspector is ReflectionInspector ri) + ri.ParentCacheObject = sourceCache; + } + + UIManager.SetPanelActive(UIManager.Panels.Inspector, true); + inspector.UIRoot.transform.SetParent(InspectorPanel.Instance.ContentHolder.transform, false); + + if (inspector is ReflectionInspector reflectInspector) + reflectInspector.StaticOnly = staticReflection; + + inspector.OnBorrowedFromPool(target); + SetInspectorActive(inspector); } - public void ConstructInspectorPane() + internal static void ReleaseInspector(T inspector) where T : InspectorBase { - var mainObj = UIFactory.CreateVerticalGroup(HomePage.Instance.Content, - "InspectorManager_Root", - true, true, true, true, - 4, - new Vector4(4,4,4,4)); + if (lastActiveInspector == inspector) + lastActiveInspector = null; - UIFactory.SetLayoutElement(mainObj, preferredHeight: 400, flexibleHeight: 9000, preferredWidth: 620, flexibleWidth: 9000); + bool wasActive = ActiveInspector == inspector; + int wasIdx = Inspectors.IndexOf(inspector); - var topRowObj = UIFactory.CreateHorizontalGroup(mainObj, "TopRow", false, true, true, true, 15); - - var inspectorTitle = UIFactory.CreateLabel(topRowObj, "Title", "Inspector", TextAnchor.MiddleLeft, default, true, 25); + Inspectors.Remove(inspector); + inspector.OnReturnToPool(); + Pool.Return(inspector); - UIFactory.SetLayoutElement(inspectorTitle.gameObject, minHeight: 30, flexibleHeight: 0, minWidth: 90, flexibleWidth: 20000); - - ConstructToolbar(topRowObj); - - // inspector tab bar - - m_tabBarContent = UIFactory.CreateGridGroup(mainObj, "TabHolder", new Vector2(185, 20), new Vector2(5, 2), new Color(0.1f, 0.1f, 0.1f, 1)); - - var gridGroup = m_tabBarContent.GetComponent(); - gridGroup.padding.top = 3; - gridGroup.padding.left = 3; - gridGroup.padding.right = 3; - gridGroup.padding.bottom = 3; - - // inspector content area - - m_inspectorContent = UIFactory.CreateVerticalGroup(mainObj, "InspectorContent", - true, true, true, true, - 0, - new Vector4(2,2,2,2), - new Color(0.1f, 0.1f, 0.1f)); - - UIFactory.SetLayoutElement(m_inspectorContent, preferredHeight: 900, flexibleHeight: 10000, preferredWidth: 600, flexibleWidth: 10000); + if (wasActive) + { + ActiveInspector = null; + // Try focus another inspector, or close the window. + if (lastActiveInspector != null) + { + SetInspectorActive(lastActiveInspector); + lastActiveInspector = null; + } + else if (Inspectors.Any()) + { + int newIdx = Math.Min(Inspectors.Count - 1, Math.Max(0, wasIdx - 1)); + SetInspectorActive(Inspectors[newIdx]); + } + else + { + UIManager.SetPanelActive(UIManager.Panels.Inspector, false); + } + } } - private static void ConstructToolbar(GameObject topRowObj) + internal static void Update() { - // invisible group - UIFactory.CreateHorizontalGroup(topRowObj, "Toolbar", false, false, true, true, 10, new Vector4(2, 2, 2, 2), new Color(1,1,1,0)); - - // inspect under mouse button - AddMouseInspectButton(topRowObj, "UI", InspectUnderMouse.MouseInspectMode.UI); - AddMouseInspectButton(topRowObj, "3D", InspectUnderMouse.MouseInspectMode.World); + for (int i = Inspectors.Count - 1; i >= 0; i--) + Inspectors[i].Update(); } - private static void AddMouseInspectButton(GameObject topRowObj, string suffix, InspectUnderMouse.MouseInspectMode mode) + internal static void OnPanelResized(float width) { - string lbl = $"Mouse Inspect ({suffix})"; + PanelWidth = width; - var inspectObj = UIFactory.CreateButton(topRowObj, - lbl, - lbl, - () => { InspectUnderMouse.StartInspect(mode); }, - new Color(0.2f, 0.2f, 0.2f)); - - UIFactory.SetLayoutElement(inspectObj.gameObject, minWidth: 150, flexibleWidth: 0); + foreach (var obj in Inspectors) + { + if (obj is ReflectionInspector inspector) + { + inspector.SetLayouts(); + } + } } } } diff --git a/src/UI/Inspectors/InspectorTab.cs b/src/UI/Inspectors/InspectorTab.cs new file mode 100644 index 0000000..ae8f22a --- /dev/null +++ b/src/UI/Inspectors/InspectorTab.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Inspectors +{ + public class InspectorTab : IPooledObject + { + public GameObject UIRoot { get; set; } + + public float DefaultHeight => 25f; + + public ButtonRef TabButton; + public Text TabText; + + public ButtonRef CloseButton; + + private static readonly Color _enabledTabColor = new Color(0.15f, 0.22f, 0.15f); + private static readonly Color _disabledTabColor = new Color(0.13f, 0.13f, 0.13f); + + public void SetTabColor(bool active) + { + if (active) + RuntimeProvider.Instance.SetColorBlock(TabButton.Component, _enabledTabColor, _enabledTabColor * 1.2f); + else + RuntimeProvider.Instance.SetColorBlock(TabButton.Component, _disabledTabColor, _disabledTabColor * 1.2f); + } + + public GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateHorizontalGroup(parent, "TabObject", false, true, true, true, 0, + default, new Color(0.13f, 0.13f, 0.13f), childAlignment: TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(UIRoot, minWidth: 200, flexibleWidth: 0); + UIRoot.AddComponent(); + + TabButton = UIFactory.CreateButton(UIRoot, "TabButton", ""); + UIFactory.SetLayoutElement(TabButton.Component.gameObject, minWidth: 175, flexibleWidth: 0); + UIFactory.SetLayoutGroup(TabButton.Component.gameObject, false, false, true, true, 0, 0, 0, 3); + + TabText = TabButton.Component.GetComponentInChildren(); + UIFactory.SetLayoutElement(TabText.gameObject, minHeight: 25, minWidth: 175, flexibleWidth: 0); + TabText.alignment = TextAnchor.MiddleLeft; + TabText.fontSize = 12; + TabText.horizontalOverflow = HorizontalWrapMode.Overflow; + + CloseButton = UIFactory.CreateButton(UIRoot, "CloseButton", "X", new Color(0.2f, 0.2f, 0.2f, 1)); + UIFactory.SetLayoutElement(CloseButton.Component.gameObject, minHeight: 25, minWidth: 25, flexibleWidth: 0); + var closeBtnText = CloseButton.Component.GetComponentInChildren(); + closeBtnText.color = Color.red; + + return UIRoot; + } + } +} diff --git a/src/UI/Inspectors/Reflection/InstanceInspector.cs b/src/UI/Inspectors/Reflection/InstanceInspector.cs deleted file mode 100644 index 8806b4a..0000000 --- a/src/UI/Inspectors/Reflection/InstanceInspector.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.Config; -using UnityExplorer.Core.Runtime; - -namespace UnityExplorer.UI.Inspectors.Reflection -{ - public enum MemberScopes - { - All, - Instance, - Static - } - - public class InstanceInspector : ReflectionInspector - { - public override string TabLabel => $" [R] {base.TabLabel}"; - - internal MemberScopes m_scopeFilter; - internal Button m_lastActiveScopeButton; - - public InstanceInspector(object target) : base(target) { } - - internal void OnScopeFilterClicked(MemberScopes type, Button button) - { - if (m_lastActiveScopeButton) - RuntimeProvider.Instance.SetColorBlock(m_lastActiveScopeButton, new Color(0.2f, 0.2f, 0.2f)); - - m_scopeFilter = type; - m_lastActiveScopeButton = button; - - RuntimeProvider.Instance.SetColorBlock(m_lastActiveScopeButton, new Color(0.2f, 0.6f, 0.2f)); - - FilterMembers(null, true); - m_sliderScroller.m_slider.value = 1f; - } - - public void ConstructUnityInstanceHelpers() - { - if (!typeof(UnityEngine.Object).IsAssignableFrom(m_targetType)) - return; - - var rowObj = UIFactory.CreateHorizontalGroup(Content, "InstanceHelperRow", true, true, true, true, 5, new Vector4(2,2,2,2), - new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleWidth: 5000); - - if (typeof(Component).IsAssignableFrom(m_targetType)) - ConstructCompHelper(rowObj); - - ConstructUnityObjHelper(rowObj); - - if (m_targetType == typeof(Texture2D)) - ConstructTextureHelper(); - } - - internal void ConstructCompHelper(GameObject rowObj) - { - var gameObjectLabel = UIFactory.CreateLabel(rowObj, "GameObjectLabel", "GameObject:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(gameObjectLabel.gameObject, minWidth: 90, minHeight: 25, flexibleWidth: 0); - - var comp = Target.Cast(typeof(Component)) as Component; - - var btn = UIFactory.CreateButton(rowObj, - "GameObjectButton", - comp.name, - () => { InspectorManager.Instance.Inspect(comp.gameObject); }, - new Color(0.2f, 0.5f, 0.2f)); - UIFactory.SetLayoutElement(btn.gameObject, minHeight: 25, minWidth: 200, flexibleWidth: 0); - } - - internal void ConstructUnityObjHelper(GameObject rowObj) - { - var label = UIFactory.CreateLabel(rowObj, "NameLabel", "Name:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(label.gameObject, minWidth: 60, minHeight: 25, flexibleWidth: 0); - - var uObj = Target.Cast(typeof(UnityEngine.Object)) as UnityEngine.Object; - - var inputObj = UIFactory.CreateInputField(rowObj, "NameInput", "...", 14, 3, 1); - UIFactory.SetLayoutElement(inputObj, minHeight: 25, flexibleWidth: 2000); - var inputField = inputObj.GetComponent(); - inputField.readOnly = true; - inputField.text = uObj.name; - } - - internal bool showingTextureHelper; - internal bool constructedTextureViewer; - - internal GameObject m_textureViewerObj; - - internal void ConstructTextureHelper() - { - var rowObj = UIFactory.CreateHorizontalGroup(Content, "TextureHelper", true, false, true, true, 5, new Vector4(3,3,3,3), - new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleHeight: 0); - - var showBtn = UIFactory.CreateButton(rowObj, "ShowButton", "Show", null, new Color(0.2f, 0.6f, 0.2f)); - UIFactory.SetLayoutElement(showBtn.gameObject, minWidth: 50, flexibleWidth: 0); - - UIFactory.CreateLabel(rowObj, "TextureViewerLabel", "Texture Viewer", TextAnchor.MiddleLeft); - - var textureViewerObj = UIFactory.CreateScrollView(Content, "TextureViewerContent", out GameObject scrollContent, out _, new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutGroup(textureViewerObj, false, false, true, true); - UIFactory.SetLayoutElement(textureViewerObj, minHeight: 100, flexibleHeight: 9999, flexibleWidth: 9999); - - textureViewerObj.SetActive(false); - - m_textureViewerObj = textureViewerObj; - - var showText = showBtn.GetComponentInChildren(); - showBtn.onClick.AddListener(() => - { - showingTextureHelper = !showingTextureHelper; - - if (showingTextureHelper) - { - if (!constructedTextureViewer) - ConstructTextureViewerArea(scrollContent); - - showText.text = "Hide"; - ToggleTextureViewer(true); - } - else - { - showText.text = "Show"; - ToggleTextureViewer(false); - } - }); - } - - internal void ConstructTextureViewerArea(GameObject parent) - { - constructedTextureViewer = true; - - var tex = Target.Cast(typeof(Texture2D)) as Texture2D; - - if (!tex) - { - ExplorerCore.LogWarning("Could not cast the target instance to Texture2D! Maybe its null or destroyed?"); - return; - } - - // Save helper - - var saveRowObj = UIFactory.CreateHorizontalGroup(parent, "SaveRow", true, true, true, true, 2, new Vector4(2,2,2,2), - new Color(0.1f, 0.1f, 0.1f)); - - var saveBtn = UIFactory.CreateButton(saveRowObj, "SaveButton", "Save .PNG", null, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(saveBtn.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); - - var inputObj = UIFactory.CreateInputField(saveRowObj, "SaveInput", "..."); - UIFactory.SetLayoutElement(inputObj, minHeight: 25, minWidth: 100, flexibleWidth: 9999); - var inputField = inputObj.GetComponent(); - - var name = tex.name; - if (string.IsNullOrEmpty(name)) - name = "untitled"; - - inputField.text = Path.Combine(ConfigManager.Default_Output_Path.Value, $"{name}.png"); - - saveBtn.onClick.AddListener(() => - { - if (tex && !string.IsNullOrEmpty(inputField.text)) - { - var path = inputField.text; - if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) - { - ExplorerCore.LogWarning("Desired save path must end with '.png'!"); - return; - } - - var dir = Path.GetDirectoryName(path); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - if (File.Exists(path)) - File.Delete(path); - - if (!TextureUtilProvider.IsReadable(tex)) - tex = TextureUtilProvider.ForceReadTexture(tex); - - byte[] data = TextureUtilProvider.Instance.EncodeToPNG(tex); - - File.WriteAllBytes(path, data); - } - }); - - // Actual texture viewer - - var imageObj = UIFactory.CreateUIObject("TextureViewerImage", parent); - var image = imageObj.AddComponent(); - var sprite = TextureUtilProvider.Instance.CreateSprite(tex); - image.sprite = sprite; - - var fitter = imageObj.AddComponent(); - fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; - - var imageLayout = imageObj.AddComponent(); - imageLayout.preferredHeight = sprite.rect.height; - imageLayout.preferredWidth = sprite.rect.width; - } - - internal void ToggleTextureViewer(bool enabled) - { - m_textureViewerObj.SetActive(enabled); - - m_filterAreaObj.SetActive(!enabled); - m_memberListObj.SetActive(!enabled); - m_updateRowObj.SetActive(!enabled); - } - - public void ConstructInstanceFilters(GameObject parent) - { - var memberFilterRowObj = UIFactory.CreateHorizontalGroup(parent, "InstanceFilterRow", false, false, true, true, 5, default, - new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(memberFilterRowObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 5000); - - var memLabel = UIFactory.CreateLabel(memberFilterRowObj, "MemberLabel", "Filter scope:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(memLabel.gameObject, minWidth: 100, minHeight: 25, flexibleWidth: 0); - - AddFilterButton(memberFilterRowObj, MemberScopes.All, true); - AddFilterButton(memberFilterRowObj, MemberScopes.Instance); - AddFilterButton(memberFilterRowObj, MemberScopes.Static); - } - - private void AddFilterButton(GameObject parent, MemberScopes type, bool setEnabled = false) - { - var btn = UIFactory.CreateButton(parent, - "ScopeFilterButton_" + type, - type.ToString(), - null, - new Color(0.2f, 0.2f, 0.2f)); - - UIFactory.SetLayoutElement(btn.gameObject, minHeight: 25, minWidth: 70); - - btn.onClick.AddListener(() => { OnScopeFilterClicked(type, btn); }); - - RuntimeProvider.Instance.SetColorBlock(btn, highlighted: new Color(0.3f, 0.7f, 0.3f)); - - if (setEnabled) - { - RuntimeProvider.Instance.SetColorBlock(btn, new Color(0.2f, 0.6f, 0.2f)); - m_scopeFilter = type; - m_lastActiveScopeButton = btn; - } - } - } -} diff --git a/src/UI/Inspectors/Reflection/ReflectionInspector.cs b/src/UI/Inspectors/Reflection/ReflectionInspector.cs deleted file mode 100644 index 0787477..0000000 --- a/src/UI/Inspectors/Reflection/ReflectionInspector.cs +++ /dev/null @@ -1,518 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.Config; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.CacheObject; -using UnityExplorer.UI.Main; -using UnityExplorer.UI.Main.Home; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.Inspectors.Reflection -{ - public class ReflectionInspector : InspectorBase - { - #region STATIC - - public static ReflectionInspector ActiveInstance { get; private set; } - - static ReflectionInspector() - { - PanelDragger.OnFinishResize += (RectTransform _) => OnContainerResized(); - SceneExplorer.OnToggleShow += OnContainerResized; - } - - private static void OnContainerResized(bool _ = false) - { - if (ActiveInstance == null) - return; - - ActiveInstance.m_widthUpdateWanted = true; - } - - // Blacklists - private static readonly HashSet bl_typeAndMember = new HashSet - { -#if CPP - // these cause a crash in IL2CPP - "Type.DeclaringMethod", - "Rigidbody2D.Cast", - "Collider2D.Cast", - "Collider2D.Raycast", - "Texture2D.SetPixelDataImpl", - "Camera.CalculateProjectionMatrixFromPhysicalProperties", -#endif - }; - private static readonly HashSet bl_memberNameStartsWith = new HashSet - { - // these are redundant - "get_", - "set_", - }; - - #endregion - - #region INSTANCE - - public override string TabLabel => m_targetTypeShortName; - - internal CacheObjectBase ParentMember { get; set; } - - internal readonly Type m_targetType; - internal readonly string m_targetTypeShortName; - - // all cached members of the target - internal CacheMember[] m_allMembers; - // filtered members based on current filters - internal readonly List m_membersFiltered = new List(); - // actual shortlist of displayed members - internal readonly CacheMember[] m_displayedMembers = new CacheMember[ConfigManager.Default_Page_Limit.Value]; - - internal bool m_autoUpdate; - - public ReflectionInspector(object target) : base(target) - { - if (this is StaticInspector) - m_targetType = target as Type; - else - m_targetType = ReflectionUtility.GetActualType(target); - - m_targetTypeShortName = SignatureHighlighter.ParseFullSyntax(m_targetType, false); - - ConstructUI(); - - CacheMembers(m_targetType); - - FilterMembers(); - } - - public override void SetActive() - { - base.SetActive(); - ActiveInstance = this; - } - - public override void SetInactive() - { - base.SetInactive(); - ActiveInstance = null; - } - - public override void Destroy() - { - base.Destroy(); - - if (this.Content) - GameObject.Destroy(this.Content); - } - - internal void OnPageTurned() - { - RefreshDisplay(); - } - - internal bool IsBlacklisted(string sig) => bl_typeAndMember.Any(it => sig.Contains(it)); - internal bool IsBlacklisted(MethodInfo method) => bl_memberNameStartsWith.Any(it => method.Name.StartsWith(it)); - - internal string GetSig(MemberInfo member) => $"{member.DeclaringType.Name}.{member.Name}"; - internal string AppendArgsToSig(ParameterInfo[] args) - { - string ret = " ("; - foreach (var param in args) - ret += $"{param.ParameterType.Name} {param.Name}, "; - ret += ")"; - return ret; - } - - public void CacheMembers(Type type) - { - var list = new List(); - var cachedSigs = new HashSet(); - - var types = ReflectionUtility.GetAllBaseTypes(type); - - var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static; - if (this is InstanceInspector) - flags |= BindingFlags.Instance; - - foreach (var declaringType in types) - { - var target = Target; - target = target.Cast(declaringType); - - IEnumerable infos = declaringType.GetMethods(flags); - infos = infos.Concat(declaringType.GetProperties(flags)); - infos = infos.Concat(declaringType.GetFields(flags)); - - foreach (var member in infos) - { - try - { - var sig = GetSig(member); - - //ExplorerCore.Log($"Trying to cache member {sig}..."); - //ExplorerCore.Log(member.DeclaringType.FullName + "." + member.Name); - - var mi = member as MethodInfo; - var pi = member as PropertyInfo; - var fi = member as FieldInfo; - - if (IsBlacklisted(sig) || (mi != null && IsBlacklisted(mi))) - continue; - - var args = mi?.GetParameters() ?? pi?.GetIndexParameters(); - if (args != null) - { - if (!CacheMember.CanProcessArgs(args)) - continue; - - sig += AppendArgsToSig(args); - } - - if (cachedSigs.Contains(sig)) - continue; - - cachedSigs.Add(sig); - - if (mi != null) - list.Add(new CacheMethod(mi, target, m_scrollContent)); - else if (pi != null) - list.Add(new CacheProperty(pi, target, m_scrollContent)); - else - list.Add(new CacheField(fi, target, m_scrollContent)); - - list.Last().ParentInspector = this; - } - catch (Exception e) - { - ExplorerCore.LogWarning($"Exception caching member {member.DeclaringType.FullName}.{member.Name}!"); - ExplorerCore.Log(e.ToString()); - } - } - } - - var typeList = types.ToList(); - - var sorted = new List(); - sorted.AddRange(list.Where(it => it is CacheMethod) - .OrderBy(it => typeList.IndexOf(it.DeclaringType)) - .ThenBy(it => it.NameForFiltering)); - sorted.AddRange(list.Where(it => it is CacheProperty) - .OrderBy(it => typeList.IndexOf(it.DeclaringType)) - .ThenBy(it => it.NameForFiltering)); - sorted.AddRange(list.Where(it => it is CacheField) - .OrderBy(it => typeList.IndexOf(it.DeclaringType)) - .ThenBy(it => it.NameForFiltering)); - - m_allMembers = sorted.ToArray(); - } - - public override void Update() - { - base.Update(); - - if (m_autoUpdate) - { - foreach (var member in m_displayedMembers) - { - if (member == null) break; - member.UpdateValue(); - } - } - - if (m_widthUpdateWanted) - { - if (!m_widthUpdateWaiting) - m_widthUpdateWaiting = true; - else - { - UpdateWidths(); - m_widthUpdateWaiting = false; - m_widthUpdateWanted = false; - } - } - } - - internal void OnMemberFilterClicked(MemberTypes type, Button button) - { - if (m_lastActiveMemButton) - RuntimeProvider.Instance.SetColorBlock(m_lastActiveMemButton, new Color(0.2f, 0.2f, 0.2f)); - - m_memberFilter = type; - m_lastActiveMemButton = button; - - RuntimeProvider.Instance.SetColorBlock(m_lastActiveMemButton, new Color(0.2f, 0.6f, 0.2f)); - - FilterMembers(null, true); - m_sliderScroller.m_slider.value = 1f; - } - - public void FilterMembers(string nameFilter = null, bool force = false) - { - int lastCount = m_membersFiltered.Count; - m_membersFiltered.Clear(); - - nameFilter = nameFilter?.ToLower() ?? m_nameFilterText.text.ToLower(); - - foreach (var mem in m_allMembers) - { - // membertype filter - if (m_memberFilter != MemberTypes.All && mem.MemInfo.MemberType != m_memberFilter) - continue; - - if (this is InstanceInspector ii && ii.m_scopeFilter != MemberScopes.All) - { - if (mem.IsStatic && ii.m_scopeFilter != MemberScopes.Static) - continue; - else if (!mem.IsStatic && ii.m_scopeFilter != MemberScopes.Instance) - continue; - } - - // name filter - if (!string.IsNullOrEmpty(nameFilter) && !mem.NameForFiltering.Contains(nameFilter)) - continue; - - m_membersFiltered.Add(mem); - } - - if (force || lastCount != m_membersFiltered.Count) - RefreshDisplay(); - } - - public void RefreshDisplay() - { - var members = m_membersFiltered; - m_pageHandler.ListCount = members.Count; - - // disable current members - for (int i = 0; i < m_displayedMembers.Length; i++) - { - var mem = m_displayedMembers[i]; - if (mem != null) - mem.Disable(); - else - break; - } - - if (members.Count < 1) - return; - - foreach (var itemIndex in m_pageHandler) - { - if (itemIndex >= members.Count) - break; - - CacheMember member = members[itemIndex]; - m_displayedMembers[itemIndex - m_pageHandler.StartIndex] = member; - member.Enable(); - } - - m_widthUpdateWanted = true; - } - - internal void UpdateWidths() - { - float labelWidth = 125; - - foreach (var cache in m_displayedMembers) - { - if (cache == null) - break; - - var width = cache.GetMemberLabelWidth(m_scrollContentRect); - - if (width > labelWidth) - labelWidth = width; - } - - float valueWidth = m_scrollContentRect.rect.width - labelWidth - 20; - - foreach (var cache in m_displayedMembers) - { - if (cache == null) - break; - cache.SetWidths(labelWidth, valueWidth); - } - } - - - #endregion // end instance - - #region UI - - private GameObject m_content; - public override GameObject Content - { - get => m_content; - set => m_content = value; - } - - internal Text m_nameFilterText; - internal MemberTypes m_memberFilter; - internal Button m_lastActiveMemButton; - - internal PageHandler m_pageHandler; - internal SliderScrollbar m_sliderScroller; - internal GameObject m_scrollContent; - internal RectTransform m_scrollContentRect; - - internal bool m_widthUpdateWanted; - internal bool m_widthUpdateWaiting; - - internal GameObject m_filterAreaObj; - internal GameObject m_updateRowObj; - internal GameObject m_memberListObj; - - internal void ConstructUI() - { - var parent = InspectorManager.m_inspectorContent; - - this.Content = UIFactory.CreateVerticalGroup(parent, "ReflectionInspector", true, false, true, true, 5, new Vector4(4,4,4,4), - new Color(0.15f, 0.15f, 0.15f)); - - ConstructTopArea(); - - ConstructMemberList(); - } - - internal void ConstructTopArea() - { - var nameRowObj = UIFactory.CreateHorizontalGroup(Content, "NameRowObj", true, true, true, true, 2, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(nameRowObj, minHeight: 25, flexibleHeight: 0, minWidth: 200, flexibleWidth: 5000); - - var typeLabelText = UIFactory.CreateLabel(nameRowObj, "TypeLabel", "Type:", TextAnchor.MiddleLeft); - typeLabelText.horizontalOverflow = HorizontalWrapMode.Overflow; - UIFactory.SetLayoutElement(typeLabelText.gameObject, minWidth: 40, flexibleWidth: 0, minHeight: 25); - - var typeDisplay = UIFactory.CreateLabel(nameRowObj, "TypeDisplayText", SignatureHighlighter.ParseFullSyntax(m_targetType, true), - TextAnchor.MiddleLeft); - - UIFactory.SetLayoutElement(typeDisplay.gameObject, minHeight: 25, flexibleWidth: 5000); - - // instance helper tools - - if (this is InstanceInspector instanceInspector) - { - instanceInspector.ConstructUnityInstanceHelpers(); - } - - ConstructFilterArea(); - - ConstructUpdateRow(); - } - - internal void ConstructFilterArea() - { - // Filters - - m_filterAreaObj = UIFactory.CreateVerticalGroup(Content, "FilterGroup", true, true, true, true, 4, new Vector4(4,4,4,4), - new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(m_filterAreaObj, minHeight: 60); - - // name filter - - var nameFilterRowObj = UIFactory.CreateHorizontalGroup(m_filterAreaObj, "NameFilterRow", false, false, true, true, 5, default, - new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(nameFilterRowObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 5000); - - var nameLabel = UIFactory.CreateLabel(nameFilterRowObj, "NameLabel", "Filter names:", TextAnchor.MiddleLeft, Color.grey); - UIFactory.SetLayoutElement(nameLabel.gameObject, minWidth: 100, minHeight: 25, flexibleWidth: 0); - - var nameInputObj = UIFactory.CreateInputField(nameFilterRowObj, "NameInput", "...", 14, (int)TextAnchor.MiddleLeft, (int)HorizontalWrapMode.Overflow); - UIFactory.SetLayoutElement(nameInputObj, flexibleWidth: 5000, minWidth: 100, minHeight: 25); - var nameInput = nameInputObj.GetComponent(); - nameInput.onValueChanged.AddListener((string val) => { FilterMembers(val); }); - m_nameFilterText = nameInput.textComponent; - - // membertype filter - - var memberFilterRowObj = UIFactory.CreateHorizontalGroup(m_filterAreaObj, "MemberFilter", false, false, true, true, 5, default, - new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(memberFilterRowObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 5000); - - var memLabel = UIFactory.CreateLabel(memberFilterRowObj, "MemberFilterLabel", "Filter members:", TextAnchor.MiddleLeft, Color.grey); - UIFactory.SetLayoutElement(memLabel.gameObject, minWidth: 100, minHeight: 25, flexibleWidth: 0); - - AddFilterButton(memberFilterRowObj, MemberTypes.All); - AddFilterButton(memberFilterRowObj, MemberTypes.Method); - AddFilterButton(memberFilterRowObj, MemberTypes.Property, true); - AddFilterButton(memberFilterRowObj, MemberTypes.Field); - - // Instance filters - - if (this is InstanceInspector instanceInspector) - { - instanceInspector.ConstructInstanceFilters(m_filterAreaObj); - } - } - - private void AddFilterButton(GameObject parent, MemberTypes type, bool setEnabled = false) - { - var btn = UIFactory.CreateButton(parent, - "FilterButton_" + type, - type.ToString(), - null, - new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(btn.gameObject, minHeight: 25, minWidth: 70); - btn.onClick.AddListener(() => { OnMemberFilterClicked(type, btn); }); - - RuntimeProvider.Instance.SetColorBlock(btn, highlighted: new Color(0.3f, 0.7f, 0.3f)); - - if (setEnabled) - { - RuntimeProvider.Instance.SetColorBlock(btn, new Color(0.2f, 0.6f, 0.2f)); - m_memberFilter = type; - m_lastActiveMemButton = btn; - } - } - - internal void ConstructUpdateRow() - { - m_updateRowObj = UIFactory.CreateHorizontalGroup(Content, "UpdateRow", false, true, true, true, 10, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(m_updateRowObj, minHeight: 25); - - // update button - - var updateBtn = UIFactory.CreateButton(m_updateRowObj, "UpdateButton", "Update Values", null, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(updateBtn.gameObject, minWidth: 110, flexibleWidth: 0); - updateBtn.onClick.AddListener(() => - { - bool orig = m_autoUpdate; - m_autoUpdate = true; - Update(); - if (!orig) - m_autoUpdate = orig; - }); - - // auto update - - var autoUpdateObj = UIFactory.CreateToggle(m_updateRowObj, "UpdateToggle", out Toggle autoUpdateToggle, out Text autoUpdateText); - var autoUpdateLayout = autoUpdateObj.AddComponent(); - autoUpdateLayout.minWidth = 150; - autoUpdateLayout.minHeight = 25; - autoUpdateText.text = "Auto-update?"; - autoUpdateToggle.isOn = false; - autoUpdateToggle.onValueChanged.AddListener((bool val) => { m_autoUpdate = val; }); - } - - internal void ConstructMemberList() - { - m_memberListObj = UIFactory.CreateScrollView(Content, "MemberList", out m_scrollContent, out m_sliderScroller, new Color(0.05f, 0.05f, 0.05f)); - - m_scrollContentRect = m_scrollContent.GetComponent(); - - UIFactory.SetLayoutGroup(m_scrollContent, forceHeight: true, spacing: 3, padLeft: 0, padRight: 0); - - m_pageHandler = new PageHandler(m_sliderScroller); - m_pageHandler.ConstructUI(Content); - m_pageHandler.OnPageChanged += OnPageTurned; - } - } - - #endregion -} \ No newline at end of file diff --git a/src/UI/Inspectors/Reflection/StaticInspector.cs b/src/UI/Inspectors/Reflection/StaticInspector.cs deleted file mode 100644 index eeeb537..0000000 --- a/src/UI/Inspectors/Reflection/StaticInspector.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace UnityExplorer.UI.Inspectors.Reflection -{ - public class StaticInspector : ReflectionInspector - { - public override string TabLabel => $" [S] {base.TabLabel}"; - - public StaticInspector(Type type) : base(type) { } - } -} diff --git a/src/UI/Inspectors/ReflectionInspector.cs b/src/UI/Inspectors/ReflectionInspector.cs new file mode 100644 index 0000000..9caee23 --- /dev/null +++ b/src/UI/Inspectors/ReflectionInspector.cs @@ -0,0 +1,694 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.Core.Runtime; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Inspectors +{ + public class ReflectionInspector : InspectorBase, ICellPoolDataSource, ICacheObjectController + { + public CacheObjectBase ParentCacheObject { get; set; } + public Type TargetType { get; private set; } + public bool StaticOnly { get; internal set; } + public bool CanWrite => true; + + private List members = new List(); + private readonly List filteredMembers = new List(); + + public bool AutoUpdateWanted => autoUpdateToggle.isOn; + + private BindingFlags FlagsFilter; + private string NameFilter; + + private MemberFlags MemberFilter = MemberFlags.All; + private enum MemberFlags + { + None = 0, + Property = 1, + Field = 2, + Method = 4, + All = 7 + } + + // UI + + public ScrollPool MemberScrollPool { get; private set; } + + public Text NameText; + public Text AssemblyText; + private Toggle autoUpdateToggle; + + private string currentBaseTabText; + + private readonly Color disabledButtonColor = new Color(0.24f, 0.24f, 0.24f); + private readonly Color enabledButtonColor = new Color(0.2f, 0.27f, 0.2f); + private readonly Dictionary scopeFilterButtons = new Dictionary(); + private readonly List memberTypeToggles = new List(); + private InputFieldRef filterInputField; + + // Setup / return + + public override void OnBorrowedFromPool(object target) + { + base.OnBorrowedFromPool(target); + CalculateLayouts(); + + SetTarget(target); + + RuntimeProvider.Instance.StartCoroutine(InitCoroutine()); + } + + private IEnumerator InitCoroutine() + { + yield return null; + + LayoutRebuilder.ForceRebuildLayoutImmediate(InspectorPanel.Instance.ContentRect); + } + + public override void CloseInspector() + { + InspectorManager.ReleaseInspector(this); + } + + public override void OnReturnToPool() + { + foreach (var member in members) + { + member.UnlinkFromView(); + member.ReleasePooledObjects(); + } + + members.Clear(); + filteredMembers.Clear(); + + autoUpdateToggle.isOn = false; + + UnityObjectRef = null; + ComponentRef = null; + TextureRef = null; + CleanupTextureViewer(); + + base.OnReturnToPool(); + } + + // Setting target + + private void SetTarget(object target) + { + string prefix; + if (StaticOnly) + { + Target = null; + TargetType = target as Type; + prefix = "[S]"; + } + else + { + TargetType = target.GetActualType(); + prefix = "[R]"; + } + + // Setup main labels and tab text + currentBaseTabText = $"{prefix} {SignatureHighlighter.Parse(TargetType, false)}"; + Tab.TabText.text = currentBaseTabText; + NameText.text = SignatureHighlighter.Parse(TargetType, true); + + string asmText; + if (TargetType.Assembly is AssemblyBuilder || string.IsNullOrEmpty(TargetType.Assembly.Location)) + asmText = $"{TargetType.Assembly.GetName().Name} (in memory)"; + else + asmText = Path.GetFileName(TargetType.Assembly.Location); + AssemblyText.text = $"Assembly: {asmText}"; + + // unity helpers + SetUnityTargets(); + + // Get cache members + + this.members = CacheMember.GetCacheMembers(Target, TargetType, this); + + // reset filters + + this.filterInputField.Text = ""; + + SetFilter("", StaticOnly ? BindingFlags.Static : BindingFlags.Instance); + scopeFilterButtons[BindingFlags.Default].Component.gameObject.SetActive(!StaticOnly); + scopeFilterButtons[BindingFlags.Instance].Component.gameObject.SetActive(!StaticOnly); + + foreach (var toggle in memberTypeToggles) + toggle.isOn = true; + + refreshWanted = true; + } + + // Updating + + private bool refreshWanted; + private string lastNameFilter; + private BindingFlags lastFlagsFilter; + private MemberFlags lastMemberFilter = MemberFlags.All; + private float timeOfLastAutoUpdate; + + public override void Update() + { + if (!this.IsActive) + return; + + if (!StaticOnly && Target.IsNullOrDestroyed(false)) + { + InspectorManager.ReleaseInspector(this); + return; + } + + // check filter changes or force-refresh + if (refreshWanted || NameFilter != lastNameFilter || FlagsFilter != lastFlagsFilter || lastMemberFilter != MemberFilter) + { + lastNameFilter = NameFilter; + lastFlagsFilter = FlagsFilter; + lastMemberFilter = MemberFilter; + + FilterMembers(); + MemberScrollPool.Refresh(true, true); + refreshWanted = false; + } + + // once-per-second updates + if (timeOfLastAutoUpdate.OccuredEarlierThan(1)) + { + timeOfLastAutoUpdate = Time.realtimeSinceStartup; + + if (this.UnityObjectRef) + { + nameInput.Text = UnityObjectRef.name; + this.Tab.TabText.text = $"{currentBaseTabText} \"{UnityObjectRef.name}\""; + } + + if (AutoUpdateWanted) + UpdateDisplayedMembers(); + } + } + + public void UpdateClicked() + { + UpdateDisplayedMembers(); + } + + // Filtering + + public void SetFilter(string filter) => SetFilter(filter, FlagsFilter); + + public void SetFilter(BindingFlags flagsFilter) => SetFilter(NameFilter, flagsFilter); + + public void SetFilter(string nameFilter, BindingFlags flagsFilter) + { + this.NameFilter = nameFilter; + + if (flagsFilter != FlagsFilter) + { + var btn = scopeFilterButtons[FlagsFilter].Component; + RuntimeProvider.Instance.SetColorBlock(btn, disabledButtonColor, disabledButtonColor * 1.3f); + + this.FlagsFilter = flagsFilter; + btn = scopeFilterButtons[FlagsFilter].Component; + RuntimeProvider.Instance.SetColorBlock(btn, enabledButtonColor, enabledButtonColor * 1.3f); + } + } + + private void OnMemberTypeToggled(MemberFlags flag, bool val) + { + if (!val) + MemberFilter &= ~flag; + else + MemberFilter |= flag; + } + + private void FilterMembers() + { + filteredMembers.Clear(); + + for (int i = 0; i < members.Count; i++) + { + var member = members[i]; + + if (FlagsFilter != BindingFlags.Default) + { + if (FlagsFilter == BindingFlags.Instance && member.IsStatic + || FlagsFilter == BindingFlags.Static && !member.IsStatic) + continue; + } + + if ((member is CacheMethod && !MemberFilter.HasFlag(MemberFlags.Method)) + || (member is CacheField && !MemberFilter.HasFlag(MemberFlags.Field)) + || (member is CacheProperty && !MemberFilter.HasFlag(MemberFlags.Property))) + continue; + + if (!string.IsNullOrEmpty(NameFilter) && !member.NameForFiltering.ContainsIgnoreCase(NameFilter)) + continue; + + filteredMembers.Add(member); + } + } + + private void UpdateDisplayedMembers() + { + bool shouldRefresh = false; + foreach (var cell in MemberScrollPool.CellPool) + { + if (!cell.Enabled || cell.Occupant == null) + continue; + var member = cell.MemberOccupant; + if (member.ShouldAutoEvaluate) + { + shouldRefresh = true; + member.Evaluate(); + member.SetDataToCell(member.CellView); + } + } + + if (shouldRefresh) + MemberScrollPool.Refresh(false); + } + + // Member cells + + public int ItemCount => filteredMembers.Count; + + public void OnCellBorrowed(CacheMemberCell cell) { } // not needed + + public void SetCell(CacheMemberCell cell, int index) + { + CacheObjectControllerHelper.SetCell(cell, index, filteredMembers, SetCellLayout); + } + + // Cell layout (fake table alignment) + + private static int LeftGroupWidth { get; set; } + private static int RightGroupWidth { get; set; } + + internal void SetLayouts() + { + CalculateLayouts(); + + foreach (var cell in MemberScrollPool.CellPool) + SetCellLayout(cell); + } + + private void CalculateLayouts() + { + // Calculate sizes + LeftGroupWidth = (int)Math.Max(200, (0.4f * InspectorManager.PanelWidth) - 5);// Math.Min(450f, 0.4f * InspectorManager.PanelWidth - 5)); + RightGroupWidth = (int)Math.Max(200, InspectorManager.PanelWidth - LeftGroupWidth - 65); + + //memberTitleLayout.minWidth = LeftGroupWidth; + } + + private void SetCellLayout(CacheObjectCell cell) + { + cell.NameLayout.minWidth = LeftGroupWidth; + cell.RightGroupLayout.minWidth = RightGroupWidth; + + if (cell.Occupant?.IValue != null) + cell.Occupant.IValue.SetLayout(); + } + + // UI Construction + + private GameObject mainContentHolder; + + public override GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "ReflectionInspector", true, true, true, true, 5, + new Vector4(4, 4, 4, 4), new Color(0.065f, 0.065f, 0.065f)); + + // Class name, assembly + + NameText = UIFactory.CreateLabel(UIRoot, "Title", "not set", TextAnchor.MiddleLeft, fontSize: 17); + UIFactory.SetLayoutElement(NameText.gameObject, minHeight: 25, flexibleHeight: 0); + + AssemblyText = UIFactory.CreateLabel(UIRoot, "AssemblyLabel", "not set", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(AssemblyText.gameObject, minHeight: 25, flexibleWidth: 9999); + + ConstructUnityObjectRow(); + + mainContentHolder = UIFactory.CreateVerticalGroup(UIRoot, "MemberHolder", false, false, true, true, 5, new Vector4(2,2,2,2), + new Color(0.12f, 0.12f, 0.12f)); + UIFactory.SetLayoutElement(mainContentHolder, flexibleWidth: 9999, flexibleHeight: 9999); + + ConstructFirstRow(mainContentHolder); + + ConstructSecondRow(mainContentHolder); + + // Member scroll pool + + var memberBorder = UIFactory.CreateVerticalGroup(mainContentHolder, "ScrollPoolHolder", false, false, true, true, padding: new Vector4(2,2,2,2), + bgColor: new Color(0.05f, 0.05f, 0.05f)); + UIFactory.SetLayoutElement(memberBorder, flexibleWidth: 9999, flexibleHeight: 9999); + + MemberScrollPool = UIFactory.CreateScrollPool(memberBorder, "MemberList", out GameObject scrollObj, + out GameObject _, new Color(0.09f, 0.09f, 0.09f)); + UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999); + MemberScrollPool.Initialize(this); + + // For debugging scroll pool + //InspectorPanel.Instance.UIRoot.GetComponent().enabled = false; + //MemberScrollPool.Viewport.GetComponent().enabled = false; + //MemberScrollPool.Viewport.GetComponent().color = new Color(0.12f, 0.12f, 0.12f); + + return UIRoot; + } + + // First row + + private void ConstructFirstRow(GameObject parent) + { + var rowObj = UIFactory.CreateUIObject("FirstRow", parent); + UIFactory.SetLayoutGroup(rowObj, true, true, true, true, 5, 2, 2, 2, 2); + UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + var nameLabel = UIFactory.CreateLabel(rowObj, "NameFilterLabel", "Filter names:", TextAnchor.MiddleLeft, Color.grey); + UIFactory.SetLayoutElement(nameLabel.gameObject, minHeight: 25, minWidth: 90, flexibleWidth: 0); + + filterInputField = UIFactory.CreateInputField(rowObj, "NameFilterInput", "..."); + UIFactory.SetLayoutElement(filterInputField.UIRoot, minHeight: 25, flexibleWidth: 300); + filterInputField.OnValueChanged += (string val) => { SetFilter(val); }; + + var spacer = UIFactory.CreateUIObject("Spacer", rowObj); + UIFactory.SetLayoutElement(spacer, minWidth: 25); + + // Update button and toggle + + var updateButton = UIFactory.CreateButton(rowObj, "UpdateButton", "Update displayed values", new Color(0.22f, 0.28f, 0.22f)); + UIFactory.SetLayoutElement(updateButton.Component.gameObject, minHeight: 25, minWidth: 175, flexibleWidth: 0); + updateButton.OnClick += UpdateClicked; + + var toggleObj = UIFactory.CreateToggle(rowObj, "AutoUpdateToggle", out autoUpdateToggle, out Text toggleText); + UIFactory.SetLayoutElement(toggleObj, minWidth: 125, minHeight: 25); + autoUpdateToggle.isOn = false; + toggleText.text = "Auto-update"; + } + + // Second row + + private void ConstructSecondRow(GameObject parent) + { + var rowObj = UIFactory.CreateUIObject("SecondRow", parent); + UIFactory.SetLayoutGroup(rowObj, false, false, true, true, 5, 2, 2, 2, 2); + UIFactory.SetLayoutElement(rowObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + // Scope buttons + + var scopeLabel = UIFactory.CreateLabel(rowObj, "ScopeLabel", "Scope:", TextAnchor.MiddleLeft, Color.grey); + UIFactory.SetLayoutElement(scopeLabel.gameObject, minHeight: 25, minWidth: 60, flexibleWidth: 0); + AddScopeFilterButton(rowObj, BindingFlags.Default, true); + AddScopeFilterButton(rowObj, BindingFlags.Instance); + AddScopeFilterButton(rowObj, BindingFlags.Static); + + var spacer = UIFactory.CreateUIObject("Spacer", rowObj); + UIFactory.SetLayoutElement(spacer, minWidth: 15); + + // Member type toggles + + AddMemberTypeToggle(rowObj, MemberTypes.Property, 90); + AddMemberTypeToggle(rowObj, MemberTypes.Field, 70); + AddMemberTypeToggle(rowObj, MemberTypes.Method, 90); + } + + private void AddScopeFilterButton(GameObject parent, BindingFlags flags, bool setAsActive = false) + { + string lbl = flags == BindingFlags.Default ? "All" : flags.ToString(); + var color = setAsActive ? enabledButtonColor : disabledButtonColor; + + var button = UIFactory.CreateButton(parent, "Filter_" + flags, lbl, color); + UIFactory.SetLayoutElement(button.Component.gameObject, minHeight: 25, flexibleHeight: 0, minWidth: 70, flexibleWidth: 0); + scopeFilterButtons.Add(flags, button); + + button.OnClick += () => { SetFilter(flags); }; + } + + private void AddMemberTypeToggle(GameObject parent, MemberTypes type, int width) + { + var toggleObj = UIFactory.CreateToggle(parent, "Toggle_" + type, out Toggle toggle, out Text toggleText); + UIFactory.SetLayoutElement(toggleObj, minHeight: 25, minWidth: width); + var color = SignatureHighlighter.GetMemberInfoColor(type); + toggleText.text = $"{type}"; + + toggle.graphic.TryCast().color = color.ToColor() * 0.65f; + + MemberFlags flag; + switch (type) + { + case MemberTypes.Method: flag = MemberFlags.Method; break; + case MemberTypes.Property: flag = MemberFlags.Property; break; + case MemberTypes.Field: flag = MemberFlags.Field; break; + default: return; + } + + toggle.onValueChanged.AddListener((bool val) => { OnMemberTypeToggled(flag, val); }); + + memberTypeToggles.Add(toggle); + } + + + // Todo should probably put this in a separate class or maybe as a widget + + #region UNITY OBJECT SPECIFIC + + // Unity object helpers + + private UnityEngine.Object UnityObjectRef; + private Component ComponentRef; + private Texture2D TextureRef; + private bool TextureViewerWanted; + private GameObject unityObjectRow; + private ButtonRef gameObjectButton; + private InputFieldRef nameInput; + private InputFieldRef instanceIdInput; + private ButtonRef textureButton; + private GameObject textureViewer; + + private void SetUnityTargets() + { + if (!typeof(UnityEngine.Object).IsAssignableFrom(TargetType)) + { + unityObjectRow.SetActive(false); + textureViewer.SetActive(false); + return; + } + + UnityObjectRef = (UnityEngine.Object)Target.TryCast(typeof(UnityEngine.Object)); + unityObjectRow.SetActive(true); + + nameInput.Text = UnityObjectRef.name; + instanceIdInput.Text = UnityObjectRef.GetInstanceID().ToString(); + + if (typeof(Component).IsAssignableFrom(TargetType)) + { + ComponentRef = (Component)Target.TryCast(typeof(Component)); + gameObjectButton.Component.gameObject.SetActive(true); + } + else + gameObjectButton.Component.gameObject.SetActive(false); + + if (typeof(Texture2D).IsAssignableFrom(TargetType)) + { + TextureRef = (Texture2D)Target.TryCast(typeof(Texture2D)); + textureButton.Component.gameObject.SetActive(true); + } + else + textureButton.Component.gameObject.SetActive(false); + } + + private void OnGameObjectButtonClicked() + { + if (!ComponentRef) + { + ExplorerCore.LogWarning("Component reference is null or destroyed!"); + return; + } + + InspectorManager.Inspect(ComponentRef.gameObject); + } + + private void ToggleTextureViewer() + { + if (TextureViewerWanted) + { + // disable + TextureViewerWanted = false; + textureViewer.SetActive(false); + mainContentHolder.SetActive(true); + textureButton.ButtonText.text = "View Texture"; + } + else + { + if (!textureImage.sprite) + { + // First show, need to create sprite for displaying texture + SetTextureViewer(); + } + + // enable + TextureViewerWanted = true; + textureViewer.SetActive(true); + mainContentHolder.gameObject.SetActive(false); + textureButton.ButtonText.text = "Hide Texture"; + } + } + + // UI construction + + private void ConstructUnityObjectRow() + { + unityObjectRow = UIFactory.CreateUIObject("UnityObjectRow", UIRoot); + UIFactory.SetLayoutGroup(unityObjectRow, false, false, true, true, 5); + UIFactory.SetLayoutElement(unityObjectRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + textureButton = UIFactory.CreateButton(unityObjectRow, "TextureButton", "View Texture", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(textureButton.Component.gameObject, minHeight: 25, minWidth: 150); + textureButton.OnClick += ToggleTextureViewer; + + var nameLabel = UIFactory.CreateLabel(unityObjectRow, "NameLabel", "Name:", TextAnchor.MiddleLeft, Color.grey); + UIFactory.SetLayoutElement(nameLabel.gameObject, minHeight: 25, minWidth: 45, flexibleWidth: 0); + + nameInput = UIFactory.CreateInputField(unityObjectRow, "NameInput", "untitled"); + UIFactory.SetLayoutElement(nameInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 1000); + nameInput.Component.readOnly = true; + + gameObjectButton = UIFactory.CreateButton(unityObjectRow, "GameObjectButton", "Inspect GameObject", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(gameObjectButton.Component.gameObject, minHeight: 25, minWidth: 160); + gameObjectButton.OnClick += OnGameObjectButtonClicked; + + var instanceLabel = UIFactory.CreateLabel(unityObjectRow, "InstanceLabel", "Instance ID:", TextAnchor.MiddleRight, Color.grey); + UIFactory.SetLayoutElement(instanceLabel.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); + + instanceIdInput = UIFactory.CreateInputField(unityObjectRow, "InstanceIDInput", "ERROR"); + UIFactory.SetLayoutElement(instanceIdInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 0); + instanceIdInput.Component.readOnly = true; + + unityObjectRow.SetActive(false); + + ConstructTextureHelper(); + } + + // Texture viewer helper + + private InputFieldRef textureSavePathInput; + private Image textureImage; + private LayoutElement textureImageLayout; + + private void CleanupTextureViewer() + { + if (textureImage.sprite) + GameObject.Destroy(textureImage.sprite); + + if (TextureViewerWanted) + ToggleTextureViewer(); + } + + private void ConstructTextureHelper() + { + textureViewer = UIFactory.CreateVerticalGroup(UIRoot, "TextureViewer", false, false, true, true, 2, new Vector4(5, 5, 5, 5), + new Color(0.1f, 0.1f, 0.1f)); + UIFactory.SetLayoutElement(textureViewer, flexibleWidth: 9999, flexibleHeight: 9999); + + // Save helper + + var saveRowObj = UIFactory.CreateHorizontalGroup(textureViewer, "SaveRow", false, false, true, true, 2, new Vector4(2, 2, 2, 2), + new Color(0.1f, 0.1f, 0.1f)); + + var saveBtn = UIFactory.CreateButton(saveRowObj, "SaveButton", "Save .PNG", new Color(0.2f, 0.25f, 0.2f)); + UIFactory.SetLayoutElement(saveBtn.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); + saveBtn.OnClick += OnSaveTextureClicked; + + textureSavePathInput = UIFactory.CreateInputField(saveRowObj, "SaveInput", "..."); + UIFactory.SetLayoutElement(textureSavePathInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 9999); + + // Actual texture viewer + + var imageObj = UIFactory.CreateUIObject("TextureViewerImage", textureViewer); + textureImage = imageObj.AddComponent(); + textureImageLayout = textureImage.gameObject.AddComponent(); + + var fitter = imageObj.AddComponent(); + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + textureViewer.SetActive(false); + } + + private void SetTextureViewer() + { + if (!this.TextureRef) + return; + + var name = TextureRef.name; + if (string.IsNullOrEmpty(name)) + name = "untitled"; + + textureSavePathInput.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, $"{name}.png"); + + var sprite = TextureUtilProvider.Instance.CreateSprite(TextureRef); + textureImage.sprite = sprite; + + textureImageLayout.preferredHeight = sprite.rect.height; + textureImageLayout.preferredWidth = sprite.rect.width; + } + + private void OnSaveTextureClicked() + { + if (!TextureRef) + { + ExplorerCore.LogWarning("Ref Texture is null, maybe it was destroyed?"); + return; + } + + if (string.IsNullOrEmpty(textureSavePathInput.Text)) + { + ExplorerCore.LogWarning("Save path cannot be empty!"); + return; + } + + var path = textureSavePathInput.Text; + if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + { + ExplorerCore.LogWarning("Desired save path must end with '.png'!"); + return; + } + + path = IOUtility.EnsureValidDirectory(path); + + if (File.Exists(path)) + File.Delete(path); + + var tex = TextureRef; + + if (!TextureUtilProvider.IsReadable(tex)) + tex = TextureUtilProvider.ForceReadTexture(tex); + + byte[] data = TextureUtilProvider.Instance.EncodeToPNG(tex); + + File.WriteAllBytes(path, data); + + if (tex != TextureRef) + { + // cleanup temp texture if we had to force-read it. + GameObject.Destroy(tex); + } + } + + #endregion + } +} diff --git a/src/UI/InteractiveValues/InteractiveBool.cs b/src/UI/InteractiveValues/InteractiveBool.cs deleted file mode 100644 index 15970f5..0000000 --- a/src/UI/InteractiveValues/InteractiveBool.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using UnityExplorer.UI.CacheObject; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveBool : InteractiveValue - { - public InteractiveBool(object value, Type valueType) : base(value, valueType) { } - - public override bool HasSubContent => false; - public override bool SubContentWanted => false; - public override bool WantInspectBtn => false; - - internal Toggle m_toggle; - internal Button m_applyBtn; - - public override void OnValueUpdated() - { - base.OnValueUpdated(); - } - - public override void RefreshUIForValue() - { - GetDefaultLabel(); - - if (Owner.HasEvaluated) - { - var val = (bool)Value; - - if (Owner.CanWrite) - { - if (!m_toggle.gameObject.activeSelf) - m_toggle.gameObject.SetActive(true); - - if (!m_applyBtn.gameObject.activeSelf) - m_applyBtn.gameObject.SetActive(true); - - if (val != m_toggle.isOn) - m_toggle.isOn = val; - } - - var color = val - ? "6bc981" // on - : "c96b6b"; // off - - m_baseLabel.text = $"{val}"; - } - else - { - m_baseLabel.text = DefaultLabel; - } - } - - public override void OnException(CacheMember member) - { - base.OnException(member); - - if (Owner.CanWrite) - { - if (m_toggle.gameObject.activeSelf) - m_toggle.gameObject.SetActive(false); - - if (m_applyBtn.gameObject.activeSelf) - m_applyBtn.gameObject.SetActive(false); - } - } - - internal void OnToggleValueChanged(bool val) - { - Value = val; - RefreshUIForValue(); - } - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - - var baseLayout = m_baseLabel.gameObject.GetComponent(); - baseLayout.flexibleWidth = 0; - baseLayout.minWidth = 50; - - if (Owner.CanWrite) - { - var toggleObj = UIFactory.CreateToggle(m_mainContent, "InteractiveBoolToggle", out m_toggle, out _, new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(toggleObj, minWidth: 24); - m_toggle.onValueChanged.AddListener(OnToggleValueChanged); - - m_baseLabel.transform.SetAsLastSibling(); - - m_applyBtn = UIFactory.CreateButton(m_mainContent, - "ApplyButton", - "Apply", - () => { Owner.SetValue(); }, - new Color(0.2f, 0.2f, 0.2f)); - - UIFactory.SetLayoutElement(m_applyBtn.gameObject, minWidth: 50, minHeight: 25, flexibleWidth: 0); - - toggleObj.SetActive(false); - m_applyBtn.gameObject.SetActive(false); - } - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveColor.cs b/src/UI/InteractiveValues/InteractiveColor.cs deleted file mode 100644 index 501d129..0000000 --- a/src/UI/InteractiveValues/InteractiveColor.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveColor : InteractiveValue - { - //~~~~~~~~~ Instance ~~~~~~~~~~ - - public InteractiveColor(object value, Type valueType) : base(value, valueType) { } - - public override bool HasSubContent => true; - public override bool SubContentWanted => true; - public override bool WantInspectBtn => true; - - public override void RefreshUIForValue() - { - base.RefreshUIForValue(); - - if (m_subContentConstructed) - RefreshUI(); - } - - private void RefreshUI() - { - var color = (Color)this.Value; - - m_inputs[0].text = color.r.ToString(); - m_inputs[1].text = color.g.ToString(); - m_inputs[2].text = color.b.ToString(); - m_inputs[3].text = color.a.ToString(); - - if (m_colorImage) - m_colorImage.color = color; - } - - internal override void OnToggleSubcontent(bool toggle) - { - base.OnToggleSubcontent(toggle); - - RefreshUI(); - } - - #region UI CONSTRUCTION - - private Image m_colorImage; - - private readonly InputField[] m_inputs = new InputField[4]; - private readonly Slider[] m_sliders = new Slider[4]; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - - //// Limit the label width for colors, they're always about the same so make use of that space. - //UIFactory.SetLayoutElement(this.m_baseLabel.gameObject, flexibleWidth: 0, minWidth: 250); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - var horiGroup = UIFactory.CreateHorizontalGroup(m_subContentParent, "ColorEditor", false, false, true, true, 5, - default, default, TextAnchor.MiddleLeft); - - var editorContainer = UIFactory.CreateVerticalGroup(horiGroup, "EditorContent", false, true, true, true, 2, new Vector4(4, 4, 4, 4), - new Color(0.08f, 0.08f, 0.08f)); - UIFactory.SetLayoutElement(editorContainer, minWidth: 300, flexibleWidth: 0); - - for (int i = 0; i < 4; i++) - AddEditorRow(i, editorContainer); - - if (Owner.CanWrite) - { - var applyBtn = UIFactory.CreateButton(editorContainer, "ApplyButton", "Apply", OnSetValue, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(applyBtn.gameObject, minWidth: 175, minHeight: 25, flexibleWidth: 0); - - void OnSetValue() - { - Owner.SetValue(); - RefreshUIForValue(); - } - } - - var imgHolder = UIFactory.CreateVerticalGroup(horiGroup, "ImgHolder", true, true, true, true, 0, new Vector4(1, 1, 1, 1), - new Color(0.08f, 0.08f, 0.08f)); - UIFactory.SetLayoutElement(imgHolder, minWidth: 128, minHeight: 128, flexibleWidth: 0, flexibleHeight: 0); - - var imgObj = UIFactory.CreateUIObject("ColorImageHelper", imgHolder, new Vector2(100, 25)); - m_colorImage = imgObj.AddComponent(); - m_colorImage.color = (Color)this.Value; - } - - private static readonly string[] s_fieldNames = new[] { "R", "G", "B", "A" }; - - internal void AddEditorRow(int index, GameObject groupObj) - { - var row = UIFactory.CreateHorizontalGroup(groupObj, "EditorRow_" + s_fieldNames[index], - false, true, true, true, 5, default, new Color(1, 1, 1, 0)); - - var label = UIFactory.CreateLabel(row, "RowLabel", $"{s_fieldNames[index]}:", TextAnchor.MiddleRight, Color.cyan); - UIFactory.SetLayoutElement(label.gameObject, minWidth: 50, flexibleWidth: 0, minHeight: 25); - - var inputFieldObj = UIFactory.CreateInputField(row, "InputField", "...", 14, 3, 1); - UIFactory.SetLayoutElement(inputFieldObj, minWidth: 120, minHeight: 25, flexibleWidth: 0); - - var inputField = inputFieldObj.GetComponent(); - m_inputs[index] = inputField; - inputField.characterValidation = InputField.CharacterValidation.Decimal; - - inputField.onValueChanged.AddListener((string value) => - { - float val = float.Parse(value); - SetValueToColor(val); - m_sliders[index].value = val; - }); - - var sliderObj = UIFactory.CreateSlider(row, "Slider", out Slider slider); - m_sliders[index] = slider; - UIFactory.SetLayoutElement(sliderObj, minWidth: 200, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); - slider.minValue = 0; - slider.maxValue = 1; - slider.value = GetValueFromColor(); - - slider.onValueChanged.AddListener((float value) => - { - inputField.text = value.ToString(); - SetValueToColor(value); - m_inputs[index].text = value.ToString(); - }); - - // methods for writing to the color for this field - - void SetValueToColor(float floatValue) - { - Color _color = (Color)Value; - switch (index) - { - case 0: _color.r = floatValue; break; - case 1: _color.g = floatValue; break; - case 2: _color.b = floatValue; break; - case 3: _color.a = floatValue; break; - } - Value = _color; - m_colorImage.color = _color; - } - - float GetValueFromColor() - { - Color _color = (Color)Value; - switch (index) - { - case 0: return _color.r; - case 1: return _color.g; - case 2: return _color.b; - case 3: return _color.a; - default: throw new NotImplementedException(); - } - } - } - - #endregion - } -} diff --git a/src/UI/InteractiveValues/InteractiveDictionary.cs b/src/UI/InteractiveValues/InteractiveDictionary.cs deleted file mode 100644 index e2a182c..0000000 --- a/src/UI/InteractiveValues/InteractiveDictionary.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Config; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using System.Reflection; -using UnityExplorer.UI.CacheObject; -using UnityExplorer.Core; -using UnityExplorer.UI.Utility; -#if CPP -using AltIDictionary = Il2CppSystem.Collections.IDictionary; -#else -using AltIDictionary = System.Collections.IDictionary; -#endif - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveDictionary : InteractiveValue - { - public InteractiveDictionary(object value, Type valueType) : base(value, valueType) - { - if (valueType.IsGenericType) - { - var gArgs = valueType.GetGenericArguments(); - m_typeOfKeys = gArgs[0]; - m_typeofValues = gArgs[1]; - } - else - { - m_typeOfKeys = typeof(object); - m_typeofValues = typeof(object); - } - } - - public override bool WantInspectBtn => false; - public override bool HasSubContent => true; - public override bool SubContentWanted - { - get - { - if (m_recacheWanted) - return true; - else return m_entries.Count > 0; - } - } - - internal IDictionary RefIDictionary; - internal AltIDictionary RefAltIDictionary; - internal Type m_typeOfKeys; - internal Type m_typeofValues; - - internal readonly List> m_entries - = new List>(); - - internal readonly KeyValuePair[] m_displayedEntries - = new KeyValuePair[ConfigManager.Default_Page_Limit.Value]; - - internal bool m_recacheWanted = true; - - public override void OnDestroy() - { - base.OnDestroy(); - } - - public override void OnValueUpdated() - { - RefIDictionary = Value as IDictionary; - - if (RefIDictionary == null) - { - try { RefAltIDictionary = Value.TryCast(); } - catch { } - } - - if (m_subContentParent.activeSelf) - { - GetCacheEntries(); - RefreshDisplay(); - } - else - m_recacheWanted = true; - - base.OnValueUpdated(); - } - - internal void OnPageTurned() - { - RefreshDisplay(); - } - - public override void RefreshUIForValue() - { - GetDefaultLabel(); - - if (Value != null) - { - string count = "?"; - if (m_recacheWanted && RefIDictionary != null) - count = RefIDictionary.Count.ToString(); - else if (!m_recacheWanted) - count = m_entries.Count.ToString(); - - m_baseLabel.text = $"[{count}] {m_richValueType}"; - } - else - { - m_baseLabel.text = DefaultLabel; - } - } - - public void GetCacheEntries() - { - if (m_entries.Any()) - { - // maybe improve this, probably could be more efficient i guess - - foreach (var pair in m_entries) - { - pair.Key.Destroy(); - pair.Value.Destroy(); - } - - m_entries.Clear(); - } - - if (RefIDictionary == null && Value != null) - RefIDictionary = RuntimeProvider.Instance.Reflection.EnumerateDictionary(Value, m_typeOfKeys, m_typeofValues); - - if (RefIDictionary != null) - { - int index = 0; - - foreach (var key in RefIDictionary.Keys) - { - var value = RefIDictionary[key]; - - var cacheKey = new CachePaired(index, this, this.RefIDictionary, PairTypes.Key, m_listContent); - cacheKey.CreateIValue(key, this.m_typeOfKeys); - cacheKey.Disable(); - - var cacheValue = new CachePaired(index, this, this.RefIDictionary, PairTypes.Value, m_listContent); - cacheValue.CreateIValue(value, this.m_typeofValues); - cacheValue.Disable(); - - //holder.SetActive(false); - - m_entries.Add(new KeyValuePair(cacheKey, cacheValue)); - - index++; - } - } - - RefreshDisplay(); - } - - public void RefreshDisplay() - { - var entries = m_entries; - m_pageHandler.ListCount = entries.Count; - - for (int i = 0; i < m_displayedEntries.Length; i++) - { - var entry = m_displayedEntries[i]; - if (entry.Key != null && entry.Value != null) - { - //m_rowHolders[i].SetActive(false); - entry.Key.Disable(); - entry.Value.Disable(); - } - else - break; - } - - if (entries.Count < 1) - return; - - foreach (var itemIndex in m_pageHandler) - { - if (itemIndex >= entries.Count) - break; - - var entry = entries[itemIndex]; - m_displayedEntries[itemIndex - m_pageHandler.StartIndex] = entry; - - //m_rowHolders[itemIndex].SetActive(true); - entry.Key.Enable(); - entry.Value.Enable(); - } - - //UpdateSubcontentHeight(); - } - - internal override void OnToggleSubcontent(bool active) - { - base.OnToggleSubcontent(active); - - if (active && m_recacheWanted) - { - m_recacheWanted = false; - GetCacheEntries(); - RefreshUIForValue(); - } - - RefreshDisplay(); - } - - internal GameObject m_listContent; - internal LayoutElement m_listLayout; - - internal PageHandler m_pageHandler; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - m_pageHandler = new PageHandler(null); - m_pageHandler.ConstructUI(m_subContentParent); - m_pageHandler.OnPageChanged += OnPageTurned; - - m_listContent = UIFactory.CreateVerticalGroup(m_subContentParent, "DictionaryContent", true, true, true, true, 2, new Vector4(5,5,5,5), - new Color(0.08f, 0.08f, 0.08f)); - - var scrollRect = m_listContent.GetComponent(); - scrollRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 0); - - m_listLayout = Owner.m_mainContent.GetComponent(); - m_listLayout.minHeight = 25; - m_listLayout.flexibleHeight = 0; - - Owner.m_mainRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 25); - - var contentFitter = m_listContent.AddComponent(); - contentFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; - contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveEnum.cs b/src/UI/InteractiveValues/InteractiveEnum.cs deleted file mode 100644 index 5384013..0000000 --- a/src/UI/InteractiveValues/InteractiveEnum.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveEnum : InteractiveValue - { - internal static Dictionary[]> s_enumNamesCache = new Dictionary[]>(); - - public InteractiveEnum(object value, Type valueType) : base(value, valueType) - { - GetNames(); - } - - public override bool HasSubContent => true; - public override bool SubContentWanted => Owner.CanWrite; - public override bool WantInspectBtn => false; - - internal KeyValuePair[] m_values = new KeyValuePair[0]; - - internal Type m_lastEnumType; - - internal void GetNames() - { - var type = Value?.GetType() ?? FallbackType; - - if (m_lastEnumType == type) - return; - - m_lastEnumType = type; - - if (m_subContentConstructed) - { - DestroySubContent(); - } - - if (!s_enumNamesCache.ContainsKey(type)) - { - // using GetValues not GetNames, to catch instances of weird enums (eg CameraClearFlags) - var values = Enum.GetValues(type); - - var list = new List>(); - var set = new HashSet(); - - foreach (var value in values) - { - var name = value.ToString(); - - if (set.Contains(name)) - continue; - - set.Add(name); - - var backingType = Enum.GetUnderlyingType(type); - int intValue; - try - { - // this approach is necessary, a simple '(int)value' is not sufficient. - - var unbox = Convert.ChangeType(value, backingType); - - intValue = (int)Convert.ChangeType(unbox, typeof(int)); - } - catch (Exception ex) - { - ExplorerCore.LogWarning("[InteractiveEnum] Could not Unbox underlying type " + backingType.Name + " from " + type.FullName); - ExplorerCore.Log(ex.ToString()); - continue; - } - - list.Add(new KeyValuePair(intValue, name)); - } - - s_enumNamesCache.Add(type, list.ToArray()); - } - - m_values = s_enumNamesCache[type]; - } - - public override void OnValueUpdated() - { - GetNames(); - - base.OnValueUpdated(); - } - - public override void RefreshUIForValue() - { - base.RefreshUIForValue(); - - if (m_subContentConstructed && !(this is InteractiveFlags)) - { - m_dropdownText.text = Value?.ToString() ?? ""; - } - } - - internal override void OnToggleSubcontent(bool toggle) - { - base.OnToggleSubcontent(toggle); - - RefreshUIForValue(); - } - - private void SetValueFromDropdown() - { - var type = Value?.GetType() ?? FallbackType; - var index = m_dropdown.value; - - var value = Enum.Parse(type, s_enumNamesCache[type][index].Value); - - if (value != null) - { - Value = value; - Owner.SetValue(); - RefreshUIForValue(); - } - } - - internal Dropdown m_dropdown; - internal Text m_dropdownText; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - if (Owner.CanWrite) - { - var groupObj = UIFactory.CreateHorizontalGroup(m_subContentParent, "InteractiveEnumGroup", false, true, true, true, 5, - new Vector4(3,3,3,3),new Color(1, 1, 1, 0)); - - // apply button - - var apply = UIFactory.CreateButton(groupObj, "ApplyButton", "Apply", SetValueFromDropdown, new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(apply.gameObject, minHeight: 25, minWidth: 50); - - // dropdown - - var dropdownObj = UIFactory.CreateDropdown(groupObj, out m_dropdown, "", 14, null); - UIFactory.SetLayoutElement(dropdownObj, minWidth: 150, minHeight: 25, flexibleWidth: 120); - - foreach (var kvp in m_values) - { - m_dropdown.options.Add(new Dropdown.OptionData - { - text = $"{kvp.Key}: {kvp.Value}" - }); - } - - m_dropdownText = m_dropdown.transform.Find("Label").GetComponent(); - } - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveEnumerable.cs b/src/UI/InteractiveValues/InteractiveEnumerable.cs deleted file mode 100644 index 65731db..0000000 --- a/src/UI/InteractiveValues/InteractiveEnumerable.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.Config; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using UnityExplorer.UI.CacheObject; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveEnumerable : InteractiveValue - { - public InteractiveEnumerable(object value, Type valueType) : base(value, valueType) - { - if (valueType.IsGenericType) - m_baseEntryType = valueType.GetGenericArguments()[0]; - else - m_baseEntryType = typeof(object); - } - - public override bool WantInspectBtn => false; - public override bool HasSubContent => true; - public override bool SubContentWanted - { - get - { - if (m_recacheWanted) - return true; - else return m_entries.Count > 0; - } - } - - internal IEnumerable RefIEnumerable; - internal IList RefIList; - - internal readonly Type m_baseEntryType; - - internal readonly List m_entries = new List(); - internal readonly CacheEnumerated[] m_displayedEntries = new CacheEnumerated[ConfigManager.Default_Page_Limit.Value]; - internal bool m_recacheWanted = true; - - public override void OnValueUpdated() - { - RefIEnumerable = Value as IEnumerable; - RefIList = Value as IList; - - if (m_subContentParent.activeSelf) - { - GetCacheEntries(); - RefreshDisplay(); - } - else - m_recacheWanted = true; - - base.OnValueUpdated(); - } - - public override void OnException(CacheMember member) - { - base.OnException(member); - } - - private void OnPageTurned() - { - RefreshDisplay(); - } - - public override void RefreshUIForValue() - { - GetDefaultLabel(); - - if (Value != null) - { - string count = "?"; - if (m_recacheWanted && RefIList != null) - count = RefIList.Count.ToString(); - else if (!m_recacheWanted) - count = m_entries.Count.ToString(); - - m_baseLabel.text = $"[{count}] {m_richValueType}"; - } - else - { - m_baseLabel.text = DefaultLabel; - } - } - - public void GetCacheEntries() - { - if (m_entries.Any()) - { - // maybe improve this, probably could be more efficient i guess - - foreach (var entry in m_entries) - entry.Destroy(); - - m_entries.Clear(); - } - - if (RefIEnumerable == null && Value != null) - RefIEnumerable = RuntimeProvider.Instance.Reflection.EnumerateEnumerable(Value); - - if (RefIEnumerable != null) - { - int index = 0; - foreach (var entry in RefIEnumerable) - { - var cache = new CacheEnumerated(index, this, RefIList, this.m_listContent); - cache.CreateIValue(entry, m_baseEntryType); - m_entries.Add(cache); - - cache.Disable(); - - index++; - } - } - - RefreshDisplay(); - } - - public void RefreshDisplay() - { - var entries = m_entries; - m_pageHandler.ListCount = entries.Count; - - for (int i = 0; i < m_displayedEntries.Length; i++) - { - var entry = m_displayedEntries[i]; - if (entry != null) - entry.Disable(); - else - break; - } - - if (entries.Count < 1) - return; - - foreach (var itemIndex in m_pageHandler) - { - if (itemIndex >= entries.Count) - break; - - CacheEnumerated entry = entries[itemIndex]; - m_displayedEntries[itemIndex - m_pageHandler.StartIndex] = entry; - entry.Enable(); - } - - //UpdateSubcontentHeight(); - } - - internal override void OnToggleSubcontent(bool active) - { - base.OnToggleSubcontent(active); - - if (active && m_recacheWanted) - { - m_recacheWanted = false; - GetCacheEntries(); - RefreshUIForValue(); - } - - RefreshDisplay(); - } - - - internal GameObject m_listContent; - internal LayoutElement m_listLayout; - - internal PageHandler m_pageHandler; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - m_pageHandler = new PageHandler(null); - m_pageHandler.ConstructUI(m_subContentParent); - m_pageHandler.OnPageChanged += OnPageTurned; - - m_listContent = UIFactory.CreateVerticalGroup(this.m_subContentParent, "EnumerableContent", true, true, true, true, 2, new Vector4(5,5,5,5), - new Color(0.08f, 0.08f, 0.08f)); - - var scrollRect = m_listContent.GetComponent(); - scrollRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 0); - - m_listLayout = Owner.m_mainContent.GetComponent(); - m_listLayout.minHeight = 25; - m_listLayout.flexibleHeight = 0; - Owner.m_mainRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 25); - - var contentFitter = m_listContent.AddComponent(); - contentFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; - contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveFlags.cs b/src/UI/InteractiveValues/InteractiveFlags.cs deleted file mode 100644 index 48b94dc..0000000 --- a/src/UI/InteractiveValues/InteractiveFlags.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveFlags : InteractiveEnum - { - public InteractiveFlags(object value, Type valueType) : base(value, valueType) - { - m_toggles = new Toggle[m_values.Length]; - m_enabledFlags = new bool[m_values.Length]; - } - - public override bool HasSubContent => true; - public override bool SubContentWanted => Owner.CanWrite; - public override bool WantInspectBtn => false; - - internal bool[] m_enabledFlags; - internal Toggle[] m_toggles; - - public override void OnValueUpdated() - { - if (Owner.CanWrite) - { - var enabledNames = new List(); - - var enabled = Value?.ToString().Split(',').Select(it => it.Trim()); - if (enabled != null) - enabledNames.AddRange(enabled); - - for (int i = 0; i < m_values.Length; i++) - m_enabledFlags[i] = enabledNames.Contains(m_values[i].Value); - } - - base.OnValueUpdated(); - } - - public override void RefreshUIForValue() - { - GetDefaultLabel(); - m_baseLabel.text = DefaultLabel; - - base.RefreshUIForValue(); - - if (m_subContentConstructed) - { - for (int i = 0; i < m_values.Length; i++) - { - var toggle = m_toggles[i]; - if (toggle.isOn != m_enabledFlags[i]) - toggle.isOn = m_enabledFlags[i]; - } - } - } - - private void SetValueFromToggles() - { - string val = ""; - for (int i = 0; i < m_values.Length; i++) - { - if (m_enabledFlags[i]) - { - if (val != "") val += ", "; - val += m_values[i].Value; - } - } - var type = Value?.GetType() ?? FallbackType; - Value = Enum.Parse(type, val); - RefreshUIForValue(); - Owner.SetValue(); - } - - internal override void OnToggleSubcontent(bool toggle) - { - base.OnToggleSubcontent(toggle); - - RefreshUIForValue(); - } - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - } - - public override void ConstructSubcontent() - { - m_subContentConstructed = true; - - if (Owner.CanWrite) - { - var groupObj = UIFactory.CreateVerticalGroup(m_subContentParent, "InteractiveFlagsContent", false, true, true, true, 5, - new Vector4(3,3,3,3), new Color(1, 1, 1, 0)); - - // apply button - - var apply = UIFactory.CreateButton(groupObj, "ApplyButton", "Apply", SetValueFromToggles, new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(apply.gameObject, minWidth: 50, minHeight: 25); - - // toggles - - for (int i = 0; i < m_values.Length; i++) - AddToggle(i, groupObj); - } - } - - internal void AddToggle(int index, GameObject groupObj) - { - var value = m_values[index]; - - var toggleObj = UIFactory.CreateToggle(groupObj, "FlagToggle", out Toggle toggle, out Text text, new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(toggleObj, minWidth: 100, flexibleWidth: 2000, minHeight: 25); - - m_toggles[index] = toggle; - - toggle.onValueChanged.AddListener((bool val) => { m_enabledFlags[index] = val; }); - - text.text = $"{value.Key}: {value.Value}"; - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveFloatStruct.cs b/src/UI/InteractiveValues/InteractiveFloatStruct.cs deleted file mode 100644 index 81a1bef..0000000 --- a/src/UI/InteractiveValues/InteractiveFloatStruct.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using System.Reflection; - -namespace UnityExplorer.UI.InteractiveValues -{ - // Class for supporting any "float struct" (ie Vector, Rect, etc). - // Supports any struct where all the public instance fields are floats (or types assignable to float) - - public class StructInfo - { - public string[] FieldNames { get; } - private readonly FieldInfo[] m_fields; - - public StructInfo(Type type) - { - m_fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) - .Where(it => !it.IsLiteral) - .ToArray(); - - FieldNames = m_fields.Select(it => it.Name) - .ToArray(); - } - - public object SetValue(ref object instance, int fieldIndex, float val) - { - m_fields[fieldIndex].SetValue(instance, val); - return instance; - } - - public float GetValue(object instance, int fieldIndex) - => (float)m_fields[fieldIndex].GetValue(instance); - - public void RefreshUI(InputField[] inputs, object instance) - { - try - { - for (int i = 0; i < m_fields.Length; i++) - { - var field = m_fields[i]; - float val = (float)field.GetValue(instance); - inputs[i].text = val.ToString(); - } - } - catch (Exception ex) - { - ExplorerCore.Log(ex); - } - } - } - - public class InteractiveFloatStruct : InteractiveValue - { - private static readonly Dictionary _typeSupportCache = new Dictionary(); - public static bool IsTypeSupported(Type type) - { - if (!type.IsValueType) - return false; - - if (string.IsNullOrEmpty(type.AssemblyQualifiedName)) - return false; - - if (_typeSupportCache.TryGetValue(type.AssemblyQualifiedName, out bool ret)) - return ret; - - ret = true; - var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - foreach (var field in fields) - { - if (field.IsLiteral) - continue; - - if (!typeof(float).IsAssignableFrom(field.FieldType)) - { - ret = false; - break; - } - } - _typeSupportCache.Add(type.AssemblyQualifiedName, ret); - return ret; - } - - //~~~~~~~~~ Instance ~~~~~~~~~~ - - public InteractiveFloatStruct(object value, Type valueType) : base(value, valueType) { } - - public override bool HasSubContent => true; - public override bool SubContentWanted => true; - - public StructInfo StructInfo; - - public override void RefreshUIForValue() - { - InitializeStructInfo(); - - base.RefreshUIForValue(); - - if (m_subContentConstructed) - StructInfo.RefreshUI(m_inputs, this.Value); - } - - internal override void OnToggleSubcontent(bool toggle) - { - InitializeStructInfo(); - - base.OnToggleSubcontent(toggle); - - StructInfo.RefreshUI(m_inputs, this.Value); - } - - internal Type m_lastStructType; - - internal void InitializeStructInfo() - { - var type = Value?.GetType() ?? FallbackType; - - if (StructInfo != null && type == m_lastStructType) - return; - - if (StructInfo != null && m_subContentConstructed) - DestroySubContent(); - - m_lastStructType = type; - - StructInfo = new StructInfo(type); - - if (m_subContentParent.activeSelf) - ConstructSubcontent(); - } - - #region UI CONSTRUCTION - - internal InputField[] m_inputs; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - if (StructInfo == null) - { - ExplorerCore.LogWarning("Setting up subcontent but structinfo is null"); - return; - } - - var editorContainer = UIFactory.CreateVerticalGroup(m_subContentParent, "EditorContent", false, true, true, true, 2, new Vector4(4, 4, 4, 4), - new Color(0.08f, 0.08f, 0.08f)); - - m_inputs = new InputField[StructInfo.FieldNames.Length]; - - for (int i = 0; i < StructInfo.FieldNames.Length; i++) - AddEditorRow(i, editorContainer); - - RefreshUIForValue(); - } - - internal void AddEditorRow(int index, GameObject groupObj) - { - try - { - var row = UIFactory.CreateHorizontalGroup(groupObj, "EditorRow", false, true, true, true, 5, default, new Color(1, 1, 1, 0)); - - string name = StructInfo.FieldNames[index]; - - var label = UIFactory.CreateLabel(row, "RowLabel", $"{name}:", TextAnchor.MiddleRight, Color.cyan); - UIFactory.SetLayoutElement(label.gameObject, minWidth: 30, flexibleWidth: 0, minHeight: 25); - - var inputFieldObj = UIFactory.CreateInputField(row, "InputField", "...", 14, 3, 1); - UIFactory.SetLayoutElement(inputFieldObj, minWidth: 120, minHeight: 25, flexibleWidth: 0); - - var inputField = inputFieldObj.GetComponent(); - m_inputs[index] = inputField; - - inputField.onValueChanged.AddListener((string val) => - { - try - { - float f = float.Parse(val); - Value = StructInfo.SetValue(ref this.Value, index, f); - Owner.SetValue(); - } - catch { } - }); - } - catch (Exception ex) - { - ExplorerCore.Log(ex); - } - } - - #endregion - } -} diff --git a/src/UI/InteractiveValues/InteractiveNumber.cs b/src/UI/InteractiveValues/InteractiveNumber.cs deleted file mode 100644 index 32b3d3e..0000000 --- a/src/UI/InteractiveValues/InteractiveNumber.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Reflection; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using UnityExplorer.Core; -using UnityExplorer.UI.Utility; -using UnityExplorer.UI.CacheObject; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveNumber : InteractiveValue - { - public InteractiveNumber(object value, Type valueType) : base(value, valueType) { } - - public override bool HasSubContent => false; - public override bool SubContentWanted => false; - public override bool WantInspectBtn => false; - - public override void OnValueUpdated() - { - base.OnValueUpdated(); - } - - public override void OnException(CacheMember member) - { - base.OnException(member); - - if (m_valueInput.gameObject.activeSelf) - m_valueInput.gameObject.SetActive(false); - - if (Owner.CanWrite) - { - if (m_applyBtn.gameObject.activeSelf) - m_applyBtn.gameObject.SetActive(false); - } - } - - public override void RefreshUIForValue() - { - if (!Owner.HasEvaluated) - { - GetDefaultLabel(); - m_baseLabel.text = DefaultLabel; - return; - } - - m_baseLabel.text = SignatureHighlighter.ParseFullSyntax(FallbackType, false); - m_valueInput.text = Value.ToString(); - - var type = Value.GetType(); - if (type == typeof(float) - || type == typeof(double) - || type == typeof(decimal)) - { - m_valueInput.characterValidation = InputField.CharacterValidation.Decimal; - } - else - { - m_valueInput.characterValidation = InputField.CharacterValidation.Integer; - } - - if (Owner.CanWrite) - { - if (!m_applyBtn.gameObject.activeSelf) - m_applyBtn.gameObject.SetActive(true); - } - - if (!m_valueInput.gameObject.activeSelf) - m_valueInput.gameObject.SetActive(true); - } - - public MethodInfo ParseMethod => m_parseMethod ?? (m_parseMethod = Value.GetType().GetMethod("Parse", new Type[] { typeof(string) })); - private MethodInfo m_parseMethod; - - internal void OnApplyClicked() - { - try - { - Value = ParseMethod.Invoke(null, new object[] { m_valueInput.text }); - Owner.SetValue(); - RefreshUIForValue(); - } - catch (Exception e) - { - ExplorerCore.LogWarning("Could not parse input! " + ReflectionUtility.ReflectionExToString(e, true)); - } - } - - internal InputField m_valueInput; - internal Button m_applyBtn; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - - var labelLayout = m_baseLabel.gameObject.GetComponent(); - labelLayout.minWidth = 50; - labelLayout.flexibleWidth = 0; - - var inputObj = UIFactory.CreateInputField(m_mainContent, "InteractiveNumberInput", "..."); - UIFactory.SetLayoutElement(inputObj, minWidth: 120, minHeight: 25, flexibleWidth: 0); - - m_valueInput = inputObj.GetComponent(); - m_valueInput.gameObject.SetActive(false); - - if (Owner.CanWrite) - { - m_applyBtn = UIFactory.CreateButton(m_mainContent, "ApplyButton", "Apply", OnApplyClicked, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(m_applyBtn.gameObject, minWidth: 50, minHeight: 25, flexibleWidth: 0); - } - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveString.cs b/src/UI/InteractiveValues/InteractiveString.cs deleted file mode 100644 index c14d758..0000000 --- a/src/UI/InteractiveValues/InteractiveString.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Reflection; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using UnityExplorer.UI.Utility; -using UnityExplorer.UI.CacheObject; -using UnityExplorer.Core.Runtime; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveString : InteractiveValue - { - public InteractiveString(object value, Type valueType) : base(value, valueType) { } - - public override bool HasSubContent => true; - public override bool SubContentWanted => true; - - public override bool WantInspectBtn => false; - - public override void OnValueUpdated() - { - if (!(Value is string) && Value != null) - Value = RuntimeProvider.Instance.Reflection.UnboxString(Value); - - base.OnValueUpdated(); - } - - public override void OnException(CacheMember member) - { - base.OnException(member); - - if (m_subContentConstructed && m_hiddenObj.gameObject.activeSelf) - m_hiddenObj.gameObject.SetActive(false); - - m_labelLayout.minWidth = 200; - m_labelLayout.flexibleWidth = 5000; - } - - public override void RefreshUIForValue() - { - GetDefaultLabel(false); - - if (!Owner.HasEvaluated) - { - m_baseLabel.text = DefaultLabel; - return; - } - - m_baseLabel.text = m_richValueType; - - if (m_subContentConstructed) - { - if (!m_hiddenObj.gameObject.activeSelf) - m_hiddenObj.gameObject.SetActive(true); - } - - if (!string.IsNullOrEmpty((string)Value)) - { - var toString = (string)Value; - if (toString.Length > 15000) - toString = toString.Substring(0, 15000); - - m_readonlyInput.text = toString; - - if (m_subContentConstructed) - { - m_valueInput.text = toString; - m_placeholderText.text = toString; - } - } - else - { - string s = Value == null - ? "null" - : "empty"; - - m_readonlyInput.text = $"{s}"; - - if (m_subContentConstructed) - { - m_valueInput.text = ""; - m_placeholderText.text = s; - } - } - - m_labelLayout.minWidth = 50; - m_labelLayout.flexibleWidth = 0; - } - - internal void SetValueFromInput() - { - Value = m_valueInput.text; - - if (!typeof(string).IsAssignableFrom(Owner.FallbackType)) - ReflectionProvider.Instance.BoxStringToType(ref Value, Owner.FallbackType); - - Owner.SetValue(); - - // revert back to string now - OnValueUpdated(); - - RefreshUIForValue(); - } - - // for the default label - internal LayoutElement m_labelLayout; - - //internal InputField m_readonlyInput; - internal Text m_readonlyInput; - - // for input - internal InputField m_valueInput; - internal GameObject m_hiddenObj; - internal Text m_placeholderText; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - - GetDefaultLabel(false); - m_richValueType = SignatureHighlighter.ParseFullSyntax(FallbackType, false); - - m_labelLayout = m_baseLabel.gameObject.GetComponent(); - - m_readonlyInput = UIFactory.CreateLabel(m_mainContent, "ReadonlyLabel", "", TextAnchor.MiddleLeft); - m_readonlyInput.horizontalOverflow = HorizontalWrapMode.Overflow; - - var testFitter = m_readonlyInput.gameObject.AddComponent(); - testFitter.verticalFit = ContentSizeFitter.FitMode.MinSize; - - UIFactory.SetLayoutElement(m_readonlyInput.gameObject, minHeight: 25, preferredHeight: 25, flexibleHeight: 0); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - var groupObj = UIFactory.CreateVerticalGroup(m_subContentParent, "SubContent", false, false, true, true, 4, new Vector4(3,3,3,3), - new Color(1, 1, 1, 0)); - - m_hiddenObj = UIFactory.CreateLabel(groupObj, "HiddenLabel", "", TextAnchor.MiddleLeft).gameObject; - m_hiddenObj.SetActive(false); - var hiddenText = m_hiddenObj.GetComponent(); - hiddenText.color = Color.clear; - hiddenText.fontSize = 14; - hiddenText.raycastTarget = false; - hiddenText.supportRichText = false; - var hiddenFitter = m_hiddenObj.AddComponent(); - hiddenFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; - UIFactory.SetLayoutElement(m_hiddenObj, minHeight: 25, flexibleHeight: 500, minWidth: 250, flexibleWidth: 9000); - UIFactory.SetLayoutGroup(m_hiddenObj, true, true, true, true); - - var inputObj = UIFactory.CreateInputField(m_hiddenObj, "StringInputField", "...", 14, 3); - UIFactory.SetLayoutElement(inputObj, minWidth: 120, minHeight: 25, flexibleWidth: 5000, flexibleHeight: 5000); - - m_valueInput = inputObj.GetComponent(); - m_valueInput.lineType = InputField.LineType.MultiLineNewline; - - m_placeholderText = m_valueInput.placeholder.GetComponent(); - - m_placeholderText.supportRichText = false; - m_valueInput.textComponent.supportRichText = false; - - m_valueInput.onValueChanged.AddListener((string val) => - { - hiddenText.text = val ?? ""; - LayoutRebuilder.ForceRebuildLayoutImmediate(Owner.m_mainRect); - }); - - if (Owner.CanWrite) - { - var apply = UIFactory.CreateButton(groupObj, "ApplyButton", "Apply", SetValueFromInput, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(apply.gameObject, minWidth: 50, minHeight: 25, flexibleWidth: 0); - } - else - { - m_valueInput.readOnly = true; - } - - RefreshUIForValue(); - } - } -} diff --git a/src/UI/InteractiveValues/InteractiveValue.cs b/src/UI/InteractiveValues/InteractiveValue.cs deleted file mode 100644 index 343a9b7..0000000 --- a/src/UI/InteractiveValues/InteractiveValue.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -using UnityEngine.EventSystems; -using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.Unity; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI; -using UnityExplorer.UI.Utility; -using UnityExplorer.UI.CacheObject; -using UnityExplorer.UI.Main.Home; -using UnityExplorer.UI.Inspectors; - -namespace UnityExplorer.UI.InteractiveValues -{ - public class InteractiveValue - { - /// - /// Get the subclass which supports the provided . - /// - /// The which you want the Type for. - /// The best subclass of which supports the provided . - public static Type GetIValueForType(Type type) - { - // rather ugly but I couldn't think of a cleaner way that was worth it. - // switch-case doesn't really work here. - - // arbitrarily check some types, fastest methods first. - if (type == typeof(bool)) - return typeof(InteractiveBool); - // if type is primitive then it must be a number if its not a bool. Also check for decimal. - else if (type.IsPrimitive || type == typeof(decimal)) - return typeof(InteractiveNumber); - // check for strings - else if (type == typeof(string)) - return typeof(InteractiveString); - // check for enum/flags - else if (typeof(Enum).IsAssignableFrom(type)) - { - // NET 3.5 doesn't have "GetCustomAttribute", gotta use the multiple version. - if (type.GetCustomAttributes(typeof(FlagsAttribute), true) is object[] fa && fa.Any()) - return typeof(InteractiveFlags); - else - return typeof(InteractiveEnum); - } - // check for unity struct types - else if (typeof(Color).IsAssignableFrom(type)) - return typeof(InteractiveColor); - else if (InteractiveFloatStruct.IsTypeSupported(type)) - return typeof(InteractiveFloatStruct); - // check Transform, force InteractiveValue so they dont become InteractiveEnumerables. - else if (typeof(Transform).IsAssignableFrom(type)) - return typeof(InteractiveValue); - // check Dictionaries before Enumerables - else if (ReflectionUtility.IsDictionary(type)) - return typeof(InteractiveDictionary); - // finally check for Enumerables - else if (ReflectionUtility.IsEnumerable(type)) - return typeof(InteractiveEnumerable); - // fallback to default - else - return typeof(InteractiveValue); - } - - public static InteractiveValue Create(object value, Type fallbackType) - { - var type = ReflectionUtility.GetActualType(value) ?? fallbackType; - var iType = GetIValueForType(type); - - return (InteractiveValue)Activator.CreateInstance(iType, new object[] { value, type }); - } - - // ~~~~~~~~~ Instance ~~~~~~~~~ - - public InteractiveValue(object value, Type valueType) - { - this.Value = value; - this.FallbackType = valueType; - } - - public CacheObjectBase Owner; - - public object Value; - public readonly Type FallbackType; - - public virtual bool HasSubContent => false; - public virtual bool SubContentWanted => false; - public virtual bool WantInspectBtn => true; - - public string DefaultLabel => m_defaultLabel ?? GetDefaultLabel(); - internal string m_defaultLabel; - internal string m_richValueType; - - public bool m_UIConstructed; - - public virtual void OnDestroy() - { - if (this.m_mainContent) - { - m_mainContent.transform.SetParent(null, false); - m_mainContent.SetActive(false); - GameObject.Destroy(this.m_mainContent.gameObject); - } - - DestroySubContent(); - } - - public virtual void DestroySubContent() - { - if (this.m_subContentParent && HasSubContent) - { - for (int i = 0; i < this.m_subContentParent.transform.childCount; i++) - { - var child = m_subContentParent.transform.GetChild(i); - if (child) - GameObject.Destroy(child.gameObject); - } - } - - m_subContentConstructed = false; - } - - public virtual void OnValueUpdated() - { - if (!m_UIConstructed) - ConstructUI(m_mainContentParent, m_subContentParent); - - if (Owner is CacheMember ownerMember && !string.IsNullOrEmpty(ownerMember.ReflectionException)) - OnException(ownerMember); - else - RefreshUIForValue(); - } - - public virtual void OnException(CacheMember member) - { - if (m_UIConstructed) - m_baseLabel.text = "" + member.ReflectionException + ""; - - Value = null; - } - - public virtual void RefreshUIForValue() - { - GetDefaultLabel(); - m_baseLabel.text = DefaultLabel; - } - - public void RefreshElementsAfterUpdate() - { - if (WantInspectBtn) - { - bool shouldShowInspect = !Value.IsNullOrDestroyed(); - - if (m_inspectButton.activeSelf != shouldShowInspect) - m_inspectButton.SetActive(shouldShowInspect); - } - - bool subContentWanted = SubContentWanted; - if (Owner is CacheMember cm && (!cm.HasEvaluated || !string.IsNullOrEmpty(cm.ReflectionException))) - subContentWanted = false; - - if (HasSubContent) - { - if (m_subExpandBtn.gameObject.activeSelf != subContentWanted) - m_subExpandBtn.gameObject.SetActive(subContentWanted); - - if (!subContentWanted && m_subContentParent.activeSelf) - ToggleSubcontent(); - } - } - - public virtual void ConstructSubcontent() - { - m_subContentConstructed = true; - } - - public void ToggleSubcontent() - { - if (!this.m_subContentParent.activeSelf) - { - this.m_subContentParent.SetActive(true); - this.m_subContentParent.transform.SetAsLastSibling(); - m_subExpandBtn.GetComponentInChildren().text = "▼"; - } - else - { - this.m_subContentParent.SetActive(false); - m_subExpandBtn.GetComponentInChildren().text = "▲"; - } - - OnToggleSubcontent(m_subContentParent.activeSelf); - - RefreshElementsAfterUpdate(); - } - - internal virtual void OnToggleSubcontent(bool toggle) - { - if (!m_subContentConstructed) - ConstructSubcontent(); - } - - internal MethodInfo m_toStringMethod; - internal MethodInfo m_toStringFormatMethod; - internal bool m_gotToStringMethods; - - public string GetDefaultLabel(bool updateType = true) - { - var valueType = Value?.GetType() ?? this.FallbackType; - if (updateType) - m_richValueType = SignatureHighlighter.ParseFullSyntax(valueType, true); - - if (!Owner.HasEvaluated) - return m_defaultLabel = $"Not yet evaluated ({m_richValueType})"; - - if (Value.IsNullOrDestroyed()) - return m_defaultLabel = $"null ({m_richValueType})"; - - string label; - - // Two dirty fixes for TextAsset and EventSystem, which can have very long ToString results. - if (Value is TextAsset textAsset) - { - label = textAsset.text; - - if (label.Length > 10) - label = $"{label.Substring(0, 10)}..."; - - label = $"\"{label}\" {textAsset.name} ({m_richValueType})"; - } - else if (Value is EventSystem) - { - label = m_richValueType; - } - else // For everything else... - { - if (!m_gotToStringMethods) - { - m_gotToStringMethods = true; - - m_toStringMethod = valueType.GetMethod("ToString", new Type[0]); - m_toStringFormatMethod = valueType.GetMethod("ToString", new Type[] { typeof(string) }); - - // test format method actually works - try - { - m_toStringFormatMethod.Invoke(Value, new object[] { "F3" }); - } - catch - { - m_toStringFormatMethod = null; - } - } - - string toString; - if (m_toStringFormatMethod != null) - toString = (string)m_toStringFormatMethod.Invoke(Value, new object[] { "F3" }); - else - toString = (string)m_toStringMethod.Invoke(Value, new object[0]); - - toString = toString ?? ""; - - string typeName = valueType.FullName; - if (typeName.StartsWith("Il2CppSystem.")) - typeName = typeName.Substring(6, typeName.Length - 6); - - toString = ReflectionProvider.Instance.ProcessTypeNameInString(valueType, toString, ref typeName); - - // If the ToString is just the type name, use our syntax highlighted type name instead. - if (toString == typeName) - { - label = m_richValueType; - } - else // Otherwise, parse the result and put our highlighted name in. - { - if (toString.Length > 200) - toString = toString.Substring(0, 200) + "..."; - - label = toString; - - var unityType = $"({valueType.FullName})"; - if (Value is UnityEngine.Object && label.Contains(unityType)) - label = label.Replace(unityType, $"({m_richValueType})"); - else - label += $" ({m_richValueType})"; - } - } - - return m_defaultLabel = label; - } - - #region UI CONSTRUCTION - - internal GameObject m_mainContentParent; - internal GameObject m_subContentParent; - - internal GameObject m_mainContent; - internal GameObject m_inspectButton; - internal Text m_baseLabel; - - internal Button m_subExpandBtn; - internal bool m_subContentConstructed; - - public virtual void ConstructUI(GameObject parent, GameObject subGroup) - { - m_UIConstructed = true; - - m_mainContent = UIFactory.CreateHorizontalGroup(parent, $"InteractiveValue_{this.GetType().Name}", false, false, true, true, 4, default, - new Color(1, 1, 1, 0), TextAnchor.UpperLeft); - - var mainRect = m_mainContent.GetComponent(); - mainRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 25); - - UIFactory.SetLayoutElement(m_mainContent, flexibleWidth: 9000, minWidth: 175, minHeight: 25, flexibleHeight: 0); - - // subcontent expand button - if (HasSubContent) - { - m_subExpandBtn = UIFactory.CreateButton(m_mainContent, "ExpandSubcontentButton", "▲", ToggleSubcontent, new Color(0.3f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(m_subExpandBtn.gameObject, minHeight: 25, minWidth: 25, flexibleWidth: 0, flexibleHeight: 0); - } - - // inspect button - - var inspectBtn = UIFactory.CreateButton(m_mainContent, - "InspectButton", - "Inspect", - () => - { - if (!Value.IsNullOrDestroyed(false)) - InspectorManager.Instance.Inspect(this.Value, this.Owner); - }, - new Color(0.3f, 0.3f, 0.3f, 0.2f)); - - m_inspectButton = inspectBtn.gameObject; - UIFactory.SetLayoutElement(m_inspectButton, minWidth: 60, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0); - - m_inspectButton.SetActive(false); - - // value label - - m_baseLabel = UIFactory.CreateLabel(m_mainContent, "ValueLabel", "", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(m_baseLabel.gameObject, flexibleWidth: 9000, minHeight: 25); - - m_subContentParent = subGroup; - } - -#endregion - } -} diff --git a/src/UI/Main/BaseMenuPage.cs b/src/UI/Main/BaseMenuPage.cs deleted file mode 100644 index 6ae78bb..0000000 --- a/src/UI/Main/BaseMenuPage.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; - -namespace UnityExplorer.UI.Main -{ - public enum MenuPages - { - Home, - Search, - CSConsole, - Options - } - - public abstract class BaseMenuPage - { - public abstract string Name { get; } - public abstract MenuPages Type { get; } - public bool WasDisabled { get; internal set; } - - public GameObject Content; - public Button RefNavbarButton { get; set; } - - public bool Enabled - { - get => Content?.activeSelf ?? false; - set => Content?.SetActive(true); - } - - public abstract bool Init(); - public abstract void Update(); - } -} diff --git a/src/UI/Main/CSConsole/AutoCompleter.cs b/src/UI/Main/CSConsole/AutoCompleter.cs deleted file mode 100644 index 281ff12..0000000 --- a/src/UI/Main/CSConsole/AutoCompleter.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.EventSystems; -using UnityEngine.UI; -using UnityExplorer.Core.CSharp; -using UnityExplorer.Core.Input; -using UnityExplorer.Core.Runtime; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; -using UnityExplorer.UI.Main; - -namespace UnityExplorer.UI.Main.CSConsole -{ - public class AutoCompleter - { - public static AutoCompleter Instance; - - public const int MAX_LABELS = 500; - private const int UPDATES_PER_BATCH = 100; - - public static GameObject m_mainObj; - - private static readonly List m_suggestionButtons = new List(); - private static readonly List m_suggestionTexts = new List(); - private static readonly List m_hiddenSuggestionTexts = new List(); - - private static bool m_suggestionsDirty; - private static Suggestion[] m_suggestions = new Suggestion[0]; - private static int m_lastBatchIndex; - - private static string m_prevInput = "NULL"; - private static int m_lastCaretPos; - - public static void Init() - { - ConstructUI(); - - m_mainObj.SetActive(false); - } - - public static void Update() - { - if (!m_mainObj) - return; - - if (!CSharpConsole.EnableAutocompletes) - { - if (m_mainObj.activeSelf) - m_mainObj.SetActive(false); - - return; - } - - RefreshButtons(); - - UpdatePosition(); - } - - public static void SetSuggestions(Suggestion[] suggestions) - { - m_suggestions = suggestions; - - m_suggestionsDirty = true; - m_lastBatchIndex = 0; - } - - private static void RefreshButtons() - { - if (!m_suggestionsDirty) - { - return; - } - - if (m_suggestions.Length < 1) - { - if (m_mainObj.activeSelf) - { - m_mainObj?.SetActive(false); - } - return; - } - - if (!m_mainObj.activeSelf) - { - m_mainObj.SetActive(true); - } - - if (m_suggestions.Length < 1 || m_lastBatchIndex >= MAX_LABELS) - { - m_suggestionsDirty = false; - return; - } - - int end = m_lastBatchIndex + UPDATES_PER_BATCH; - for (int i = m_lastBatchIndex; i < end && i < MAX_LABELS; i++) - { - if (i >= m_suggestions.Length) - { - if (m_suggestionButtons[i].activeSelf) - { - m_suggestionButtons[i].SetActive(false); - } - } - else - { - if (!m_suggestionButtons[i].activeSelf) - { - m_suggestionButtons[i].SetActive(true); - } - - var suggestion = m_suggestions[i]; - var label = m_suggestionTexts[i]; - var hiddenLabel = m_hiddenSuggestionTexts[i]; - - label.text = suggestion.Full; - hiddenLabel.text = suggestion.Addition; - - label.color = suggestion.TextColor; - } - - m_lastBatchIndex = i; - } - - m_lastBatchIndex++; - } - - private static void UpdatePosition() - { - try - { - var editor = CSharpConsole.Instance; - - if (!editor.InputField.isFocused) - return; - - var textGen = editor.InputText.cachedTextGenerator; - int caretPos = editor.m_lastCaretPos; - - if (caretPos == m_lastCaretPos) - return; - - m_lastCaretPos = caretPos; - - if (caretPos >= 1) - caretPos--; - - var pos = textGen.characters[caretPos].cursorPos; - - pos = editor.InputField.transform.TransformPoint(pos); - - m_mainObj.transform.position = new Vector3(pos.x + 10, pos.y - 20, 0); - } - catch //(Exception e) - { - //ExplorerCore.Log(e.ToString()); - } - } - - private static readonly char[] splitChars = new[] { '{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?' }; - - public static void CheckAutocomplete() - { - var m_codeEditor = CSharpConsole.Instance; - string input = m_codeEditor.InputField.text; - int caretIndex = m_codeEditor.InputField.caretPosition; - - if (!string.IsNullOrEmpty(input)) - { - try - { - int start = caretIndex <= 0 ? 0 : input.LastIndexOfAny(splitChars, caretIndex - 1) + 1; - input = input.Substring(start, caretIndex - start).Trim(); - } - catch (ArgumentException) { } - } - - if (!string.IsNullOrEmpty(input) && input != m_prevInput) - { - GetAutocompletes(input); - } - else - { - ClearAutocompletes(); - } - - m_prevInput = input; - } - - public static void ClearAutocompletes() - { - if (CSharpConsole.AutoCompletes.Any()) - { - CSharpConsole.AutoCompletes.Clear(); - } - } - - public static void GetAutocompletes(string input) - { - try - { - // Credit ManylMarco - CSharpConsole.AutoCompletes.Clear(); - string[] completions = CSharpConsole.Instance.Evaluator.GetCompletions(input, out string prefix); - if (completions != null) - { - if (prefix == null) - { - prefix = input; - } - - CSharpConsole.AutoCompletes.AddRange(completions - .Where(x => !string.IsNullOrEmpty(x)) - .Select(x => new Suggestion(x, prefix, Suggestion.Contexts.Other)) - ); - } - - string trimmed = input.Trim(); - if (trimmed.StartsWith("using")) - { - trimmed = trimmed.Remove(0, 5).Trim(); - } - - IEnumerable namespaces = Suggestion.Namespaces - .Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length) - .Select(x => new Suggestion( - x.Substring(trimmed.Length), - x.Substring(0, trimmed.Length), - Suggestion.Contexts.Namespace)); - - CSharpConsole.AutoCompletes.AddRange(namespaces); - - IEnumerable keywords = Suggestion.Keywords - .Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length) - .Select(x => new Suggestion( - x.Substring(trimmed.Length), - x.Substring(0, trimmed.Length), - Suggestion.Contexts.Keyword)); - - CSharpConsole.AutoCompletes.AddRange(keywords); - } - catch (Exception ex) - { - ExplorerCore.Log("Autocomplete error:\r\n" + ex.ToString()); - ClearAutocompletes(); - } - } - - #region UI Construction - - private static void ConstructUI() - { - var parent = UIManager.CanvasRoot; - - var obj = UIFactory.CreateScrollView(parent, "AutoCompleterScrollView", out GameObject content, out _, new Color(0.1f, 0.1f, 0.1f, 0.95f)); - - m_mainObj = obj; - - var mainRect = obj.GetComponent(); - //m_thisRect = mainRect; - mainRect.pivot = new Vector2(0f, 1f); - mainRect.anchorMin = new Vector2(0.45f, 0.45f); - mainRect.anchorMax = new Vector2(0.65f, 0.6f); - mainRect.offsetMin = Vector2.zero; - mainRect.offsetMax = Vector2.zero; - - var mainGroup = content.GetComponent(); - mainGroup.SetChildControlHeight(false); - mainGroup.SetChildControlWidth(true); - mainGroup.childForceExpandHeight = false; - mainGroup.childForceExpandWidth = true; - - for (int i = 0; i < MAX_LABELS; i++) - { - var btn = UIFactory.CreateButton(content, "AutoCompleteButton", "", null); - RuntimeProvider.Instance.SetColorBlock(btn, new Color(0, 0, 0, 0), highlighted: new Color(0.2f, 0.2f, 0.2f, 1.0f)); - - var nav = btn.navigation; - nav.mode = Navigation.Mode.Vertical; - btn.navigation = nav; - - UIFactory.SetLayoutElement(btn.gameObject, minHeight: 20); - - var text = btn.GetComponentInChildren(); - text.alignment = TextAnchor.MiddleLeft; - text.color = Color.white; - - var hiddenChild = UIFactory.CreateUIObject("HiddenText", btn.gameObject); - hiddenChild.SetActive(false); - var hiddenText = hiddenChild.AddComponent(); - m_hiddenSuggestionTexts.Add(hiddenText); - btn.onClick.AddListener(() => { CSharpConsole.Instance.UseAutocomplete(hiddenText.text); }); - - m_suggestionButtons.Add(btn.gameObject); - m_suggestionTexts.Add(text); - } - } - - #endregion - } -} diff --git a/src/UI/Main/CSConsole/CSLexerHighlighter.cs b/src/UI/Main/CSConsole/CSLexerHighlighter.cs deleted file mode 100644 index 10654bd..0000000 --- a/src/UI/Main/CSConsole/CSLexerHighlighter.cs +++ /dev/null @@ -1,290 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using UnityEngine; -using UnityExplorer.UI.Main.CSConsole.Lexer; - -namespace UnityExplorer.UI.Main.CSConsole -{ - public struct LexerMatchInfo - { - public int startIndex; - public int endIndex; - public string htmlColor; - } - - public enum DelimiterType - { - Start, - End, - }; - - public class CSLexerHighlighter - { - private string inputString; - private readonly Matcher[] matchers; - private readonly HashSet startDelimiters; - private readonly HashSet endDelimiters; - private int currentIndex; - private int currentLookaheadIndex; - - public char Current { get; private set; } - public char Previous { get; private set; } - - public bool EndOfStream => currentLookaheadIndex >= inputString.Length; - - public static char indentOpen = '{'; - public static char indentClose = '}'; - private static StringBuilder indentBuilder = new StringBuilder(); - - public static char[] delimiters = new[] - { - '[', ']', '(', ')', '{', '}', ';', ':', ',', '.' - }; - - public static CommentMatch commentMatcher = new CommentMatch(); - public static SymbolMatch symbolMatcher = new SymbolMatch(); - public static NumberMatch numberMatcher = new NumberMatch(); - public static StringMatch stringMatcher = new StringMatch(); - public static KeywordMatch validKeywordMatcher = new KeywordMatch(); - - // ~~~~~~~ ctor ~~~~~~~ - - public CSLexerHighlighter() - { - startDelimiters = new HashSet(delimiters); - endDelimiters = new HashSet(delimiters); - - this.matchers = new Matcher[] - { - commentMatcher, - symbolMatcher, - numberMatcher, - stringMatcher, - validKeywordMatcher, - }; - - foreach (Matcher lexer in matchers) - { - foreach (char c in lexer.StartChars) - { - if (!startDelimiters.Contains(c)) - startDelimiters.Add(c); - } - - foreach (char c in lexer.EndChars) - { - if (!endDelimiters.Contains(c)) - endDelimiters.Add(c); - } - } - } - - // ~~~~~~~ Lex Matching ~~~~~~~ - - public IEnumerable GetMatches(string input) - { - if (input == null || matchers == null || matchers.Length == 0) - { - yield break; - } - - inputString = input; - Current = ' '; - Previous = ' '; - currentIndex = 0; - currentLookaheadIndex = 0; - - while (!EndOfStream) - { - bool didMatchLexer = false; - - ReadWhiteSpace(); - - foreach (Matcher matcher in matchers) - { - int startIndex = currentIndex; - - bool isMatched = matcher.IsMatch(this); - - if (isMatched) - { - int endIndex = currentIndex; - - didMatchLexer = true; - - yield return new LexerMatchInfo - { - startIndex = startIndex, - endIndex = endIndex, - htmlColor = matcher.HexColor, - }; - - break; - } - } - - if (!didMatchLexer) - { - ReadNext(); - Commit(); - } - } - } - - // ~~~~~~~ Indent ~~~~~~~ - - public static string GetIndentForInput(string input, int indent, out int caretPosition) - { - indentBuilder = new StringBuilder(); - - indent += 1; - - bool stringState = false; - - for (int i = 0; i < input.Length; i++) - { - if (input[i] == '"') - { - stringState = !stringState; - } - - if (input[i] == '\n') - { - indentBuilder.Append('\n'); - for (int j = 0; j < indent; j++) - { - indentBuilder.Append("\t"); - } - } - else if (input[i] == '\t') - { - continue; - } - else if (!stringState && input[i] == indentOpen) - { - indentBuilder.Append(indentOpen); - indent++; - } - else if (!stringState && input[i] == indentClose) - { - indentBuilder.Append(indentClose); - indent--; - } - else - { - indentBuilder.Append(input[i]); - } - } - - string formattedSection = indentBuilder.ToString(); - - caretPosition = formattedSection.Length - 1; - - for (int i = formattedSection.Length - 1; i >= 0; i--) - { - if (formattedSection[i] == '\n') - { - continue; - } - - caretPosition = i; - break; - } - - return formattedSection; - } - - public static int GetIndentLevel(string inputString, int startIndex, int endIndex) - { - int indent = 0; - - for (int i = startIndex; i < endIndex; i++) - { - if (inputString[i] == '\t') - { - indent++; - } - - // Check for end line or other characters - if (inputString[i] == '\n' || inputString[i] != ' ') - { - break; - } - } - - return indent; - } - - // Lexer reading - - public char ReadNext() - { - if (EndOfStream) - { - return '\0'; - } - - Previous = Current; - - Current = inputString[currentLookaheadIndex]; - currentLookaheadIndex++; - - return Current; - } - - public void Rollback(int amount = -1) - { - if (amount == -1) - { - currentLookaheadIndex = currentIndex; - } - else - { - if (currentLookaheadIndex > currentIndex) - { - currentLookaheadIndex -= amount; - } - } - - int previousIndex = currentLookaheadIndex - 1; - - if (previousIndex >= inputString.Length) - { - Previous = inputString[inputString.Length - 1]; - } - else if (previousIndex >= 0) - { - Previous = inputString[previousIndex]; - } - else - { - Previous = ' '; - } - } - - public void Commit() - { - currentIndex = currentLookaheadIndex; - } - - public bool IsSpecialSymbol(char character, DelimiterType position = DelimiterType.Start) - { - if (position == DelimiterType.Start) - { - return startDelimiters.Contains(character); - } - - return endDelimiters.Contains(character); - } - - private void ReadWhiteSpace() - { - while (char.IsWhiteSpace(ReadNext()) == true) - { - Commit(); - } - - Rollback(); - } - } -} \ No newline at end of file diff --git a/src/UI/Main/CSConsole/CSharpConsole.cs b/src/UI/Main/CSConsole/CSharpConsole.cs deleted file mode 100644 index 43d66a0..0000000 --- a/src/UI/Main/CSConsole/CSharpConsole.cs +++ /dev/null @@ -1,593 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -using UnityExplorer.Core.CSharp; -using System.Linq; -using UnityExplorer.Core.Input; -using UnityEngine; -using UnityEngine.EventSystems; -using UnityEngine.UI; -using UnityExplorer.UI.Main.CSConsole; -using UnityExplorer.Core; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.Main.CSConsole -{ - public class CSharpConsole : BaseMenuPage - { - public override string Name => "C# Console"; - public override MenuPages Type => MenuPages.CSConsole; - - public static CSharpConsole Instance { get; private set; } - - public ScriptEvaluator Evaluator; - internal StringBuilder m_evalLogBuilder; - - public static List UsingDirectives; - - public static readonly string[] DefaultUsing = new string[] - { - "System", - "System.Linq", - "System.Collections", - "System.Collections.Generic", - "System.Reflection", - "UnityEngine", -#if CPP - "UnhollowerBaseLib", - "UnhollowerRuntimeLib", -#endif - }; - - public override bool Init() - { - Instance = this; - - try - { - InitConsole(); - - AutoCompleter.Init(); - - ResetConsole(false); - // Make sure compiler is supported on this platform - Evaluator.Compile("new object();"); - - return true; - } - catch (Exception e) - { - string info = "The C# Console has been disabled because"; - if (e is NotSupportedException && e.TargetSite?.Name == "DefineDynamicAssembly") - info += " Reflection.Emit is not supported."; - else - info += $" of an unknown error.\r\n({e.ReflectionExToString()})"; - - ExplorerCore.LogWarning(info); - - this.RefNavbarButton.GetComponentInChildren().text += " (disabled)"; - - return false; - } - } - - public void ResetConsole(bool log = true) - { - if (Evaluator != null) - Evaluator.Dispose(); - - m_evalLogBuilder = new StringBuilder(); - - Evaluator = new ScriptEvaluator(new StringWriter(m_evalLogBuilder)) { InteractiveBaseClass = typeof(ScriptInteraction) }; - - UsingDirectives = new List(); - - foreach (string use in DefaultUsing) - AddUsing(use); - - if (log) - ExplorerCore.Log($"C# Console reset. Using directives:\r\n{Evaluator.GetUsing()}"); - } - - public override void Update() - { - UpdateConsole(); - - AutoCompleter.Update(); - } - - public void AddUsing(string asm) - { - if (!UsingDirectives.Contains(asm)) - { - Evaluate($"using {asm};", true); - UsingDirectives.Add(asm); - } - } - - public void Evaluate(string code, bool supressLog = false) - { - try - { - Evaluator.Run(code); - - string output = ScriptEvaluator._textWriter.ToString(); - var outputSplit = output.Split('\n'); - if (outputSplit.Length >= 2) - output = outputSplit[outputSplit.Length - 2]; - m_evalLogBuilder.Clear(); - - if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) - throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); - - if (!supressLog) - ExplorerCore.Log("Code executed successfully."); - } - catch (FormatException fex) - { - if (!supressLog) - ExplorerCore.LogWarning(fex.Message); - } - catch (Exception ex) - { - if (!supressLog) - ExplorerCore.LogWarning(ex); - } - } - - // ================================================================================================= - - // UI stuff - - public InputField InputField { get; internal set; } - public Text InputText { get; internal set; } - public int CurrentIndent { get; private set; } - - public static bool EnableCtrlRShortcut { get; set; } = true; - public static bool EnableAutoIndent { get; set; } = true; - public static bool EnableAutocompletes { get; set; } = true; - public static List AutoCompletes = new List(); - - public string HighlightedText => inputHighlightText.text; - private Text inputHighlightText; - - private CSLexerHighlighter highlightLexer; - - internal int m_lastCaretPos; - internal int m_fixCaretPos; - internal bool m_fixwanted; - internal float m_lastSelectAlpha; - - private static readonly KeyCode[] onFocusKeys = - { - KeyCode.Return, KeyCode.Backspace, KeyCode.UpArrow, - KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow - }; - - internal const string STARTUP_TEXT = @"Welcome to the UnityExplorer C# Console. - -The following helper methods are available: - -* Log(""message"") logs a message to the debug console - -* StartCoroutine(IEnumerator routine) start the IEnumerator as a UnityEngine.Coroutine - -* CurrentTarget() returns the currently inspected target on the Home page - -* AllTargets() returns an object[] array containing all inspected instances - -* Inspect(someObject) to inspect an instance, eg. Inspect(Camera.main); - -* Inspect(typeof(SomeClass)) to inspect a Class with static reflection - -* AddUsing(""SomeNamespace"") adds a using directive to the C# console - -* GetUsing() logs the current using directives to the debug console - -* Reset() resets all using directives and variables -"; - - public void InitConsole() - { - highlightLexer = new CSLexerHighlighter(); - - ConstructUI(); - - InputField.onValueChanged.AddListener((string s) => { OnInputChanged(s); }); - } - - public void UpdateConsole() - { - if (Time.time > s_timeOfLastInternalSet) - Writing = false; - - if (EnableCtrlRShortcut) - { - if ((InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl)) - && InputManager.GetKeyDown(KeyCode.R)) - { - var text = InputField.text.Trim(); - if (!string.IsNullOrEmpty(text)) - { - Evaluate(text); - return; - } - } - } - - if (EnableAutoIndent && InputManager.GetKeyDown(KeyCode.Return)) - AutoIndentCaret(); - - if (EnableAutocompletes && InputField.isFocused) - { - if (InputManager.GetMouseButton(0) || onFocusKeys.Any(it => InputManager.GetKeyDown(it))) - UpdateAutocompletes(); - } - - if (m_fixCaretPos > 0) - { - if (!m_fixwanted) - { - EventSystem.current.SetSelectedGameObject(InputField.gameObject, null); - m_fixwanted = true; - } - else - { - InputField.caretPosition = m_fixCaretPos; - InputField.selectionFocusPosition = m_fixCaretPos; - - m_fixwanted = false; - m_fixCaretPos = -1; - - var color = InputField.selectionColor; - color.a = m_lastSelectAlpha; - InputField.selectionColor = color; - } - } - else if (InputField.caretPosition > 0) - { - m_lastCaretPos = InputField.caretPosition; - } - } - - internal void UpdateAutocompletes() - { - AutoCompleter.CheckAutocomplete(); - AutoCompleter.SetSuggestions(AutoCompletes.ToArray()); - } - - public void UseAutocomplete(string suggestion) - { - Writing = true; - - string input = InputField.text; - input = input.Insert(m_lastCaretPos, suggestion); - InputField.text = input; - - m_fixCaretPos = m_lastCaretPos += suggestion.Length; - - var color = InputField.selectionColor; - m_lastSelectAlpha = color.a; - color.a = 0f; - InputField.selectionColor = color; - - AutoCompleter.ClearAutocompletes(); - } - - private static float s_timeOfLastUpdate; - private static bool Writing - { - get => s_writing; - set - { - if (value) - s_timeOfLastInternalSet = Time.time; - s_writing = value; - } - } - private static bool s_writing; - private static float s_timeOfLastInternalSet; - - public void OnInputChanged(string newText, bool forceUpdate = false) - { - if (!Writing && Time.time <= s_timeOfLastUpdate) - return; - - s_timeOfLastUpdate = Time.time; - - if (EnableAutoIndent) - UpdateIndent(newText); - - Writing = true; - - if (!forceUpdate && string.IsNullOrEmpty(newText)) - inputHighlightText.text = string.Empty; - else - inputHighlightText.text = SyntaxHighlightContent(newText); - - if (EnableAutocompletes) - UpdateAutocompletes(); - } - - private void UpdateIndent(string newText) - { - int caret = InputField.caretPosition; - - int len = newText.Length; - if (caret < 0 || caret >= len) - { - while (caret >= 0 && caret >= len) - caret--; - - if (caret < 0) - return; - } - - CurrentIndent = 0; - - bool stringState = false; - - for (int i = 0; i < caret && i < newText.Length; i++) - { - char character = newText[i]; - - if (character == '"') - stringState = !stringState; - else if (!stringState && character == CSLexerHighlighter.indentOpen) - CurrentIndent++; - else if (!stringState && character == CSLexerHighlighter.indentClose) - CurrentIndent--; - } - - if (CurrentIndent < 0) - CurrentIndent = 0; - } - - private const string CLOSE_COLOR_TAG = ""; - - private string SyntaxHighlightContent(string inputText) - { - int offset = 0; - - //Console.WriteLine("Highlighting input text:\r\n" + inputText); - - string ret = ""; - - foreach (var match in highlightLexer.GetMatches(inputText)) - { - for (int i = offset; i < match.startIndex; i++) - ret += inputText[i]; - - ret += $"{match.htmlColor}"; - - for (int i = match.startIndex; i < match.endIndex; i++) - ret += inputText[i]; - - ret += CLOSE_COLOR_TAG; - - offset = match.endIndex; - } - - for (int i = offset; i < inputText.Length; i++) - ret += inputText[i]; - - return ret; - } - - private void AutoIndentCaret() - { - Writing = true; - - if (CurrentIndent > 0) - { - string indent = GetAutoIndentTab(CurrentIndent); - - if (indent.Length > 0) - { - int caretPos = InputField.caretPosition; - - string indentMinusOne = indent.Substring(0, indent.Length - 1); - - // get last index of { - // chuck it on the next line if its not already - string text = InputField.text; - string sub = InputField.text.Substring(0, InputField.caretPosition); - int lastIndex = sub.LastIndexOf("{"); - int offset = lastIndex - 1; - if (offset >= 0 && text[offset] != '\n' && text[offset] != '\t') - { - string open = "\n" + indentMinusOne; - - InputField.text = text.Insert(offset + 1, open); - - caretPos += open.Length; - } - - // check if should add auto-close } - int numOpen = InputField.text.Where(x => x == CSLexerHighlighter.indentOpen).Count(); - int numClose = InputField.text.Where(x => x == CSLexerHighlighter.indentClose).Count(); - - if (numOpen > numClose) - { - // add auto-indent closing - indentMinusOne = $"\n{indentMinusOne}}}"; - InputField.text = InputField.text.Insert(caretPos, indentMinusOne); - } - - // insert the actual auto indent now - InputField.text = InputField.text.Insert(caretPos, indent); - - //InputField.stringPosition = caretPos + indent.Length; - InputField.caretPosition = caretPos + indent.Length; - } - } - - // Update line column and indent positions - UpdateIndent(InputField.text); - - InputText.text = InputField.text; - //inputText.SetText(InputField.text, true); - InputText.Rebuild(CanvasUpdate.Prelayout); - InputField.ForceLabelUpdate(); - InputField.Rebuild(CanvasUpdate.Prelayout); - - OnInputChanged(InputText.text, true); - } - - private string GetAutoIndentTab(int amount) - { - string tab = string.Empty; - - for (int i = 0; i < amount; i++) - { - tab += "\t"; - } - - return tab; - } - - // ========== UI CONSTRUCTION =========== // - - public void ConstructUI() - { - Content = UIFactory.CreateVerticalGroup(MainMenu.Instance.PageViewport, "CSharpConsole", true, true, true, true); - UIFactory.SetLayoutElement(Content, preferredHeight: 500, flexibleHeight: 9000); - - #region TOP BAR - - // Main group object - - var topBarObj = UIFactory.CreateHorizontalGroup(Content, "TopBar", true, true, true, true, 10, new Vector4(8, 8, 30, 30), - default, TextAnchor.LowerCenter); - UIFactory.SetLayoutElement(topBarObj, minHeight: 50, flexibleHeight: 0); - - // Top label - - var topBarLabel = UIFactory.CreateLabel(topBarObj, "TopLabel", "C# Console", TextAnchor.MiddleLeft, default, true, 25); - UIFactory.SetLayoutElement(topBarLabel.gameObject, preferredWidth: 150, flexibleWidth: 5000); - - // Enable Ctrl+R toggle - - var ctrlRToggleObj = UIFactory.CreateToggle(topBarObj, "CtrlRToggle", out Toggle ctrlRToggle, out Text ctrlRToggleText); - ctrlRToggle.onValueChanged.AddListener((bool val) => { EnableCtrlRShortcut = val; }); - - ctrlRToggleText.text = "Run on Ctrl+R"; - ctrlRToggleText.alignment = TextAnchor.UpperLeft; - UIFactory.SetLayoutElement(ctrlRToggleObj, minWidth: 140, flexibleWidth: 0, minHeight: 25); - - // Enable Suggestions toggle - - var suggestToggleObj = UIFactory.CreateToggle(topBarObj, "SuggestionToggle", out Toggle suggestToggle, out Text suggestToggleText); - suggestToggle.onValueChanged.AddListener((bool val) => - { - EnableAutocompletes = val; - AutoCompleter.Update(); - }); - - suggestToggleText.text = "Suggestions"; - suggestToggleText.alignment = TextAnchor.UpperLeft; - - UIFactory.SetLayoutElement(suggestToggleObj, minWidth: 120, flexibleWidth: 0, minHeight: 25); - - // Enable Auto-indent toggle - - var autoIndentToggleObj = UIFactory.CreateToggle(topBarObj, "IndentToggle", out Toggle autoIndentToggle, out Text autoIndentToggleText); - autoIndentToggle.onValueChanged.AddListener((bool val) => EnableAutoIndent = val); - - autoIndentToggleText.text = "Auto-indent on Enter"; - autoIndentToggleText.alignment = TextAnchor.UpperLeft; - - UIFactory.SetLayoutElement(autoIndentToggleObj, minWidth: 180, flexibleWidth: 0, minHeight: 25); - - #endregion - - #region CONSOLE INPUT - - int fontSize = 16; - - var inputObj = UIFactory.CreateSrollInputField(Content, "ConsoleInput", STARTUP_TEXT, out InputFieldScroller consoleScroll, fontSize); - - var inputField = consoleScroll.inputField; - - var mainTextObj = inputField.textComponent.gameObject; - var mainTextInput = inputField.textComponent; - mainTextInput.supportRichText = false; - mainTextInput.color = new Color(1, 1, 1, 0.5f); - - var placeHolderText = inputField.placeholder.GetComponent(); - placeHolderText.fontSize = fontSize; - - var highlightTextObj = UIFactory.CreateUIObject("HighlightText", mainTextObj.gameObject); - var highlightTextRect = highlightTextObj.GetComponent(); - highlightTextRect.pivot = new Vector2(0, 1); - highlightTextRect.anchorMin = Vector2.zero; - highlightTextRect.anchorMax = Vector2.one; - highlightTextRect.offsetMin = new Vector2(20, 0); - highlightTextRect.offsetMax = new Vector2(14, 0); - - var highlightTextInput = highlightTextObj.AddComponent(); - highlightTextInput.supportRichText = true; - highlightTextInput.fontSize = fontSize; - - #endregion - - #region COMPILE BUTTON BAR - - var horozGroupObj = UIFactory.CreateHorizontalGroup(Content, "BigButtons", true, true, true, true, 0, new Vector4(2,2,2,2), - new Color(1, 1, 1, 0)); - - var resetButton = UIFactory.CreateButton(horozGroupObj, "ResetButton", "Reset", () => ResetConsole(), "666666".ToColor()); - var resetBtnText = resetButton.GetComponentInChildren(); - resetBtnText.fontSize = 18; - UIFactory.SetLayoutElement(resetButton.gameObject, preferredWidth: 80, flexibleWidth: 0, minHeight: 45, flexibleHeight: 0); - - var compileButton = UIFactory.CreateButton(horozGroupObj, "CompileButton", "Compile", CompileCallback, - new Color(14f / 255f, 80f / 255f, 14f / 255f)); - var btnText = compileButton.GetComponentInChildren(); - btnText.fontSize = 18; - UIFactory.SetLayoutElement(compileButton.gameObject, preferredWidth: 80, flexibleWidth: 0, minHeight: 45, flexibleHeight: 0); - - void CompileCallback() - { - if (!string.IsNullOrEmpty(inputField.text)) - Evaluate(inputField.text.Trim()); - else - ExplorerCore.Log("Cannot evaluate empty input!"); - } - - #endregion - - //mainTextInput.supportRichText = false; - - mainTextInput.font = UIManager.ConsoleFont; - placeHolderText.font = UIManager.ConsoleFont; - highlightTextInput.font = UIManager.ConsoleFont; - - // reset this after formatting finalized - highlightTextRect.anchorMin = Vector2.zero; - highlightTextRect.anchorMax = Vector2.one; - highlightTextRect.offsetMin = Vector2.zero; - highlightTextRect.offsetMax = Vector2.zero; - - // assign references - - this.InputField = inputField; - - this.InputText = mainTextInput; - this.inputHighlightText = highlightTextInput; - } - - - - // ================================================================================================ - - private class VoidType - { - public static readonly VoidType Value = new VoidType(); - private VoidType() { } - } - } -} diff --git a/src/UI/Main/CSConsole/Lexer/CommentMatch.cs b/src/UI/Main/CSConsole/Lexer/CommentMatch.cs deleted file mode 100644 index 35fac37..0000000 --- a/src/UI/Main/CSConsole/Lexer/CommentMatch.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace UnityExplorer.UI.Main.CSConsole.Lexer -{ - public class CommentMatch : Matcher - { - public string lineCommentStart = @"//"; - public string blockCommentStart = @"/*"; - public string blockCommentEnd = @"*/"; - - public override Color HighlightColor => new Color(0.34f, 0.65f, 0.29f, 1.0f); - public override IEnumerable StartChars => new char[] { lineCommentStart[0], blockCommentStart[0] }; - public override IEnumerable EndChars => new char[] { blockCommentEnd[0] }; - public override bool IsImplicitMatch(CSLexerHighlighter lexer) => IsMatch(lexer, lineCommentStart) || IsMatch(lexer, blockCommentStart); - - private bool IsMatch(CSLexerHighlighter lexer, string commentType) - { - if (!string.IsNullOrEmpty(commentType)) - { - lexer.Rollback(); - - bool match = true; - for (int i = 0; i < commentType.Length; i++) - { - if (commentType[i] != lexer.ReadNext()) - { - match = false; - break; - } - } - - if (match) - { - // Read until end of line or file - while (!IsEndLineOrEndFile(lexer, lexer.ReadNext())) { } - - return true; - } - } - return false; - } - - private bool IsEndLineOrEndFile(CSLexerHighlighter lexer, char character) => lexer.EndOfStream || character == '\n' || character == '\r'; - } -} diff --git a/src/UI/Main/CSConsole/Lexer/KeywordMatch.cs b/src/UI/Main/CSConsole/Lexer/KeywordMatch.cs deleted file mode 100644 index 08b9c0f..0000000 --- a/src/UI/Main/CSConsole/Lexer/KeywordMatch.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace UnityExplorer.UI.Main.CSConsole.Lexer -{ - // I use two different KeywordMatch instances (valid and invalid). - // This class just contains common implementations. - public class KeywordMatch : Matcher - { - public string[] Keywords = new[] {"add", "as", "ascending", "await", "bool", "break", "by", "byte", -"case", "catch", "char", "checked", "const", "continue", "decimal", "default", "descending", "do", "dynamic", -"else", "equals", "false", "finally", "float", "for", "foreach", "from", "global", "goto", "group", -"if", "in", "int", "into", "is", "join", "let", "lock", "long", "new", "null", "object", "on", "orderby", "out", -"ref", "remove", "return", "sbyte", "select", "short", "sizeof", "stackalloc", "string", -"switch", "throw", "true", "try", "typeof", "uint", "ulong", "ushort", "var", "where", "while", "yield", -"abstract", "async", "base", "class", "delegate", "enum", "explicit", "extern", "fixed", "get", -"implicit", "interface", "internal", "namespace", "operator", "override", "params", "private", "protected", "public", -"using", "partial", "readonly", "sealed", "set", "static", "struct", "this", "unchecked", "unsafe", "value", "virtual", "volatile", "void" }; - - public override Color HighlightColor => highlightColor; - public Color highlightColor = new Color(0.33f, 0.61f, 0.83f, 1.0f); - - private readonly HashSet shortlist = new HashSet(); - private readonly Stack removeList = new Stack(); - - public override bool IsImplicitMatch(CSLexerHighlighter lexer) - { - if (!char.IsWhiteSpace(lexer.Previous) && - !lexer.IsSpecialSymbol(lexer.Previous, DelimiterType.End)) - { - return false; - } - - shortlist.Clear(); - - int currentIndex = 0; - char currentChar = lexer.ReadNext(); - - for (int i = 0; i < Keywords.Length; i++) - { - if (Keywords[i][0] == currentChar) - { - shortlist.Add(Keywords[i]); - } - } - - if (shortlist.Count == 0) - { - return false; - } - - do - { - if (lexer.EndOfStream) - { - RemoveLongStrings(currentIndex + 1); - break; - } - - currentChar = lexer.ReadNext(); - currentIndex++; - - if (char.IsWhiteSpace(currentChar) || - lexer.IsSpecialSymbol(currentChar, DelimiterType.Start)) - { - RemoveLongStrings(currentIndex); - lexer.Rollback(1); - break; - } - - foreach (string keyword in shortlist) - { - if (currentIndex >= keyword.Length || keyword[currentIndex] != currentChar) - { - removeList.Push(keyword); - } - } - - while (removeList.Count > 0) - { - shortlist.Remove(removeList.Pop()); - } - } - while (shortlist.Count > 0); - - return shortlist.Count > 0; - } - - private void RemoveLongStrings(int length) - { - foreach (string keyword in shortlist) - { - if (keyword.Length > length) - { - removeList.Push(keyword); - } - } - - while (removeList.Count > 0) - { - shortlist.Remove(removeList.Pop()); - } - } - } -} diff --git a/src/UI/Main/CSConsole/Lexer/Matcher.cs b/src/UI/Main/CSConsole/Lexer/Matcher.cs deleted file mode 100644 index 0b5b53f..0000000 --- a/src/UI/Main/CSConsole/Lexer/Matcher.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using System.Linq; -using UnityExplorer.Core.Unity; - -namespace UnityExplorer.UI.Main.CSConsole.Lexer -{ - public abstract class Matcher - { - public abstract Color HighlightColor { get; } - - public string HexColor => htmlColor ?? (htmlColor = ""); - private string htmlColor; - - public virtual IEnumerable StartChars => Enumerable.Empty(); - public virtual IEnumerable EndChars => Enumerable.Empty(); - - public abstract bool IsImplicitMatch(CSLexerHighlighter lexer); - - public bool IsMatch(CSLexerHighlighter lexer) - { - if (IsImplicitMatch(lexer)) - { - lexer.Commit(); - return true; - } - - lexer.Rollback(); - return false; - } - } -} diff --git a/src/UI/Main/CSConsole/Lexer/NumberMatch.cs b/src/UI/Main/CSConsole/Lexer/NumberMatch.cs deleted file mode 100644 index 75b352d..0000000 --- a/src/UI/Main/CSConsole/Lexer/NumberMatch.cs +++ /dev/null @@ -1,39 +0,0 @@ -using UnityEngine; - -namespace UnityExplorer.UI.Main.CSConsole.Lexer -{ - public class NumberMatch : Matcher - { - public override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f); - - public override bool IsImplicitMatch(CSLexerHighlighter lexer) - { - if (!char.IsWhiteSpace(lexer.Previous) && - !lexer.IsSpecialSymbol(lexer.Previous, DelimiterType.End)) - { - return false; - } - - bool matchedNumber = false; - - while (!lexer.EndOfStream) - { - if (IsNumberOrDecimalPoint(lexer.ReadNext())) - { - matchedNumber = true; - lexer.Commit(); - } - else - { - lexer.Rollback(); - break; - } - } - - return matchedNumber; - } - - private bool IsNumberOrDecimalPoint(char character) => char.IsNumber(character) || character == '.'; - } - -} diff --git a/src/UI/Main/CSConsole/Lexer/StringMatch.cs b/src/UI/Main/CSConsole/Lexer/StringMatch.cs deleted file mode 100644 index 8712ea3..0000000 --- a/src/UI/Main/CSConsole/Lexer/StringMatch.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace UnityExplorer.UI.Main.CSConsole.Lexer -{ - public class StringMatch : Matcher - { - public override Color HighlightColor => new Color(0.79f, 0.52f, 0.32f, 1.0f); - - public override IEnumerable StartChars => new[] { '"' }; - public override IEnumerable EndChars => new[] { '"' }; - - public override bool IsImplicitMatch(CSLexerHighlighter lexer) - { - if (lexer.ReadNext() == '"') - { - while (!IsClosingQuoteOrEndFile(lexer, lexer.ReadNext())) { } - - return true; - } - return false; - } - - private bool IsClosingQuoteOrEndFile(CSLexerHighlighter lexer, char character) => lexer.EndOfStream || character == '"'; - } -} diff --git a/src/UI/Main/CSConsole/Lexer/SymbolMatch.cs b/src/UI/Main/CSConsole/Lexer/SymbolMatch.cs deleted file mode 100644 index 9ba817b..0000000 --- a/src/UI/Main/CSConsole/Lexer/SymbolMatch.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -namespace UnityExplorer.UI.Main.CSConsole.Lexer -{ - public class SymbolMatch : Matcher - { - public override Color HighlightColor => new Color(0.58f, 0.47f, 0.37f, 1.0f); - - private readonly string[] symbols = new[] - { - "[", "]", "(", ")", ".", "?", ":", "+", "-", "*", "/", "%", "&", "|", "^", "~", "=", "<", ">", - "++", "--", "&&", "||", "<<", ">>", "==", "!=", "<=", ">=", "+=", "-=", "*=", "/=", "%=", "&=", - "|=", "^=", "<<=", ">>=", "->", "??", "=>", - }; - - private static readonly List shortlist = new List(); - private static readonly Stack removeList = new Stack(); - - public override IEnumerable StartChars => symbols.Select(s => s[0]); - public override IEnumerable EndChars => symbols.Select(s => s[0]); - - public override bool IsImplicitMatch(CSLexerHighlighter lexer) - { - if (lexer == null) - return false; - - if (!char.IsWhiteSpace(lexer.Previous) && - !char.IsLetter(lexer.Previous) && - !char.IsDigit(lexer.Previous) && - !lexer.IsSpecialSymbol(lexer.Previous, DelimiterType.End)) - { - return false; - } - - shortlist.Clear(); - - int currentIndex = 0; - char currentChar = lexer.ReadNext(); - - for (int i = symbols.Length - 1; i >= 0; i--) - { - if (symbols[i][0] == currentChar) - shortlist.Add(symbols[i]); - } - - if (shortlist.Count == 0) - return false; - - do - { - if (lexer.EndOfStream) - { - RemoveLongStrings(currentIndex + 1); - break; - } - - currentChar = lexer.ReadNext(); - currentIndex++; - - if (char.IsWhiteSpace(currentChar) || - char.IsLetter(currentChar) || - char.IsDigit(currentChar) || - lexer.IsSpecialSymbol(currentChar, DelimiterType.Start)) - { - RemoveLongStrings(currentIndex); - lexer.Rollback(1); - break; - } - - foreach (string symbol in shortlist) - { - if (currentIndex >= symbol.Length || symbol[currentIndex] != currentChar) - { - removeList.Push(symbol); - } - } - - while (removeList.Count > 0) - { - shortlist.Remove(removeList.Pop()); - } - } - while (shortlist.Count > 0); - - return shortlist.Count > 0; - } - - private void RemoveLongStrings(int length) - { - foreach (string keyword in shortlist) - { - if (keyword.Length > length) - { - removeList.Push(keyword); - } - } - - while (removeList.Count > 0) - { - shortlist.Remove(removeList.Pop()); - } - } - } -} diff --git a/src/UI/Main/DebugConsole.cs b/src/UI/Main/DebugConsole.cs deleted file mode 100644 index 3b0c92d..0000000 --- a/src/UI/Main/DebugConsole.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Config; -using System.IO; -using System.Linq; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.Main -{ - public class DebugConsole - { - public static DebugConsole Instance { get; private set; } - - public static bool LogUnity { get; set; } - //public static bool SaveToDisk { get; set; } = ModConfig.Instance.Save_Logs_To_Disk; - - internal static StreamWriter s_streamWriter; - - public static readonly List AllMessages = new List(); - public static readonly List MessageHolders = new List(); - - // logs that occured before the actual UI was ready. - // these ones include the hex color codes. - internal static readonly List s_preInitMessages = new List(); - - private InputField m_textInput; - internal const int MAX_TEXT_LEN = 10000; - - public DebugConsole(GameObject parent) - { - Instance = this; - LogUnity = ConfigManager.Log_Unity_Debug.Value; - - ConstructUI(parent); - - if (!ConfigManager.Last_DebugConsole_State.Value) - ToggleShow(); - - // append messages that logged before we were set up - string preAppend = ""; - for (int i = s_preInitMessages.Count - 1; i >= 0; i--) - { - var msg = s_preInitMessages[i]; - if (preAppend != "") - preAppend += "\r\n"; - preAppend += msg; - } - m_textInput.text = preAppend; - - // set up IO - - var path = Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Logs"); - - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - - // clean old log(s) - var files = Directory.GetFiles(path); - if (files.Length >= 10) - { - var sorted = files.ToList(); - // sort by 'datetime.ToString("u")' will put the oldest ones first - sorted.Sort(); - for (int i = 0; i < files.Length - 9; i++) - File.Delete(files[i]); - } - - var fileName = "UnityExplorer " + DateTime.Now.ToString("u") + ".txt"; - fileName = RemoveInvalidFilenameChars(fileName); - - var stream = File.Create(path + @"\" + fileName); - s_streamWriter = new StreamWriter(stream) - { - AutoFlush = true - }; - - foreach (var msg in AllMessages) - s_streamWriter.WriteLine(msg); - } - - public static bool Hiding; - - private GameObject m_logAreaObj; - private Text m_hideBtnText; - private LayoutElement m_mainLayout; - - public static Action OnToggleShow; - - public void ToggleShow() - { - if (m_logAreaObj.activeSelf) - { - Hiding = true; - m_logAreaObj.SetActive(false); - m_hideBtnText.text = "Show"; - m_mainLayout.minHeight = 30; - } - else - { - Hiding = false; - m_logAreaObj.SetActive(true); - m_hideBtnText.text = "Hide"; - m_mainLayout.minHeight = 190; - } - - OnToggleShow?.Invoke(!Hiding); - } - - public static string RemoveInvalidFilenameChars(string s) - { - var invalid = Path.GetInvalidFileNameChars(); - foreach (var c in invalid) - { - s = s.Replace(c.ToString(), ""); - } - return s; - } - - public static void Log(string message) - { - Log(message, null); - } - - public static void Log(string message, Color color) - { - Log(message, color.ToHex()); - } - - public static void Log(string message, string hexColor) - { - message = $"{AllMessages.Count}: {message}"; - - AllMessages.Add(message); - s_streamWriter?.WriteLine(message); - - if (hexColor != null) - message = $"{message}"; - - if (Instance?.m_textInput) - { - var input = Instance.m_textInput; - var wanted = $"{message}\n{input.text}"; - - if (wanted.Length > MAX_TEXT_LEN) - wanted = wanted.Substring(0, MAX_TEXT_LEN); - - input.text = wanted; - } - else - s_preInitMessages.Add(message); - } - - public void ConstructUI(GameObject parent) - { - var mainObj = UIFactory.CreateVerticalGroup(parent, "DebugConsole", true, true, true, true, 0, default, new Color(0.1f, 0.1f, 0.1f, 1.0f)); - var mainImage = mainObj.GetComponent(); - mainImage.maskable = true; - var mask = mainObj.AddComponent(); - mask.showMaskGraphic = true; - - m_mainLayout = mainObj.AddComponent(); - m_mainLayout.minHeight = 190; - m_mainLayout.flexibleHeight = 0; - - #region LOG AREA - m_logAreaObj = UIFactory.CreateHorizontalGroup(mainObj, "LogArea", true, true, true, true); - UIFactory.SetLayoutElement(m_logAreaObj, preferredHeight: 190, flexibleHeight: 0); - - var inputScrollerObj = UIFactory.CreateSrollInputField(m_logAreaObj, - "DebugConsoleOutput", - "", - out InputFieldScroller inputScroll, - 14, - new Color(0.05f, 0.05f, 0.05f)); - - inputScroll.inputField.textComponent.font = UIManager.ConsoleFont; - inputScroll.inputField.readOnly = true; - - m_textInput = inputScroll.inputField; - #endregion - - #region BOTTOM BAR - - var bottomBarObj = UIFactory.CreateHorizontalGroup(mainObj, "BottomBar", false, true, true, true, 10, new Vector4(2,2,10,10), - default, TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(bottomBarObj, minHeight: 30, flexibleHeight: 0); - - // Debug Console label - - var bottomLabel = UIFactory.CreateLabel(bottomBarObj, "DebugConsoleLabel", "Debug Console", TextAnchor.MiddleLeft); - bottomLabel.fontStyle = FontStyle.Bold; - UIFactory.SetLayoutElement(bottomLabel.gameObject, minWidth: 100, flexibleWidth: 0); - - // Hide button - - var hideButton = UIFactory.CreateButton(bottomBarObj, "HideButton", "Hide", ToggleShow); - UIFactory.SetLayoutElement(hideButton.gameObject, minWidth: 80, flexibleWidth: 0); - m_hideBtnText = hideButton.GetComponentInChildren(); - - // Clear button - - var clearButton = UIFactory.CreateButton(bottomBarObj, "ClearButton", "Clear", () => - { - m_textInput.text = ""; - AllMessages.Clear(); - }); - UIFactory.SetLayoutElement(clearButton.gameObject, minWidth: 80, flexibleWidth: 0); - - // Unity log toggle - - var unityToggleObj = UIFactory.CreateToggle(bottomBarObj, "UnityLogToggle", out Toggle unityToggle, out Text unityToggleText); - - unityToggle.onValueChanged.AddListener((bool val) => - { - LogUnity = val; - ConfigManager.Log_Unity_Debug.Value = val; - }); - - ConfigManager.Log_Unity_Debug.OnValueChanged += (bool val) => { unityToggle.isOn = val; }; - - unityToggle.isOn = LogUnity; - unityToggleText.text = "Log Unity Debug?"; - unityToggleText.alignment = TextAnchor.MiddleLeft; - - UIFactory.SetLayoutElement(unityToggleObj, minWidth: 170, flexibleWidth: 0); - - var unityToggleRect = unityToggleObj.transform.Find("Background").GetComponent(); - var pos = unityToggleRect.localPosition; - pos.y = -4; - unityToggleRect.localPosition = pos; - - #endregion - } - } -} diff --git a/src/UI/Main/Home/HomePage.cs b/src/UI/Main/Home/HomePage.cs deleted file mode 100644 index 91a0eeb..0000000 --- a/src/UI/Main/Home/HomePage.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.UI.Inspectors; - -namespace UnityExplorer.UI.Main.Home -{ - public class HomePage : BaseMenuPage - { - public override string Name => "Home"; - - public static HomePage Instance { get; internal set; } - - public override MenuPages Type => MenuPages.Home; - - public override bool Init() - { - Instance = this; - - ConstructMenu(); - - new SceneExplorer(); - - new InspectorManager(); - - SceneExplorer.Instance.Init(); - - return true; - } - - public override void Update() - { - SceneExplorer.Instance.Update(); - InspectorManager.Instance.Update(); - } - - private void ConstructMenu() - { - GameObject parent = MainMenu.Instance.PageViewport; - - Content = UIFactory.CreateHorizontalGroup(parent, "HomePage", true, true, true, true, 3, new Vector4(1,1,1,1)).gameObject; - } - } -} diff --git a/src/UI/Main/Home/SceneExplorer.cs b/src/UI/Main/Home/SceneExplorer.cs deleted file mode 100644 index ee6a5f6..0000000 --- a/src/UI/Main/Home/SceneExplorer.cs +++ /dev/null @@ -1,553 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityExplorer.UI; -using UnityExplorer.UI.Main; -using UnityEngine; -using UnityEngine.SceneManagement; -using UnityEngine.UI; -using UnityExplorer.Core.Runtime; -using UnityExplorer.UI.Main.Home; -using UnityExplorer.Core.Config; -using UnityExplorer.UI.Utility; -using UnityExplorer.UI.Main.Search; -using System.IO; -using UnityExplorer.Core; -using UnityExplorer.UI.Inspectors; - -namespace UnityExplorer.UI.Main.Home -{ - public class SceneExplorer - { - public static SceneExplorer Instance; - - internal static Action OnToggleShow; - - public SceneExplorer() - { - Instance = this; - - ConstructScenePane(); - } - - internal bool Hiding; - - private const float UPDATE_INTERVAL = 1f; - private float m_timeOfLastSceneUpdate; - - // private int m_currentSceneHandle = -1; - public static Scene DontDestroyScene => DontDestroyObject.scene; - internal Scene m_currentScene; - internal Scene[] m_currentScenes = new Scene[0]; - - internal GameObject[] m_allObjects = new GameObject[0]; - - internal GameObject m_selectedSceneObject; - internal int m_lastCount; - - private Dropdown m_sceneDropdown; - private Text m_sceneDropdownText; - private Text m_scenePathText; - private GameObject m_mainInspectBtn; - private GameObject m_backButtonObj; - - public PageHandler m_pageHandler; - private GameObject m_pageContent; - private readonly List m_shortListTexts = new List(); - private readonly List m_shortListToggles = new List(); - - internal readonly List m_shortList = new List(); - - private Text m_hideText; - private GameObject m_sceneDropdownObj; - private GameObject m_scenePathGroupObj; - private GameObject m_mainContent; - - internal static GameObject DontDestroyObject - { - get - { - if (!s_dontDestroyObject) - { - s_dontDestroyObject = new GameObject("DontDestroyMe"); - GameObject.DontDestroyOnLoad(s_dontDestroyObject); - } - return s_dontDestroyObject; - } - } - internal static GameObject s_dontDestroyObject; - - public void Init() - { - RefreshSceneSelector(); - - if (!ConfigManager.Last_SceneExplorer_State.Value) - ToggleShow(); - } - - public void ToggleShow() - { - if (!Hiding) - { - Hiding = true; - - m_hideText.text = "►"; - m_mainContent.SetActive(false); - m_pageHandler.Hide(); - } - else - { - Hiding = false; - - m_hideText.text = "◄"; - m_mainContent.SetActive(true); - m_pageHandler.Show(); - - Update(); - } - - InvokeOnToggleShow(); - } - - public void Update() - { - if (Hiding || Time.realtimeSinceStartup - m_timeOfLastSceneUpdate < UPDATE_INTERVAL) - return; - - RefreshSceneSelector(); - - if (!m_selectedSceneObject) - { - if (m_currentScene != default) - { - var rootObjects = RuntimeProvider.Instance.GetRootGameObjects(m_currentScene); - SetSceneObjectList(rootObjects); - } - } - else - { - RefreshSelectedSceneObject(); - } - } - - private void RefreshSceneSelector() - { - var newNames = new List(); - var newScenes = new List(); - - if (m_currentScenes == null) - m_currentScenes = new Scene[0]; - - bool anyChange = SceneManager.sceneCount != m_currentScenes.Length - 1; - - for (int i = 0; i < SceneManager.sceneCount; i++) - { - Scene scene = SceneManager.GetSceneAt(i); - - if (scene == default) - continue; - - int handle = RuntimeProvider.Instance.GetSceneHandle(scene); - - if (!anyChange && !m_currentScenes.Any(it => handle == RuntimeProvider.Instance.GetSceneHandle(it))) - anyChange = true; - - newScenes.Add(scene); - newNames.Add(scene.name); - } - - if (anyChange) - { - newNames.Add("DontDestroyOnLoad"); - newScenes.Add(DontDestroyScene); - m_currentScenes = newScenes.ToArray(); - - OnActiveScenesChanged(newNames); - - SetTargetScene(newScenes[0]); - - SearchPage.Instance.OnSceneChange(); - } - } - - public void SetTargetScene(int index) - => SetTargetScene(m_currentScenes[index]); - - public void SetTargetScene(Scene scene) - { - if (scene == default) - return; - - m_currentScene = scene; - var rootObjs = RuntimeProvider.Instance.GetRootGameObjects(scene); - SetSceneObjectList(rootObjs); - - m_selectedSceneObject = null; - - OnSceneSelected(); - } - - public void SetSceneObjectParent() - { - if (!m_selectedSceneObject || !m_selectedSceneObject.transform.parent?.gameObject) - { - m_selectedSceneObject = null; - SetTargetScene(m_currentScene); - } - else - { - SetTargetObject(m_selectedSceneObject.transform.parent.gameObject); - } - } - - public void SetTargetObject(GameObject obj) - { - if (!obj) - return; - - OnGameObjectSelected(obj); - - m_selectedSceneObject = obj; - - RefreshSelectedSceneObject(); - } - - private void RefreshSelectedSceneObject() - { - GameObject[] list = new GameObject[m_selectedSceneObject.transform.childCount]; - for (int i = 0; i < m_selectedSceneObject.transform.childCount; i++) - { - list[i] = m_selectedSceneObject.transform.GetChild(i).gameObject; - } - - SetSceneObjectList(list); - } - - private void SetSceneObjectList(GameObject[] objects) - { - m_allObjects = objects; - RefreshSceneObjectList(); - } - - internal void RefreshSceneObjectList() - { - m_timeOfLastSceneUpdate = Time.realtimeSinceStartup; - - RefreshSceneObjectList(m_allObjects, out int newCount); - - m_lastCount = newCount; - } - - internal static void InspectSelectedGameObject() - { - InspectorManager.Instance.Inspect(Instance.m_selectedSceneObject); - } - - internal static void InvokeOnToggleShow() - { - OnToggleShow?.Invoke(!Instance.Hiding); - } - - public void OnActiveScenesChanged(List newNames) - { - m_sceneDropdown.options.Clear(); - - foreach (string scene in newNames) - { - m_sceneDropdown.options.Add(new Dropdown.OptionData { text = scene }); - } - - m_sceneDropdown.OnCancel(null); - m_sceneDropdownText.text = newNames[0]; - } - - private void SceneListObjectClicked(int index) - { - if (index >= m_shortList.Count || !m_shortList[index]) - { - return; - } - - var obj = m_shortList[index]; - if (obj.transform.childCount > 0) - SetTargetObject(obj); - else - InspectorManager.Instance.Inspect(obj); - } - - internal void RefreshSceneObjectList(GameObject[] allObjects, out int newCount) - { - var objects = allObjects; - m_pageHandler.ListCount = objects.Length; - - //int startIndex = m_sceneListPageHandler.StartIndex; - - newCount = 0; - - foreach (var itemIndex in m_pageHandler) - { - newCount++; - - // normalized index starting from 0 - var i = itemIndex - m_pageHandler.StartIndex; - - if (itemIndex >= objects.Length) - { - if (i > SceneExplorer.Instance.m_lastCount || i >= m_shortListTexts.Count) - break; - - GameObject label = m_shortListTexts[i].transform.parent.parent.gameObject; - if (label.activeSelf) - label.SetActive(false); - } - else - { - GameObject obj = objects[itemIndex]; - - if (!obj) - continue; - - if (i >= m_shortList.Count) - { - m_shortList.Add(obj); - AddObjectListButton(); - } - else - { - m_shortList[i] = obj; - } - - var text = m_shortListTexts[i]; - - var name = obj.name; - - if (obj.transform.childCount > 0) - name = $"[{obj.transform.childCount}] {name}"; - - text.text = name; - text.color = obj.activeSelf ? Color.green : Color.red; - - var tog = m_shortListToggles[i]; - tog.isOn = obj.activeSelf; - - var label = text.transform.parent.parent.gameObject; - if (!label.activeSelf) - { - label.SetActive(true); - } - } - } - } - - private void OnSceneListPageTurn() - { - RefreshSceneObjectList(); - } - - private void OnToggleClicked(int index, bool val) - { - if (index >= m_shortList.Count || !m_shortList[index]) - return; - - var obj = m_shortList[index]; - obj.SetActive(val); - } - - internal void OnGameObjectSelected(GameObject obj) - { - m_scenePathText.text = obj.name; - if (!m_backButtonObj.activeSelf) - { - m_backButtonObj.SetActive(true); - m_mainInspectBtn.SetActive(true); - } - } - - internal void OnSceneSelected() - { - if (m_backButtonObj.activeSelf) - { - m_backButtonObj.SetActive(false); - m_mainInspectBtn.SetActive(false); - } - - m_scenePathText.text = "Scene root:"; - //m_scenePathText.ForceMeshUpdate(); - } - - #region UI CONSTRUCTION - - public void ConstructScenePane() - { - var coreGroup = UIFactory.CreateHorizontalGroup(HomePage.Instance.Content, "SceneExplorer", true, true, true, true, 4, new Vector4(2, 2, 2, 2), - new Color(0.05f, 0.05f, 0.05f)); - - // hide button - - var hideButton = UIFactory.CreateButton(coreGroup, "HideButton", "◄", ToggleShow, new Color(0.15f, 0.15f, 0.15f)); - hideButton.GetComponentInChildren().fontSize = 13; - m_hideText = hideButton.GetComponentInChildren(); - UIFactory.SetLayoutElement(hideButton.gameObject, minWidth: 20, minHeight: 20, flexibleWidth: 0, flexibleHeight: 9999); - - m_mainContent = UIFactory.CreateVerticalGroup(coreGroup, "SceneGroup", true, false, true, true, 0, default, - new Color(72f / 255f, 72f / 255f, 72f / 255f)); - UIFactory.SetLayoutElement(m_mainContent, minWidth: 350, flexibleWidth: 0); - - UIFactory.SetLayoutGroup(m_mainContent, true, true, true, true, spacing: 4, padTop: 8, 4, 4, 4); - - var titleObj = UIFactory.CreateLabel(m_mainContent, "SceneExplorerTitle", "Scene Explorer", TextAnchor.UpperLeft, default, true, 20).gameObject; - UIFactory.SetLayoutElement(titleObj, minHeight: 30, flexibleHeight: 0); - - m_sceneDropdownObj = UIFactory.CreateDropdown(m_mainContent, out m_sceneDropdown, "", 14, null); - UIFactory.SetLayoutElement(m_sceneDropdownObj, minHeight: 40, minWidth: 320, flexibleWidth: 0, flexibleHeight: 0); - - m_sceneDropdownText = m_sceneDropdown.transform.Find("Label").GetComponent(); - m_sceneDropdown.onValueChanged.AddListener((int val) => { SetTargetScene(val); }); - - m_scenePathGroupObj = UIFactory.CreateHorizontalGroup(m_mainContent, "ScenePathGroup", true, true, true, true, 5, default, new Color(1, 1, 1, 0f)); - UIFactory.SetLayoutElement(m_scenePathGroupObj, minHeight: 20, minWidth: 335); - - var backBtnObj = UIFactory.CreateButton(m_scenePathGroupObj, - "BackButton", - "◄", - () => { SetSceneObjectParent(); }, - new Color(0.12f, 0.12f, 0.12f)); - - m_backButtonObj = backBtnObj.gameObject; - - UIFactory.SetLayoutElement(m_backButtonObj, minWidth: 40, flexibleWidth: 0); - - GameObject scenePathLabel = UIFactory.CreateHorizontalGroup(m_scenePathGroupObj, "ScenePathLabel", false, false, false, false); - Image image = scenePathLabel.GetComponent(); - image.color = Color.white; - scenePathLabel.AddComponent().showMaskGraphic = false; - - UIFactory.SetLayoutElement(scenePathLabel, minWidth: 210, minHeight: 20, flexibleWidth: 120); - - m_scenePathText = UIFactory.CreateLabel(scenePathLabel, "ScenePathLabelText", "Scene root:", TextAnchor.MiddleLeft, default, true, 15); - m_scenePathText.horizontalOverflow = HorizontalWrapMode.Overflow; - - UIFactory.SetLayoutElement(m_scenePathText.gameObject, minWidth: 210, flexibleWidth: 120, minHeight: 20); - - var mainInspectButton = UIFactory.CreateButton(m_scenePathGroupObj, - "MainInspectButton", - "Inspect", - () => { InspectSelectedGameObject(); }, - new Color(0.12f, 0.12f, 0.12f)); - - m_mainInspectBtn = mainInspectButton.gameObject; - UIFactory.SetLayoutElement(m_mainInspectBtn, minWidth: 65); - - var scrollObj = UIFactory.CreateScrollView(m_mainContent, "SceneExplorerScrollView", - out m_pageContent, out SliderScrollbar scroller, new Color(0.1f, 0.1f, 0.1f)); - - m_pageHandler = new PageHandler(scroller); - m_pageHandler.ConstructUI(m_mainContent); - m_pageHandler.OnPageChanged += OnSceneListPageTurn; - - // Scene Loader - try - { - Type sceneUtil = ReflectionUtility.GetTypeByName("UnityEngine.SceneManagement.SceneUtility"); - if (sceneUtil == null) - throw new Exception("This version of Unity does not ship with the 'SceneUtility' class, or it was not unstripped."); - var method = sceneUtil.GetMethod("GetScenePathByBuildIndex", ReflectionUtility.AllFlags); - - var title2 = UIFactory.CreateLabel(m_mainContent, "SceneLoaderLabel", "Scene Loader", TextAnchor.MiddleLeft, Color.white, true, 20); - UIFactory.SetLayoutElement(title2.gameObject, minHeight: 30, flexibleHeight: 0); - - var allSceneDropObj = UIFactory.CreateDropdown(m_mainContent, out Dropdown allSceneDrop, "", 14, null); - UIFactory.SetLayoutElement(allSceneDropObj, minHeight: 40, minWidth: 320, flexibleWidth: 0, flexibleHeight: 0); - - int sceneCount = SceneManager.sceneCountInBuildSettings; - for (int i = 0; i < sceneCount; i++) - { - var scenePath = (string)method.Invoke(null, new object[] { i }); - allSceneDrop.options.Add(new Dropdown.OptionData(Path.GetFileNameWithoutExtension(scenePath))); - } - allSceneDrop.value = 1; - allSceneDrop.value = 0; - - var buttonRow = UIFactory.CreateHorizontalGroup(m_mainContent, "LoadButtons", true, true, true, true, 4); - - var loadButton = UIFactory.CreateButton(buttonRow, "LoadSceneButton", "Load (Single)", () => - { - try - { - SceneManager.LoadScene(allSceneDrop.options[allSceneDrop.value].text); - } - catch (Exception ex) - { - ExplorerCore.LogWarning($"Unable to load the Scene! {ex.ReflectionExToString()}"); - } - }, new Color(0.1f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(loadButton.gameObject, minHeight: 40, minWidth: 150); - - var loadAdditiveButton = UIFactory.CreateButton(buttonRow, "LoadSceneButton", "Load (Additive)", () => - { - try - { - SceneManager.LoadScene(allSceneDrop.options[allSceneDrop.value].text, LoadSceneMode.Additive); - } - catch (Exception ex) - { - ExplorerCore.LogWarning($"Unable to load the Scene! {ex.ReflectionExToString()}"); - } - }, new Color(0.1f, 0.3f, 0.3f)); - UIFactory.SetLayoutElement(loadAdditiveButton.gameObject, minHeight: 40, minWidth: 150); - } - catch (Exception ex) - { - ExplorerCore.LogWarning($"Could not create the Scene Loader helper! {ex.ReflectionExToString()}"); - } - } - - private void AddObjectListButton() - { - int thisIndex = m_shortListTexts.Count(); - - GameObject btnGroupObj = UIFactory.CreateHorizontalGroup(m_pageContent, "SceneObjectButton", true, false, true, true, - 0, default, new Color(0.1f, 0.1f, 0.1f)); - - UIFactory.SetLayoutElement(btnGroupObj, flexibleWidth: 320, minHeight: 25); - - btnGroupObj.AddComponent(); - - var toggleObj = UIFactory.CreateToggle(btnGroupObj, "ObjectToggleButton", out Toggle toggle, out Text toggleText, new Color(0.1f, 0.1f, 0.1f)); - var toggleLayout = toggleObj.AddComponent(); - toggleLayout.minHeight = 25; - toggleLayout.minWidth = 25; - toggleText.text = ""; - toggle.isOn = false; - m_shortListToggles.Add(toggle); - toggle.onValueChanged.AddListener((bool val) => { OnToggleClicked(thisIndex, val); }); - - var mainButton = UIFactory.CreateButton(btnGroupObj, - "MainButton", - "", - () => { SceneListObjectClicked(thisIndex); }); - - RuntimeProvider.Instance.SetColorBlock(mainButton, new Color(0.1f, 0.1f, 0.1f), - new Color(0.2f, 0.2f, 0.2f), new Color(0.05f, 0.05f, 0.05f)); - - UIFactory.SetLayoutElement(mainButton.gameObject, minHeight: 25, minWidth: 230); - - Text mainText = mainButton.GetComponentInChildren(); - mainText.alignment = TextAnchor.MiddleLeft; - mainText.horizontalOverflow = HorizontalWrapMode.Overflow; - m_shortListTexts.Add(mainText); - - var inspectButton = UIFactory.CreateButton(btnGroupObj, - "InspectButton", - "Inspect", - () => { InspectorManager.Instance.Inspect(m_shortList[thisIndex]); }); - - RuntimeProvider.Instance.SetColorBlock(inspectButton, new Color(0.15f, 0.15f, 0.15f), - new Color(0.2f, 0.2f, 0.2f), new Color(0.1f, 0.1f, 0.1f)); - - UIFactory.SetLayoutElement(inspectButton.gameObject, minWidth: 60, minHeight: 25); - } - - #endregion - } -} diff --git a/src/UI/Main/MainMenu.cs b/src/UI/Main/MainMenu.cs deleted file mode 100644 index cac610b..0000000 --- a/src/UI/Main/MainMenu.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityExplorer.Core.CSharp; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Config; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI.Main; -using UnityExplorer.UI.Main.Home; -using UnityExplorer.UI.Main.Search; -using UnityExplorer.UI.Main.CSConsole; -using UnityExplorer.UI.Main.Options; -using UnityExplorer.Core.Runtime; - -namespace UnityExplorer.UI.Main -{ - public class MainMenu - { - public static MainMenu Instance { get; set; } - - public PanelDragger Dragger { get; private set; } - - public GameObject MainPanel { get; private set; } - public GameObject PageViewport { get; private set; } - - public readonly List Pages = new List(); - private BaseMenuPage m_activePage; - - public static Action OnActiveTabChanged; - - // Navbar buttons - private Button m_lastNavButtonPressed; - private readonly Color m_navButtonNormal = new Color(0.3f, 0.3f, 0.3f, 1); - private readonly Color m_navButtonHighlight = new Color(0.3f, 0.6f, 0.3f); - private readonly Color m_navButtonSelected = new Color(0.2f, 0.5f, 0.2f, 1); - - internal Vector3 initPos; - internal bool pageLayoutInit; - internal int layoutInitIndex; - - public static void Create() - { - if (Instance != null) - { - ExplorerCore.LogWarning("An instance of MainMenu already exists, cannot create another!"); - return; - } - - Instance = new MainMenu(); - Instance.Init(); - } - - private void Init() - { - Pages.Add(new HomePage()); - Pages.Add(new SearchPage()); - Pages.Add(new CSharpConsole()); - Pages.Add(new OptionsPage()); - - ConstructMenu(); - - for (int i = 0; i < Pages.Count; i++) - { - var page = Pages[i]; - - if (!page.Init()) - { - page.WasDisabled = true; - // page init failed. - Pages.RemoveAt(i); - i--; - - if (page.RefNavbarButton) - page.RefNavbarButton.interactable = false; - - if (page.Content) - GameObject.Destroy(page.Content); - } - } - - // hide menu until each page has init layout (bit of a hack) - initPos = MainPanel.transform.position; - MainPanel.transform.position = new Vector3(9999, 9999); - } - - public void Update() - { - if (!pageLayoutInit) - { - if (layoutInitIndex < Pages.Count) - { - SetPage(Pages[layoutInitIndex]); - layoutInitIndex++; - } - else - { - pageLayoutInit = true; - MainPanel.transform.position = initPos; - - SetPage(ConfigManager.Default_Tab.Value); - } - return; - } - - m_activePage?.Update(); - } - - public void SetPage(MenuPages page) - { - var pageObj = Pages.Find(it => it.Type == page); - if (pageObj == null || pageObj.WasDisabled) - return; - SetPage(pageObj); - } - - public void SetPage(BaseMenuPage page) - { - if (page == null || m_activePage == page || page.WasDisabled) - return; - - m_activePage?.Content?.SetActive(false); - - // unique case for console page, at the moment this will just go here - if (m_activePage is CSharpConsole) - AutoCompleter.m_mainObj?.SetActive(false); - - m_activePage = page; - - m_activePage.Content?.SetActive(true); - - Button button = page.RefNavbarButton; - SetButtonActiveColors(button); - - if (m_lastNavButtonPressed && m_lastNavButtonPressed != button) - SetButtonInactiveColors(m_lastNavButtonPressed); - - m_lastNavButtonPressed = button; - - OnActiveTabChanged?.Invoke(m_activePage.Type); - } - - internal void SetButtonActiveColors(Button button) - { - RuntimeProvider.Instance.SetColorBlock(button, m_navButtonSelected); - } - - internal void SetButtonInactiveColors(Button button) - { - RuntimeProvider.Instance.SetColorBlock(button, m_navButtonNormal); - } - - #region UI Construction - - private void ConstructMenu() - { - MainPanel = UIFactory.CreatePanel("MainMenu", out GameObject content, - ConfigManager.Last_Window_Anchors.Value, ConfigManager.Last_Window_Position.Value); - - ConstructTitleBar(content); - - ConstructNavbar(content); - - PageViewport = UIFactory.CreateHorizontalGroup(content, "MainViewPort", true, true, true, true); - - new DebugConsole(content); - } - - private void ConstructTitleBar(GameObject content) - { - // Core title bar holder - - GameObject titleBar = UIFactory.CreateHorizontalGroup(content, "MainTitleBar", true, true, true, true, 0, new Vector4(3,3,15,3)); - UIFactory.SetLayoutElement(titleBar, minWidth: 25, flexibleHeight: 0); - - // Main title label - - var text = UIFactory.CreateLabel(titleBar, "TitleLabel", $"UnityExplorer v{ExplorerCore.VERSION}", TextAnchor.MiddleLeft, - default, true, 15); - UIFactory.SetLayoutElement(text.gameObject, flexibleWidth: 5000); - - // Add PanelDragger using the label object - - Dragger = new PanelDragger(titleBar.GetComponent(), MainPanel.GetComponent()); - - // Hide button - - var hideButton = UIFactory.CreateButton(titleBar, - "HideButton", - $"Hide ({ConfigManager.Main_Menu_Toggle.Value})", - () => { UIManager.ShowMenu = false; }); - - RuntimeProvider.Instance.SetColorBlock(hideButton, new Color(65f / 255f, 23f / 255f, 23f / 255f), - new Color(35f / 255f, 10f / 255f, 10f / 255f), new Color(156f / 255f, 0f, 0f)); - - UIFactory.SetLayoutElement(hideButton.gameObject, minWidth: 90, flexibleWidth: 0); - - Text hideText = hideButton.GetComponentInChildren(); - hideText.color = Color.white; - hideText.resizeTextForBestFit = true; - hideText.resizeTextMinSize = 8; - hideText.resizeTextMaxSize = 14; - - ConfigManager.Main_Menu_Toggle.OnValueChanged += (KeyCode val) => - { - hideText.text = $"Hide ({val})"; - }; - } - - private void ConstructNavbar(GameObject content) - { - GameObject navbarObj = UIFactory.CreateHorizontalGroup(content, "MainNavBar", true, true, true, true, 5); - UIFactory.SetLayoutElement(navbarObj, minHeight: 25, flexibleHeight: 0); - - foreach (var page in Pages) - { - Button btn = UIFactory.CreateButton(navbarObj, - $"Button_{page.Name}", - page.Name, - () => { SetPage(page); }); - - RuntimeProvider.Instance.SetColorBlock(btn, m_navButtonNormal, m_navButtonHighlight, m_navButtonSelected); - - page.RefNavbarButton = btn; - } - } - - #endregion - } -} diff --git a/src/UI/Main/Options/OptionsPage.cs b/src/UI/Main/Options/OptionsPage.cs deleted file mode 100644 index 973685b..0000000 --- a/src/UI/Main/Options/OptionsPage.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Config; -using UnityExplorer.UI.CacheObject; -using UnityExplorer.UI.Utility; - -namespace UnityExplorer.UI.Main.Options -{ - public class OptionsPage : BaseMenuPage - { - public override string Name => "Options"; - public override MenuPages Type => MenuPages.Options; - - internal static readonly List _cachedConfigEntries = new List(); - - public override bool Init() - { - ConstructUI(); - - return true; - } - - public override void Update() - { - // Not needed - } - - #region UI CONSTRUCTION - - internal GameObject m_contentObj; - - internal void ConstructUI() - { - GameObject parent = MainMenu.Instance.PageViewport; - - Content = UIFactory.CreateVerticalGroup(parent, "OptionsPage", false, true, true, true, 5, new Vector4(4,4,4,4), - new Color(0.15f, 0.15f, 0.15f)); - UIFactory.SetLayoutElement(Content, minHeight: 340, flexibleHeight: 9999); - - // ~~~~~ Title ~~~~~ - - var titleLabel = UIFactory.CreateLabel(Content, "Title", "Options", TextAnchor.UpperLeft, default, true, 25); - UIFactory.SetLayoutElement(titleLabel.gameObject, minHeight: 30, flexibleHeight: 0, flexibleWidth: 9999); - - // Save button - - var btn = UIFactory.CreateButton(Content, - "SaveButton", - "Save Config File", - () => { ConfigManager.Handler.SaveConfig(); }, - new Color(0.25f, 0.6f, 0.25f)); - UIFactory.SetLayoutElement(btn.gameObject, minWidth: 200, flexibleWidth: 0, minHeight: 30, flexibleHeight: 0); - - // ~~~~~ Actual options ~~~~~ - - UIFactory.CreateScrollView(Content, "ConfigList", out m_contentObj, out _, new Color(0.05f, 0.05f, 0.05f)); - UIFactory.SetLayoutGroup(m_contentObj, forceHeight: true, spacing: 3, padLeft: 3, padRight: 3); - - _cachedConfigEntries.AddRange(ConfigManager.ConfigElements.Values - .Where(it => !it.IsInternal) - .Select(it => new CacheConfigEntry(it, m_contentObj))); - - foreach (var entry in _cachedConfigEntries) - entry.Enable(); - } - - - #endregion - } -} diff --git a/src/UI/Main/PanelDragger.cs b/src/UI/Main/PanelDragger.cs deleted file mode 100644 index b401f60..0000000 --- a/src/UI/Main/PanelDragger.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Input; -using System.IO; -using System.Diagnostics; -using UnityExplorer.UI.Main.Home; - -namespace UnityExplorer.UI.Main -{ - // Handles dragging and resizing for the main explorer window. - - public class PanelDragger - { - public static PanelDragger Instance { get; private set; } - - public RectTransform Panel { get; set; } - - public static event Action OnFinishResize; - public static event Action OnFinishDrag; - - public PanelDragger(RectTransform dragArea, RectTransform panelToDrag) - { - Instance = this; - DragableArea = dragArea; - Panel = panelToDrag; - - UpdateResizeCache(); - - SceneExplorer.OnToggleShow += OnEndResize; - } - - public void Update() - { - Vector3 rawMousePos = InputManager.MousePosition; - - ResizeTypes type; - Vector3 resizePos = Panel.InverseTransformPoint(rawMousePos); - - Vector3 dragPos = DragableArea.InverseTransformPoint(rawMousePos); - bool inDragPos = DragableArea.rect.Contains(dragPos); - - if (WasHoveringResize && s_resizeCursorObj) - { - UpdateHoverImagePos(); - } - - // If Mouse pressed this frame - if (InputManager.GetMouseButtonDown(0)) - { - if (inDragPos) - { - OnBeginDrag(); - return; - } - else if (MouseInResizeArea(resizePos)) - { - type = GetResizeType(resizePos); - if (type != ResizeTypes.NONE) - { - OnBeginResize(type); - } - } - } - // If mouse still pressed from last frame - else if (InputManager.GetMouseButton(0)) - { - if (WasDragging) - { - OnDrag(); - } - else if (WasResizing) - { - OnResize(); - } - } - // If mouse not pressed - else - { - if (WasDragging) - { - OnEndDrag(); - } - else if (WasResizing) - { - OnEndResize(); - } - else if (!inDragPos && MouseInResizeArea(resizePos) && (type = GetResizeType(resizePos)) != ResizeTypes.NONE) - { - OnHoverResize(type); - } - else if (WasHoveringResize) - { - OnHoverResizeEnd(); - } - } - - return; - } - - #region DRAGGING - - public RectTransform DragableArea { get; set; } - public bool WasDragging { get; set; } - private Vector3 m_lastDragPosition; - - public void OnBeginDrag() - { - WasDragging = true; - m_lastDragPosition = InputManager.MousePosition; - } - - public void OnDrag() - { - Vector3 diff = InputManager.MousePosition - m_lastDragPosition; - m_lastDragPosition = InputManager.MousePosition; - - // update position while preserving the z value - Vector3 pos = Panel.localPosition; - float z = pos.z; - pos += diff; - pos.z = z; - Panel.localPosition = pos; - } - - public void OnEndDrag() - { - WasDragging = false; - - OnFinishDrag?.Invoke(Panel); - } - - #endregion - - #region RESIZE - - private const int RESIZE_THICKNESS = 15; - - internal readonly Vector2 minResize = new Vector2(400, 400); - - private bool WasResizing { get; set; } - private ResizeTypes m_currentResizeType = ResizeTypes.NONE; - private Vector2 m_lastResizePos; - - private bool WasHoveringResize { get; set; } - private ResizeTypes m_lastResizeHoverType; - public static GameObject s_resizeCursorObj; - - private Rect m_resizeRect; - - private readonly Dictionary m_resizeMask = new Dictionary - { - { ResizeTypes.Top, default }, - { ResizeTypes.Left, default }, - { ResizeTypes.Right, default }, - { ResizeTypes.Bottom, default }, - }; - - [Flags] - public enum ResizeTypes - { - NONE = 0, - Top = 1, - Left = 2, - Right = 4, - Bottom = 8, - TopLeft = Top | Left, - TopRight = Top | Right, - BottomLeft = Bottom | Left, - BottomRight = Bottom | Right, - } - - private const int HALF_THICKESS = RESIZE_THICKNESS / 2; - - private void UpdateResizeCache() - { - m_resizeRect = new Rect(Panel.rect.x - HALF_THICKESS, - Panel.rect.y - HALF_THICKESS, - Panel.rect.width + RESIZE_THICKNESS, - Panel.rect.height + RESIZE_THICKNESS); - - // calculate the four cross sections to use as flags - - m_resizeMask[ResizeTypes.Bottom] = new Rect(m_resizeRect.x, m_resizeRect.y, m_resizeRect.width, RESIZE_THICKNESS); - - m_resizeMask[ResizeTypes.Left] = new Rect(m_resizeRect.x, m_resizeRect.y, RESIZE_THICKNESS, m_resizeRect.height); - - m_resizeMask[ResizeTypes.Top] = new Rect(m_resizeRect.x, m_resizeRect.y + Panel.rect.height, m_resizeRect.width, RESIZE_THICKNESS); - - m_resizeMask[ResizeTypes.Right] = new Rect(m_resizeRect.x + Panel.rect.width, m_resizeRect.y, RESIZE_THICKNESS, m_resizeRect.height); - } - - private bool MouseInResizeArea(Vector2 mousePos) - { - return m_resizeRect.Contains(mousePos); - } - - private ResizeTypes GetResizeType(Vector2 mousePos) - { - // Calculate which part of the resize area we're in, if any. - // More readable method commented out below. - - int mask = 0; - mask |= (int)ResizeTypes.Top * (m_resizeMask[ResizeTypes.Top].Contains(mousePos) ? 1 : 0); - mask |= (int)ResizeTypes.Bottom * (m_resizeMask[ResizeTypes.Bottom].Contains(mousePos) ? 1 : 0); - mask |= (int)ResizeTypes.Left * (m_resizeMask[ResizeTypes.Left].Contains(mousePos) ? 1 : 0); - mask |= (int)ResizeTypes.Right * (m_resizeMask[ResizeTypes.Right].Contains(mousePos) ? 1 : 0); - - //if (m_resizeMask[ResizeTypes.Top].Contains(mousePos)) - // mask |= ResizeTypes.Top; - //else if (m_resizeMask[ResizeTypes.Bottom].Contains(mousePos)) - // mask |= ResizeTypes.Bottom; - - //if (m_resizeMask[ResizeTypes.Left].Contains(mousePos)) - // mask |= ResizeTypes.Left; - //else if (m_resizeMask[ResizeTypes.Right].Contains(mousePos)) - // mask |= ResizeTypes.Right; - - return (ResizeTypes)mask; - } - - public void OnHoverResize(ResizeTypes resizeType) - { - if (WasHoveringResize && m_lastResizeHoverType == resizeType) - return; - - // we are entering resize, or the resize type has changed. - - WasHoveringResize = true; - m_lastResizeHoverType = resizeType; - - s_resizeCursorObj.SetActive(true); - - // set the rotation for the resize icon - float iconRotation = 0f; - switch (resizeType) - { - case ResizeTypes.TopRight: - case ResizeTypes.BottomLeft: - iconRotation = 45f; break; - case ResizeTypes.Top: - case ResizeTypes.Bottom: - iconRotation = 90f; break; - case ResizeTypes.TopLeft: - case ResizeTypes.BottomRight: - iconRotation = 135f; break; - } - - Quaternion rot = s_resizeCursorObj.transform.rotation; - rot.eulerAngles = new Vector3(0, 0, iconRotation); - s_resizeCursorObj.transform.rotation = rot; - - UpdateHoverImagePos(); - } - - // update the resize icon position to be above the mouse - private void UpdateHoverImagePos() - { - RectTransform t = UIManager.CanvasRoot.GetComponent(); - s_resizeCursorObj.transform.localPosition = t.InverseTransformPoint(InputManager.MousePosition); - } - - public void OnHoverResizeEnd() - { - WasHoveringResize = false; - s_resizeCursorObj.SetActive(false); - } - - public void OnBeginResize(ResizeTypes resizeType) - { - m_currentResizeType = resizeType; - m_lastResizePos = InputManager.MousePosition; - WasResizing = true; - } - - public void OnResize() - { - Vector3 mousePos = InputManager.MousePosition; - Vector2 diff = m_lastResizePos - (Vector2)mousePos; - - if ((Vector2)mousePos == m_lastResizePos) - return; - - m_lastResizePos = mousePos; - - float diffX = (float)((decimal)diff.x / Screen.width); - float diffY = (float)((decimal)diff.y / Screen.height); - - Vector2 anchorMin = Panel.anchorMin; - Vector2 anchorMax = Panel.anchorMax; - - if (m_currentResizeType.HasFlag(ResizeTypes.Left)) - anchorMin.x -= diffX; - else if (m_currentResizeType.HasFlag(ResizeTypes.Right)) - anchorMax.x -= diffX; - - if (m_currentResizeType.HasFlag(ResizeTypes.Top)) - anchorMax.y -= diffY; - else if (m_currentResizeType.HasFlag(ResizeTypes.Bottom)) - anchorMin.y -= diffY; - - Panel.anchorMin = new Vector2(anchorMin.x, anchorMin.y); - Panel.anchorMax = new Vector2(anchorMax.x, anchorMax.y); - - var newWidth = (anchorMax.x - anchorMin.x) * Screen.width; - var newHeight = (anchorMax.y - anchorMin.y) * Screen.height; - - if (newWidth >= minResize.x) - { - Panel.anchorMin = new Vector2(anchorMin.x, Panel.anchorMin.y); - Panel.anchorMax = new Vector2(anchorMax.x, Panel.anchorMax.y); - } - if (newHeight >= minResize.y) - { - Panel.anchorMin = new Vector2(Panel.anchorMin.x, anchorMin.y); - Panel.anchorMax = new Vector2(Panel.anchorMax.x, anchorMax.y); - } - } - - public void OnEndResize(bool showing = false) - { - WasResizing = false; - UpdateResizeCache(); - OnFinishResize?.Invoke(Panel); - } - - internal static void CreateCursorUI() - { - try - { - var text = UIFactory.CreateLabel(UIManager.CanvasRoot.gameObject, "ResizeCursor", "↔", TextAnchor.MiddleCenter, Color.white, true, 35); - s_resizeCursorObj = text.gameObject; - - RectTransform rect = s_resizeCursorObj.GetComponent(); - rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 64); - rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 64); - - s_resizeCursorObj.SetActive(false); - } - catch (Exception e) - { - ExplorerCore.LogWarning("Exception creating Resize Cursor UI!\r\n" + e.ToString()); - } - } - - #endregion - } - - // Just to allow Enum to do .HasFlag() in NET 3.5 - public static class Net35FlagsEx - { - public static bool HasFlag(this Enum flags, Enum value) - { - ulong num = Convert.ToUInt64(value); - return (Convert.ToUInt64(flags) & num) == num; - } - } -} \ No newline at end of file diff --git a/src/UI/Main/Search/SearchPage.cs b/src/UI/Main/Search/SearchPage.cs deleted file mode 100644 index 25f43d6..0000000 --- a/src/UI/Main/Search/SearchPage.cs +++ /dev/null @@ -1,460 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.SceneManagement; -using UnityEngine.UI; -using System.Reflection; -using UnityExplorer.Core.Runtime; -using UnityExplorer.Core; -using UnityExplorer.UI.Utility; -using UnityExplorer.Core.Search; -using UnityExplorer.UI.Main.Home; -using UnityExplorer.UI.Inspectors; - -namespace UnityExplorer.UI.Main.Search -{ - public class SearchPage : BaseMenuPage - { - public override string Name => "Search"; - public override MenuPages Type => MenuPages.Search; - - public static SearchPage Instance; - - internal SearchContext m_context; - private SceneFilter m_sceneFilter; - private ChildFilter m_childFilter; - - // ui elements - - private Text m_resultCountText; - - private InputField m_customTypeInput; - - private InputField m_nameInput; - - private Button m_selectedContextButton; - private readonly Dictionary m_contextButtons = new Dictionary(); - - internal Dropdown m_sceneDropdown; - private int m_lastSceneCount = -1; - - private GameObject m_extraFilterRow; - - // Results - - internal object[] m_results; - internal readonly List m_resultShortList = new List(); - - private int m_lastCount; - public PageHandler m_resultListPageHandler; - private GameObject m_resultListContent; - private readonly List m_resultListTexts = new List(); - - public SearchPage() - { - Instance = this; - } - - public override bool Init() - { - ConstructUI(); - - return true; - } - - public void OnSceneChange() - { - m_results = new object[0]; - RefreshResultList(); - } - - public override void Update() - { - if (HaveScenesChanged()) - { - RefreshSceneDropdown(); - } - - if (m_customTypeInput.isFocused && m_context != SearchContext.Custom) - { - OnContextButtonClicked(SearchContext.Custom); - } - } - - // Updating result list content - - private void RefreshResultList() - { - if (m_resultListPageHandler == null || m_results == null) - return; - - m_resultListPageHandler.ListCount = m_results.Length; - - int newCount = 0; - - foreach (var itemIndex in m_resultListPageHandler) - { - newCount++; - - // normalized index starting from 0 - var i = itemIndex - m_resultListPageHandler.StartIndex; - - if (itemIndex >= m_results.Length) - { - if (i > m_lastCount || i >= m_resultListTexts.Count) - break; - - GameObject label = m_resultListTexts[i].transform.parent.parent.gameObject; - if (label.activeSelf) - label.SetActive(false); - } - else - { - var obj = m_results[itemIndex]; - var unityObj = obj as UnityEngine.Object; - - if (obj == null || (unityObj != null && !unityObj)) - continue; - - if (i >= m_resultShortList.Count) - { - m_resultShortList.Add(obj); - AddResultButton(); - } - else - { - m_resultShortList[i] = obj; - } - - var text = m_resultListTexts[i]; - - if (m_context != SearchContext.StaticClass) - { - var name = SignatureHighlighter.ParseFullSyntax(ReflectionUtility.GetActualType(obj), true); - - if (unityObj && m_context != SearchContext.Singleton) - { - if (unityObj && !string.IsNullOrEmpty(unityObj.name)) - name += $": {unityObj.name}"; - else - name += ": untitled"; - } - - text.text = name; - } - else - { - var type = obj as Type; - text.text = SignatureHighlighter.ParseFullSyntax(type, true); - } - - var label = text.transform.parent.parent.gameObject; - if (!label.activeSelf) - label.SetActive(true); - } - } - - m_lastCount = newCount; - } - - // scene dropdown update - - internal bool HaveScenesChanged() - { - if (m_lastSceneCount != SceneManager.sceneCount) - return true; - - for (int i = 0; i < SceneManager.sceneCount; i++) - { - int dropdownIndex = i + 3; - if (dropdownIndex >= m_sceneDropdown.options.Count - || m_sceneDropdown.options[dropdownIndex].text != SceneManager.GetSceneAt(i).name) - return true; - } - - return false; - } - - internal void RefreshSceneDropdown() - { - m_sceneDropdown.OnCancel(null); - - m_sceneDropdown.options.Clear(); - - m_sceneDropdown.options.Add(new Dropdown.OptionData - { - text = "Any" - }); - - m_sceneDropdown.options.Add(new Dropdown.OptionData - { - text = "None (Asset / Resource)" - }); - m_sceneDropdown.options.Add(new Dropdown.OptionData - { - text = "DontDestroyOnLoad" - }); - - m_lastSceneCount = 0; - for (int i = 0; i < SceneManager.sceneCount; i++) - { - m_lastSceneCount++; - - var scene = SceneManager.GetSceneAt(i).name; - m_sceneDropdown.options.Add(new Dropdown.OptionData - { - text = scene - }); - } - - m_sceneDropdown.itemText.text = "Any"; - m_sceneFilter = SceneFilter.Any; - } - - // ~~~~~ UI Callbacks ~~~~~ - - internal void OnSearchClicked() - { - m_resultListPageHandler.CurrentPage = 0; - - if (m_context == SearchContext.StaticClass) - m_results = SearchProvider.StaticClassSearch(m_nameInput.text); - else if (m_context == SearchContext.Singleton) - m_results = SearchProvider.SingletonSearch(m_nameInput.text); - else - m_results = SearchProvider.UnityObjectSearch(m_nameInput.text, m_customTypeInput.text, m_context, m_childFilter, m_sceneFilter); - - if (m_results == null) - m_results = new object[0]; - - RefreshResultList(); - - if (m_results.Length > 0) - m_resultCountText.text = $"{m_results.Length} Results"; - else - m_resultCountText.text = "No results..."; - } - - private void OnResultPageTurn() - { - RefreshResultList(); - } - - internal void OnResultClicked(int index) - { - if (m_context == SearchContext.StaticClass) - InspectorManager.Instance.Inspect((Type)m_resultShortList[index]); - else - InspectorManager.Instance.Inspect(m_resultShortList[index]); - } - - internal void OnContextButtonClicked(SearchContext context) - { - if (m_selectedContextButton && m_context == context) - return; - - if (m_selectedContextButton) - UIFactory.SetDefaultSelectableColors(m_selectedContextButton); - - var button = m_contextButtons[context]; - - m_selectedContextButton = button; - - RuntimeProvider.Instance.SetColorBlock(m_selectedContextButton, - new Color(0.35f, 0.7f, 0.35f), new Color(0.35f, 0.7f, 0.35f)); - - m_context = context; - - // if extra filters are valid - if (context == SearchContext.Component - || context == SearchContext.GameObject - || context == SearchContext.Custom) - { - m_extraFilterRow?.SetActive(true); - } - else - { - m_extraFilterRow?.SetActive(false); - } - } - - #region UI CONSTRUCTION - - internal void ConstructUI() - { - GameObject parent = MainMenu.Instance.PageViewport; - - Content = UIFactory.CreateVerticalGroup(parent, "SearchPage", true, true, true, true, 5, new Vector4(4,4,4,4)); - - ConstructTopArea(); - - ConstructResultsArea(); - } - - internal void ConstructTopArea() - { - var topAreaObj = UIFactory.CreateVerticalGroup(Content, "TitleArea", true, false, true, true, 5, new Vector4(5,5,5,5), - new Color(0.15f, 0.15f, 0.15f)); - - var titleLabel = UIFactory.CreateLabel(topAreaObj, "SearchTitle", "Search", TextAnchor.UpperLeft, Color.white, true, 25); - UIFactory.SetLayoutElement(titleLabel.gameObject, minHeight: 30, flexibleHeight: 0); - - // top area options - - var optionsGroupObj = UIFactory.CreateVerticalGroup(topAreaObj, "OptionsArea", true, false, true, true, 10, new Vector4(4,4,4,4), - new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(optionsGroupObj, minWidth: 500, minHeight: 70, flexibleHeight: 100); - - // search context row - - var contextRowObj = UIFactory.CreateHorizontalGroup(optionsGroupObj, "ContextFilters", false, false, true, true, 3, default, - new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(contextRowObj, minHeight: 25); - - var contextLabelObj = UIFactory.CreateLabel(contextRowObj, "ContextLabel", "Searching for:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(contextLabelObj.gameObject, minWidth: 125, minHeight: 25); - - // context buttons - - AddContextButton(contextRowObj, "UnityEngine.Object", SearchContext.UnityObject, 140); - AddContextButton(contextRowObj, "GameObject", SearchContext.GameObject); - AddContextButton(contextRowObj, "Component", SearchContext.Component); - AddContextButton(contextRowObj, "Custom...", SearchContext.Custom); - - // custom type input - - var customTypeObj = UIFactory.CreateInputField(contextRowObj, "CustomTypeInput", "eg. UnityEngine.Texture2D, etc..."); - UIFactory.SetLayoutElement(customTypeObj, minWidth: 250, flexibleWidth: 2000, minHeight: 25, flexibleHeight: 0); - m_customTypeInput = customTypeObj.GetComponent(); - - // static class and singleton buttons - - var secondRow = UIFactory.CreateHorizontalGroup(optionsGroupObj, "SecondRow", false, false, true, true, 3, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(secondRow, minHeight: 25); - - var spacer = UIFactory.CreateUIObject("spacer", secondRow); - UIFactory.SetLayoutElement(spacer, minWidth: 25, minHeight: 25); - - AddContextButton(secondRow, "Static Class", SearchContext.StaticClass); - AddContextButton(secondRow, "Singleton", SearchContext.Singleton); - - // search input - - var nameRowObj = UIFactory.CreateHorizontalGroup(optionsGroupObj, "SearchInput", true, false, true, true, 0, default, new Color(1, 1, 1, 0)); - UIFactory.SetLayoutElement(nameRowObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 5000); - - var nameLabel = UIFactory.CreateLabel(nameRowObj, "NameLabel", "Name contains:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(nameLabel.gameObject, minWidth: 125, minHeight: 25); - - var nameInputObj = UIFactory.CreateInputField(nameRowObj, "NameInputField", "..."); - m_nameInput = nameInputObj.GetComponent(); - UIFactory.SetLayoutElement(nameInputObj, minWidth: 150, flexibleWidth: 5000, minHeight: 25); - - // extra filter row - - m_extraFilterRow = UIFactory.CreateHorizontalGroup(optionsGroupObj, "ExtraFilterRow", false, true, true, true, 0, default, new Color(1, 1, 1, 0)); - m_extraFilterRow.SetActive(false); - UIFactory.SetLayoutElement(m_extraFilterRow, minHeight: 25, minWidth: 125, flexibleHeight: 0, flexibleWidth: 150); - - // scene filter - - var sceneLabelObj = UIFactory.CreateLabel(m_extraFilterRow, "SceneFilterLabel", "Scene filter:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(sceneLabelObj.gameObject, minWidth: 125, minHeight: 25); - - var sceneDropObj = UIFactory.CreateDropdown(m_extraFilterRow, - out m_sceneDropdown, - "Any", - 12, - (int value) => { m_sceneFilter = (SceneFilter)value; } - ); - - UIFactory.SetLayoutElement(sceneDropObj, minWidth: 220, minHeight: 25); - - // invisible space - - var invis = UIFactory.CreateUIObject("spacer", m_extraFilterRow); - UIFactory.SetLayoutElement(invis, minWidth: 25, flexibleWidth: 0); - - // children filter - - var childLabelObj = UIFactory.CreateLabel(m_extraFilterRow, "ChildFilterLabel", "Child filter:", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(childLabelObj.gameObject, minWidth: 100, minHeight: 25); - - var childDropObj = UIFactory.CreateDropdown(m_extraFilterRow, - out Dropdown childDrop, - "Any", - 12, - (int value) => { m_childFilter = (ChildFilter)value; }, - new[] { "Any", "Root Objects Only", "Children Only" }); - - UIFactory.SetLayoutElement(childDropObj, minWidth: 180, minHeight: 25); - - // search button - - var searchBtnObj = UIFactory.CreateButton(topAreaObj, "SearchButton", "Search", OnSearchClicked); - UIFactory.SetLayoutElement(searchBtnObj.gameObject, minHeight: 30, flexibleHeight: 0); - } - - internal void AddContextButton(GameObject parent, string label, SearchContext context, float width = 110) - { - var btn = UIFactory.CreateButton(parent, $"Context_{context}", label, () => { OnContextButtonClicked(context); }); - UIFactory.SetLayoutElement(btn.gameObject, minHeight: 25, minWidth: (int)width); - - m_contextButtons.Add(context, btn); - - // if first button - if (!m_selectedContextButton) - OnContextButtonClicked(context); - } - - internal void ConstructResultsArea() - { - // Result group holder (NOT actual result list content) - - var resultGroupObj = UIFactory.CreateVerticalGroup(Content, "SearchResults", true, false, true, true, 5, new Vector4(5,5,5,5), - new Color(1, 1, 1, 0)); - - m_resultCountText = UIFactory.CreateLabel(resultGroupObj, "ResultsLabel", "No results...", TextAnchor.MiddleCenter); - - GameObject scrollObj = UIFactory.CreateScrollView(resultGroupObj, - "ResultsScrollView", - out m_resultListContent, - out SliderScrollbar scroller, - new Color(0.07f, 0.07f, 0.07f, 1)); - - m_resultListPageHandler = new PageHandler(scroller); - m_resultListPageHandler.ConstructUI(resultGroupObj); - m_resultListPageHandler.OnPageChanged += OnResultPageTurn; - - UIFactory.SetLayoutGroup(m_resultListContent, forceHeight: false, childControlHeight: true, spacing: 2); - } - - internal void AddResultButton() - { - int thisIndex = m_resultListTexts.Count(); - - GameObject btnGroupObj = UIFactory.CreateHorizontalGroup(m_resultListContent, "ResultButtonGroup", - true, false, true, true, 0, new Vector4(1,1,1,1), new Color(0.1f, 0.1f, 0.1f)); - UIFactory.SetLayoutElement(btnGroupObj, flexibleWidth: 320, minHeight: 25, flexibleHeight: 0); - btnGroupObj.AddComponent(); - - var mainButton = UIFactory.CreateButton(btnGroupObj, - "ResultButton", - "", - () => { OnResultClicked(thisIndex); }); - - RuntimeProvider.Instance.SetColorBlock(mainButton, new Color(0.1f, 0.1f, 0.1f), - new Color(0.2f, 0.2f, 0.2f), new Color(0.05f, 0.05f, 0.05f)); - - UIFactory.SetLayoutElement(mainButton.gameObject, minHeight: 25, flexibleHeight: 0, minWidth: 320, flexibleWidth: 0); - - Text text = mainButton.GetComponentInChildren(); - text.alignment = TextAnchor.MiddleLeft; - text.horizontalOverflow = HorizontalWrapMode.Overflow; - m_resultListTexts.Add(text); - } - - #endregion - } -} diff --git a/src/UI/Models/ButtonRef.cs b/src/UI/Models/ButtonRef.cs new file mode 100644 index 0000000..40b409c --- /dev/null +++ b/src/UI/Models/ButtonRef.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine.UI; +using UnityExplorer.UI.Models; + +namespace UnityExplorer.UI +{ + // A simple helper class to handle a button's OnClick more effectively. + + public class ButtonRef + { + public Action OnClick; + + public Button Component { get; } + public Text ButtonText { get; } + + public ButtonRef(Button button) + { + this.Component = button; + this.ButtonText = button.GetComponentInChildren(); + + button.onClick.AddListener(() => { OnClick?.Invoke(); }); + } + } +} diff --git a/src/UI/Models/InputFieldRef.cs b/src/UI/Models/InputFieldRef.cs new file mode 100644 index 0000000..fca7f37 --- /dev/null +++ b/src/UI/Models/InputFieldRef.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Models; + +namespace UnityExplorer.UI +{ + public class InputFieldRef : UIModel + { + public static readonly HashSet inputsPendingUpdate = new HashSet(); + + public static void UpdateInstances() + { + if (inputsPendingUpdate.Any()) + { + foreach (var entry in inputsPendingUpdate) + { + LayoutRebuilder.MarkLayoutForRebuild(entry.Rect); + entry.OnValueChanged?.Invoke(entry.Component.text); + } + + inputsPendingUpdate.Clear(); + } + } + + public InputFieldRef(InputField component) + { + this.Component = component; + Rect = component.GetComponent(); + PlaceholderText = component.placeholder.TryCast(); + component.onValueChanged.AddListener(OnInputChanged); + } + + public event Action OnValueChanged; + + public InputField Component; + public Text PlaceholderText; + public RectTransform Rect; + + public string Text + { + get => Component.text; + set => Component.text = value; + } + + public TextGenerator TextGenerator => Component.cachedInputTextGenerator; + public bool ReachedMaxVerts => TextGenerator.vertexCount >= UIManager.MAX_TEXT_VERTS; + + + private void OnInputChanged(string value) + { + if (!inputsPendingUpdate.Contains(this)) + inputsPendingUpdate.Add(this); + } + + public override GameObject UIRoot => Component.gameObject; + + public override void ConstructUI(GameObject parent) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/UI/Models/UIBehaviourModel.cs b/src/UI/Models/UIBehaviourModel.cs new file mode 100644 index 0000000..cfa6398 --- /dev/null +++ b/src/UI/Models/UIBehaviourModel.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.UI.Models +{ + public abstract class UIBehaviourModel : UIModel + { + private static readonly List Instances = new List(); + + public static void UpdateInstances() + { + if (!Instances.Any()) + return; + + try + { + for (int i = Instances.Count - 1; i >= 0; i--) + { + var instance = Instances[i]; + if (instance == null || !instance.UIRoot) + { + Instances.RemoveAt(i); + continue; + } + if (instance.Enabled) + instance.Update(); + } + } + catch (Exception ex) + { + ExplorerCore.Log(ex); + } + } + + public UIBehaviourModel() + { + Instances.Add(this); + } + + /// + /// Default empty method, override and implement if NeedsUpdateTick is true. + /// + public virtual void Update() + { + } + + public override void Destroy() + { + if (Instances.Contains(this)) + Instances.Remove(this); + + base.Destroy(); + } + } +} diff --git a/src/UI/Models/UIModel.cs b/src/UI/Models/UIModel.cs new file mode 100644 index 0000000..bb1c885 --- /dev/null +++ b/src/UI/Models/UIModel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.UI.Models +{ + public abstract class UIModel + { + public abstract GameObject UIRoot { get; } + + public bool Enabled + { + get => UIRoot && UIRoot.activeInHierarchy; + set + { + if (!UIRoot || Enabled == value) + return; + UIRoot.SetActive(value); + OnToggleEnabled?.Invoke(value); + } + } + + public event Action OnToggleEnabled; + + public abstract void ConstructUI(GameObject parent); + + public virtual void Toggle() => SetActive(!Enabled); + + public virtual void SetActive(bool active) + { + UIRoot?.SetActive(active); + } + + public virtual void Destroy() + { + if (UIRoot) + GameObject.Destroy(UIRoot); + } + } +} diff --git a/src/UI/ObjectExplorer/ObjectSearch.cs b/src/UI/ObjectExplorer/ObjectSearch.cs new file mode 100644 index 0000000..e09145b --- /dev/null +++ b/src/UI/ObjectExplorer/ObjectSearch.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Inspectors; +using UnityExplorer.UI.Models; +using UnityExplorer.UI.ObjectPool; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.ObjectExplorer +{ + public class ObjectSearch : UIModel + { + public ObjectExplorerPanel Parent { get; } + + public ObjectSearch(ObjectExplorerPanel parent) + { + Parent = parent; + } + + private SearchContext m_context = SearchContext.UnityObject; + private SceneFilter m_sceneFilter = SceneFilter.Any; + private ChildFilter m_childFilter = ChildFilter.Any; + private string desiredTypeInput; + private string lastCheckedTypeInput; + private bool lastTypeCanHaveGO; + + public ButtonListHandler dataHandler; + + private ScrollPool resultsScrollPool; + private List currentResults = new List(); + + public TypeCompleter typeAutocompleter; + + public override GameObject UIRoot => uiRoot; + private GameObject uiRoot; + + private GameObject sceneFilterRow; + private GameObject childFilterRow; + private GameObject unityObjectClassRow; + private InputFieldRef nameInputField; + + private Text resultsLabel; + + public List GetEntries() => currentResults; + + public void DoSearch() + { + cachedCellTexts.Clear(); + + if (m_context == SearchContext.Singleton) + currentResults = SearchProvider.SingletonSearch(nameInputField.Text); + else if (m_context == SearchContext.StaticClass) + currentResults = SearchProvider.StaticClassSearch(nameInputField.Text); + else + { + string compType = ""; + if (m_context == SearchContext.UnityObject) + compType = this.desiredTypeInput; + + currentResults = SearchProvider.UnityObjectSearch(nameInputField.Text, compType, m_context, m_childFilter, m_sceneFilter); + } + + dataHandler.RefreshData(); + resultsScrollPool.Refresh(true); + + resultsLabel.text = $"{currentResults.Count} results"; + } + + public void Update() + { + if (m_context == SearchContext.UnityObject && lastCheckedTypeInput != desiredTypeInput) + { + lastCheckedTypeInput = desiredTypeInput; + + //var type = ReflectionUtility.GetTypeByName(desiredTypeInput); + if (ReflectionUtility.AllTypes.TryGetValue(desiredTypeInput, out var cachedType)) + { + var type = cachedType; + lastTypeCanHaveGO = typeof(Component).IsAssignableFrom(type) || type == typeof(GameObject); + sceneFilterRow.SetActive(lastTypeCanHaveGO); + childFilterRow.SetActive(lastTypeCanHaveGO); + } + else + { + sceneFilterRow.SetActive(false); + childFilterRow.SetActive(false); + lastTypeCanHaveGO = false; + } + } + } + + // UI Callbacks + + private void OnContextDropdownChanged(int value) + { + m_context = (SearchContext)value; + + lastCheckedTypeInput = null; + sceneFilterRow.SetActive(false); + childFilterRow.SetActive(false); + + unityObjectClassRow.SetActive(m_context == SearchContext.UnityObject); + } + + private void OnSceneFilterDropChanged(int value) => m_sceneFilter = (SceneFilter)value; + + private void OnChildFilterDropChanged(int value) => m_childFilter = (ChildFilter)value; + + private void OnTypeInputChanged(string val) + { + desiredTypeInput = val; + + if (string.IsNullOrEmpty(val)) + { + sceneFilterRow.SetActive(false); + childFilterRow.SetActive(false); + lastCheckedTypeInput = val; + } + } + + // Cache the syntax-highlighted text for each search result to reduce allocs. + private static readonly Dictionary cachedCellTexts = new Dictionary(); + + public void SetCell(ButtonCell cell, int index) + { + if (!cachedCellTexts.ContainsKey(index)) + { + string text; + if (m_context == SearchContext.StaticClass) + text = SignatureHighlighter.Parse(currentResults[index] as Type, true); + else + text = ToStringUtility.ToStringWithType(currentResults[index], currentResults[index]?.GetActualType()); + + cachedCellTexts.Add(index, text); + } + + cell.Button.ButtonText.text = cachedCellTexts[index]; + } + + private void OnCellClicked(int dataIndex) + { + if (m_context == SearchContext.StaticClass) + InspectorManager.Inspect(currentResults[dataIndex] as Type); + else + InspectorManager.Inspect(currentResults[dataIndex]); + } + + private bool ShouldDisplayCell(object arg1, string arg2) => true; + + public override void ConstructUI(GameObject parent) + { + uiRoot = UIFactory.CreateVerticalGroup(parent, "ObjectSearch", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(uiRoot, flexibleHeight: 9999); + + // Search context row + + var contextGroup = UIFactory.CreateHorizontalGroup(uiRoot, "SearchContextRow", false, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(contextGroup, minHeight: 25, flexibleHeight: 0); + + var contextLbl = UIFactory.CreateLabel(contextGroup, "SearchContextLabel", "Searching for:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(contextLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + var contextDropObj = UIFactory.CreateDropdown(contextGroup, out Dropdown contextDrop, null, 14, OnContextDropdownChanged); + foreach (var name in Enum.GetNames(typeof(SearchContext))) + contextDrop.options.Add(new Dropdown.OptionData(name)); + UIFactory.SetLayoutElement(contextDropObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + // Unity class input + + unityObjectClassRow = UIFactory.CreateHorizontalGroup(uiRoot, "UnityClassRow", false, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(unityObjectClassRow, minHeight: 25, flexibleHeight: 0); + + var unityClassLbl = UIFactory.CreateLabel(unityObjectClassRow, "UnityClassLabel", "Class filter:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(unityClassLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + var classInputField = UIFactory.CreateInputField(unityObjectClassRow, "ClassInput", "..."); + UIFactory.SetLayoutElement(classInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + typeAutocompleter = new TypeCompleter(typeof(UnityEngine.Object), classInputField); + classInputField.OnValueChanged += OnTypeInputChanged; + + //unityObjectClassRow.SetActive(false); + + // Child filter row + + childFilterRow = UIFactory.CreateHorizontalGroup(uiRoot, "ChildFilterRow", false, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(childFilterRow, minHeight: 25, flexibleHeight: 0); + + var childLbl = UIFactory.CreateLabel(childFilterRow, "ChildLabel", "Child filter:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(childLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + var childDropObj = UIFactory.CreateDropdown(childFilterRow, out Dropdown childDrop, null, 14, OnChildFilterDropChanged); + foreach (var name in Enum.GetNames(typeof(ChildFilter))) + childDrop.options.Add(new Dropdown.OptionData(name)); + UIFactory.SetLayoutElement(childDropObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + childFilterRow.SetActive(false); + + // Scene filter row + + sceneFilterRow = UIFactory.CreateHorizontalGroup(uiRoot, "SceneFilterRow", false, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(sceneFilterRow, minHeight: 25, flexibleHeight: 0); + + var sceneLbl = UIFactory.CreateLabel(sceneFilterRow, "SceneLabel", "Scene filter:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(sceneLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + var sceneDropObj = UIFactory.CreateDropdown(sceneFilterRow, out Dropdown sceneDrop, null, 14, OnSceneFilterDropChanged); + foreach (var name in Enum.GetNames(typeof(SceneFilter))) + sceneDrop.options.Add(new Dropdown.OptionData(name)); + UIFactory.SetLayoutElement(sceneDropObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + sceneFilterRow.SetActive(false); + + // Name filter input + + var nameRow = UIFactory.CreateHorizontalGroup(uiRoot, "NameRow", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(nameRow, minHeight: 25, flexibleHeight: 0); + + var nameLbl = UIFactory.CreateLabel(nameRow, "NameFilterLabel", "Name contains:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(nameLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + nameInputField = UIFactory.CreateInputField(nameRow, "NameFilterInput", "..."); + UIFactory.SetLayoutElement(nameInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + // Search button + + var searchButton = UIFactory.CreateButton(uiRoot, "SearchButton", "Search"); + UIFactory.SetLayoutElement(searchButton.Component.gameObject, minHeight: 25, flexibleHeight: 0); + searchButton.OnClick += DoSearch; + + // Results count label + + var resultsCountRow = UIFactory.CreateHorizontalGroup(uiRoot, "ResultsCountRow", true, true, true, true); + UIFactory.SetLayoutElement(resultsCountRow, minHeight: 25, flexibleHeight: 0); + + resultsLabel = UIFactory.CreateLabel(resultsCountRow, "ResultsLabel", "0 results", TextAnchor.MiddleCenter); + + // RESULTS SCROLL POOL + + dataHandler = new ButtonListHandler(resultsScrollPool, GetEntries, SetCell, ShouldDisplayCell, OnCellClicked); + resultsScrollPool = UIFactory.CreateScrollPool(uiRoot, "ResultsList", out GameObject scrollObj, + out GameObject scrollContent); + + resultsScrollPool.Initialize(dataHandler); + UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999); + } + } +} diff --git a/src/UI/ObjectExplorer/SceneExplorer.cs b/src/UI/ObjectExplorer/SceneExplorer.cs new file mode 100644 index 0000000..ea94762 --- /dev/null +++ b/src/UI/ObjectExplorer/SceneExplorer.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UI; +using UnityExplorer.Core; +using UnityExplorer.UI.Models; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.ObjectExplorer +{ + public class SceneExplorer : UIModel + { + public ObjectExplorerPanel Parent { get; } + + public SceneExplorer(ObjectExplorerPanel parent) + { + Parent = parent; + + SceneHandler.OnInspectedSceneChanged += SceneHandler_OnInspectedSceneChanged; + SceneHandler.OnLoadedScenesChanged += SceneHandler_OnLoadedScenesChanged; + } + + public override GameObject UIRoot => m_uiRoot; + private GameObject m_uiRoot; + + /// + /// Whether to automatically update per auto-update interval or not. + /// + public bool AutoUpdate = false; + + public TransformTree Tree; + private float timeOfLastUpdate = -1f; + + private GameObject refreshRow; + private Dropdown sceneDropdown; + private readonly Dictionary sceneToDropdownOption = new Dictionary(); + + private IEnumerable GetRootEntries() => SceneHandler.CurrentRootObjects; + + public void Update() + { + if ((AutoUpdate || !SceneHandler.InspectingAssetScene) && timeOfLastUpdate.OccuredEarlierThan(1)) + { + timeOfLastUpdate = Time.realtimeSinceStartup; + UpdateTree(); + } + } + + public void UpdateTree() + { + SceneHandler.Update(); + Tree.RefreshData(true); + } + + private void OnDropdownChanged(int value) + { + if (value < 0 || SceneHandler.LoadedScenes.Count <= value) + return; + + SceneHandler.SelectedScene = SceneHandler.LoadedScenes[value]; + SceneHandler.Update(); + Tree.RefreshData(true); + OnSelectedSceneChanged(SceneHandler.SelectedScene.Value); + } + + private void SceneHandler_OnInspectedSceneChanged(Scene scene) + { + if (!sceneToDropdownOption.ContainsKey(scene.handle)) + PopulateSceneDropdown(); + + if (sceneToDropdownOption.ContainsKey(scene.handle)) + { + var opt = sceneToDropdownOption[scene.handle]; + int idx = sceneDropdown.options.IndexOf(opt); + if (sceneDropdown.value != idx) + sceneDropdown.value = idx; + else + sceneDropdown.captionText.text = opt.text; + } + + OnSelectedSceneChanged(scene); + } + + private void OnSelectedSceneChanged(Scene scene) + { + if (refreshRow) + refreshRow.SetActive(!scene.IsValid()); + } + + private void SceneHandler_OnLoadedScenesChanged(ReadOnlyCollection loadedScenes) + { + PopulateSceneDropdown(); + } + + private void PopulateSceneDropdown() + { + sceneToDropdownOption.Clear(); + sceneDropdown.options.Clear(); + + foreach (var scene in SceneHandler.LoadedScenes) + { + string name = scene.name?.Trim(); + + if (!scene.IsValid()) + name = "HideAndDontSave"; + else if (string.IsNullOrEmpty(name)) + name = ""; + + var option = new Dropdown.OptionData(name); + sceneDropdown.options.Add(option); + sceneToDropdownOption.Add(scene.handle, option); + } + } + + private void OnFilterInput(string input) + { + if ((!string.IsNullOrEmpty(input) && !Tree.Filtering) || (string.IsNullOrEmpty(input) && Tree.Filtering)) + { + Tree.displayedObjects.Clear(); + } + + Tree.CurrentFilter = input; + Tree.RefreshData(true, true); + } + + private void TryLoadScene(LoadSceneMode mode, Dropdown allSceneDrop) + { + var text = allSceneDrop.options[allSceneDrop.value].text; + + if (text == DEFAULT_LOAD_TEXT) + return; + + try + { + SceneManager.LoadScene(text, mode); + allSceneDrop.value = 0; + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Unable to load the Scene! {ex.ReflectionExToString()}"); + } + } + + public override void ConstructUI(GameObject content) + { + m_uiRoot = UIFactory.CreateUIObject("SceneExplorer", content); + UIFactory.SetLayoutGroup(m_uiRoot, true, true, true, true, 0, 2, 2, 2, 2); + UIFactory.SetLayoutElement(m_uiRoot, flexibleHeight: 9999); + + // Tool bar (top area) + + var toolbar = UIFactory.CreateVerticalGroup(m_uiRoot, "Toolbar", true, true, true, true, 2, new Vector4(2, 2, 2, 2), + new Color(0.15f, 0.15f, 0.15f)); + + // Scene selector dropdown + + var dropRow = UIFactory.CreateHorizontalGroup(toolbar, "DropdownRow", true, true, true, true, 5, default, new Color(1, 1, 1, 0)); + UIFactory.SetLayoutElement(dropRow, minHeight: 25, flexibleWidth: 9999); + + var dropLabel = UIFactory.CreateLabel(dropRow, "SelectorLabel", "Scene:", TextAnchor.MiddleLeft, Color.cyan, false, 15); + UIFactory.SetLayoutElement(dropLabel.gameObject, minHeight: 25, minWidth: 60, flexibleWidth: 0); + + var dropdownObj = UIFactory.CreateDropdown(dropRow, out sceneDropdown, "", 13, OnDropdownChanged); + UIFactory.SetLayoutElement(dropdownObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + SceneHandler.Update(); + PopulateSceneDropdown(); + sceneDropdown.captionText.text = sceneToDropdownOption.First().Value.text; + + // Filter row + + var filterRow = UIFactory.CreateHorizontalGroup(toolbar, "FilterGroup", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(filterRow, minHeight: 25, flexibleHeight: 0); + + //Filter input field + var inputField = UIFactory.CreateInputField(filterRow, "FilterInput", "Search..."); + inputField.Component.targetGraphic.color = new Color(0.2f, 0.2f, 0.2f); + RuntimeProvider.Instance.SetColorBlock(inputField.Component, new Color(0.4f, 0.4f, 0.4f), new Color(0.2f, 0.2f, 0.2f), + new Color(0.08f, 0.08f, 0.08f)); + UIFactory.SetLayoutElement(inputField.UIRoot, minHeight: 25); + inputField.OnValueChanged += OnFilterInput; + + // refresh row + + refreshRow = UIFactory.CreateHorizontalGroup(toolbar, "RefreshGroup", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(refreshRow, minHeight: 30, flexibleHeight: 0); + + var refreshButton = UIFactory.CreateButton(refreshRow, "RefreshButton", "Update"); + UIFactory.SetLayoutElement(refreshButton.Component.gameObject, minWidth: 65, flexibleWidth: 0); + refreshButton.OnClick += UpdateTree; + + var refreshToggle = UIFactory.CreateToggle(refreshRow, "RefreshToggle", out Toggle toggle, out Text text); + UIFactory.SetLayoutElement(refreshToggle, flexibleWidth: 9999); + text.text = "Auto-update (1 second)"; + text.alignment = TextAnchor.MiddleLeft; + text.color = Color.white; + text.fontSize = 12; + toggle.isOn = false; + toggle.onValueChanged.AddListener((bool val) => AutoUpdate = val); + + refreshRow.SetActive(false); + + // Transform Tree + + var scrollPool = UIFactory.CreateScrollPool(m_uiRoot, "TransformTree", out GameObject scrollObj, + out GameObject scrollContent, new Color(0.11f, 0.11f, 0.11f)); + UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999); + UIFactory.SetLayoutElement(scrollContent, flexibleHeight: 9999); + + Tree = new TransformTree(scrollPool, GetRootEntries); + Tree.Init(); + Tree.RefreshData(true, true); + //scrollPool.Viewport.GetComponent().enabled = false; + //UIRoot.GetComponent().enabled = false; + + // Scene Loader + + ConstructSceneLoader(); + } + + private const string DEFAULT_LOAD_TEXT = "[Select a scene]"; + + private void ConstructSceneLoader() + { + // Scene Loader + try + { + if (SceneHandler.WasAbleToGetScenesInBuild) + { + var sceneLoaderObj = UIFactory.CreateVerticalGroup(m_uiRoot, "SceneLoader", true, true, true, true); + UIFactory.SetLayoutElement(sceneLoaderObj, minHeight: 25); + //sceneLoaderObj.SetActive(false); + + var loaderTitle = UIFactory.CreateLabel(sceneLoaderObj, "SceneLoaderLabel", "Scene Loader", TextAnchor.MiddleLeft, Color.white, true, 14); + UIFactory.SetLayoutElement(loaderTitle.gameObject, minHeight: 25, flexibleHeight: 0); + + var allSceneDropObj = UIFactory.CreateDropdown(sceneLoaderObj, out Dropdown allSceneDrop, "", 14, null); + UIFactory.SetLayoutElement(allSceneDropObj, minHeight: 25, minWidth: 150, flexibleWidth: 0, flexibleHeight: 0); + + allSceneDrop.options.Add(new Dropdown.OptionData(DEFAULT_LOAD_TEXT)); + + foreach (var scene in SceneHandler.AllSceneNames) + allSceneDrop.options.Add(new Dropdown.OptionData(Path.GetFileNameWithoutExtension(scene))); + + allSceneDrop.value = 1; + allSceneDrop.value = 0; + + var buttonRow = UIFactory.CreateHorizontalGroup(sceneLoaderObj, "LoadButtons", true, true, true, true, 4); + + var loadButton = UIFactory.CreateButton(buttonRow, "LoadSceneButton", "Load (Single)", new Color(0.1f, 0.3f, 0.3f)); + UIFactory.SetLayoutElement(loadButton.Component.gameObject, minHeight: 25, minWidth: 150); + loadButton.OnClick += () => + { + TryLoadScene(LoadSceneMode.Single, allSceneDrop); + }; + + var loadAdditiveButton = UIFactory.CreateButton(buttonRow, "LoadSceneButton", "Load (Additive)", new Color(0.1f, 0.3f, 0.3f)); + UIFactory.SetLayoutElement(loadAdditiveButton.Component.gameObject, minHeight: 25, minWidth: 150); + loadAdditiveButton.OnClick += () => + { + TryLoadScene(LoadSceneMode.Additive, allSceneDrop); + }; + + var disabledColor = new Color(0.24f, 0.24f, 0.24f); + RuntimeProvider.Instance.SetColorBlock(loadButton.Component, disabled: disabledColor); + RuntimeProvider.Instance.SetColorBlock(loadAdditiveButton.Component, disabled: disabledColor); + + loadButton.Component.interactable = false; + loadAdditiveButton.Component.interactable = false; + + allSceneDrop.onValueChanged.AddListener((int val) => + { + var text = allSceneDrop.options[val].text; + if (text == DEFAULT_LOAD_TEXT) + { + loadButton.Component.interactable = false; + loadAdditiveButton.Component.interactable = false; + } + else + { + loadButton.Component.interactable = true; + loadAdditiveButton.Component.interactable = true; + } + }); + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Could not create the Scene Loader helper! {ex.ReflectionExToString()}"); + } + } + } +} diff --git a/src/UI/ObjectExplorer/SearchProvider.cs b/src/UI/ObjectExplorer/SearchProvider.cs new file mode 100644 index 0000000..04753e0 --- /dev/null +++ b/src/UI/ObjectExplorer/SearchProvider.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityExplorer.Core; +using UnityExplorer.Core.Runtime; + +namespace UnityExplorer.UI.ObjectExplorer +{ + public enum SearchContext + { + UnityObject, + // GameObject, + Singleton, + StaticClass + } + + public enum ChildFilter + { + Any, + RootObject, + HasParent + } + + public enum SceneFilter + { + Any, + ActivelyLoaded, + DontDestroyOnLoad, + HideAndDontSave, + } + + public static class SearchProvider + { + + private static bool Filter(Scene scene, SceneFilter filter) + { + switch (filter) + { + case SceneFilter.Any: + return true; + case SceneFilter.DontDestroyOnLoad: + return scene == SceneHandler.DontDestroyScene; + case SceneFilter.HideAndDontSave: + return scene == SceneHandler.AssetScene; + case SceneFilter.ActivelyLoaded: + return scene != SceneHandler.DontDestroyScene && scene != SceneHandler.AssetScene; + default: + return false; + } + } + + internal static List UnityObjectSearch(string input, string customTypeInput, SearchContext context, + ChildFilter childFilter, SceneFilter sceneFilter) + { + var results = new List(); + + Type searchType; + switch (context) + { + //case SearchContext.GameObject: + // searchType = typeof(GameObject); + // break; + + case SearchContext.UnityObject: + default: + + if (!string.IsNullOrEmpty(customTypeInput)) + { + if (ReflectionUtility.GetTypeByName(customTypeInput) is Type customType) + { + if (typeof(UnityEngine.Object).IsAssignableFrom(customType)) + { + searchType = customType; + break; + } + else + ExplorerCore.LogWarning($"Custom type '{customType.FullName}' is not assignable from UnityEngine.Object!"); + } + else + ExplorerCore.LogWarning($"Could not find any type by name '{customTypeInput}'!"); + } + + searchType = typeof(UnityEngine.Object); + break; + } + + + if (searchType == null) + return results; + + var allObjects = RuntimeProvider.Instance.FindObjectsOfTypeAll(searchType); + + // perform filter comparers + + string nameFilter = null; + if (!string.IsNullOrEmpty(input)) + nameFilter = input; + + bool canGetGameObject = searchType == typeof(GameObject) || typeof(Component).IsAssignableFrom(searchType); + + foreach (var obj in allObjects) + { + // name check + if (!string.IsNullOrEmpty(nameFilter) && !obj.name.ContainsIgnoreCase(nameFilter)) + continue; + + if (canGetGameObject) + { + var go = searchType == typeof(GameObject) + ? obj.TryCast() + : obj.TryCast().gameObject; + + if (go) + { + // scene check + if (sceneFilter != SceneFilter.Any) + { + if (!Filter(go.scene, sceneFilter)) + continue; + } + + if (childFilter != ChildFilter.Any) + { + if (!go) + continue; + + // root object check (no parent) + if (childFilter == ChildFilter.HasParent && !go.transform.parent) + continue; + else if (childFilter == ChildFilter.RootObject && go.transform.parent) + continue; + } + } + } + + results.Add(obj); + } + + return results; + } + + internal static List StaticClassSearch(string input) + { + var list = new List(); + + var nameFilter = ""; + if (!string.IsNullOrEmpty(input)) + nameFilter = input; + + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + foreach (var type in asm.TryGetTypes().Where(it => it.IsSealed && it.IsAbstract)) + { + if (!string.IsNullOrEmpty(nameFilter) && !type.FullName.ContainsIgnoreCase(nameFilter)) + continue; + + list.Add(type); + } + } + + return list; + } + + internal static string[] instanceNames = new string[] + { + "m_instance", + "m_Instance", + "s_instance", + "s_Instance", + "_instance", + "_Instance", + "instance", + "Instance", + "k__BackingField", + "k__BackingField", + }; + + internal static List SingletonSearch(string input) + { + var instances = new List(); + + var nameFilter = ""; + if (!string.IsNullOrEmpty(input)) + nameFilter = input; + + var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; + + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + // Search all non-static, non-enum classes. + foreach (var type in asm.TryGetTypes().Where(it => !(it.IsSealed && it.IsAbstract) && !it.IsEnum)) + { + try + { + if (!string.IsNullOrEmpty(nameFilter) && !type.FullName.ContainsIgnoreCase(nameFilter)) + continue; + + ReflectionUtility.FindSingleton(instanceNames, type, flags, instances); + } + catch { } + } + } + + return instances; + } + + } +} diff --git a/src/UI/ObjectPool/IPooledObject.cs b/src/UI/ObjectPool/IPooledObject.cs new file mode 100644 index 0000000..3b08751 --- /dev/null +++ b/src/UI/ObjectPool/IPooledObject.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.UI.ObjectPool +{ + public interface IPooledObject + { + GameObject UIRoot { get; set; } + + GameObject CreateContent(GameObject parent); + + float DefaultHeight { get; } + + //GameObject CreatePrototype(); + } +} \ No newline at end of file diff --git a/src/UI/ObjectPool/Pool.cs b/src/UI/ObjectPool/Pool.cs new file mode 100644 index 0000000..497c99d --- /dev/null +++ b/src/UI/ObjectPool/Pool.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.UI.ObjectPool +{ + // Abstract non-generic class, handles the pool dictionary and interfacing with the generic pools. + public abstract class Pool + { + protected static readonly Dictionary pools = new Dictionary(); + + public static Pool GetPool(Type type) + { + if (!pools.TryGetValue(type, out Pool pool)) + pool = CreatePool(type); + return pool; + } + + protected static Pool CreatePool(Type type) + { + Pool pool = (Pool)Activator.CreateInstance(typeof(Pool<>).MakeGenericType(new[] { type })); + pools.Add(type, pool); + return pool; + } + + public static IPooledObject Borrow(Type type) + { + return GetPool(type).TryBorrow(); + } + + public static void Return(Type type, IPooledObject obj) + { + GetPool(type).TryReturn(obj); + } + + protected abstract IPooledObject TryBorrow(); + protected abstract void TryReturn(IPooledObject obj); + } + + // Each generic implementation has its own pool, business logic is here + public class Pool : Pool where T : IPooledObject + { + public static Pool GetPool() => (Pool)GetPool(typeof(T)); + + public static T Borrow() + { + return GetPool().BorrowObject(); + } + + public static void Return(T obj) + { + GetPool().ReturnObject(obj); + } + + // Instance + + public static Pool Instance + { + get => s_instance ?? (Pool)CreatePool(typeof(T)); + } + private static Pool s_instance; + + public Pool() + { + s_instance = this; + + //ExplorerCore.LogWarning("Creating Pool<" + typeof(T).Name + ">"); + + InactiveHolder = new GameObject($"PoolHolder_{typeof(T).Name}"); + InactiveHolder.transform.parent = UIManager.PoolHolder.transform; + InactiveHolder.hideFlags |= HideFlags.HideAndDontSave; + InactiveHolder.SetActive(false); + + // Create an instance (not content) to grab the default height + var obj = (T)Activator.CreateInstance(typeof(T)); + DefaultHeight = obj.DefaultHeight; + } + + public GameObject InactiveHolder { get; } + public float DefaultHeight { get; } + + private readonly HashSet available = new HashSet(); + private readonly HashSet borrowed = new HashSet(); + + public int AvailableCount => available.Count; + + private void IncrementPool() + { + var obj = (T)Activator.CreateInstance(typeof(T)); + obj.CreateContent(InactiveHolder); + available.Add(obj); + } + + public T BorrowObject() + { + if (available.Count <= 0) + IncrementPool(); + + var obj = available.First(); + available.Remove(obj); + borrowed.Add(obj); + + return obj; + } + + public void ReturnObject(T obj) + { + if (!borrowed.Contains(obj)) + ExplorerCore.LogWarning($"Returning an item to object pool ({typeof(T).Name}) but the item didn't exist in the borrowed list?"); + else + borrowed.Remove(obj); + + available.Add(obj); + obj.UIRoot.transform.SetParent(InactiveHolder.transform, false); + } + + protected override IPooledObject TryBorrow() => Borrow(); + + protected override void TryReturn(IPooledObject obj) => Return((T)obj); + } +} diff --git a/src/UI/Panels/CSConsolePanel.cs b/src/UI/Panels/CSConsolePanel.cs new file mode 100644 index 0000000..73f4ddf --- /dev/null +++ b/src/UI/Panels/CSConsolePanel.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.UI.CSConsole; +using UnityExplorer.UI.Utility; + +namespace UnityExplorer.UI.Panels +{ + public class CSConsolePanel : UIPanel + { + public override string Name => "C# Console"; + public override UIManager.Panels PanelType => UIManager.Panels.CSConsole; + public override int MinWidth => 750; + public override int MinHeight => 300; + + public InputFieldScroller InputScroll { get; private set; } + public InputFieldRef Input => InputScroll.InputField; + public Text InputText { get; private set; } + public Text HighlightText { get; private set; } + + 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; + + private void InvokeOnValueChanged(string value) + { + if (value.Length == UIManager.MAX_INPUTFIELD_CHARS) + ExplorerCore.LogWarning($"Reached maximum InputField character length! ({UIManager.MAX_INPUTFIELD_CHARS})"); + + OnInputChanged?.Invoke(value); + } + + public override void Update() + { + base.Update(); + + ConsoleController.Update(); + } + + // Saving + + public override void DoSaveToConfigElement() + { + ConfigManager.CSConsoleData.Value = this.ToSaveData(); + } + + public override string GetSaveDataFromConfigManager() => ConfigManager.CSConsoleData.Value; + + // UI Construction + + protected internal override void DoSetDefaultPosAndAnchors() + { + mainPanelRect.localPosition = Vector2.zero; + mainPanelRect.pivot = new Vector2(0f, 1f); + mainPanelRect.anchorMin = new Vector2(0.4f, 0.175f); + mainPanelRect.anchorMax = new Vector2(0.85f, 0.925f); + } + + public override void ConstructPanelContent() + { + // Tools Row + + var toolsRow = UIFactory.CreateHorizontalGroup(this.content, "ToggleRow", false, false, true, true, 5, new Vector4(8, 8, 10, 5), + 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); + UIFactory.SetLayoutElement(ctrlRToggleObj, minWidth: 150, flexibleWidth: 0, minHeight: 25); + ctrlRToggleText.alignment = TextAnchor.UpperLeft; + ctrlRToggleText.text = "Compile on Ctrl+R"; + CtrlRToggle.onValueChanged.AddListener((bool val) => { OnCtrlRToggled?.Invoke(val); }); + + // Enable Suggestions toggle + + var suggestToggleObj = UIFactory.CreateToggle(toolsRow, "SuggestionToggle", out var SuggestionsToggle, out Text suggestToggleText); + UIFactory.SetLayoutElement(suggestToggleObj, minWidth: 120, flexibleWidth: 0, minHeight: 25); + suggestToggleText.alignment = TextAnchor.UpperLeft; + suggestToggleText.text = "Suggestions"; + SuggestionsToggle.onValueChanged.AddListener((bool val) => { OnSuggestionsToggled?.Invoke(val); }); + + // Enable Auto-indent toggle + + var autoIndentToggleObj = UIFactory.CreateToggle(toolsRow, "IndentToggle", out var AutoIndentToggle, out Text autoIndentToggleText); + 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); }); + + // Console Input + + int fontSize = 16; + + var inputObj = UIFactory.CreateScrollInputField(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; + 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); + var highlightTextRect = highlightTextObj.GetComponent(); + highlightTextRect.pivot = new Vector2(0, 1); + highlightTextRect.anchorMin = Vector2.zero; + highlightTextRect.anchorMax = Vector2.one; + highlightTextRect.offsetMin = Vector2.zero; + highlightTextRect.offsetMax = Vector2.zero; + + HighlightText = highlightTextObj.AddComponent(); + HighlightText.color = Color.white; + HighlightText.supportRichText = true; + HighlightText.fontSize = fontSize; + + // Set fonts + InputText.font = UIManager.ConsoleFont; + Input.PlaceholderText.font = UIManager.ConsoleFont; + HighlightText.font = UIManager.ConsoleFont; + + + } + } +} diff --git a/src/UI/Panels/InspectorPanel.cs b/src/UI/Panels/InspectorPanel.cs new file mode 100644 index 0000000..1c08e14 --- /dev/null +++ b/src/UI/Panels/InspectorPanel.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.UI.Inspectors; + +namespace UnityExplorer.UI.Panels +{ + public class InspectorPanel : UIPanel + { + public static InspectorPanel Instance { get; private set; } + + public InspectorPanel() { Instance = this; } + + public override string Name => "Inspector"; + public override UIManager.Panels PanelType => UIManager.Panels.Inspector; + public override bool ShouldSaveActiveState => false; + public override int MinWidth => 810; + public override int MinHeight => 350; + + public GameObject NavbarHolder; + public GameObject ContentHolder; + public RectTransform ContentRect; + + public static float CurrentPanelWidth => Instance.mainPanelRect.rect.width; + public static float CurrentPanelHeight => Instance.mainPanelRect.rect.height; + + public override void Update() + { + InspectorManager.Update(); + } + + public override void OnFinishResize(RectTransform panel) + { + base.OnFinishResize(panel); + + InspectorManager.PanelWidth = this.mainPanelRect.rect.width; + InspectorManager.OnPanelResized(panel.rect.width); + } + + public override string GetSaveDataFromConfigManager() => ConfigManager.InspectorData.Value; + + public override void DoSaveToConfigElement() + { + ConfigManager.InspectorData.Value = this.ToSaveData(); + } + + protected internal override void DoSetDefaultPosAndAnchors() + { + mainPanelRect.localPosition = Vector2.zero; + mainPanelRect.pivot = new Vector2(0f, 1f); + mainPanelRect.anchorMin = new Vector2(0.35f, 0.175f); + mainPanelRect.anchorMax = new Vector2(0.8f, 0.925f); + } + + public override void ConstructPanelContent() + { + // add close all button to titlebar + + var closeAllBtn = UIFactory.CreateButton(this.titleBar.transform.Find("CloseHolder").gameObject, "CloseAllBtn", "Close All", + new Color(0.3f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(closeAllBtn.Component.gameObject, minHeight: 25, minWidth: 80); + closeAllBtn.Component.transform.SetSiblingIndex(closeAllBtn.Component.transform.GetSiblingIndex() - 1); + closeAllBtn.OnClick += InspectorManager.CloseAllTabs; + + // this.UIRoot.GetComponent().enabled = false; + + UIFactory.SetLayoutGroup(this.content, true, true, true, true, 4, padLeft: 5, padRight: 5); + + this.NavbarHolder = UIFactory.CreateGridGroup(this.content, "Navbar", new Vector2(200, 22), new Vector2(4, 4), + new Color(0.05f, 0.05f, 0.05f)); + //UIFactory.SetLayoutElement(NavbarHolder, flexibleWidth: 9999, minHeight: 0, preferredHeight: 0, flexibleHeight: 9999); + NavbarHolder.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + this.ContentHolder = UIFactory.CreateVerticalGroup(this.content, "ContentHolder", true, true, true, true, 0, default, + new Color(0.1f, 0.1f, 0.1f)); + UIFactory.SetLayoutElement(ContentHolder, flexibleHeight: 9999); + ContentRect = ContentHolder.GetComponent(); + + UIManager.SetPanelActive(PanelType, false); + } + } +} \ No newline at end of file diff --git a/src/UI/Panels/LogPanel.cs b/src/UI/Panels/LogPanel.cs new file mode 100644 index 0000000..43a5db6 --- /dev/null +++ b/src/UI/Panels/LogPanel.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Panels +{ + // TODO move the logic out of this class into a LogUtil class (also move ExplorerCore.Log into that) + + public class LogPanel : UIPanel, ICellPoolDataSource + { + public struct LogInfo + { + public string message; + public LogType type; + + public LogInfo(string message, LogType type) { this.message = message; this.type = type; } + } + + private static readonly List Logs = new List(); + private static string CurrentStreamPath; + + public override string Name => "Log"; + public override UIManager.Panels PanelType => UIManager.Panels.ConsoleLog; + + public override int MinWidth => 350; + public override int MinHeight => 75; + public override bool ShouldSaveActiveState => true; + public override bool ShowByDefault => true; + + public int ItemCount => Logs.Count; + + private static ScrollPool logScrollPool; + + public LogPanel() + { + SetupIO(); + } + + private static bool DoneScrollPoolInit; + + public override void SetActive(bool active) + { + base.SetActive(active); + + if (active && !DoneScrollPoolInit) + { + LayoutRebuilder.ForceRebuildLayoutImmediate(this.mainPanelRect); + logScrollPool.Initialize(this); + DoneScrollPoolInit = true; + } + + logScrollPool.Refresh(true, false); + } + + private void SetupIO() + { + var path = Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Logs"); + path = IOUtility.EnsureValidDirectory(path); + + // clean old log(s) + var files = Directory.GetFiles(path); + if (files.Length >= 10) + { + var sorted = files.ToList(); + // sort by 'datetime.ToString("u")' will put the oldest ones first + sorted.Sort(); + for (int i = 0; i < files.Length - 9; i++) + File.Delete(files[i]); + } + + var fileName = $"UnityExplorer {DateTime.Now:u}.txt"; + fileName = IOUtility.EnsureValidFilename(fileName); + + CurrentStreamPath = Path.Combine(path, fileName); + + File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message).ToArray()); + } + + // Logging + + public static void Log(string message, LogType type) + { + Logs.Add(new LogInfo(message, type)); + + if (CurrentStreamPath != null) + File.AppendAllText(CurrentStreamPath, '\n' + message); + + if (logScrollPool != null) + logScrollPool.Refresh(true, false); + } + + private static void ClearLogs() + { + Logs.Clear(); + logScrollPool.Refresh(true, true); + } + + private static void OpenLogFile() + { + if (File.Exists(CurrentStreamPath)) + Process.Start(CurrentStreamPath); + } + + // Cell pool + + private static readonly Dictionary logColors = new Dictionary + { + { LogType.Log, Color.white }, + { LogType.Warning, Color.yellow }, + { LogType.Assert, Color.yellow }, + { LogType.Error, Color.red }, + { LogType.Exception, Color.red }, + }; + + private readonly Color logEvenColor = new Color(0.34f, 0.34f, 0.34f); + private readonly Color logOddColor = new Color(0.28f, 0.28f, 0.28f); + + public void OnCellBorrowed(ConsoleLogCell cell) { } + + public void SetCell(ConsoleLogCell cell, int index) + { + if (index >= Logs.Count) + { + cell.Disable(); + return; + } + + // Logs are displayed in reverse order (newest at top) + index = Logs.Count - index - 1; + + var log = Logs[index]; + cell.IndexLabel.text = $"{index}:"; + cell.Input.Text = log.message; + cell.Input.Component.textComponent.color = logColors[log.type]; + + var color = index % 2 == 0 ? logEvenColor : logOddColor; + RuntimeProvider.Instance.SetColorBlock(cell.Input.Component, color); + } + + // Panel save data + + public override string GetSaveDataFromConfigManager() + { + return ConfigManager.ConsoleLogData.Value; + } + + public override void DoSaveToConfigElement() + { + ConfigManager.ConsoleLogData.Value = this.ToSaveData(); + } + + protected internal override void DoSetDefaultPosAndAnchors() + { + mainPanelRect.localPosition = Vector2.zero; + mainPanelRect.pivot = new Vector2(0f, 1f); + mainPanelRect.anchorMin = new Vector2(0.5f, 0.03f); + mainPanelRect.anchorMax = new Vector2(0.9f, 0.2f); + } + + // UI Construction + + public override void ConstructPanelContent() + { + // Log scroll pool + + logScrollPool = UIFactory.CreateScrollPool(this.content, "Logs", out GameObject scrollObj, + out GameObject scrollContent, new Color(0.03f, 0.03f, 0.03f)); + UIFactory.SetLayoutElement(scrollObj, flexibleWidth: 9999, flexibleHeight: 9999); + + // Buttons and toggles + + var optionsRow = UIFactory.CreateUIObject("OptionsRow", this.content); + UIFactory.SetLayoutElement(optionsRow, minHeight: 25, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(optionsRow, false, false, true, true, 5, 2, 2, 2, 2); + + var clearButton = UIFactory.CreateButton(optionsRow, "ClearButton", "Clear", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(clearButton.Component.gameObject, minHeight: 23, flexibleHeight: 0, minWidth: 60); + clearButton.OnClick += ClearLogs; + clearButton.Component.transform.SetSiblingIndex(1); + + var fileButton = UIFactory.CreateButton(optionsRow, "FileButton", "Open Log File", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(fileButton.Component.gameObject, minHeight: 23, flexibleHeight: 0, minWidth: 100); + fileButton.OnClick += OpenLogFile; + fileButton.Component.transform.SetSiblingIndex(2); + + var unityToggle = UIFactory.CreateToggle(optionsRow, "UnityLogToggle", out var toggle, out var toggleText); + UIFactory.SetLayoutElement(unityToggle, minHeight: 25, minWidth: 150); + toggleText.text = "Log Unity Debug?"; + toggle.isOn = ConfigManager.Log_Unity_Debug.Value; + ConfigManager.Log_Unity_Debug.OnValueChanged += (bool val) => toggle.isOn = val; + toggle.onValueChanged.AddListener((bool val) => ConfigManager.Log_Unity_Debug.Value = val); + } + } + + #region Log Cell View + + public class ConsoleLogCell : ICell + { + public Text IndexLabel; + public InputFieldRef Input; + + public RectTransform Rect { get; set; } + public GameObject UIRoot { get; set; } + + public float DefaultHeight => 25; + + public bool Enabled => UIRoot.activeInHierarchy; + public void Enable() => UIRoot.SetActive(true); + public void Disable() => UIRoot.SetActive(false); + + + public GameObject CreateContent(GameObject parent) + { + UIRoot = UIFactory.CreateUIObject("LogCell", parent, new Vector2(25, 25)); + Rect = UIRoot.GetComponent(); + UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, 3); + UIFactory.SetLayoutElement(UIRoot, minHeight: 25, minWidth: 50, flexibleWidth: 9999); + + IndexLabel = UIFactory.CreateLabel(UIRoot, "IndexLabel", "i:", TextAnchor.MiddleCenter, Color.grey, false, 12); + UIFactory.SetLayoutElement(IndexLabel.gameObject, minHeight: 25, minWidth: 30, flexibleWidth: 40); + + Input = UIFactory.CreateInputField(UIRoot, "Input", ""); + //Input.Component.gameObject.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; + UIFactory.SetLayoutElement(Input.UIRoot, minHeight: 25, flexibleWidth: 9999); + RuntimeProvider.Instance.SetColorBlock(Input.Component, new Color(0.1f, 0.1f, 0.1f), new Color(0.13f, 0.13f, 0.13f), + new Color(0.07f, 0.07f, 0.07f)); + Input.Component.GetComponent().color = new Color(0.2f, 0.2f, 0.2f); + + Input.Component.readOnly = true; + Input.Component.textComponent.supportRichText = true; + Input.Component.lineType = InputField.LineType.MultiLineNewline; + Input.Component.textComponent.font = UIManager.ConsoleFont; + Input.PlaceholderText.font = UIManager.ConsoleFont; + + return UIRoot; + } + } + + #endregion +} diff --git a/src/UI/Panels/ObjectExplorerPanel.cs b/src/UI/Panels/ObjectExplorerPanel.cs new file mode 100644 index 0000000..9d32e89 --- /dev/null +++ b/src/UI/Panels/ObjectExplorerPanel.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UI; +using UnityExplorer.Core; +using UnityExplorer.Core.Config; +using UnityExplorer.UI.Models; +using UnityExplorer.UI.ObjectExplorer; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Panels +{ + public class ObjectExplorerPanel : UIPanel + { + public override string Name => "Object Explorer"; + public override UIManager.Panels PanelType => UIManager.Panels.ObjectExplorer; + public override int MinWidth => 350; + public override int MinHeight => 200; + + public SceneExplorer SceneExplorer; + public ObjectSearch ObjectSearch; + + public override bool ShowByDefault => true; + public override bool ShouldSaveActiveState => true; + + public int SelectedTab = 0; + private readonly List tabPages = new List(); + private readonly List tabButtons = new List(); + + public void SetTab(int tabIndex) + { + if (SelectedTab != -1) + DisableTab(SelectedTab); + + var content = tabPages[tabIndex]; + content.SetActive(true); + + var button = tabButtons[tabIndex]; + RuntimeProvider.Instance.SetColorBlock(button.Component, UIManager.enabledButtonColor, UIManager.enabledButtonColor * 1.2f); + + SelectedTab = tabIndex; + SaveToConfigManager(); + } + + private void DisableTab(int tabIndex) + { + tabPages[tabIndex].SetActive(false); + RuntimeProvider.Instance.SetColorBlock(tabButtons[tabIndex].Component, UIManager.disabledButtonColor, UIManager.disabledButtonColor * 1.2f); + } + + public override void Update() + { + if (SelectedTab == 0) + SceneExplorer.Update(); + else + ObjectSearch.Update(); + } + + public override string GetSaveDataFromConfigManager() => ConfigManager.ObjectExplorerData.Value; + + public override void DoSaveToConfigElement() + { + ConfigManager.ObjectExplorerData.Value = this.ToSaveData(); + } + + public override string ToSaveData() + { + string ret = base.ToSaveData(); + ret += "|" + SelectedTab; + return ret; + } + + public override void ApplySaveData(string data) + { + base.ApplySaveData(data); + + try + { + int tab = int.Parse(data.Split('|').Last()); + SelectedTab = tab; + } + catch + { + SelectedTab = 0; + } + + SelectedTab = Math.Max(0, SelectedTab); + SelectedTab = Math.Min(1, SelectedTab); + + SetTab(SelectedTab); + } + + protected internal override void DoSetDefaultPosAndAnchors() + { + mainPanelRect.localPosition = Vector2.zero; + mainPanelRect.pivot = new Vector2(0f, 1f); + mainPanelRect.anchorMin = new Vector2(0.125f, 0.175f); + mainPanelRect.anchorMax = new Vector2(0.325f, 0.925f); + //mainPanelRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 350); + } + + public override void ConstructPanelContent() + { + // Tab bar + var tabGroup = UIFactory.CreateHorizontalGroup(content, "TabBar", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(tabGroup, minHeight: 25, flexibleHeight: 0); + + // Scene Explorer + SceneExplorer = new SceneExplorer(this); + SceneExplorer.ConstructUI(content); + tabPages.Add(SceneExplorer); + + // Object search + ObjectSearch = new ObjectSearch(this); + ObjectSearch.ConstructUI(content); + tabPages.Add(ObjectSearch); + + // set up tabs + AddTabButton(tabGroup, "Scene Explorer"); + AddTabButton(tabGroup, "Object Search"); + + // default active state: Active + UIManager.SetPanelActive(PanelType, true); + } + + private void AddTabButton(GameObject tabGroup, string label) + { + var button = UIFactory.CreateButton(tabGroup, $"Button_{label}", label); + + int idx = tabButtons.Count; + //button.onClick.AddListener(() => { SetTab(idx); }); + button.OnClick += () => { SetTab(idx); }; + + tabButtons.Add(button); + + DisableTab(tabButtons.Count - 1); + } + } +} diff --git a/src/UI/Panels/OptionsPanel.cs b/src/UI/Panels/OptionsPanel.cs new file mode 100644 index 0000000..4da55bc --- /dev/null +++ b/src/UI/Panels/OptionsPanel.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.UI.CacheObject; +using UnityExplorer.UI.CacheObject.Views; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Panels +{ + // TODO move the logic out of this class into ConfigManager + + public class OptionsPanel : UIPanel, ICacheObjectController, ICellPoolDataSource + { + public override string Name => "Options"; + public override UIManager.Panels PanelType => UIManager.Panels.Options; + + public override int MinWidth => 600; + public override int MinHeight => 200; + + public override bool ShouldSaveActiveState => false; + public override bool ShowByDefault => false; + + // Entry holders + private readonly List configEntries = new List(); + + // ICacheObjectController + public CacheObjectBase ParentCacheObject => null; + public object Target => null; + public Type TargetType => null; + public bool CanWrite => true; + + // ICellPoolDataSource + public int ItemCount => configEntries.Count; + + public OptionsPanel() + { + foreach (var entry in ConfigManager.ConfigElements) + { + var cache = new CacheConfigEntry(entry.Value); + cache.Owner = this; + configEntries.Add(cache); + } + } + + public void OnCellBorrowed(ConfigEntryCell cell) + { + } + + public void SetCell(ConfigEntryCell cell, int index) + { + CacheObjectControllerHelper.SetCell(cell, index, this.configEntries, null); + } + + // Panel save data + + public override string GetSaveDataFromConfigManager() + { + return ConfigManager.OptionsPanelData.Value; + } + + public override void DoSaveToConfigElement() + { + ConfigManager.OptionsPanelData.Value = this.ToSaveData(); + } + + protected internal override void DoSetDefaultPosAndAnchors() + { + mainPanelRect.localPosition = Vector2.zero; + mainPanelRect.pivot = new Vector2(0f, 1f); + mainPanelRect.anchorMin = new Vector2(0.5f, 0.1f); + mainPanelRect.anchorMax = new Vector2(0.5f, 0.85f); + mainPanelRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 600f); + } + + // UI Construction + + public override void ConstructPanelContent() + { + // Save button + + var saveBtn = UIFactory.CreateButton(this.content, "Save", "Save Options", new Color(0.2f, 0.3f, 0.2f)); + UIFactory.SetLayoutElement(saveBtn.Component.gameObject, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 0); + saveBtn.OnClick += ConfigManager.Handler.SaveConfig; + + // Config entries + + var scrollPool = UIFactory.CreateScrollPool(this.content, "ConfigEntries", out GameObject scrollObj, + out GameObject scrollContent); + + scrollPool.Initialize(this); + + foreach (var config in configEntries) + config.UpdateValueFromSource(); + } + } +} diff --git a/src/UI/Panels/PanelDragger.cs b/src/UI/Panels/PanelDragger.cs new file mode 100644 index 0000000..abc856e --- /dev/null +++ b/src/UI/Panels/PanelDragger.cs @@ -0,0 +1,494 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Input; +using System.IO; +using System.Diagnostics; +using UnityExplorer.UI.Models; +using System.Linq; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.Panels +{ + public class PanelDragger + { + #region Static + + public static bool Resizing { get; private set; } + + public static bool ResizePrompting => s_resizeCursorObj && s_resizeCursorObj.activeSelf; + + internal static void ForceEnd() + { + s_resizeCursorObj.SetActive(false); + wasAnyDragging = false; + + foreach (var instance in Instances) + { + instance.WasDragging = false; + instance.WasResizing = false; + } + } + + internal static List Instances = new List(); + + static PanelDragger() + { + UIPanel.OnPanelsReordered += OnPanelsReordered; + } + + public static void OnPanelsReordered() + { + Instances.Sort((a, b) => b.Panel.GetSiblingIndex().CompareTo(a.Panel.GetSiblingIndex())); + + // move AutoCompleter to bottom + if (AutoCompleteModal.Instance != null) + { + var idx = Instances.IndexOf(AutoCompleteModal.Instance.Dragger); + Instances.RemoveAt(idx); + Instances.Insert(0, AutoCompleteModal.Instance.Dragger); + } + } + + private enum MouseState + { + Down, + Held, + NotPressed + } + + private static bool handledInstanceThisFrame; + + public static void UpdateInstances() + { + if (!s_resizeCursorObj) + CreateCursorUI(); + + MouseState state; + if (InputManager.GetMouseButtonDown(0)) + state = MouseState.Down; + else if (InputManager.GetMouseButton(0)) + state = MouseState.Held; + else + state = MouseState.NotPressed; + + var mousePos = InputManager.MousePosition; + + handledInstanceThisFrame = false; + foreach (var instance in Instances) + { + if (!instance.Panel.gameObject.activeSelf) + continue; + + instance.Update(state, mousePos); + if (handledInstanceThisFrame) + break; + } + + if (wasAnyDragging && state == MouseState.NotPressed) + { + foreach (var instance in Instances) + instance.WasDragging = false; + wasAnyDragging = false; + } + } + + #endregion + + public static GameObject s_resizeCursorObj; + + internal static bool wasAnyDragging; + + // Instance + + public UIPanel UIPanel { get; private set; } + public bool AllowDragAndResize => UIPanel.CanDragAndResize; + + public RectTransform Panel { get; set; } + public event Action OnFinishResize; + public event Action OnFinishDrag; + + private readonly RectTransform canvasTransform; + + // Dragging + public RectTransform DragableArea { get; set; } + public bool WasDragging { get; set; } + private Vector2 m_lastDragPosition; + + // Resizing + private const int RESIZE_THICKNESS = 10; + + //internal readonly Vector2 minResize = new Vector2(200, 50); + + private bool WasResizing { get; set; } + private ResizeTypes m_currentResizeType = ResizeTypes.NONE; + private Vector2 m_lastResizePos; + + private bool WasHoveringResize => s_resizeCursorObj.activeInHierarchy; + + private ResizeTypes m_lastResizeHoverType; + + private Rect m_totalResizeRect; + + public PanelDragger(RectTransform dragArea, RectTransform panelToDrag, UIPanel panel) + { + this.UIPanel = panel; + Instances.Add(this); + DragableArea = dragArea; + Panel = panelToDrag; + + if (!canvasTransform) + canvasTransform = Panel.GetComponentInParent().GetComponent(); + + UpdateResizeCache(); + } + + public void Destroy() + { + if (s_resizeCursorObj) + GameObject.Destroy(s_resizeCursorObj); + + if (Instances.Contains(this)) + Instances.Remove(this); + } + + private void Update(MouseState state, Vector3 rawMousePos) + { + ResizeTypes type; + Vector3 resizePos = Panel.InverseTransformPoint(rawMousePos); + bool inResizePos = !UIManager.NavBarRect.rect.Contains(UIManager.NavBarRect.InverseTransformPoint(rawMousePos)) + && MouseInResizeArea(resizePos); + + Vector3 dragPos = DragableArea.InverseTransformPoint(rawMousePos); + bool inDragPos = DragableArea.rect.Contains(dragPos); + + if (WasHoveringResize && s_resizeCursorObj) + UpdateHoverImagePos(); + + switch (state) + { + case MouseState.Down: + if (inDragPos || inResizePos) + UIManager.SetPanelActive(Panel, true); + + if (inDragPos) + { + if (AllowDragAndResize) + OnBeginDrag(); + handledInstanceThisFrame = true; + return; + } + else if (inResizePos) + { + type = GetResizeType(resizePos); + if (type != ResizeTypes.NONE) + OnBeginResize(type); + + handledInstanceThisFrame = true; + } + break; + + case MouseState.Held: + if (WasDragging) + { + OnDrag(); + handledInstanceThisFrame = true; + } + else if (WasResizing) + { + OnResize(); + handledInstanceThisFrame = true; + } + break; + + case MouseState.NotPressed: + if (AllowDragAndResize && inDragPos) + { + if (WasDragging) + OnEndDrag(); + + if (WasHoveringResize) + OnHoverResizeEnd(); + + handledInstanceThisFrame = true; + } + else if (inResizePos || WasResizing) + { + if (WasResizing) + OnEndResize(); + + type = GetResizeType(resizePos); + if (type != ResizeTypes.NONE) + OnHoverResize(type); + else if (WasHoveringResize) + OnHoverResizeEnd(); + + handledInstanceThisFrame = true; + } + else if (WasHoveringResize) + OnHoverResizeEnd(); + break; + } + + return; + } + + #region DRAGGING + + public void OnBeginDrag() + { + wasAnyDragging = true; + WasDragging = true; + m_lastDragPosition = InputManager.MousePosition; + } + + public void OnDrag() + { + var mousePos = InputManager.MousePosition; + + Vector2 diff = (Vector2)mousePos - m_lastDragPosition; + m_lastDragPosition = mousePos; + + var pos = Panel.localPosition + (Vector3)diff; + + // Prevent panel going oustide screen bounds + var halfW = Screen.width * 0.5f; + var halfH = Screen.height * 0.5f; + pos.x = Math.Max(-halfW, Math.Min(pos.x, halfW - Panel.rect.width)); + pos.y = Math.Max(-halfH + Panel.rect.height, Math.Min(pos.y, halfH)); + + Panel.localPosition = pos; + } + + public void OnEndDrag() + { + WasDragging = false; + + OnFinishDrag?.Invoke(Panel); + } + + #endregion + + #region RESIZE + + private readonly Dictionary m_resizeMask = new Dictionary + { + { ResizeTypes.Top, default }, + { ResizeTypes.Left, default }, + { ResizeTypes.Right, default }, + { ResizeTypes.Bottom, default }, + }; + + [Flags] + public enum ResizeTypes : ulong + { + NONE = 0, + Top = 1, + Left = 2, + Right = 4, + Bottom = 8, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right, + } + + // private const int HALF_THICKESS = RESIZE_THICKNESS / 2; + private const int DBL_THICKESS = RESIZE_THICKNESS * 2; + + private void UpdateResizeCache() + { + m_totalResizeRect = new Rect(Panel.rect.x - RESIZE_THICKNESS + 1, + Panel.rect.y - RESIZE_THICKNESS + 1, + Panel.rect.width + DBL_THICKESS - 2, + Panel.rect.height + DBL_THICKESS - 2); + + // calculate the four cross sections to use as flags + if (AllowDragAndResize) + { + m_resizeMask[ResizeTypes.Bottom] = new Rect( + m_totalResizeRect.x, + m_totalResizeRect.y, + m_totalResizeRect.width, + RESIZE_THICKNESS); + + m_resizeMask[ResizeTypes.Left] = new Rect( + m_totalResizeRect.x, + m_totalResizeRect.y, + RESIZE_THICKNESS, + m_totalResizeRect.height); + + m_resizeMask[ResizeTypes.Top] = new Rect( + m_totalResizeRect.x, + Panel.rect.y + Panel.rect.height - 2, + m_totalResizeRect.width, + RESIZE_THICKNESS); + + m_resizeMask[ResizeTypes.Right] = new Rect( + m_totalResizeRect.x + Panel.rect.width + RESIZE_THICKNESS - 2, + m_totalResizeRect.y, + RESIZE_THICKNESS, + m_totalResizeRect.height); + } + } + + private bool MouseInResizeArea(Vector2 mousePos) + { + return m_totalResizeRect.Contains(mousePos); + } + + private ResizeTypes GetResizeType(Vector2 mousePos) + { + // Calculate which part of the resize area we're in, if any. + // More readable method commented out below. + + int mask = 0; + mask |= (int)ResizeTypes.Top * (m_resizeMask[ResizeTypes.Top].Contains(mousePos) ? 1 : 0); + mask |= (int)ResizeTypes.Bottom * (m_resizeMask[ResizeTypes.Bottom].Contains(mousePos) ? 1 : 0); + mask |= (int)ResizeTypes.Left * (m_resizeMask[ResizeTypes.Left].Contains(mousePos) ? 1 : 0); + mask |= (int)ResizeTypes.Right * (m_resizeMask[ResizeTypes.Right].Contains(mousePos) ? 1 : 0); + + //if (m_resizeMask[ResizeTypes.Top].Contains(mousePos)) + // mask |= ResizeTypes.Top; + //else if (m_resizeMask[ResizeTypes.Bottom].Contains(mousePos)) + // mask |= ResizeTypes.Bottom; + + //if (m_resizeMask[ResizeTypes.Left].Contains(mousePos)) + // mask |= ResizeTypes.Left; + //else if (m_resizeMask[ResizeTypes.Right].Contains(mousePos)) + // mask |= ResizeTypes.Right; + + return (ResizeTypes)mask; + } + + public void OnHoverResize(ResizeTypes resizeType) + { + if (WasHoveringResize && m_lastResizeHoverType == resizeType) + return; + + // we are entering resize, or the resize type has changed. + + //WasHoveringResize = true; + m_lastResizeHoverType = resizeType; + + s_resizeCursorObj.SetActive(true); + s_resizeCursorObj.transform.SetAsLastSibling(); + + // set the rotation for the resize icon + float iconRotation = 0f; + switch (resizeType) + { + case ResizeTypes.TopRight: + case ResizeTypes.BottomLeft: + iconRotation = 45f; break; + case ResizeTypes.Top: + case ResizeTypes.Bottom: + iconRotation = 90f; break; + case ResizeTypes.TopLeft: + case ResizeTypes.BottomRight: + iconRotation = 135f; break; + } + + Quaternion rot = s_resizeCursorObj.transform.rotation; + rot.eulerAngles = new Vector3(0, 0, iconRotation); + s_resizeCursorObj.transform.rotation = rot; + + UpdateHoverImagePos(); + } + + // update the resize icon position to be above the mouse + private void UpdateHoverImagePos() + { + s_resizeCursorObj.transform.localPosition = canvasTransform.InverseTransformPoint(InputManager.MousePosition); + } + + public void OnHoverResizeEnd() + { + //WasHoveringResize = false; + s_resizeCursorObj.SetActive(false); + } + + public void OnBeginResize(ResizeTypes resizeType) + { + m_currentResizeType = resizeType; + m_lastResizePos = InputManager.MousePosition; + WasResizing = true; + Resizing = true; + } + + public void OnResize() + { + Vector3 mousePos = InputManager.MousePosition; + Vector2 diff = m_lastResizePos - (Vector2)mousePos; + + if ((Vector2)mousePos == m_lastResizePos) + return; + + m_lastResizePos = mousePos; + + float diffX = (float)((decimal)diff.x / Screen.width); + float diffY = (float)((decimal)diff.y / Screen.height); + + Vector2 anchorMin = Panel.anchorMin; + Vector2 anchorMax = Panel.anchorMax; + + if (m_currentResizeType.HasFlag(ResizeTypes.Left)) + anchorMin.x -= diffX; + else if (m_currentResizeType.HasFlag(ResizeTypes.Right)) + anchorMax.x -= diffX; + + if (m_currentResizeType.HasFlag(ResizeTypes.Top)) + anchorMax.y -= diffY; + else if (m_currentResizeType.HasFlag(ResizeTypes.Bottom)) + anchorMin.y -= diffY; + + var prevMin = Panel.anchorMin; + var prevMax = Panel.anchorMax; + + Panel.anchorMin = new Vector2(anchorMin.x, anchorMin.y); + Panel.anchorMax = new Vector2(anchorMax.x, anchorMax.y); + + if (Panel.rect.width < UIPanel.MinWidth) + { + Panel.anchorMin = new Vector2(prevMin.x, Panel.anchorMin.y); + Panel.anchorMax = new Vector2(prevMax.x, Panel.anchorMax.y); + } + if (Panel.rect.height < UIPanel.MinHeight) + { + Panel.anchorMin = new Vector2(Panel.anchorMin.x, prevMin.y); + Panel.anchorMax = new Vector2(Panel.anchorMax.x, prevMax.y); + } + } + + public void OnEndResize() + { + WasResizing = false; + Resizing = false; + try { OnHoverResizeEnd(); } catch { } + UpdateResizeCache(); + OnFinishResize?.Invoke(Panel); + } + + internal static void CreateCursorUI() + { + try + { + var text = UIFactory.CreateLabel(UIManager.CanvasRoot, "ResizeCursor", "↔", TextAnchor.MiddleCenter, Color.white, true, 35); + s_resizeCursorObj = text.gameObject; + + RectTransform rect = s_resizeCursorObj.GetComponent(); + rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 64); + rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 64); + + s_resizeCursorObj.SetActive(false); + } + catch (Exception e) + { + ExplorerCore.LogWarning("Exception creating Resize Cursor UI!\r\n" + e.ToString()); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/UI/Panels/UIPanel.cs b/src/UI/Panels/UIPanel.cs new file mode 100644 index 0000000..68aa2cf --- /dev/null +++ b/src/UI/Panels/UIPanel.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Core.Config; +using UnityExplorer.Core.Input; +using UnityExplorer.UI.Models; +using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; + +namespace UnityExplorer.UI.Panels +{ + public abstract class UIPanel : UIBehaviourModel + { + #region STATIC + + internal static void InvokeOnPanelsReordered() => OnPanelsReordered?.Invoke(); + + public static event Action OnPanelsReordered; + public static event Action OnClickedOutsidePanels; + + internal static readonly List instances = new List(); + internal static readonly Dictionary transformToPanelDict = new Dictionary(); + + public static void UpdateFocus() + { + if (PanelDragger.ResizePrompting) + return; + + // if the user is clicking + if (InputManager.GetMouseButtonDown(0) || InputManager.GetMouseButtonDown(1)) + { + int count = UIManager.PanelHolder.transform.childCount; + var mousePos = InputManager.MousePosition; + bool clickedInAny = false; + for (int i = count - 1; i >= 0; i--) + { + // make sure this is a real recognized panel + var transform = UIManager.PanelHolder.transform.GetChild(i); + if (!transformToPanelDict.TryGetValue(transform.GetInstanceID(), out UIPanel panel)) + continue; + + // check if our mouse is clicking inside the panel + var pos = panel.mainPanelRect.InverseTransformPoint(mousePos); + if (!panel.Enabled || !panel.mainPanelRect.rect.Contains(pos)) + continue; + + // if this is not the top panel, reorder and invoke the onchanged event + if (transform.GetSiblingIndex() != count - 1) + { + transform.SetAsLastSibling(); + OnPanelsReordered?.Invoke(); + } + // panel was found, break + clickedInAny = true; + break; + } + + if (!clickedInAny) + OnClickedOutsidePanels?.Invoke(); + } + } + + #endregion + + // INSTANCE + + public UIPanel() + { + instances.Add(this); + } + + public abstract UIManager.Panels PanelType { get; } + public abstract string Name { get; } + public abstract int MinWidth { get; } + public abstract int MinHeight { get; } + + public virtual bool ShowByDefault => false; + public virtual bool ShouldSaveActiveState => true; + public virtual bool CanDragAndResize => true; + public virtual bool NavButtonWanted => true; + + public ButtonRef NavButton; + public PanelDragger Dragger; + + public override GameObject UIRoot => uiRoot; + protected GameObject uiRoot; + protected RectTransform mainPanelRect; + public GameObject content; + + public abstract void ConstructPanelContent(); + + public virtual void OnFinishResize(RectTransform panel) + { + SaveToConfigManager(); + } + + public virtual void OnFinishDrag(RectTransform panel) + { + SaveToConfigManager(); + } + + public override void SetActive(bool active) + { + if (this.Enabled.Equals(active)) + return; + + base.SetActive(active); + + if (!ApplyingSaveData) + SaveToConfigManager(); + + if (NavButtonWanted) + { + if (active) + RuntimeProvider.Instance.SetColorBlock(NavButton.Component, UIManager.enabledButtonColor, UIManager.enabledButtonColor * 1.2f); + else + RuntimeProvider.Instance.SetColorBlock(NavButton.Component, UIManager.disabledButtonColor, UIManager.disabledButtonColor * 1.2f); + } + + if (!active) + this.Dragger.WasDragging = false; + } + + public override void Destroy() + { + instances.Remove(this); + base.Destroy(); + } + + protected internal abstract void DoSetDefaultPosAndAnchors(); + + public void SetTransformDefaults() + { + DoSetDefaultPosAndAnchors(); + + if (mainPanelRect.rect.width < MinWidth) + mainPanelRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, MinWidth); + if (mainPanelRect.rect.height < MinHeight) + mainPanelRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, MinHeight); + } + + public GameObject titleBar; + + public void ConstructUI() + { + //this.Enabled = true; + + if (NavButtonWanted) + { + // create navbar button + + NavButton = UIFactory.CreateButton(UIManager.NavbarButtonHolder, $"Button_{PanelType}", Name); + var navBtn = NavButton.Component.gameObject; + navBtn.AddComponent().horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + UIFactory.SetLayoutGroup(navBtn, false, true, true, true, 0, 0, 0, 5, 5, TextAnchor.MiddleCenter); + UIFactory.SetLayoutElement(navBtn, minWidth: 80); + + RuntimeProvider.Instance.SetColorBlock(NavButton.Component, UIManager.disabledButtonColor, UIManager.disabledButtonColor * 1.2f); + NavButton.OnClick += () => { UIManager.TogglePanel(PanelType); }; + + var txtObj = navBtn.transform.Find("Text").gameObject; + txtObj.AddComponent().horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + } + + // create core canvas + uiRoot = UIFactory.CreatePanel(Name, out GameObject panelContent); + mainPanelRect = this.uiRoot.GetComponent(); + UIFactory.SetLayoutGroup(this.uiRoot, false, false, true, true, 0, 2, 2, 2, 2, TextAnchor.UpperLeft); + + int id = this.uiRoot.transform.GetInstanceID(); + transformToPanelDict.Add(id, this); + + content = panelContent; + UIFactory.SetLayoutGroup(this.content, false, false, true, true, 2, 2, 2, 2, 2, TextAnchor.UpperLeft); + + // always apply default pos and anchors (save data may only be partial) + SetTransformDefaults(); + + // Title bar + titleBar = UIFactory.CreateHorizontalGroup(content, "TitleBar", false, true, true, true, 2, + new Vector4(2, 2, 2, 2), new Color(0.06f, 0.06f, 0.06f)); + UIFactory.SetLayoutElement(titleBar, minHeight: 25, flexibleHeight: 0); + + // Title text + + var titleTxt = UIFactory.CreateLabel(titleBar, "TitleBar", Name, TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(titleTxt.gameObject, minWidth: 250, minHeight: 25, flexibleHeight: 0); + + // close button + + var closeHolder = UIFactory.CreateUIObject("CloseHolder", titleBar); + UIFactory.SetLayoutElement(closeHolder, minHeight: 25, flexibleHeight: 0, minWidth: 30, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(closeHolder, false, false, true, true, 3, childAlignment: TextAnchor.MiddleRight); + var closeBtn = UIFactory.CreateButton(closeHolder, "CloseButton", "—"); + UIFactory.SetLayoutElement(closeBtn.Component.gameObject, minHeight: 25, minWidth: 25, flexibleWidth: 0); + RuntimeProvider.Instance.SetColorBlock(closeBtn.Component, new Color(0.33f, 0.32f, 0.31f)); + + closeBtn.OnClick += () => + { + UIManager.SetPanelActive(this.PanelType, false); + SaveToConfigManager(); + }; + + if (!CanDragAndResize) + titleBar.SetActive(false); + + // Panel dragger + + Dragger = new PanelDragger(titleBar.GetComponent(), mainPanelRect, this); + Dragger.OnFinishResize += OnFinishResize; + Dragger.OnFinishDrag += OnFinishDrag; + + // content (abstract) + + ConstructPanelContent(); + + UIManager.SetPanelActive(this.PanelType, true); + UIManager.SetPanelActive(this.PanelType, false); + UIManager.SetPanelActive(this.PanelType, ShowByDefault); + + ApplyingSaveData = true; + // apply panel save data or revert to default + try + { + ApplySaveData(GetSaveDataFromConfigManager()); + } + catch (Exception ex) + { + ExplorerCore.Log($"Exception loading panel save data: {ex}"); + SetTransformDefaults(); + } + + Dragger.OnEndResize(); + + // simple listener for saving enabled state + this.OnToggleEnabled += (bool val) => + { + SaveToConfigManager(); + }; + ApplyingSaveData = false; + } + + public override void ConstructUI(GameObject parent) => ConstructUI(); + + // SAVE DATA + + public abstract void DoSaveToConfigElement(); + + public void SaveToConfigManager() + { + if (UIManager.Initializing) + return; + + DoSaveToConfigElement(); + } + + public abstract string GetSaveDataFromConfigManager(); + + public bool ApplyingSaveData { get; set; } + + public virtual string ToSaveData() + { + try + { + return $"{ShouldSaveActiveState && Enabled}" + + $"|{mainPanelRect.RectAnchorsToString()}" + + $"|{mainPanelRect.RectPositionToString()}"; + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Exception generating Panel save data: {ex}"); + return ""; + } + } + + public virtual void ApplySaveData(string data) + { + if (string.IsNullOrEmpty(data)) + return; + + var split = data.Split('|'); + + try + { + mainPanelRect.SetAnchorsFromString(split[1]); + mainPanelRect.SetPositionFromString(split[2]); + UIManager.SetPanelActive(this.PanelType, bool.Parse(split[0])); + } + catch + { + ExplorerCore.LogWarning("Invalid or corrupt panel save data! Restoring to default."); + SetTransformDefaults(); + } + } + } + + #region WINDOW ANCHORS / POSITION HELPERS + + public static class RectSaveExtensions + { + // Window Anchors helpers + + internal static string RectAnchorsToString(this RectTransform rect) + { + if (!rect) + throw new ArgumentNullException("rect"); + + return string.Format(ParseUtility.en_US, "{0},{1},{2},{3}", new object[] + { + rect.anchorMin.x, + rect.anchorMin.y, + rect.anchorMax.x, + rect.anchorMax.y + }); + } + + internal static void SetAnchorsFromString(this RectTransform panel, string stringAnchors) + { + if (string.IsNullOrEmpty(stringAnchors)) + throw new ArgumentNullException("stringAnchors"); + + var split = stringAnchors.Split(','); + + if (split.Length != 4) + throw new Exception($"stringAnchors split is unexpected length: {split.Length}"); + + Vector4 anchors; + anchors.x = float.Parse(split[0], ParseUtility.en_US); + anchors.y = float.Parse(split[1], ParseUtility.en_US); + anchors.z = float.Parse(split[2], ParseUtility.en_US); + anchors.w = float.Parse(split[3], ParseUtility.en_US); + + panel.anchorMin = new Vector2(anchors.x, anchors.y); + panel.anchorMax = new Vector2(anchors.z, anchors.w); + } + + internal static string RectPositionToString(this RectTransform rect) + { + if (!rect) + throw new ArgumentNullException("rect"); + + return string.Format(ParseUtility.en_US, "{0},{1}", new object[] + { + rect.localPosition.x, rect.localPosition.y + }); + } + + internal static void SetPositionFromString(this RectTransform rect, string stringPosition) + { + var split = stringPosition.Split(','); + + if (split.Length != 2) + throw new Exception($"stringPosition split is unexpected length: {split.Length}"); + + Vector3 vector = rect.localPosition; + vector.x = float.Parse(split[0], ParseUtility.en_US); + vector.y = float.Parse(split[1], ParseUtility.en_US); + rect.localPosition = vector; + } + } + + #endregion +} diff --git a/src/UI/UIFactory.cs b/src/UI/UIFactory.cs index 18da679..914e14c 100644 --- a/src/UI/UIFactory.cs +++ b/src/UI/UIFactory.cs @@ -4,14 +4,18 @@ using UnityEngine.Events; using UnityEngine.UI; using UnityExplorer.Core.Config; using UnityExplorer.Core.Runtime; +using UnityExplorer.UI.Models; using UnityExplorer.UI.Utility; +using UnityExplorer.UI.Widgets; namespace UnityExplorer.UI { public static class UIFactory { - internal static Vector2 _largeElementSize = new Vector2(160f, 30f); - internal static Vector2 _smallElementSize = new Vector2(160f, 20f); + #region Init, Core + + internal static Vector2 _largeElementSize = new Vector2(100, 30); + internal static Vector2 _smallElementSize = new Vector2(25, 25); internal static Color _defaultTextColor = Color.white; internal static Font _defaultFont; @@ -20,26 +24,25 @@ namespace UnityExplorer.UI _defaultFont = Resources.GetBuiltinResource("Arial.ttf"); } - public static GameObject CreateUIObject(string name, GameObject parent = null, Vector2 size = default) + public static GameObject CreateUIObject(string name, GameObject parent, Vector2 size = default) { if (!parent) { - ExplorerCore.LogWarning("Cannot create UI object as the parent is null or destroyed! (" + name + ")"); - return null; + ExplorerCore.LogWarning($"Warning: Creating {name} but parent is null"); + ExplorerCore.Log(Environment.StackTrace); } var obj = new GameObject(name) { layer = 5, - hideFlags = HideFlags.HideAndDontSave + hideFlags = HideFlags.HideAndDontSave, }; - obj.transform.SetParent(parent.transform, false); + if (parent) + obj.transform.SetParent(parent.transform, false); RectTransform rect = obj.AddComponent(); - rect.sizeDelta = size == default - ? _smallElementSize - : size; + rect.sizeDelta = size; return obj; } @@ -54,17 +57,18 @@ namespace UnityExplorer.UI { RuntimeProvider.Instance.SetColorBlock(selectable, new Color(0.2f, 0.2f, 0.2f), new Color(0.3f, 0.3f, 0.3f), new Color(0.15f, 0.15f, 0.15f)); - - // Deselect all Buttons after they are clicked. - if (selectable is Button button) - button.onClick.AddListener(() => { button.OnDeselect(null); }); } + #endregion + + + #region Default Layout Components + /// /// Get and/or Add a LayoutElement component to the GameObject, and set any of the values on it. /// public static LayoutElement SetLayoutElement(GameObject gameObject, int? minWidth = null, int? minHeight = null, - int? flexibleWidth = null, int? flexibleHeight = null, int? preferredWidth = null, int? preferredHeight = null, + int? flexibleWidth = null, int? flexibleHeight = null, int? preferredWidth = null, int? preferredHeight = null, bool? ignoreLayout = null) { var layout = gameObject.GetComponent(); @@ -99,15 +103,15 @@ namespace UnityExplorer.UI /// Get and/or Add a HorizontalOrVerticalLayoutGroup (must pick one) to the GameObject, and set the values on it. /// public static T SetLayoutGroup(GameObject gameObject, bool? forceWidth = null, bool? forceHeight = null, - bool? childControlWidth = null, bool? childControlHeight = null, int? spacing = null, int? padTop = null, - int? padBottom = null, int? padLeft = null, int? padRight = null, TextAnchor? childAlignment = null) + bool? childControlWidth = null, bool? childControlHeight = null, int? spacing = null, int? padTop = null, + int? padBottom = null, int? padLeft = null, int? padRight = null, TextAnchor? childAlignment = null) where T : HorizontalOrVerticalLayoutGroup { var group = gameObject.GetComponent(); if (!group) group = gameObject.AddComponent(); - return SetLayoutGroup(group, forceWidth, forceHeight, childControlWidth, childControlHeight, spacing, padTop, + return SetLayoutGroup(group, forceWidth, forceHeight, childControlWidth, childControlHeight, spacing, padTop, padBottom, padLeft, padRight, childAlignment); } @@ -116,7 +120,7 @@ namespace UnityExplorer.UI /// public static T SetLayoutGroup(T group, bool? forceWidth = null, bool? forceHeight = null, bool? childControlWidth = null, bool? childControlHeight = null, int? spacing = null, int? padTop = null, - int? padBottom = null, int? padLeft = null, int? padRight = null, TextAnchor? childAlignment = null) + int? padBottom = null, int? padLeft = null, int? padRight = null, TextAnchor? childAlignment = null) where T : HorizontalOrVerticalLayoutGroup { if (forceWidth != null) @@ -146,32 +150,29 @@ namespace UnityExplorer.UI /// /// Create a Panel on the UI Canvas. /// - public static GameObject CreatePanel(string name, out GameObject contentHolder, string anchors = null, string position = null) + public static GameObject CreatePanel(string name, out GameObject contentHolder, Color? bgColor = null) { - var panelObj = CreateUIObject(name, UIManager.CanvasRoot); + var panelObj = CreateUIObject(name, UIManager.PanelHolder); + SetLayoutGroup(panelObj, true, true, true, true); + var rect = panelObj.GetComponent(); rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.anchoredPosition = Vector2.zero; rect.sizeDelta = Vector2.zero; - if (anchors != null) - rect.SetAnchorsFromString(anchors); - - if (position != null) - rect.SetPositionFromString(position); - var maskImg = panelObj.AddComponent(); - maskImg.color = Color.white; - panelObj.AddComponent().showMaskGraphic = false; - - SetLayoutGroup(panelObj, true, true, true, true); + maskImg.color = Color.black; + panelObj.AddComponent().showMaskGraphic = true; contentHolder = CreateUIObject("Content", panelObj); Image bgImage = contentHolder.AddComponent(); bgImage.type = Image.Type.Filled; - bgImage.color = new Color(0.1f, 0.1f, 0.1f); + if (bgColor == null) + bgImage.color = new Color(0.07f, 0.07f, 0.07f); + else + bgImage.color = (Color)bgColor; SetLayoutGroup(contentHolder, true, true, true, true, 3, 3, 3, 3, 3); @@ -239,6 +240,11 @@ namespace UnityExplorer.UI return groupObj; } + #endregion + + + #region Default Control Elements + /// /// Create a Label object. /// @@ -259,19 +265,19 @@ namespace UnityExplorer.UI return textComp; } - public static Button CreateButton(GameObject parent, string name, string text, Action onClick = null, Color? normalColor = null) + public static ButtonRef CreateButton(GameObject parent, string name, string text, Color? normalColor = null) { var colors = new ColorBlock(); normalColor = normalColor ?? new Color(0.25f, 0.25f, 0.25f); - var btn = CreateButton(parent, name, text, onClick, colors); + var btn = CreateButton(parent, name, text, colors); - RuntimeProvider.Instance.SetColorBlock(btn, normalColor, new Color(0.4f, 0.4f, 0.4f), new Color(0.15f, 0.15f, 0.15f)); + RuntimeProvider.Instance.SetColorBlock(btn.Component, normalColor, normalColor * 1.2f, normalColor * 0.7f); return btn; } - public static Button CreateButton(GameObject parent, string name, string text, Action onClick, ColorBlock colors) + public static ButtonRef CreateButton(GameObject parent, string name, string text, ColorBlock colors) { GameObject buttonObj = CreateUIObject(name, parent, _smallElementSize); @@ -279,7 +285,7 @@ namespace UnityExplorer.UI Image image = buttonObj.AddComponent(); image.type = Image.Type.Sliced; - image.color = new Color(1, 1, 1, 0.75f); + image.color = new Color(1, 1, 1, 1); var button = buttonObj.AddComponent