Second commit
This commit is contained in:
commit
36840f3ad4
999 files changed
+61364
No files matched your search
@@ -0,0 +1,558 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TinyGoose.Tremble.Editor
|
||||
{
|
||||
public class ManualWindow : EditorWindow
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
// Statics - for storing state between reloads
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
private const string MANUAL_PREFIX = "Manual_";
|
||||
private const int MAX_BOOKMARKS = 5;
|
||||
|
||||
private static readonly EditorPrefsBackedString s_Path = new(MANUAL_PREFIX + nameof(s_Path));
|
||||
private static readonly EditorPrefsBackedString s_HomePath = new(MANUAL_PREFIX + nameof(s_HomePath));
|
||||
private static readonly EditorPrefsBackedString s_SortOrderFirst = new(MANUAL_PREFIX + nameof(s_SortOrderFirst));
|
||||
private static readonly EditorPrefsBackedString s_SortOrderLast = new(MANUAL_PREFIX + nameof(s_SortOrderLast));
|
||||
private static readonly EditorPrefsBackedString s_Bookmarks = new(MANUAL_PREFIX + nameof(s_Bookmarks));
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
// State
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
private Vector2 m_ListScrollPos;
|
||||
private Vector2 m_PageScrollPos;
|
||||
|
||||
private ManualEntry m_CurrentNode;
|
||||
private ManualPageBase m_CurrentPage;
|
||||
|
||||
private ManualEntry m_TreeRoot;
|
||||
private readonly Dictionary<string, Type> m_PageClasses = new();
|
||||
|
||||
private GUIStyle m_SelectedItemStyle;
|
||||
|
||||
private float m_TreeViewWidth = 235f;
|
||||
private bool m_IsResizing;
|
||||
private readonly List<string> m_History = new();
|
||||
private int m_HistoryPtr = -1;
|
||||
|
||||
public float PageWidth => position.width - m_TreeViewWidth - 20f;
|
||||
|
||||
public static void OpenManual(string windowTitle, Type startPage = null, string[] sortOrderFirst = null, string[] sortOrderLast = null)
|
||||
{
|
||||
string startPath = null;
|
||||
|
||||
if (startPage != null)
|
||||
{
|
||||
startPage.GetCustomAttributes(out ManualPageAttribute pageInfo);
|
||||
if (pageInfo == null)
|
||||
{
|
||||
Debug.LogWarning($"Tried to open manual to page: {startPage.Name} - which is not a valid page - going home...");
|
||||
}
|
||||
else
|
||||
{
|
||||
startPath = pageInfo.Path;
|
||||
}
|
||||
}
|
||||
|
||||
OpenManual(windowTitle, startPath, sortOrderFirst, sortOrderLast);
|
||||
}
|
||||
|
||||
public static void OpenManual(string windowTitle, string startPath = null, string[] sortOrderFirst = null, string[] sortOrderLast = null)
|
||||
{
|
||||
s_Path.Value = startPath;
|
||||
s_SortOrderFirst.Values = sortOrderFirst ?? Array.Empty<string>();
|
||||
s_SortOrderLast.Values = sortOrderLast ?? Array.Empty<string>();
|
||||
|
||||
ManualWindow window = GetWindow<ManualWindow>(true, windowTitle, true);
|
||||
|
||||
Vector2 size = new(1200f, 640f);
|
||||
window.minSize = new(800f, 400f);
|
||||
window.position = new(EditorGUIUtility.GetMainWindowPosition().center - size / 2f, size);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
s_HomePath.Value = null;
|
||||
m_TreeRoot = new() { Title = "" };
|
||||
|
||||
TypeCache.TypeCollection pages = TypeCache.GetTypesWithAttribute<ManualPageAttribute>();
|
||||
foreach (Type pageClass in pages)
|
||||
{
|
||||
pageClass.GetCustomAttributes(out ManualPageAttribute pageInfo);
|
||||
|
||||
if (pageInfo.IsHomePage)
|
||||
{
|
||||
if (s_HomePath.Value != null)
|
||||
{
|
||||
Debug.LogError($"Multiple home pages in manual! Was: {s_HomePath.Values}, now using: {pageInfo.Path}!");
|
||||
}
|
||||
s_HomePath.Value = pageInfo.Path;
|
||||
}
|
||||
|
||||
m_PageClasses[pageInfo.Path] = pageClass;
|
||||
|
||||
List<string> pathList = pageInfo.Path.Split("/", StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
CreateHierarchy_Recursive(m_TreeRoot, pathList, pageInfo);
|
||||
}
|
||||
|
||||
SortHierarchy_Recursive(m_TreeRoot, s_HomePath.Value);
|
||||
GoToPage(s_Path.Value ?? s_HomePath.Value);
|
||||
}
|
||||
|
||||
public void GoToPage(Type type, bool noUpdateHistory = false)
|
||||
{
|
||||
type.GetCustomAttributes(out ManualPageAttribute manualPageAttribute);
|
||||
if (manualPageAttribute == null)
|
||||
{
|
||||
Debug.LogError($"Type {type.Name} is not a [ManualPage]");
|
||||
return;
|
||||
}
|
||||
|
||||
GoToPage(manualPageAttribute.Path, noUpdateHistory);
|
||||
}
|
||||
public void GoToPage(string path, bool noUpdateHistory = false)
|
||||
{
|
||||
if (!noUpdateHistory)
|
||||
{
|
||||
if (m_History.Count > 0 && m_HistoryPtr > -1 && m_HistoryPtr != m_History.Count - 1)
|
||||
{
|
||||
m_History.RemoveRange(m_HistoryPtr + 1, m_History.Count - m_HistoryPtr - 1);
|
||||
}
|
||||
|
||||
m_History.Add(path);
|
||||
m_HistoryPtr = m_History.Count - 1;
|
||||
}
|
||||
|
||||
if (!m_PageClasses.TryGetValue(path, out Type pageType))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Page", $"Could not go to page with path '{path}' - maybe it was removed or moved?", "Oh no!");
|
||||
RemoveBookmark(path);
|
||||
return;
|
||||
}
|
||||
|
||||
m_CurrentPage?.DeInit();
|
||||
{
|
||||
m_CurrentNode = m_TreeRoot.FindEntryByPath_Recursive(path);
|
||||
m_CurrentPage = (ManualPageBase)Activator.CreateInstance(pageType);
|
||||
}
|
||||
m_CurrentPage.Init(this);
|
||||
|
||||
s_Path.Value = path;
|
||||
m_PageScrollPos = Vector2.zero;
|
||||
|
||||
ExpandHierarchy(path);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (m_SelectedItemStyle == null)
|
||||
{
|
||||
Color lightBlue = new(0f, 0.6f, 1f);
|
||||
m_SelectedItemStyle = new(EditorStyles.wordWrappedLabel)
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = lightBlue },
|
||||
focused = { textColor = lightBlue },
|
||||
hover = { textColor = lightBlue }
|
||||
};
|
||||
}
|
||||
|
||||
DrawToolbar();
|
||||
|
||||
// Horizontal separator
|
||||
GUILayout.Box(GUIContent.none, GUILayout.ExpandWidth(true), GUILayout.Height(2f));
|
||||
|
||||
GUILayout.BeginHorizontal(GUILayout.ExpandHeight(true));
|
||||
{
|
||||
DrawTreeView(m_TreeViewWidth);
|
||||
DrawDraggableSeparator();
|
||||
DrawContent();
|
||||
|
||||
DrawBookmarkOverlayButton();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
EditorGUI.BeginDisabledGroup(m_HistoryPtr < 1);
|
||||
{
|
||||
if (GUILayout.Button("<", GUILayout.Width(20f)))
|
||||
{
|
||||
m_HistoryPtr--;
|
||||
GoToPage(m_History[m_HistoryPtr], noUpdateHistory: true);
|
||||
}
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUI.BeginDisabledGroup(m_HistoryPtr >= m_History.Count - 1);
|
||||
{
|
||||
if (GUILayout.Button(">", GUILayout.Width(20f)))
|
||||
{
|
||||
m_HistoryPtr++;
|
||||
GoToPage(m_History[m_HistoryPtr], noUpdateHistory: true);
|
||||
}
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(m_CurrentNode?.PagePath.EqualsInvariant(s_HomePath.Value) ?? false);
|
||||
{
|
||||
if (GUILayout.Button("Home", GUILayout.Width(50f)))
|
||||
{
|
||||
m_History.Clear();
|
||||
m_HistoryPtr = -1;
|
||||
|
||||
GoToPage(s_HomePath.Value);
|
||||
}
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
// Bookmarks
|
||||
GUILayout.Space(5f);
|
||||
|
||||
string[] bookmarks = s_Bookmarks.Values;
|
||||
Array.Sort(bookmarks, String.CompareOrdinal);
|
||||
|
||||
if (bookmarks.Length > MAX_BOOKMARKS)
|
||||
{
|
||||
if (EditorGUILayout.DropdownButton(new("Bookmarks"), FocusType.Passive))
|
||||
{
|
||||
GenericMenu menu = new();
|
||||
foreach (string bookmark in bookmarks)
|
||||
{
|
||||
menu.AddItem(new(bookmark), false, () => GoToPage(bookmark));
|
||||
}
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string bookmark in bookmarks)
|
||||
{
|
||||
EditorGUI.BeginDisabledGroup(s_Path.Value.EqualsInvariant(bookmark));
|
||||
{
|
||||
if (GUILayout.Button(new GUIContent(Path.GetFileName(bookmark)), EditorStyles.miniButton))
|
||||
{
|
||||
GoToPage(bookmark);
|
||||
}
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
}
|
||||
|
||||
// GUI.color = Color.gray;
|
||||
// GUILayout.Label($"Path: {m_CurrentNode?.PagePath ?? "<none>"}");
|
||||
// GUI.color = Color.white;
|
||||
|
||||
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUILayout.Label($"Scale: {ManualStyles.Scale*100f:F0}%");
|
||||
|
||||
float rawValue = GUILayout.HorizontalSlider(ManualStyles.Scale, 0.7f, 1.3f, GUILayout.Width(200f));
|
||||
ManualStyles.Scale = Mathf.FloorToInt(rawValue * 10f) / 10f;
|
||||
|
||||
EditorGUI.BeginDisabledGroup(ManualStyles.Scale == 1f);
|
||||
{
|
||||
if (GUILayout.Button(new GUIContent("x", "Reset Zoom")))
|
||||
{
|
||||
ManualStyles.Scale = 1f;
|
||||
}
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawTreeView(float width)
|
||||
{
|
||||
// Tree view
|
||||
m_ListScrollPos = GUILayout.BeginScrollView(m_ListScrollPos, GUILayout.Width(width));
|
||||
{
|
||||
DrawHierarchy_Recursive(m_TreeRoot, isRoot: true);
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawDraggableSeparator()
|
||||
{
|
||||
GUILayout.Box(GUIContent.none, GUILayout.Width(4f), GUILayout.ExpandHeight(true));
|
||||
|
||||
Rect resizeHandleRect = GUILayoutUtility.GetLastRect();
|
||||
EditorGUIUtility.AddCursorRect(resizeHandleRect, MouseCursor.ResizeHorizontal);
|
||||
|
||||
if (resizeHandleRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
// Mouse down while over - start resizing
|
||||
if (Event.current.type == EventType.MouseDown)
|
||||
{
|
||||
m_IsResizing = true;
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse up - no resizing
|
||||
if (Event.current.type == EventType.MouseUp)
|
||||
{
|
||||
m_IsResizing = false;
|
||||
}
|
||||
|
||||
// If currently resizing, update the m_TreeViewWidth based on mouse movement
|
||||
if (m_IsResizing && Event.current.type != EventType.Layout && Event.current.type != EventType.Repaint)
|
||||
{
|
||||
m_TreeViewWidth += Event.current.delta.x;
|
||||
m_TreeViewWidth = Mathf.Clamp(m_TreeViewWidth, 10f, 500f);
|
||||
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawContent()
|
||||
{
|
||||
if (m_CurrentPage != null)
|
||||
{
|
||||
EditorGUILayoutUtil.Pad(10f, () =>
|
||||
{
|
||||
m_PageScrollPos = GUILayout.BeginScrollView(m_PageScrollPos);
|
||||
{
|
||||
GUILayout.Label(m_CurrentNode.Title, ManualStyles.Styles.Title);
|
||||
m_CurrentPage.Render();
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("Please select an item on the left!", EditorStyles.centeredGreyMiniLabel, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBookmarkOverlayButton()
|
||||
{
|
||||
// Bookmark overlay button
|
||||
bool wasBookmarked = s_Bookmarks.Values.Contains(s_Path.Value);
|
||||
|
||||
Rect buttonPosition = position;
|
||||
buttonPosition.x = position.width - 32f - 30f;
|
||||
buttonPosition.y = 26f;
|
||||
buttonPosition.width = 32f;
|
||||
buttonPosition.height = 32f;
|
||||
|
||||
GUIContent bookmarkButton = wasBookmarked
|
||||
? new(ManualStyles.Styles.BookmarkIconOn, "Remove bookmark from this page")
|
||||
: new(ManualStyles.Styles.BookmarkIconOff, "Bookmark this page");
|
||||
|
||||
if (GUI.Button(buttonPosition, bookmarkButton, ManualStyles.Styles.Text))
|
||||
{
|
||||
SetBookmark(s_Path.Value, !wasBookmarked);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpandHierarchy(string path)
|
||||
{
|
||||
int endSlice = path.Length;
|
||||
|
||||
// While the path still has '/' in it...
|
||||
while (endSlice != -1)
|
||||
{
|
||||
// Chop off after last slash (no-op on first run)
|
||||
path = path.Substring(0, endSlice);
|
||||
|
||||
// Open the child, find the parent for further chopping
|
||||
EditorGUILayoutUtil.SetHeaderOpen(path, true);
|
||||
endSlice = path.LastIndexOf('/');
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBookmark(string path, bool on)
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
AddBookmark(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveBookmark(path);
|
||||
}
|
||||
}
|
||||
private void AddBookmark(string path)
|
||||
{
|
||||
s_Bookmarks.Values = s_Bookmarks.Values.Append(path).ToArray();
|
||||
ShowNotification(new("Page bookmarked!"));
|
||||
}
|
||||
|
||||
private void RemoveBookmark(string path)
|
||||
{
|
||||
s_Bookmarks.Values = s_Bookmarks.Values.Where(p => p != path).ToArray();
|
||||
ShowNotification(new("Bookmark removed!"));
|
||||
}
|
||||
|
||||
|
||||
private void CreateHierarchy_Recursive(ManualEntry node, List<string> path, ManualPageAttribute pageInfo)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
return;
|
||||
|
||||
string newNodeTitle = path[0];
|
||||
path.RemoveAt(0);
|
||||
|
||||
// Find existing parent, and go down that route
|
||||
foreach (ManualEntry existingChild in node.Children)
|
||||
{
|
||||
if (existingChild.Title.EqualsInvariant(newNodeTitle))
|
||||
{
|
||||
CreateHierarchy_Recursive(existingChild, path, pageInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Or... create a new one, and go down that route!
|
||||
bool isLeaf = path.Count == 0;
|
||||
|
||||
string newPagePath = isLeaf
|
||||
? pageInfo.Path
|
||||
: (node.PagePath.IsNullOrEmpty() ? newNodeTitle : $"{node.PagePath}/{newNodeTitle}");
|
||||
|
||||
ManualEntry entry = new()
|
||||
{
|
||||
Title = isLeaf ? pageInfo.Title : newNodeTitle,
|
||||
PagePath = newPagePath,
|
||||
ShowInTree = pageInfo.ShowInTree
|
||||
};
|
||||
node.Children.Add(entry);
|
||||
|
||||
CreateHierarchy_Recursive(entry, path, pageInfo);
|
||||
}
|
||||
|
||||
private void SortHierarchy_Recursive(ManualEntry node, string homePath)
|
||||
{
|
||||
string[] sortOrderFirst = s_SortOrderFirst.Values;
|
||||
string[] sortOrderLast = s_SortOrderLast.Values;
|
||||
|
||||
node.Children.Sort((c1, c2) =>
|
||||
{
|
||||
// Home path ALWAYS TOP
|
||||
if (c1.PagePath != null && c1.PagePath.EqualsInvariant(homePath))
|
||||
return -1;
|
||||
if (c2.PagePath != null && c2.PagePath.EqualsInvariant(homePath))
|
||||
return +1;
|
||||
|
||||
// Specific FIRST sort order first?
|
||||
int c1FirstSort = Array.IndexOf(sortOrderFirst, c1.Title);
|
||||
int c2FirstSort = Array.IndexOf(sortOrderFirst, c2.Title);
|
||||
|
||||
if (c1FirstSort != -1 || c2FirstSort != -1)
|
||||
{
|
||||
// One sorted, the other not - put sorted below
|
||||
if (c1FirstSort == -1)
|
||||
return +1;
|
||||
|
||||
if (c2FirstSort == -1)
|
||||
return -1;
|
||||
|
||||
// Both last-sorted!
|
||||
return c1FirstSort.CompareTo(c2FirstSort);
|
||||
}
|
||||
|
||||
// Specific LAST sort order first?
|
||||
int c1LastSort = Array.IndexOf(sortOrderLast, c1.Title);
|
||||
int c2LastSort = Array.IndexOf(sortOrderLast, c2.Title);
|
||||
|
||||
if (c1LastSort != -1 || c2LastSort != -1)
|
||||
{
|
||||
// One sorted, the other not - put sorted below
|
||||
if (c1LastSort == -1)
|
||||
return -1;
|
||||
|
||||
if (c2LastSort == -1)
|
||||
return +1;
|
||||
|
||||
// Both last-sorted!
|
||||
return c1LastSort.CompareTo(c2LastSort);
|
||||
}
|
||||
|
||||
// Both no specific sort at all - check titles
|
||||
return String.Compare(c1.Title, c2.Title, StringComparison.Ordinal);
|
||||
|
||||
});
|
||||
|
||||
foreach (ManualEntry child in node.Children)
|
||||
{
|
||||
SortHierarchy_Recursive(child, homePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHierarchy_Recursive(ManualEntry entry, bool isRoot = false)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
// It's a leaf!
|
||||
if (entry.Children.Count == 0)
|
||||
{
|
||||
if (!entry.ShowInTree)
|
||||
return;
|
||||
|
||||
bool isCurrentPage = entry == m_CurrentNode;
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
GUILayout.Space(26f);
|
||||
|
||||
bool wasPressed = isCurrentPage
|
||||
? GUILayout.Button(entry.Title, m_SelectedItemStyle)
|
||||
: GUILayout.Button(entry.Title, EditorStyles.wordWrappedLabel);
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (wasPressed)
|
||||
{
|
||||
GoToPage(entry.PagePath);
|
||||
}
|
||||
|
||||
GUILayout.Space(5f);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRoot)
|
||||
{
|
||||
foreach (ManualEntry child in entry.Children)
|
||||
{
|
||||
DrawHierarchy_Recursive(child);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayoutUtil.CollapsibleHeader(entry.Title, entry.PagePath, () =>
|
||||
{
|
||||
GUILayout.Space(5f);
|
||||
|
||||
foreach (ManualEntry child in entry.Children)
|
||||
{
|
||||
DrawHierarchy_Recursive(child);
|
||||
}
|
||||
});
|
||||
|
||||
if (!EditorGUILayoutUtil.IsHeaderOpen(entry.PagePath))
|
||||
{
|
||||
GUILayout.Space(5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user