├── .gitignore ├── Example.png ├── LICENSE ├── README.md ├── com.vertx.nDocumentation ├── Contents.meta ├── Contents │ ├── DocumentationContent.cs │ ├── DocumentationContent.cs.meta │ ├── DocumentationContentBase.cs │ ├── DocumentationContentBase.cs.meta │ ├── DocumentationPage.cs │ ├── DocumentationPage.cs.meta │ ├── DocumentationPageAddition.cs │ ├── DocumentationPageAddition.cs.meta │ ├── DocumentationUtility.cs │ ├── DocumentationUtility.cs.meta │ ├── IDocumentation.cs │ ├── IDocumentation.cs.meta │ ├── IDocumentationPage.cs │ └── IDocumentationPage.cs.meta ├── Images.meta ├── Images │ ├── Back.png │ ├── Back.png.meta │ ├── Back_Alt.png │ ├── Back_Alt.png.meta │ ├── Forward.png │ ├── Forward.png.meta │ ├── Forward_Alt.png │ ├── Forward_Alt.png.meta │ ├── Home.png │ ├── Home.png.meta │ ├── Home_Alt.png │ └── Home_Alt.png.meta ├── RichText.meta ├── RichText │ ├── CSharpHighlighter.cs │ ├── CSharpHighlighter.cs.meta │ ├── IButtonRegistry.cs │ ├── IButtonRegistry.cs.meta │ ├── RichTextParser.cs │ ├── RichTextParser.cs.meta │ ├── RichTextUtility.cs │ └── RichTextUtility.cs.meta ├── Styles.meta ├── Styles │ ├── CsharpHighlightingStyles.uss │ ├── CsharpHighlightingStyles.uss.meta │ ├── InbuiltFixStyles.uss │ ├── InbuiltFixStyles.uss.meta │ ├── nDocumentationStyles.uss │ └── nDocumentationStyles.uss.meta ├── Window.meta ├── Window │ ├── DocumentationWindow.cs │ ├── DocumentationWindow.cs.meta │ ├── EditorGUIExtensions.cs │ └── EditorGUIExtensions.cs.meta ├── nDocumentation.asmdef ├── nDocumentation.asmdef.meta ├── package.json └── package.json.meta └── com.vertx.nDocumentationExample ├── Example.meta ├── Example ├── Documentation.meta ├── Documentation │ ├── ButtonRegistryPage.cs │ ├── ButtonRegistryPage.cs.meta │ ├── CreatingPage.cs │ ├── CreatingPage.cs.meta │ ├── ExtendingPages.cs │ ├── ExtendingPages.cs.meta │ ├── LandingPage.cs │ ├── LandingPage.cs.meta │ ├── LayoutPage.cs │ ├── LayoutPage.cs.meta │ ├── PageAddition.cs │ ├── PageAddition.cs.meta │ ├── Styles.meta │ ├── Styles │ │ ├── MethodStylesPage.cs │ │ ├── MethodStylesPage.cs.meta │ │ ├── RichTextStylesPage.cs │ │ ├── RichTextStylesPage.cs.meta │ │ ├── SpanStylesPage.cs │ │ └── SpanStylesPage.cs.meta │ ├── StylingPage.cs │ ├── StylingPage.cs.meta │ ├── WindowPage.cs │ └── WindowPage.cs.meta ├── ExampleWindow.cs └── ExampleWindow.cs.meta ├── Styles.meta ├── Styles ├── ExampleStyles.uss └── ExampleStyles.uss.meta ├── nDocumentation-Example.asmdef ├── nDocumentation-Example.asmdef.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | 8 | # Visual Studio cache directory 9 | .vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | *.opendb 26 | 27 | # Unity3D generated meta files 28 | *.pidb.meta 29 | *.pdb.meta 30 | 31 | # Unity3D Generated File On Crash Reports 32 | sysinfo.txt 33 | 34 | # Builds 35 | *.apk 36 | *.unitypackage 37 | -------------------------------------------------------------------------------- /Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertxxyz/NDocumentation/3e7cb283576107d57422078ffc99c968121fcfbe/Example.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thomas Ingram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nDocumentation 2 | An extensible, searchable documentation window for the Unity Editor that displays rich text pages. 3 | 4 | ## 🚨 WORK IN PROGRESS AND HEAVILY EXPERIMENTAL 🚨 5 | 6 | **Minimum Currently Supported Version : Unity 2019** 7 | 8 | **2019.1.0a12 is not supported due to an issue with nested scroll views.** 9 | 10 | **Currently recommended versions: 2019.1.0a13 or a14** 11 | 12 | ---- 13 | ## Intended Use Case 14 | Editor tools spread over multiple packages can rely on an nDocumentation window for documentation inside of the Unity Editor. 15 | 16 | If a Documentation Page was defined in a core package, a package with a reliance on it can add additions to that Page's content. This enables a minimal amount of coherent pages whilst maintaining a high amount of documentation. 17 | 18 | ## Documentation 19 | The provided nDocumentationExample package is self documenting, and contains a window present in Window>Example Window. 20 | 21 | ## Installation 22 | Ensure your project is on .NET 4.x by navigating to Edit>Project Settings>Player>Configuration>Scripting Runtime Version and switching to .NET 4.x Equivalent. 23 | 24 | Use the Package Manager (Window>Package Manager) and add the appropriate package.json files present in the root of each folder. 25 | 26 | ![Example Window](Example.png) -------------------------------------------------------------------------------- /com.vertx.nDocumentation/Contents.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 183b8a2a65e9b01439e7de1cfe3ec4ee 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /com.vertx.nDocumentation/Contents/DocumentationContent.cs: -------------------------------------------------------------------------------- 1 | //#define UIELEMENT_BROWSER_BAR 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using UnityEditor; 8 | using UnityEditor.UIElements; 9 | using UnityEngine; 10 | using UnityEngine.UIElements; 11 | using static Vertx.DocumentationUtility; 12 | 13 | namespace Vertx 14 | { 15 | internal class DocumentationContent : DocumentationContentBase where T : DocumentationWindow 16 | { 17 | //The editor prefs key for the state of this DocumentationContent. 18 | private readonly string stateEditorPrefsKey; 19 | private readonly string searchEditorPrefsKey; 20 | private string searchString = string.Empty; 21 | 22 | //The default root is set as pages are added as to provide an easy way for content to be added without providing the root to functions constantly. 23 | private VisualElement _currentDefaultRoot; 24 | public override void SetCurrentDefaultRoot(VisualElement root) => _currentDefaultRoot = root; 25 | 26 | /// 27 | /// Returns the root if provided, otherwise returns the default root. 28 | /// 29 | /// Optional root to provide., 30 | /// 31 | public override VisualElement GetRoot(VisualElement root = null) => root ?? _currentDefaultRoot; 32 | 33 | /// 34 | /// Adds a VisualElement to a root (default root if null is provided) 35 | /// 36 | /// Element to add to root 37 | /// Root to add to 38 | public override void AddToRoot(VisualElement element, VisualElement root = null) => GetRoot(root).Add(element); 39 | 40 | private DocumentationPage pageRoot; 41 | private VisualElement windowRoot; 42 | private readonly VisualElement contentRoot; 43 | private readonly VisualElement searchRoot; 44 | 45 | private readonly T window; 46 | 47 | //Full Name to page 48 | private readonly Dictionary> pages = new Dictionary>(); 49 | 50 | //Page to documentation additions associated with it 51 | private readonly Dictionary, List>> additions = new Dictionary, List>>(); 52 | 53 | //Page to buttons injected into it. 54 | private readonly Dictionary, List.ButtonInjection>> injectedButtonLinks = new Dictionary, List.ButtonInjection>>(); 55 | 56 | 57 | private readonly string searchFieldName; 58 | 59 | private ToolbarButton backButton; 60 | private ToolbarButton forwardButton; 61 | private ToolbarButton homeButton; 62 | 63 | public DocumentationContent(VisualElement root, T window, string stateEditorPrefsKey = null) 64 | { 65 | this.window = window; 66 | this.stateEditorPrefsKey = stateEditorPrefsKey; 67 | 68 | searchEditorPrefsKey = $"{stateEditorPrefsKey}_Search"; 69 | if (EditorPrefs.HasKey(searchEditorPrefsKey)) 70 | searchString = EditorPrefs.GetString(searchEditorPrefsKey); 71 | searchFieldName = $"SearchField_{window.GetType().FullName}"; 72 | 73 | #if !UIELEMENT_BROWSER_BAR 74 | IMGUIContainer browserBar = new IMGUIContainer(BrowserBar) 75 | { 76 | style = 77 | { 78 | height = 18 79 | } 80 | }; 81 | root.Add(browserBar); 82 | #else 83 | BrowserBar(root); 84 | #endif 85 | 86 | /* The content root contains the styles. 87 | It also contains the scroll view and search root, with the scroll view containing the user-generated content */ 88 | VisualElement content = new VisualElement 89 | { 90 | style = 91 | { 92 | flexGrow = 1 93 | } 94 | }; 95 | root.Add(content); 96 | StyleSheet docsStyleSheet = LoadAssetOfType("nDocumentationStyles", SearchFilter.Packages); 97 | content.styleSheets.Add(docsStyleSheet); 98 | 99 | StyleSheet codeStyleSheet = LoadAssetOfType("CsharpHighlightingStyles", SearchFilter.Packages); 100 | content.styleSheets.Add(codeStyleSheet); 101 | 102 | ScrollView scrollView = new ScrollView 103 | { 104 | name = "Scroll View", 105 | showHorizontal = false, 106 | showVertical = true 107 | }; 108 | scrollView.AddToClassList("scroll-view"); 109 | scrollView.contentContainer.AddToClassList("scroll-view-content"); 110 | contentRoot = scrollView.contentContainer; 111 | content.Add(scrollView); 112 | 113 | searchRoot = new VisualElement(); 114 | searchRoot.ClearClassList(); 115 | searchRoot.AddToClassList("search-container"); 116 | content.Add(searchRoot); 117 | searchRoot.StretchToParentSize(); 118 | searchRoot.visible = false; 119 | 120 | SetCurrentDefaultRoot(contentRoot); 121 | } 122 | 123 | public override bool InitialiseContent() 124 | { 125 | if (!InitialisePages()) 126 | return false; 127 | if (EditorPrefs.HasKey(stateEditorPrefsKey)) 128 | { 129 | string page = EditorPrefs.GetString(stateEditorPrefsKey); 130 | if (!LoadPage(page)) 131 | Home(); 132 | } 133 | else 134 | { 135 | Home(); 136 | } 137 | 138 | return true; 139 | 140 | bool InitialisePages() 141 | { 142 | Type windowType = window.GetType(); 143 | //Find all the documentation and additions to it 144 | Type iDocGenericType = typeof(IDocumentation<>).MakeGenericType(windowType); 145 | IEnumerable> documentation = GetExtensionsOfTypeIE>(iDocGenericType); 146 | List> documentationPages = new List>(); 147 | List> documentationAdditions = new List>(); 148 | 149 | foreach (IDocumentation iDoc in documentation) 150 | { 151 | switch (iDoc) 152 | { 153 | case DocumentationPageAddition pageAddition: 154 | documentationAdditions.Add(pageAddition); 155 | break; 156 | case DocumentationPage page: 157 | if (page.InjectedButtonLinks?[0].ParentType == windowType) 158 | { 159 | if (pageRoot != null) 160 | { 161 | Debug.LogError($"Multiple pages are assigned to be the root for window. {this.pageRoot} & {pageRoot}."); 162 | continue; 163 | } 164 | 165 | pageRoot = page; 166 | } 167 | else 168 | { 169 | documentationPages.Add(page); 170 | } 171 | 172 | pages.Add(page.GetType().FullName, page); 173 | break; 174 | default: 175 | throw new ArgumentOutOfRangeException($"{iDoc.GetType()} is not present in the DocumentationContent.InitialiseContent page switch."); 176 | } 177 | 178 | iDoc.Initialise(window); 179 | } 180 | 181 | if (pageRoot == null) 182 | { 183 | Debug.LogError("No root page defined."); 184 | return false; 185 | } 186 | 187 | //fill the additions dictionary 188 | foreach (DocumentationPageAddition docAddition in documentationAdditions) 189 | { 190 | Type pageToAddToType = docAddition.PageToAddToType; 191 | GetDocumentationPage>(pageToAddToType, 192 | "does not provide a page to add to.", 193 | $"{docAddition.GetType().FullName}'s provided PageToAddToType", 194 | additions, 195 | docAddition); 196 | } 197 | 198 | foreach (var additionList in additions.Values) 199 | additionList.Sort((a, b) => a.Order.CompareTo(b.Order)); 200 | 201 | 202 | //Discover the injected buttons 203 | foreach (DocumentationPage iDoc in documentationPages) 204 | { 205 | DocumentationPage.ButtonInjection[] injectedButtons = iDoc.InjectedButtonLinks; 206 | if (injectedButtons == null) 207 | continue; 208 | //For all the buttons in we're injecting 209 | foreach (DocumentationPage.ButtonInjection buttonInjection in injectedButtons) 210 | { 211 | buttonInjection.PageOfOrigin = iDoc; 212 | //We find the relevant page and add our injected button to its dictionary entry 213 | GetDocumentationPage.ButtonInjection>(buttonInjection.ParentType, 214 | "does not provide a page to add to.", 215 | $"{iDoc.GetType().FullName}'s intended location for button injection {buttonInjection.ParentType.FullName}", 216 | injectedButtonLinks, buttonInjection); 217 | } 218 | } 219 | 220 | //Sort the injected buttons 221 | foreach (List.ButtonInjection> buttonInjections in injectedButtonLinks.Values) 222 | buttonInjections.Sort((a, b) => a.Order.CompareTo(b.Order)); 223 | 224 | void GetDocumentationPage(Type query, string queryNullError, string queryNotIDocError, Dictionary, List> dictionary, TType add) 225 | { 226 | if (query == null) 227 | { 228 | Debug.LogError($"{query.FullName} {queryNullError}"); 229 | return; 230 | } 231 | 232 | string key = query.FullName; 233 | if (!pages.TryGetValue(key, out IDocumentationPage pageToAddTo)) 234 | { 235 | if (!query.IsSubclassOf(typeof(IDocumentation))) 236 | Debug.LogError($"{queryNotIDocError} ({key}) is not a Documentation Page {GetType().FullName}."); 237 | return; 238 | } 239 | 240 | if (!dictionary.TryGetValue(pageToAddTo, out List addList)) 241 | { 242 | addList = new List(); 243 | dictionary.Add(pageToAddTo, addList); 244 | } 245 | 246 | addList.Add(add); 247 | } 248 | 249 | return true; 250 | } 251 | } 252 | 253 | private bool LoadPage(string pageFullName) 254 | { 255 | if (!pages.TryGetValue(pageFullName, out IDocumentationPage page)) 256 | { 257 | Debug.LogError($"Window does not contain a reference to {pageFullName}."); 258 | return false; 259 | } 260 | 261 | LoadPage(page); 262 | return true; 263 | } 264 | 265 | private void LoadPage(IDocumentationPage page, VisualElement rootOverride = null) 266 | { 267 | VisualElement root = rootOverride ?? contentRoot; 268 | SetCurrentDefaultRoot(root); 269 | root.Clear(); 270 | 271 | //Constant header 272 | window.DrawConstantHeader(); 273 | 274 | //Documentation 275 | page.DrawDocumentation(window); 276 | 277 | //Additions 278 | if (additions.TryGetValue(page, out var additionsList)) 279 | { 280 | foreach (var addition in additionsList) 281 | addition.DrawDocumentation(window); 282 | } 283 | 284 | page.DrawDocumentationAfterAdditions(window); 285 | 286 | //Button Links 287 | if (injectedButtonLinks.TryGetValue(page, out var buttonsBelow)) 288 | { 289 | VisualElement injectedButtonContainer = new VisualElement(); 290 | injectedButtonContainer.AddToClassList("injected-button-container"); 291 | AddToRoot(injectedButtonContainer); 292 | SetCurrentDefaultRoot(injectedButtonContainer); 293 | 294 | foreach (DocumentationPage.ButtonInjection button in buttonsBelow) 295 | { 296 | DocumentationPage pageOfOrigin = button.PageOfOrigin; 297 | window.AddFullWidthButton(pageOfOrigin.Title, pageOfOrigin.Color, () => GoToPage(pageOfOrigin.GetType().FullName)); 298 | } 299 | } 300 | 301 | currentPageStateName = page.GetType().FullName; 302 | } 303 | 304 | /// 305 | /// IMGUI browser bar 306 | /// 307 | void BrowserBar() 308 | { 309 | float w2 = Screen.width / 2f - 5; 310 | 311 | GUI.Box(new Rect(0, 0, Screen.width, 20), GUIContent.none, EditorStyles.toolbar); 312 | 313 | GUI.enabled = history.Count > 0; 314 | if (GUI.Button(new Rect(5, 0, 20, 20), new GUIContent(EditorGUIUtility.isProSkin ? GetTexture("Back") : GetTexture("Back_Alt"), "Back"), EditorStyles.toolbarButton)) 315 | Back(); 316 | 317 | GUI.enabled = forwardHistory.Count > 0; 318 | if (GUI.Button(new Rect(25, 0, 20, 20), new GUIContent(EditorGUIUtility.isProSkin ? GetTexture("Forward") : GetTexture("Forward_Alt"), "Forward"), EditorStyles.toolbarButton)) 319 | Forward(); 320 | 321 | GUI.enabled = currentPageStateName != null && !currentPageStateName.Equals(pageRoot.GetType().FullName) || searchRoot.visible; 322 | if (GUI.Button(new Rect(48, 0, 20, 20), new GUIContent(EditorGUIUtility.isProSkin ? GetTexture("Home") : GetTexture("Home_Alt"), "Home"), EditorStyles.toolbarButton)) 323 | { 324 | searchString = string.Empty; 325 | searchRoot.visible = false; 326 | GUI.FocusControl(null); 327 | Home(); 328 | } 329 | 330 | GUI.enabled = true; 331 | Rect searchRect = new Rect(w2, 2, w2, 16); 332 | using (EditorGUI.ChangeCheckScope changeCheckScope = new EditorGUI.ChangeCheckScope()) 333 | { 334 | GUI.SetNextControlName(searchFieldName); 335 | searchString = EditorGUIExtensions.ToolbarSearchField(searchRect, searchString); 336 | 337 | if (!searchRoot.visible && !string.IsNullOrEmpty(searchString) && GUI.GetNameOfFocusedControl().Equals(searchFieldName)) 338 | { 339 | searchRoot.visible = true; 340 | if (searchStringsCache.Count == 0) 341 | DoSearch(); 342 | } 343 | 344 | if (changeCheckScope.changed) 345 | DoSearch(); 346 | } 347 | } 348 | 349 | /// 350 | /// UI Element Browser Bar 351 | /// 352 | void BrowserBar(VisualElement root) 353 | { 354 | #region Browser Bar 355 | 356 | //Browser Bar 357 | Toolbar toolbar = new Toolbar(); 358 | toolbar.style.justifyContent = Justify.SpaceBetween; 359 | root.Add(toolbar); 360 | // Back Button 361 | backButton = AddToolbarButton(EditorGUIUtility.isProSkin ? GetTexture("Back") : GetTexture("Back_Alt"), Back, 5); 362 | backButton.tooltip = "Back"; 363 | backButton.SetEnabled(false); 364 | toolbar.Add(backButton); 365 | // Forward Button 366 | forwardButton = AddToolbarButton(EditorGUIUtility.isProSkin ? GetTexture("Forward") : GetTexture("Forward_Alt"), Forward, 0); 367 | forwardButton.tooltip = "Forward"; 368 | forwardButton.SetEnabled(false); 369 | toolbar.Add(forwardButton); 370 | homeButton = AddToolbarButton(EditorGUIUtility.isProSkin ? GetTexture("Home") : GetTexture("Home_Alt"), Home, 2); 371 | homeButton.tooltip = "Home"; 372 | toolbar.Add(homeButton); 373 | 374 | ToolbarButton AddToolbarButton(Texture texture, Action action, float marginLeft) 375 | { 376 | ToolbarButton toolbarButton = new ToolbarButton(action) 377 | { 378 | style = 379 | { 380 | marginLeft = marginLeft, 381 | width = 20, 382 | borderLeftWidth = marginLeft == 0 ? 0 : 1, 383 | borderColor = new Color(0.57f, 0.57f, 0.57f), 384 | borderTopLeftRadius = 2, 385 | alignItems = Align.Center, 386 | paddingBottom = 0, 387 | paddingLeft = 0, 388 | paddingRight = 0, 389 | } 390 | }; 391 | toolbarButton.Add(new Image 392 | { 393 | image = texture, 394 | style = 395 | { 396 | marginTop = 4, 397 | height = 10, 398 | width = 10 399 | } 400 | }); 401 | return toolbarButton; 402 | } 403 | 404 | // Search Bar 405 | ToolbarSearchField toolbarSearchField = new ToolbarSearchField(); 406 | toolbarSearchField.SetValueWithoutNotify(searchString); 407 | 408 | //ToolbarSearchField's buttons have broken hover and action pseudo-states so we have to fix that 409 | StyleSheet fixSheet = LoadAssetOfType("InbuiltFixStyles", SearchFilter.Packages); 410 | toolbarSearchField.styleSheets.Add(fixSheet); 411 | 412 | toolbarSearchField.style.flexGrow = 1; 413 | toolbarSearchField.RegisterCallback>(evt => 414 | { 415 | searchString = evt.newValue; 416 | DoSearch(); 417 | }); 418 | TextField textField = toolbarSearchField.Q(); 419 | textField.RegisterCallback(evt => 420 | { 421 | if (searchRoot.visible || string.IsNullOrEmpty(searchString)) 422 | return; 423 | //If there's a search string and we're in the search box we should enable the search container. 424 | searchRoot.visible = true; 425 | //If there isn't any search (ie. the previously cached search was never built, perform the search) 426 | if (searchStringsCache.Count == 0) 427 | DoSearch(); 428 | }); 429 | //The internal text field has a fixed width so we have to remove that 430 | StyleLength width = textField.style.width; 431 | width.keyword = StyleKeyword.Auto; 432 | textField.style.width = width; 433 | 434 | //The cancel button is improperly aligned so we have to fix that 435 | Button cancelButton = toolbarSearchField.Q"; 21 | 22 | /// 23 | /// Gets content surrounded by the appropriate button tag 24 | /// 25 | /// The key to link to (IButtonRegistry provided to AddRichText must contain key) 26 | /// The label of the button 27 | /// a rich text string describing the provided button 28 | public static string GetButtonString(string key, string label) => $"{label}"; 29 | 30 | /// 31 | /// Gets text content 32 | /// 33 | /// 34 | /// 35 | /// 36 | public static string GetColouredString(string content, Color colour) => $"{content}"; 37 | public static string GetColoredString(string content, Color color) => GetColouredString(content, color); 38 | public static string GetBoldItalicsString(string content) => $"{content}"; 39 | 40 | /// 41 | /// Adds VisualElements corresponding to the provided rich text to a root. 42 | /// 43 | /// The rich text to parse 44 | /// A registry that can be queried for keys if there are any button tags. 45 | /// Visual Element to append the rich text UI to. 46 | /// A list of all immediate children added to the root. 47 | public static List AddRichText(string text, IButtonRegistry buttonRegistry, VisualElement root) => AddRichText(text, buttonRegistry, root, false); 48 | 49 | private static List AddRichText(string text, IButtonRegistry buttonRegistry, VisualElement root, bool isInsideCodeBlock) 50 | { 51 | List results = new List(); 52 | IEnumerable richTexts = ParseRichText(text, isInsideCodeBlock); 53 | //Parse rich texts to create paragraphs. 54 | List> paragraphs = new List> {new List()}; 55 | foreach (RichText richText in richTexts) 56 | { 57 | if (richText.richTextTag.tag == RichTextTag.Tag.button || richText.richTextTag.tag == RichTextTag.Tag.code) 58 | { 59 | paragraphs[paragraphs.Count - 1].Add(richText); 60 | continue; 61 | } 62 | 63 | string[] strings = richText.associatedText.Split('\n'); 64 | for (int i = 0; i < strings.Length; i++) 65 | { 66 | if (i != 0) 67 | paragraphs.Add(new List()); 68 | //Split paragraph content (already split by tag) into individual words 69 | string[] wordSplit = Regex.Split(strings[i], @"(?<=[ -])"); //Split but keep delimiters attached. 70 | foreach (var word in wordSplit) 71 | { 72 | if (!string.IsNullOrEmpty(word)) 73 | paragraphs[paragraphs.Count - 1].Add(new RichText(richText.richTextTag, word)); 74 | } 75 | } 76 | } 77 | 78 | foreach (List paragraph in paragraphs) 79 | { 80 | //Add all the paragraphs 81 | VisualElement rootTemp = root; 82 | root = AddParagraphContainer(root); 83 | for (int i = 0; i < paragraph.Count; i++) 84 | { 85 | RichText word = paragraph[i]; 86 | if (i < paragraph.Count - 1) 87 | { 88 | //If there are more words 89 | RichText nextWord = paragraph[i + 1]; 90 | string nextText = nextWord.associatedText; 91 | if (Regex.IsMatch(nextText, "^[^a-zA-Z] ?")) 92 | { 93 | VisualElement inlineGroup = new VisualElement(); 94 | root.Add(inlineGroup); 95 | inlineGroup.AddToClassList("inline-text-group"); 96 | AddRichTextInternal(word, inlineGroup); 97 | AddRichTextInternal(nextWord, inlineGroup); 98 | ++i; 99 | continue; 100 | } 101 | } 102 | 103 | AddRichTextInternal(word, root); 104 | 105 | //Add all the words and style them. 106 | void AddRichTextInternal(RichText richText, VisualElement rootToAddTo) 107 | { 108 | RichTextTag tag = richText.richTextTag; 109 | TextElement inlineText = null; 110 | switch (tag.tag) 111 | { 112 | case RichTextTag.Tag.none: 113 | inlineText = AddInlineText(richText.associatedText, rootToAddTo); 114 | break; 115 | case RichTextTag.Tag.button: 116 | if (buttonRegistry == null) 117 | { 118 | Debug.LogWarning("There was no ButtonRegistry provided to AddRichText. Button tags will not function."); 119 | inlineText = AddInlineButton(() => Debug.LogWarning("There was no ButtonRegistry provided to AddRichText. Button tags will not function."), richText.associatedText, rootToAddTo); 120 | break; 121 | } 122 | if (!buttonRegistry.GetRegisteredButtonAction(tag.stringVariables, out Action action)) 123 | return; 124 | inlineText = AddInlineButton(action, richText.associatedText, rootToAddTo); 125 | break; 126 | case RichTextTag.Tag.code: 127 | //Scroll 128 | ScrollView codeScroll = new ScrollView(ScrollViewMode.Horizontal); 129 | VisualElement contentContainer = codeScroll.contentContainer; 130 | codeScroll.contentViewport.style.flexDirection = FlexDirection.Column; 131 | codeScroll.contentViewport.style.alignItems = Align.Stretch; 132 | codeScroll.AddToClassList("code-scroll"); 133 | root.Add(codeScroll); 134 | 135 | contentContainer.ClearClassList(); 136 | contentContainer.AddToClassList("code-container"); 137 | VisualElement codeContainer = contentContainer; 138 | 139 | CSharpHighlighter highlighter = new CSharpHighlighter 140 | { 141 | AddStyleDefinition = false 142 | }; 143 | // To add code, we first use the CSharpHighlighter to construct rich text for us. 144 | string highlit = highlighter.Highlight(richText.associatedText); 145 | // After constructing new rich text we pass the text back recursively through this function with the new parent. 146 | AddRichText(highlit, buttonRegistry, codeContainer, true); // only parse spans because this is all the CSharpHighlighter parses. 147 | //Finalise content container 148 | foreach (VisualElement child in codeContainer.Children()) 149 | { 150 | if (child.ClassListContains(paragraphContainerClass)) 151 | { 152 | child.AddToClassList("code"); 153 | if (child.childCount == 1) 154 | AddInlineText("", child);//This seems to be required to get layout to function properly. 155 | } 156 | } 157 | 158 | //Begin Hack 159 | FieldInfo m_inheritedStyle = typeof(VisualElement).GetField("inheritedStyle", BindingFlags.NonPublic | BindingFlags.Instance); 160 | if (m_inheritedStyle == null) 161 | m_inheritedStyle = typeof(VisualElement).GetField("m_InheritedStylesData", BindingFlags.NonPublic | BindingFlags.Instance); 162 | Type inheritedStylesData = Type.GetType("UnityEngine.UIElements.StyleSheets.InheritedStylesData,UnityEngine"); 163 | FieldInfo font = inheritedStylesData.GetField("font", BindingFlags.Public | BindingFlags.Instance); 164 | FieldInfo fontSize = inheritedStylesData.GetField("fontSize", BindingFlags.Public | BindingFlags.Instance); 165 | Font consola = (Font) EditorGUIUtility.Load("consola"); 166 | 167 | contentContainer.Query