diff --git a/src/Core/SceneHandler.cs b/src/Core/SceneHandler.cs index 52ad99f..7cbd0cc 100644 --- a/src/Core/SceneHandler.cs +++ b/src/Core/SceneHandler.cs @@ -65,8 +65,6 @@ namespace UnityExplorer.Core /// public static int LoadedSceneCount => SceneManager.sceneCount + 2; - // Cached on startup, will never change during runtime (and generally doesn't change between Unity versions either) - internal static Scene DontDestroyScene => DontDestroyMe.scene; internal static int DontDestroyHandle => DontDestroyScene.handle; @@ -84,6 +82,8 @@ namespace UnityExplorer.Core } private static GameObject dontDestroyObject; + public static bool InspectingAssetScene => SelectedScene == AssetScene; + internal static Scene AssetScene => AssetObject.scene; internal static int AssetHandle => AssetScene.handle; diff --git a/src/UI/Models/UIBehaviourModel.cs b/src/UI/Models/UIBehaviourModel.cs index f6228f5..f8dae4e 100644 --- a/src/UI/Models/UIBehaviourModel.cs +++ b/src/UI/Models/UIBehaviourModel.cs @@ -26,7 +26,7 @@ namespace UnityExplorer.UI.Models Instances.RemoveAt(i); continue; } - if (instance.Visible) + if (instance.Enabled) instance.Update(); } } diff --git a/src/UI/Models/UIModel.cs b/src/UI/Models/UIModel.cs index c398942..4cc4816 100644 --- a/src/UI/Models/UIModel.cs +++ b/src/UI/Models/UIModel.cs @@ -10,12 +10,20 @@ namespace UnityExplorer.UI.Models { public abstract GameObject UIRoot { get; } - public bool Visible + public bool Enabled { - get => UIRoot && UIRoot.activeInHierarchy; - set { if (UIRoot) UIRoot.SetActive(value); } + get => UIRoot && UIRoot.activeSelf; + 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 Destroy() diff --git a/src/UI/Models/UIPanel.cs b/src/UI/Models/UIPanel.cs new file mode 100644 index 0000000..23fdff6 --- /dev/null +++ b/src/UI/Models/UIPanel.cs @@ -0,0 +1,188 @@ +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.UI.Utility; + +namespace UnityExplorer.UI.Models +{ + public abstract class UIPanel : UIBehaviourModel + { + public override GameObject UIRoot => uiRoot; + protected GameObject uiRoot; + protected RectTransform mainPanelRect; + public GameObject content; + public PanelDragger dragger; + + public abstract string Name { get; } + + public override void ConstructUI(GameObject parent) + { + // create core canvas + var panel = UIFactory.CreatePanel(Name, out GameObject panelContent); + uiRoot = panel; + mainPanelRect = uiRoot.GetComponent(); + content = panelContent; + + UIFactory.SetLayoutGroup(uiRoot, true, true, true, true, 0, 0, 0, 0, 0, TextAnchor.UpperLeft); + UIFactory.SetLayoutGroup(content, true, true, true, true, 2, 2, 2, 2, 2, TextAnchor.UpperLeft); + + // always apply default pos and anchors (save data may only be partial) + SetDefaultPosAndAnchors(); + + // Title bar + + var titleBar = UIFactory.CreateLabel(content, "TitleBar", Name, TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(titleBar.gameObject, minHeight: 25, flexibleHeight: 0); + + dragger = new PanelDragger(titleBar.GetComponent(), mainPanelRect); + dragger.OnFinishResize += OnFinishResize; + dragger.OnFinishDrag += OnFinishDrag; + + // content (abstract) + + ConstructPanelContent(); + + // apply panel save data or revert to default + try + { + LoadSaveData(); + dragger.OnEndResize(); + } + catch + { + SetDefaultPosAndAnchors(); + } + + // simple listener for saving enabled state + this.OnToggleEnabled += (bool val) => + { + SaveToConfigManager(); + }; + } + + public abstract void ConstructPanelContent(); + + public virtual void OnFinishResize(RectTransform panel) + { + SaveToConfigManager(); + } + + public virtual void OnFinishDrag(RectTransform panel) + { + SaveToConfigManager(); + } + + public abstract void SaveToConfigManager(); + + public abstract void SetDefaultPosAndAnchors(); + + public abstract void LoadSaveData(); + + public string ToSaveData() + { + try + { + return $"{Enabled}" + + $"|{mainPanelRect.RectAnchorsToString()}" + + $"|{mainPanelRect.RectPositionToString()}"; + } + catch + { + return ""; + } + } + + public void ApplySaveData(string data) + { + if (string.IsNullOrEmpty(data)) + return; + + var split = data.Split('|'); + + try + { + uiRoot.SetActive(bool.Parse(split[0])); + mainPanelRect.SetAnchorsFromString(split[1]); + mainPanelRect.SetPositionFromString(split[2]); + } + catch { } + } + } + + public static class RectSaveExtensions + { + #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) + { + if (!rect) + throw new ArgumentNullException("rect"); + + return string.Format(_enCulture, "{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], _enCulture); + anchors.y = float.Parse(split[1], _enCulture); + anchors.z = float.Parse(split[2], _enCulture); + anchors.w = float.Parse(split[3], _enCulture); + + 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(_enCulture, "{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], _enCulture); + vector.y = float.Parse(split[1], _enCulture); + rect.localPosition = vector; + } + + #endregion + } +} diff --git a/src/UI/Panels/SceneExplorer.cs b/src/UI/Panels/SceneExplorer.cs index db1c5de..5b7510e 100644 --- a/src/UI/Panels/SceneExplorer.cs +++ b/src/UI/Panels/SceneExplorer.cs @@ -2,22 +2,23 @@ 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.Utility; using UnityExplorer.UI.Widgets; namespace UnityExplorer.UI.Panels { - public class SceneExplorer : UIBehaviourModel + public class SceneExplorer : UIPanel { - public override GameObject UIRoot => uiRoot; - private GameObject uiRoot; + public override string Name => "Scene Explorer"; /// /// Whether to automatically update per auto-update interval or not. @@ -27,6 +28,7 @@ namespace UnityExplorer.UI.Panels public TransformTree Tree; private float timeOfLastUpdate = -1f; + private GameObject refreshRow; private Dropdown sceneDropdown; private readonly Dictionary sceneToDropdownOption = new Dictionary(); @@ -45,7 +47,7 @@ namespace UnityExplorer.UI.Panels public override void Update() { - if (AutoUpdate && Time.realtimeSinceStartup - timeOfLastUpdate >= 1f) + if ((AutoUpdate || !SceneHandler.InspectingAssetScene) && Time.realtimeSinceStartup - timeOfLastUpdate >= 1f) { timeOfLastUpdate = Time.realtimeSinceStartup; ExpensiveUpdate(); @@ -68,6 +70,7 @@ namespace UnityExplorer.UI.Panels SceneHandler.SelectedScene = SceneHandler.LoadedScenes[value]; SceneHandler.Update(); Tree.RefreshData(true); + OnSelectedSceneChanged(SceneHandler.SelectedScene.Value); } private void SceneHandler_OnInspectedSceneChanged(Scene scene) @@ -84,6 +87,14 @@ namespace UnityExplorer.UI.Panels 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) @@ -119,9 +130,15 @@ namespace UnityExplorer.UI.Panels private float previousRectHeight; - private void SceneExplorer_OnFinishResize(RectTransform obj) + public override void OnFinishResize(RectTransform panel) { - RuntimeProvider.Instance.StartCoroutine(DelayedRefresh(obj)); + base.OnFinishResize(panel); + RuntimeProvider.Instance.StartCoroutine(DelayedRefresh(panel)); + } + + public override void SaveToConfigManager() + { + ConfigManager.SceneExplorerData.Value = this.ToSaveData(); } private IEnumerator DelayedRefresh(RectTransform obj) @@ -138,53 +155,33 @@ namespace UnityExplorer.UI.Panels } - public override void ConstructUI(GameObject parent) + public override void LoadSaveData() { - var panel = UIFactory.CreatePanel("SceneExplorer", out GameObject panelContent); - uiRoot = panel; - var panelRect = panel.GetComponent(); - panelRect.anchorMin = Vector3.zero; - panelRect.anchorMax = new Vector2(0, 1); - panelRect.sizeDelta = new Vector2(300f, panelRect.sizeDelta.y); - panelRect.anchoredPosition = new Vector2(160, 0); - panelRect.offsetMin = new Vector2(panelRect.offsetMin.x, 10); // bottom - panelRect.offsetMax = new Vector2(panelRect.offsetMax.x, -10); // top - panelRect.pivot = new Vector2(0.5f, 0.5f); - UIFactory.SetLayoutGroup(panel, true, true, true, true, 0, 0, 0, 0, 0, TextAnchor.UpperLeft); - UIFactory.SetLayoutGroup(panelContent, true, true, true, true, 2, 2, 2, 2, 2, TextAnchor.UpperLeft); + var data = ConfigManager.SceneExplorerData.Value; + ApplySaveData(data); + } - // Title bar + public override void SetDefaultPosAndAnchors() + { + mainPanelRect.localPosition = Vector2.zero; + mainPanelRect.anchorMin = Vector3.zero; + mainPanelRect.anchorMax = new Vector2(0, 1); + mainPanelRect.sizeDelta = new Vector2(300f, mainPanelRect.sizeDelta.y); + mainPanelRect.anchoredPosition = new Vector2(160, 0); + mainPanelRect.offsetMin = new Vector2(mainPanelRect.offsetMin.x, 10); // bottom + mainPanelRect.offsetMax = new Vector2(mainPanelRect.offsetMax.x, -10); // top + mainPanelRect.pivot = new Vector2(0.5f, 0.5f); + } - var titleBar = UIFactory.CreateLabel(panelContent, "TitleBar", "Scene Explorer", TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(titleBar.gameObject, minHeight: 25, flexibleHeight: 0); + public override void ConstructPanelContent() + { + // Tool bar (top area) - new PanelDragger(titleBar.GetComponent(), panelRect) - .OnFinishResize += SceneExplorer_OnFinishResize; - - // Tool bar - - var toolbar = UIFactory.CreateVerticalGroup(panelContent, "Toolbar", true, true, true, true, 2, new Vector4(2, 2, 2, 2), + var toolbar = UIFactory.CreateVerticalGroup(content, "Toolbar", true, true, true, true, 2, new Vector4(2, 2, 2, 2), new Color(0.15f, 0.15f, 0.15f)); - //UIFactory.SetLayoutElement(toolbar, minHeight: 25, flexibleHeight: 0); - - // refresh row - - var 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", ForceUpdate); - UIFactory.SetLayoutElement(refreshButton.gameObject, minWidth: 65, flexibleWidth: 0); - - 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); // Scene selector dropdown + var dropdownObj = UIFactory.CreateDropdown(toolbar, out sceneDropdown, "", 13, OnDropdownChanged); UIFactory.SetLayoutElement(dropdownObj, minHeight: 25, flexibleHeight: 0); @@ -208,9 +205,28 @@ namespace UnityExplorer.UI.Panels UIFactory.SetLayoutElement(inputFieldObj, minHeight: 25); inputField.onValueChanged.AddListener(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", ForceUpdate); + UIFactory.SetLayoutElement(refreshButton.gameObject, minWidth: 65, flexibleWidth: 0); + + 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 infiniteScroll = UIFactory.CreateInfiniteScroll(panelContent, "TransformTree", out GameObject scrollObj, + var infiniteScroll = UIFactory.CreateInfiniteScroll(content, "TransformTree", out GameObject scrollObj, out GameObject scrollContent, new Color(0.15f, 0.15f, 0.15f)); UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999); UIFactory.SetLayoutElement(scrollContent, flexibleHeight: 9999); @@ -225,10 +241,66 @@ namespace UnityExplorer.UI.Panels var prototype = TransformCell.CreatePrototypeCell(scrollContent); infiniteScroll.PrototypeCell = prototype.GetComponent(); - // Setup references + // some references Tree.Scroller = infiniteScroll; + previousRectHeight = mainPanelRect.rect.height; + + // 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(content, "SceneLoaderLabel", "Scene Loader", TextAnchor.MiddleLeft, Color.white, true, 14); + UIFactory.SetLayoutElement(title2.gameObject, minHeight: 25, flexibleHeight: 0); + + var allSceneDropObj = UIFactory.CreateDropdown(content, out Dropdown allSceneDrop, "", 14, null); + UIFactory.SetLayoutElement(allSceneDropObj, minHeight: 25, minWidth: 150, 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(content, "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: 25, 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: 25, minWidth: 150); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"Could not create the Scene Loader helper! {ex.ReflectionExToString()}"); + } - previousRectHeight = panelRect.rect.height; } } } diff --git a/src/UI/UIFactory.cs b/src/UI/UIFactory.cs index 2b0da41..0a80903 100644 --- a/src/UI/UIFactory.cs +++ b/src/UI/UIFactory.cs @@ -148,8 +148,7 @@ namespace UnityExplorer.UI /// /// Create a Panel on the UI Canvas. /// - public static GameObject CreatePanel(string name, out GameObject contentHolder, Color? bgColor = null, - 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 rect = panelObj.GetComponent(); @@ -158,12 +157,6 @@ namespace UnityExplorer.UI 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; @@ -576,6 +569,7 @@ namespace UnityExplorer.UI GameObject scrollbarObj = CreateScrollbar(templateObj, "DropdownScroll", out Scrollbar scrollbar); scrollbar.SetDirection(Scrollbar.Direction.BottomToTop, true); + RuntimeProvider.Instance.SetColorBlock(scrollbar, new Color(0.3f, 0.3f, 0.3f), new Color(0.4f, 0.4f, 0.4f), new Color(0.2f, 0.2f, 0.2f)); RectTransform scrollRectTransform = scrollbarObj.GetComponent(); scrollRectTransform.anchorMin = Vector2.right; @@ -700,7 +694,7 @@ namespace UnityExplorer.UI } public static InfiniteScrollRect CreateInfiniteScroll(GameObject parent, string name, out GameObject uiRoot, - out GameObject content, Color? bgColor = null) + out GameObject content, Color? bgColor = null, bool autoResizeSliderHandle = true) { var mainObj = CreateUIObject(name, parent, new Vector2(1, 1)); mainObj.AddComponent().color = bgColor ?? new Color(0.12f, 0.12f, 0.12f); @@ -716,7 +710,6 @@ namespace UnityExplorer.UI viewportRect.offsetMax = new Vector2(-10.0f, 0.0f); viewportObj.AddComponent().color = Color.white; viewportObj.AddComponent().showMaskGraphic = false; - //SetLayoutGroup(viewportObj, true, true, true, true); content = CreateUIObject("Content", viewportObj); var contentRect = content.GetComponent(); @@ -725,7 +718,6 @@ namespace UnityExplorer.UI contentRect.pivot = new Vector2(0.0f, 1.0f); contentRect.sizeDelta = new Vector2(0f, 0f); contentRect.offsetMax = new Vector2(0f, 0f); - //SetLayoutGroup(content, true, false, true, false, 0, 2, 2, 2, 2); var scrollRect = mainObj.AddComponent(); scrollRect.movementType = ScrollRect.MovementType.Clamped; @@ -738,13 +730,31 @@ namespace UnityExplorer.UI scrollRect.viewport = viewportRect; scrollRect.content = contentRect; - var sliderObj = SliderScrollbar.CreateSliderScrollbar(mainObj, out Slider slider); + var sliderContainer = CreateVerticalGroup(mainObj, "SliderContainer", + false, false, true, true, 0, default, new Color(0.05f, 0.05f, 0.05f)); + SetLayoutElement(sliderContainer, minWidth: 25, flexibleWidth:0, flexibleHeight: 9999); + sliderContainer.AddComponent(); + + var sliderObj = SliderScrollbar.CreateSliderScrollbar(sliderContainer, out Slider slider); slider.direction = Slider.Direction.TopToBottom; - SetLayoutElement(sliderObj, minWidth: 25, flexibleHeight: 9999); + SetLayoutElement(sliderObj, minWidth: 25, flexibleWidth: 0, flexibleHeight: 9999); + + if (autoResizeSliderHandle) + { + slider.handleRect.offsetMin = new Vector2(slider.handleRect.offsetMin.x, 0); + slider.handleRect.offsetMax = new Vector2(slider.handleRect.offsetMax.x, 0); + slider.handleRect.pivot = new Vector2(0.5f, 0.5f); + + var container = slider.m_HandleContainerRect; + container.anchorMin = Vector3.zero; + container.anchorMax = Vector3.one; + container.pivot = new Vector3(0.5f, 0.5f); + } uiRoot = mainObj; var infiniteScroll = new InfiniteScrollRect(scrollRect); + infiniteScroll.AutoResizeHandleRect = autoResizeSliderHandle; return infiniteScroll; } diff --git a/src/UI/Widgets/InfiniteScroll/InfiniteScrollRect.cs b/src/UI/Widgets/InfiniteScroll/InfiniteScrollRect.cs index c4fe947..3255075 100644 --- a/src/UI/Widgets/InfiniteScroll/InfiniteScrollRect.cs +++ b/src/UI/Widgets/InfiniteScroll/InfiniteScrollRect.cs @@ -37,6 +37,11 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll private List _cachedCells; private Bounds _recyclableViewBounds; + /// + /// Extra pooled cells above AND below the viewport (so actual extra pool is double this value). + /// + public int ExtraCellPoolSize = 2; + private readonly Vector3[] _corners = new Vector3[4]; private bool _recycling; private Vector2 _prevAnchoredPos; @@ -46,10 +51,8 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll internal int topMostCellIndex, bottomMostCellIndex; //Topmost and bottommost cell in the heirarchy internal int _topMostCellColoumn, _bottomMostCellColoumn; // used for recyling in Grid layout. top-most and bottom-most coloumn - // Flag to keep track of when we are manually setting our slider/scrollrect value directly, to avoid callback loops. - //private bool ExternallySetting = false; + public bool AutoResizeHandleRect; - // external sources use this flag, it will stay true until the start of the next frame to prevent our update overwriting it. public bool ExternallySetting { get => externallySetting; @@ -79,8 +82,6 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll // Jump to val * count (ie, 0.0 would jump to top, 1.0 would jump to bottom) var index = Math.Floor(val * DataSource.ItemCount); JumpToIndex((int)index); - - //this.ExternallySetting = false; }); } @@ -106,7 +107,70 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll // ExternallySetting = false; } - + internal void SetSliderFromScrollValue(bool forceValue = true) + { + int total = DataSource.ItemCount; + // avoid DivideByZeroException, this is harmless if count was <= 0. + if (total <= 0) + total = 1; + + var spread = _cellPool.Count - (ExtraCellPoolSize * 2); + + if (forceValue) + { + var range = GetDisplayedRange(); + if (spread >= total) + _slider.value = 0f; + else + // top-most displayed index divided by (totalCount - displayedRange) + _slider.value = (float)((decimal)range.x / (total - _cellPool.Count)); + } + + // resize the handle rect to reflect the size of the displayed content vs. the total content height. + if (AutoResizeHandleRect) + { + var viewportHeight = scrollRect.viewport.rect.height; + + var handleRatio = (decimal)spread / total; + var handleHeight = viewportHeight * (float)Math.Min(1, handleRatio); + + // need to resize the handle container area for the size of the handle (bigger handle = smaller area) + var container = _slider.m_HandleContainerRect; + container.offsetMax = new Vector2(container.offsetMax.x, -(handleHeight * 0.5f)); + container.offsetMin = new Vector2(container.offsetMin.x, handleHeight * 0.5f); + + var handle = _slider.handleRect; + + handle.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, handleHeight); + + // if slider is 100% height then make it not interactable. + _slider.interactable = !Mathf.Approximately(handleHeight, viewportHeight); + } + } + + /// + /// Try to jump to the specified index. Pretty accurate, not perfect. Currently assumes all elements are the same height. + /// + public void JumpToIndex(int index) + { + var realCount = DataSource.ItemCount; + + // clamp to real index limit + index = Math.Min(index, realCount - 1); + + // add the buffer count to desired index and set our currentItemCount to that. + currentItemCount = index + _cachedCells.Count; + currentItemCount = Math.Max(Math.Min(currentItemCount, realCount - 1), _cachedCells.Count); + Refresh(); + + // if we're jumping to the very bottom we need to show the extra pooled cells which are normally hidden. + var y = 0f; + if (index >= realCount - (ExtraCellPoolSize * 4)) + y = _cellHeight * (index - realCount + (4 * ExtraCellPoolSize)) + ExtraCellPoolSize; // add +1 to show the last entry. + + scrollRect.content.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, y); + } + /// /// Get the start and end indexes (relative to DataSource) of the cell pool /// @@ -178,47 +242,10 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll RefreshContentSize(); //internallySetting = true; - //SetSliderFromScrollValue(); + SetSliderFromScrollValue(false); //internallySetting = false; } - internal void SetSliderFromScrollValue() - { - // calculate where slider handle should be based on displayed range. - var range = GetDisplayedRange(); - int total = DataSource.ItemCount; - var spread = range.y - range.x; - - if (spread >= total) - _slider.value = 0f; - else - // top-most displayed index divided by (totalCount - displayedRange) - _slider.value = (float)((decimal)range.x / (decimal)(total - spread)); - } - - public void JumpToIndex(int index) - { - var realCount = DataSource.ItemCount; - - index = Math.Min(index, realCount - 1); - - var indexBuffer = (int)(_cachedCells.Count * (1 - (index / (decimal)(realCount - 1)))); - - currentItemCount = index + indexBuffer; - currentItemCount = Math.Max(Math.Min(currentItemCount, realCount), _cachedCells.Count); - Refresh(); - - var y = 0f; - var displayRange = scrollRect.viewport.rect.height / _cellHeight; - var poolRange = scrollRect.content.rect.height / _cellHeight; - var poolExtra = poolRange - displayRange; - - if (index >= realCount - poolExtra) - y = _cellHeight * (index - realCount + (poolExtra * 2)); - - scrollRect.content.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, y); - } - public void PopulateCells() { var width = scrollRect.viewport.GetComponent().rect.width; @@ -282,7 +309,7 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll private void SetRecyclingBounds() { scrollRect.viewport.GetCorners(_corners); - float threshHold = _cellHeight * 2; //RecyclingThreshold * (_corners[2].y - _corners[0].y); + float threshHold = _cellHeight * ExtraCellPoolSize; //RecyclingThreshold * (_corners[2].y - _corners[0].y); _recyclableViewBounds.min = new Vector3(_corners[0].x, _corners[0].y - threshHold); _recyclableViewBounds.max = new Vector3(_corners[2].x, _corners[2].y + threshHold); } @@ -319,7 +346,7 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll _cellHeight = PrototypeCell.rect.height; //Get the required pool coverage and mininum size for the Cell pool - float requiredCoverage = scrollRect.viewport.rect.height + (_cellHeight * 4); + float requiredCoverage = scrollRect.viewport.rect.height + (_cellHeight * (ExtraCellPoolSize * 2)); //create cells untill the Pool area is covered while (currentPoolCoverage < requiredCoverage) @@ -470,466 +497,4 @@ namespace UnityExplorer.UI.Widgets.InfiniteScroll #endregion } - - //public class InfiniteScrollRect : ScrollRect - //{ - - // public IListDataSource DataSource; - - // internal RectTransform PrototypeCell; - // internal Slider _slider; - - // // Cell pool - // private float _cellWidth, _cellHeight; - // private List _cellPool; - // private List _cachedCells; - // private Bounds _recyclableViewBounds; - - // private readonly Vector3[] _corners = new Vector3[4]; - // private bool _recycling; - // private Vector2 _prevAnchoredPos; - // internal Vector2 _lastScroll; - - // internal int currentItemCount; //item count corresponding to the datasource. - // internal int topMostCellIndex, bottomMostCellIndex; //Topmost and bottommost cell in the heirarchy - // internal int _topMostCellColoumn, _bottomMostCellColoumn; // used for recyling in Grid layout. top-most and bottom-most coloumn - - // // Flag to keep track of when we are manually setting our slider/scrollrect value directly, to avoid callback loops. - // private bool internallySetting = false; - - // // external sources use this flag, it will stay true until the start of the next frame to prevent our update overwriting it. - // public bool ExternallySetting - // { - // get => externallySetting; - // internal set - // { - // if (externallySetting == value) - // return; - // timeOfLastExternalSet = Time.time; - // externallySetting = true; - // } - // } - // private bool externallySetting; - // private float timeOfLastExternalSet; - - // private Vector2 zeroVector = Vector2.zero; - - // public int PoolCount => _cachedCells.Count; - - // #region MONOBEHAVIOUR - - // internal new void Start() - // { - // // Link up the Slider and ScrollRect onValueChanged to sync them. - - // _slider = this.GetComponentInChildren(); - - // onValueChanged.AddListener((Vector2 val) => - // { - // try - // { - // if (internallySetting || ExternallySetting) - // return; - // internallySetting = true; - - // SetSliderFromScrollValue(); - - // internallySetting = false; - // } - // catch (Exception ex) - // { - // ExplorerCore.Log(ex); - // } - // }); - - // _slider.onValueChanged.AddListener((float val) => - // { - // if (internallySetting || ExternallySetting) - // return; - // internallySetting = true; - - // // Jump to val * count (ie, 0.0 would jump to top, 1.0 would jump to bottom) - // var index = Math.Floor(val * DataSource.ItemCount); - // JumpToIndex((int)index); - - // internallySetting = false; - // }); - // } - - // internal void Update() - // { - // if (externallySetting && timeOfLastExternalSet < Time.time) - // externallySetting = false; - // } - - // #endregion - - // #region LISTENERS - - // internal void OnValueChangedListener(Vector2 normalizedPos) - // { - // Vector2 dir = base.content.anchoredPosition - _prevAnchoredPos; - // m_ContentStartPosition += ProcessValueChange(dir); - // _prevAnchoredPos = base.content.anchoredPosition; - // } - - // #endregion - - // public Vector2 GetDisplayedRange() - // { - // int max = currentItemCount; - // int min = max - _cachedCells.Count; - // return new Vector2(min, max); - // } - - // public void Initialize(IListDataSource dataSource) - // { - // DataSource = dataSource; - - // vertical = true; - // horizontal = false; - - // _prevAnchoredPos = base.content.anchoredPosition; - // onValueChanged.RemoveListener(OnValueChangedListener); - - // RuntimeProvider.Instance.StartCoroutine(InitCoroutine(() => - // { - // onValueChanged.AddListener(OnValueChangedListener); - // })); - // } - - // public void ReloadData() - // { - // ReloadData(DataSource); - // } - - // public void ReloadData(IListDataSource dataSource) - // { - // if (onValueChanged == null) - // return; - - // StopMovement(); - - // onValueChanged.RemoveListener(OnValueChangedListener); - - // DataSource = dataSource; - - // RuntimeProvider.Instance.StartCoroutine(InitCoroutine(() => - // onValueChanged.AddListener(OnValueChangedListener) - // )); - - // _prevAnchoredPos = base.content.anchoredPosition; - // } - - // public void Refresh() - // { - // if (DataSource == null || _cellPool == null) - // return; - - // int count = DataSource.ItemCount; - // if (currentItemCount > count) - // currentItemCount = Math.Max(count, _cellPool.Count); - - // SetRecyclingBounds(); - // RecycleBottomToTop(); - // RecycleTopToBottom(); - - // PopulateCells(); - - // RefreshContentSize(); - - // // Close, but not quite accurate enough to be useful. - // internallySetting = true; - // SetSliderFromScrollValue(); - // internallySetting = false; - // } - - // internal void SetSliderFromScrollValue() - // { - // // calculate where slider handle should be based on displayed range. - // var range = GetDisplayedRange(); - // int total = DataSource.ItemCount; - // var spread = range.y - range.x; - - // //var orig = _slider.value; - - // if (spread >= total) - // _slider.value = 0f; - // else - // // top-most displayed index divided by (totalCount - displayedRange) - // _slider.value = (float)((decimal)range.x / (decimal)(total - spread)); - // } - - // public void JumpToIndex(int index) - // { - // var realCount = DataSource.ItemCount; - - // index = Math.Min(index, realCount - 1); - - // var indexBuffer = (int)(_cachedCells.Count * (1 - (index / (decimal)(realCount - 1)))); - - // currentItemCount = index + indexBuffer; - // currentItemCount = Math.Max(Math.Min(currentItemCount, realCount), _cachedCells.Count); - // Refresh(); - - // var y = 0f; - - // var displayRange = viewport.rect.height / _cellHeight; - // var poolRange = content.rect.height / _cellHeight; - // var poolExtra = poolRange - displayRange; - - // if (index >= realCount - poolExtra) - // y = _cellHeight * (index - realCount + poolExtra); - - // content.anchoredPosition = new Vector2(content.anchoredPosition.x, y); - // } - - // public void PopulateCells() - // { - // var width = viewport.GetComponent().rect.width; - // content.sizeDelta = new Vector2(width, content.sizeDelta.y); - - // int cellIndex = topMostCellIndex; - // var itemIndex = currentItemCount - _cachedCells.Count; - // int iterated = 0; - // while (iterated < _cachedCells.Count) - // { - // var cell = _cachedCells[cellIndex]; - // cellIndex++; - // if (cellIndex < 0) - // continue; - // if (cellIndex >= _cachedCells.Count) - // cellIndex = 0; - // DataSource.SetCell(cell, itemIndex); - // itemIndex++; - - // var rect = _cellPool[cellIndex].GetComponent(); - // rect.sizeDelta = new Vector2(width, rect.sizeDelta.y); - - // iterated++; - // } - // } - - // #region RECYCLING INIT - - // private IEnumerator InitCoroutine(Action onInitialized) - // { - // yield return null; - // SetTopAnchor(content); - // content.anchoredPosition = Vector3.zero; - - // yield return null; - // SetRecyclingBounds(); - - // //Cell Poool - // CreateCellPool(); - // currentItemCount = _cellPool.Count; - // topMostCellIndex = 0; - // bottomMostCellIndex = _cellPool.Count - 1; - - // //Set content height according to no of rows - // RefreshContentSize(); - - // SetTopAnchor(content); - - // onInitialized?.Invoke(); - // } - - // private void RefreshContentSize() - // { - // int noOfRows = _cachedCells.Where(it => it.Enabled).Count(); - // float contentYSize = noOfRows * _cellHeight; - // content.sizeDelta = new Vector2(content.sizeDelta.x, contentYSize); - // } - - // private void SetRecyclingBounds() - // { - // viewport.GetWorldCorners(_corners); - // float threshHold = _cellHeight / 2; //RecyclingThreshold * (_corners[2].y - _corners[0].y); - // _recyclableViewBounds.min = new Vector3(_corners[0].x, _corners[0].y - threshHold); - // _recyclableViewBounds.max = new Vector3(_corners[2].x, _corners[2].y + threshHold); - // } - - // private void CreateCellPool() - // { - // //Reseting Pool - // if (_cellPool != null) - // { - // _cellPool.ForEach((RectTransform item) => Destroy(item.gameObject)); - // _cellPool.Clear(); - // _cachedCells.Clear(); - // } - // else - // { - // _cachedCells = new List(); - // _cellPool = new List(); - // } - - // //Set the prototype cell active and set cell anchor as top - // PrototypeCell.gameObject.SetActive(true); - // SetTopAnchor(PrototypeCell); - - // //Reset - // _topMostCellColoumn = _bottomMostCellColoumn = 0; - - // //Temps - // float currentPoolCoverage = 0; - // int poolSize = 0; - // float posY = 0; - - // //set new cell size according to its aspect ratio - // _cellWidth = content.rect.width; - // _cellHeight = PrototypeCell.rect.height; - - // //Get the required pool coverage and mininum size for the Cell pool - // float requiredCoverage = viewport.rect.height + (_cellHeight * 2); - - // //create cells untill the Pool area is covered - // while (currentPoolCoverage < requiredCoverage) - // { - // //Instantiate and add to Pool - // RectTransform item = Instantiate(PrototypeCell.gameObject).GetComponent(); - // item.name = $"Cell_{_cachedCells.Count + 1}"; - // item.sizeDelta = new Vector2(_cellWidth, _cellHeight); - // _cellPool.Add(item); - // item.SetParent(content, false); - - // item.anchoredPosition = new Vector2(0, posY); - // posY = item.anchoredPosition.y - item.rect.height; - // currentPoolCoverage += item.rect.height; - - // //Setting data for Cell - // _cachedCells.Add(item.GetComponent()); - // DataSource.SetCell(_cachedCells[_cachedCells.Count - 1], poolSize); - - // //Update the Pool size - // poolSize++; - // } - - // //Deactivate prototype cell if it is not a prefab(i.e it's present in scene) - // if (PrototypeCell.gameObject.scene.IsValid()) - // PrototypeCell.gameObject.SetActive(false); - // } - // #endregion - - // #region RECYCLING - - // public Vector2 ProcessValueChange(Vector2 direction) - // { - // if (_recycling || _cellPool == null || _cellPool.Count == 0) return zeroVector; - - // //Updating Recyclable view bounds since it can change with resolution changes. - // SetRecyclingBounds(); - - // _lastScroll = direction; - - // if (direction.y > 0 && _cellPool[bottomMostCellIndex].MaxY() > _recyclableViewBounds.min.y) - // { - // return RecycleTopToBottom(); - // } - // else if (direction.y < 0 && _cellPool[topMostCellIndex].MinY() < _recyclableViewBounds.max.y) - // { - // return RecycleBottomToTop(); - // } - - // return zeroVector; - // } - - // /// - // /// Recycles cells from top to bottom in the List heirarchy - // /// - // private Vector2 RecycleTopToBottom() - // { - // _recycling = true; - - // int n = 0; - // float posY; - - // //to determine if content size needs to be updated - // //Recycle until cell at Top is avaiable and current item count smaller than datasource - // while (_cellPool[topMostCellIndex].MinY() > _recyclableViewBounds.max.y && currentItemCount < DataSource.ItemCount) - // { - // //Move top cell to bottom - // posY = _cellPool[bottomMostCellIndex].anchoredPosition.y - _cellPool[bottomMostCellIndex].sizeDelta.y; - // _cellPool[topMostCellIndex].anchoredPosition = new Vector2(_cellPool[topMostCellIndex].anchoredPosition.x, posY); - - // //Cell for row at - // DataSource.SetCell(_cachedCells[topMostCellIndex], currentItemCount); - - // //set new indices - // bottomMostCellIndex = topMostCellIndex; - // topMostCellIndex = (topMostCellIndex + 1) % _cellPool.Count; - - // currentItemCount++; - // n++; - // } - - // //Content anchor position adjustment. - // _cellPool.ForEach((RectTransform cell) => cell.anchoredPosition += n * Vector2.up * _cellPool[topMostCellIndex].sizeDelta.y); - // content.anchoredPosition -= n * Vector2.up * _cellPool[topMostCellIndex].sizeDelta.y; - // _recycling = false; - // return -new Vector2(0, n * _cellPool[topMostCellIndex].sizeDelta.y); - // } - - // /// - // /// Recycles cells from bottom to top in the List heirarchy - // /// - // private Vector2 RecycleBottomToTop() - // { - // _recycling = true; - - // int n = 0; - // float posY = 0; - - // //to determine if content size needs to be updated - // //Recycle until cell at bottom is avaiable and current item count is greater than cellpool size - // while (_cellPool[bottomMostCellIndex].MaxY() < _recyclableViewBounds.min.y && currentItemCount > _cellPool.Count) - // { - // //Move bottom cell to top - // posY = _cellPool[topMostCellIndex].anchoredPosition.y + _cellPool[topMostCellIndex].sizeDelta.y; - // _cellPool[bottomMostCellIndex].anchoredPosition = new Vector2(_cellPool[bottomMostCellIndex].anchoredPosition.x, posY); - // n++; - - // currentItemCount--; - - // //Cell for row at - // DataSource.SetCell(_cachedCells[bottomMostCellIndex], currentItemCount - _cellPool.Count); - - // //set new indices - // topMostCellIndex = bottomMostCellIndex; - // bottomMostCellIndex = (bottomMostCellIndex - 1 + _cellPool.Count) % _cellPool.Count; - // } - - // _cellPool.ForEach((RectTransform cell) => cell.anchoredPosition -= n * Vector2.up * _cellPool[topMostCellIndex].sizeDelta.y); - // content.anchoredPosition += n * Vector2.up * _cellPool[topMostCellIndex].sizeDelta.y; - // _recycling = false; - // return new Vector2(0, n * _cellPool[topMostCellIndex].sizeDelta.y); - // } - - // #endregion - - // #region HELPERS - - // /// - // /// Anchoring cell and content rect transforms to top preset. Makes repositioning easy. - // /// - // /// - // private void SetTopAnchor(RectTransform rectTransform) - // { - // //Saving to reapply after anchoring. Width and height changes if anchoring is change. - // float width = rectTransform.rect.width; - // float height = rectTransform.rect.height; - - // //Setting top anchor - // rectTransform.anchorMin = new Vector2(0.5f, 1); - // rectTransform.anchorMax = new Vector2(0.5f, 1); - // rectTransform.pivot = new Vector2(0.5f, 1); - - // //Reapply size - // rectTransform.sizeDelta = new Vector2(width, height); - // } - - // #endregion - //} - } diff --git a/src/UI/Widgets/SliderScrollbar.cs b/src/UI/Widgets/SliderScrollbar.cs index 456d1ae..dacf726 100644 --- a/src/UI/Widgets/SliderScrollbar.cs +++ b/src/UI/Widgets/SliderScrollbar.cs @@ -116,8 +116,8 @@ namespace UnityExplorer.UI.Utility GameObject sliderObj = UIFactory.CreateUIObject("SliderScrollbar", parent, UIFactory._smallElementSize); GameObject bgObj = UIFactory.CreateUIObject("Background", sliderObj); - GameObject fillAreaObj = UIFactory.CreateUIObject("Fill Area", sliderObj); - GameObject fillObj = UIFactory.CreateUIObject("Fill", fillAreaObj); + //GameObject fillAreaObj = UIFactory.CreateUIObject("Fill Area", sliderObj); + //GameObject fillObj = UIFactory.CreateUIObject("Fill", fillAreaObj); GameObject handleSlideAreaObj = UIFactory.CreateUIObject("Handle Slide Area", sliderObj); GameObject handleObj = UIFactory.CreateUIObject("Handle", handleSlideAreaObj); @@ -131,22 +131,11 @@ namespace UnityExplorer.UI.Utility bgRect.sizeDelta = Vector2.zero; bgRect.offsetMax = new Vector2(0f, 0f); - RectTransform fillAreaRect = fillAreaObj.GetComponent(); - fillAreaRect.anchorMin = new Vector2(0f, 0.20f); - fillAreaRect.anchorMax = new Vector2(1f, 0.8f); - fillAreaRect.anchoredPosition = new Vector2(0f, 0f); - fillAreaRect.sizeDelta = new Vector2(-20f, 0f); - - Image fillImage = fillObj.AddComponent(); - fillImage.type = Image.Type.Sliced; - fillImage.color = Color.clear; - - fillObj.GetComponent().sizeDelta = new Vector2(10f, 0f); - RectTransform handleSlideRect = handleSlideAreaObj.GetComponent(); handleSlideRect.anchorMin = new Vector2(0f, 0f); handleSlideRect.anchorMax = new Vector2(1f, 1f); - handleSlideRect.offsetMin = new Vector2(25f, 30f); + handleSlideRect.pivot = new Vector2(0.5f, 0.5f); + handleSlideRect.offsetMin = new Vector2(27f, 30f); handleSlideRect.offsetMax = new Vector2(-15f, 0f); handleSlideRect.sizeDelta = new Vector2(-20f, -30f); @@ -165,13 +154,12 @@ namespace UnityExplorer.UI.Utility sliderBarLayout.flexibleHeight = 5000; slider = sliderObj.AddComponent(); - slider.fillRect = fillObj.GetComponent(); + //slider.fillRect = fillObj.GetComponent(); slider.handleRect = handleObj.GetComponent(); slider.targetGraphic = handleImage; slider.direction = Slider.Direction.BottomToTop; - RuntimeProvider.Instance.SetColorBlock( - slider, + RuntimeProvider.Instance.SetColorBlock(slider, new Color(0.25f, 0.25f, 0.25f), new Color(0.3f, 0.3f, 0.3f), new Color(0.2f, 0.2f, 0.2f)); diff --git a/src/UI/Widgets/TransformTree/TransformTree.cs b/src/UI/Widgets/TransformTree/TransformTree.cs index 1e3c01d..f62cd74 100644 --- a/src/UI/Widgets/TransformTree/TransformTree.cs +++ b/src/UI/Widgets/TransformTree/TransformTree.cs @@ -92,14 +92,15 @@ namespace UnityExplorer.UI.Widgets public void RefreshData(bool andReload = false) { - //tempObjectCache = objectCache.ToDictionary(it => it.Key, it => it.Value); displayedObjects.Clear(); - // objectCache.Clear(); var rootObjects = GetRootEntriesMethod.Invoke(); foreach (var obj in rootObjects) - Traverse(obj.transform); + { + if (obj) + Traverse(obj.transform); + } if (andReload) Scroller.Refresh(); diff --git a/src/UnityExplorer.csproj b/src/UnityExplorer.csproj index 65bdcdb..7dcd9d3 100644 --- a/src/UnityExplorer.csproj +++ b/src/UnityExplorer.csproj @@ -106,6 +106,10 @@ ..\lib\mcs.dll False + + packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll + False + @@ -156,10 +160,6 @@ ..\lib\0Harmony.dll False - - packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll - False - @@ -216,6 +216,7 @@ + @@ -261,8 +262,9 @@ - - + + +