├── Scripts ├── Editor │ ├── Core.meta │ ├── Utility.meta │ ├── Core │ │ ├── FavoritesManager.cs.meta │ │ ├── FavoritesToolbar.cs.meta │ │ ├── FavoritesContextMenu.cs.meta │ │ ├── FavoritesAdvancedDropdown.cs.meta │ │ ├── SelectionData.cs.meta │ │ ├── SelectionHistoryData.cs.meta │ │ ├── SelectionHistoryToolbar.cs.meta │ │ ├── SelectionData.cs │ │ ├── SelectionHistoryData.cs │ │ ├── FavoritesToolbar.cs │ │ ├── FavoritesContextMenu.cs │ │ ├── SelectionHistoryToolbar.cs │ │ ├── FavoritesAdvancedDropdown.cs │ │ └── FavoritesManager.cs │ ├── BrunoMikoski.HistoryPanel.asmdef.meta │ ├── Utility │ │ ├── UnityMainToolbarUtility.cs.meta │ │ └── UnityMainToolbarUtility.cs │ └── BrunoMikoski.HistoryPanel.asmdef └── Editor.meta ├── Documentation~ ├── history-panel.gif └── shortcuts-settings.png ├── LICENSE.meta ├── CHANGELOG.MD.meta ├── README.md.meta ├── package.json.meta ├── Scripts.meta ├── package.json ├── LICENSE ├── CHANGELOG.MD └── README.md /Scripts/Editor/Core.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c7e2b29fe1abd0641b6f6e4d5aafda32 3 | timeCreated: 1688664814 -------------------------------------------------------------------------------- /Scripts/Editor/Utility.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9bdbb0d6e8ef4784aaf45eefbf3b32d6 3 | timeCreated: 1688664826 -------------------------------------------------------------------------------- /Documentation~/history-panel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunomikoski/UnityHistoryPanel/HEAD/Documentation~/history-panel.gif -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b1e9796fac974f7a81c3f4ae7bb8f3b9 3 | timeCreated: 1739476667 -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesToolbar.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 763119cd4f8c4d20b485990f152420ad 3 | timeCreated: 1739476210 -------------------------------------------------------------------------------- /Documentation~/shortcuts-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunomikoski/UnityHistoryPanel/HEAD/Documentation~/shortcuts-settings.png -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesContextMenu.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cb3f7924c7854a5baa1494666af1a27b 3 | timeCreated: 1739477191 -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesAdvancedDropdown.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d8e049d647144d1384c4bd739c325d66 3 | timeCreated: 1739527935 -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b17020d8a4e58c0419a9456acafdf2a3 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CHANGELOG.MD.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1052abd30bc274ebd8e4021f367a4a82 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e00c152ecd78d94d9767fb078ff8fc3 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a46f9958e5ebe2347a2cf1ea70f6572a 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bda7375556d5343c1b1ca886f08f060c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 240c0d29897a345e192f8b1192bf30a7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor/BrunoMikoski.HistoryPanel.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c995764a90b83ae4ebf91a8222f68c06 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/SelectionData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 21a728553280c60489f420c7649487dc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/SelectionHistoryData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 20e61cb83e5f52d49a7394fb07749d10 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/SelectionHistoryToolbar.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1c9e88040d47c4244afc25afee907648 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Utility/UnityMainToolbarUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cab68dd5eb33b1b4380627af4644f591 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/BrunoMikoski.HistoryPanel.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BrunoMikoski.HistoryPanel", 3 | "references": [], 4 | "includePlatforms": [ 5 | "Editor" 6 | ], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.brunomikoski.editorhistorypanel", 3 | "displayName": "Selection History", 4 | "version": "0.3.3", 5 | "unity": "2022.3", 6 | "description": "History navigation panel retained between sessions and with hotkeys", 7 | "keywords": [ 8 | "editor" 9 | ], 10 | "category": "editor extensions", 11 | "homepage": "https://github.com/brunomikoski/UnityHistoryPanel", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/brunomikoski/UnityHistoryPanel.git" 15 | }, 16 | "bugs": "https://github.com/brunomikoski/UnityHistoryPanel/issues", 17 | "author": { 18 | "name": "Bruno Mikoski", 19 | "url": "https://www.brunomikoski.com" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bruno Andre Mikoski 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 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/SelectionData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace BrunoMikoski.SelectionHistory 9 | { 10 | [Serializable] 11 | internal class SelectionData 12 | { 13 | [SerializeField] 14 | private List guids = new List(); 15 | 16 | [SerializeField] 17 | private List instanceIDs = new List(); 18 | 19 | private string displayName; 20 | public string DisplayName 21 | { 22 | get 23 | { 24 | if (string.IsNullOrEmpty(displayName)) 25 | { 26 | displayName = string.Join(", ", GetSelectionObjects().Where(o => o != null).Select(o => o.name)); 27 | if (displayName.Length > 50) 28 | displayName = displayName.Substring(0, 47) + "..."; 29 | } 30 | return displayName; 31 | } 32 | } 33 | 34 | public bool IsValid => GetSelectionObjects().Any(selectionObj => selectionObj != null); 35 | 36 | public SelectionData(Object[] objects) 37 | { 38 | displayName = string.Empty; 39 | for (int i = 0; i < objects.Length; i++) 40 | { 41 | Object o = objects[i]; 42 | if (o == null) 43 | continue; 44 | if (o is GameObject gameObject) 45 | { 46 | instanceIDs.Add(gameObject.GetInstanceID()); 47 | } 48 | else 49 | { 50 | string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(o)); 51 | guids.Add(guid); 52 | } 53 | } 54 | } 55 | 56 | public void Select() 57 | { 58 | Selection.objects = GetSelectionObjects().Where(o => o != null).ToArray(); 59 | } 60 | 61 | private List GetSelectionObjects() 62 | { 63 | List storedObjs = new List(); 64 | for (int i = 0; i < guids.Count; i++) 65 | storedObjs.Add(AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]))); 66 | for (int i = 0; i < instanceIDs.Count; i++) 67 | storedObjs.Add(EditorUtility.InstanceIDToObject(instanceIDs[i])); 68 | return storedObjs; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [0.3.3] 9 | ## Changed 10 | - Refactored Favorites to be smarter 11 | 12 | ## [0.3.2] 13 | #Changed 14 | - Fixing favorites 15 | - Fixing History going weird sometimes 16 | 17 | ## [0.3.1] 18 | #Changed 19 | - Using AdvancedDropdown instead of the GenericMenu 20 | - Optimized things a little bit 21 | 22 | ## [0.3.0] 23 | ## Changed 24 | - Added the new Favorites feature 25 | 26 | ## [0.2.2] 27 | ## Changed 28 | - Disabled Initialization on Batch mode. 29 | 30 | ## [0.2.1] 31 | ### Deleted 32 | - Removed unused meta file 33 | 34 | ### Changed 35 | - Removed old image from readme 36 | 37 | ## [0.2.0] 38 | ### Changed 39 | - Changed initialization of the toolbar to improve performance 40 | - Updated the toolbar to cache the items only once 41 | - Toolbar Buttons now should be only visible when history has items 42 | - Updated the system to sse the SessionState instead of Editor Prefs 43 | 44 | ### Added 45 | - Forward button now also has right click support 46 | 47 | ## [0.1.1] 48 | ### Changed 49 | - Changed implementation 50 | 51 | ## [0.1.0] 52 | ### Changed 53 | - Fixed asmdef to use only `UnityEditor` 54 | - Fixed null check before displaying an invalid item 55 | 56 | ## [0.0.2] 57 | ### Changed 58 | - Removed custom implementation of hotkeys 59 | - Fixed missing package information 60 | - Setup readme to display correct information about the shortcuts 61 | - Exposed `Go Back` and `Go Forward` as MenuItem so can have hotkeys assigned by Unity Shortcut Editor 62 | 63 | 64 | ## [0.0.1] 65 | ### Added 66 | - First initial working version 67 | 68 | [0.3.3]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.3.3 69 | [0.3.2]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.3.2 70 | [0.3.1]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.3.1 71 | [0.3.0]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.3.0 72 | [0.2.2]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.2.2 73 | [0.2.1]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.2.1 74 | [0.2.0]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.2.0 75 | [0.1.1]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.1.1 76 | [0.1.0]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.1.0 77 | [0.0.2]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.0.2 78 | [0.0.1]: https://github.com/brunomikoski/UnityHistoryPanel/releases/tag/v0.0.1 79 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/SelectionHistoryData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace BrunoMikoski.SelectionHistory 7 | { 8 | [Serializable] 9 | internal class SelectionHistoryData 10 | { 11 | [SerializeField] 12 | private List selectionData = new List(); 13 | public List SelectionData => selectionData; 14 | 15 | [SerializeField] 16 | private int pointInTime; 17 | public int PointInTime => pointInTime; 18 | 19 | private bool movingInHistory; 20 | 21 | public void AddToHistory(Object[] objects) 22 | { 23 | if (movingInHistory) 24 | { 25 | movingInHistory = false; 26 | return; 27 | } 28 | 29 | SelectionData item = new SelectionData(objects); 30 | if (!item.IsValid) 31 | return; 32 | 33 | if (selectionData.Count > 0 && 34 | selectionData[selectionData.Count - 1].Equals(item)) 35 | return; 36 | 37 | if (pointInTime < selectionData.Count - 1) 38 | selectionData.RemoveRange(pointInTime + 1, 39 | selectionData.Count - pointInTime - 1); 40 | 41 | selectionData.Add(item); 42 | 43 | if (selectionData.Count > SelectionHistoryToolbar.MaximumHistoryItems) 44 | selectionData.RemoveAt(0); 45 | 46 | pointInTime = selectionData.Count - 1; 47 | } 48 | 49 | public void Back() 50 | { 51 | if (pointInTime == 0) 52 | return; 53 | 54 | for (int i = pointInTime - 1; i >= 0; i--) 55 | { 56 | if (!selectionData[i].IsValid) 57 | continue; 58 | 59 | pointInTime = i; 60 | movingInHistory = true; 61 | selectionData[i].Select(); 62 | break; 63 | } 64 | } 65 | 66 | public void Forward() 67 | { 68 | if (pointInTime >= selectionData.Count - 1) 69 | return; 70 | 71 | int i = pointInTime + 1; 72 | while (i < selectionData.Count && !selectionData[i].IsValid) 73 | i++; 74 | 75 | if (i >= selectionData.Count) 76 | return; 77 | 78 | pointInTime = i; 79 | movingInHistory = true; 80 | selectionData[i].Select(); 81 | } 82 | 83 | public void SetPointInTime(int itemIndex) 84 | { 85 | movingInHistory = true; 86 | pointInTime = itemIndex; 87 | selectionData[pointInTime].Select(); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Editor History panel 2 | 3 | 4 |

5 | 6 | GitHub license 7 | 8 | 9 |

10 |

11 | 12 | 13 | 14 | 15 | 16 | GitHub issues 17 | 18 | 19 | 20 | GitHub pull requests 21 | 22 | 23 | GitHub last commit 24 |

25 | 26 |

27 | 28 | GitHub followers 29 | 30 | 31 | Twitter Follow 32 | 33 |

34 | 35 | 36 | 37 | ## Features 38 | - Don't need to be focuses to get shortcuts, just open and keep with something, like inspector 39 | - Keep history between Editor / Playtime 40 | 41 | 42 | ## How to use 43 | 1. Just open by `Tools/History/Open History` 44 | 2. I recommend setting up shortcuts between `Go Back` and `Go Forward` using the Unity Shortcut Menu: 45 | ![wizard](/Documentation~/shortcuts-settings.png) 46 | 47 | 48 | ## System Requirements 49 | Unity 2018.4.0 or later versions 50 | 51 | 52 | ## How to install 53 | 54 |
55 | Add from OpenUPM | via scoped registry, recommended 56 | 57 | This package is available on OpenUPM: https://openupm.com/packages/com.brunomikoski.editorhistorypanel 58 | 59 | To add it the package to your project: 60 | 61 | - open `Edit/Project Settings/Package Manager` 62 | - add a new Scoped Registry: 63 | ``` 64 | Name: OpenUPM 65 | URL: https://package.openupm.com/ 66 | Scope(s): com.brunomikoski 67 | ``` 68 | - click Save 69 | - open Package Manager 70 | - click + 71 | - select Add from Git URL 72 | - paste `com.brunomikoski.editorhistorypanel` 73 | - click Add 74 |
75 | 76 |
77 | Add from GitHub | not recommended, no updates :( 78 | 79 | You can also add it directly from GitHub on Unity 2019.4+. Note that you won't be able to receive updates through Package Manager this way, you'll have to update manually. 80 | 81 | - open Package Manager 82 | - click + 83 | - select Add from Git URL 84 | - paste `https://github.com/brunomikoski/Animation-Sequencer.git` 85 | - click Add 86 |
87 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesToolbar.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEditor.IMGUI.Controls; 3 | using UnityEditor.UIElements; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | namespace BrunoMikoski.SelectionHistory 8 | { 9 | [InitializeOnLoad] 10 | internal static class FavoritesToolbar 11 | { 12 | private static bool pendingOpen; 13 | private static Rect pendingRect; 14 | private static readonly AdvancedDropdownState CACHED_STATE = new AdvancedDropdownState(); 15 | 16 | static FavoritesToolbar() 17 | { 18 | EditorApplication.delayCall += Initialize; 19 | } 20 | 21 | private static void Initialize() 22 | { 23 | VisualElement parent = new VisualElement 24 | { 25 | style = { flexGrow = 0, flexDirection = FlexDirection.Row, flexShrink = 1, minWidth = 0 } 26 | }; 27 | 28 | ToolbarMenu favoritesDropdown = new ToolbarMenu 29 | { 30 | tooltip = "Click to see your favorite assets", 31 | text = "Favorites " 32 | }; 33 | ApplyToolbarStyle(favoritesDropdown); 34 | 35 | favoritesDropdown.RegisterCallback(e => 36 | { 37 | if (e.button != 0) 38 | return; 39 | 40 | pendingRect = favoritesDropdown.worldBound; 41 | pendingOpen = true; 42 | }); 43 | 44 | parent.Add(favoritesDropdown); 45 | 46 | IMGUIContainer opener = new IMGUIContainer(); 47 | opener.style.width = 0; 48 | opener.style.height = 0; 49 | opener.onGUIHandler = () => 50 | { 51 | if (!pendingOpen) 52 | return; 53 | pendingOpen = false; 54 | 55 | Vector2 guiPos = GUIUtility.ScreenToGUIPoint(pendingRect.position); 56 | Rect guiRect = new Rect(guiPos.x, guiPos.y + pendingRect.height, pendingRect.width, 0f); 57 | FavoritesAdvancedDropdown dropdown = new FavoritesAdvancedDropdown(CACHED_STATE); 58 | dropdown.SetFavorites(FavoritesManager.GetManualFavoriteEntries(), FavoritesManager.GetLearnedFavorites()); 59 | dropdown.Show(guiRect); 60 | }; 61 | parent.Add(opener); 62 | 63 | UnityMainToolbarUtility.AddCustom(UnityMainToolbarUtility.TargetContainer.Left, 64 | UnityMainToolbarUtility.Side.Right, 65 | parent, 4); 66 | } 67 | 68 | private static void ApplyToolbarStyle(VisualElement element) 69 | { 70 | element.AddToClassList("unity-toolbar-button"); 71 | element.AddToClassList("unity-editor-toolbar-element"); 72 | element.RemoveFromClassList("unity-button"); 73 | element.style.paddingRight = 8; 74 | element.style.paddingLeft = 8; 75 | element.style.justifyContent = Justify.Center; 76 | element.style.display = DisplayStyle.Flex; 77 | element.style.borderTopLeftRadius = 2; 78 | element.style.borderTopRightRadius = 2; 79 | element.style.borderBottomLeftRadius = 2; 80 | element.style.borderBottomRightRadius = 2; 81 | element.style.height = 19; 82 | element.style.marginRight = 1; 83 | element.style.marginLeft = 1; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Scripts/Editor/Utility/UnityMainToolbarUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEditor; 4 | using UnityEditor.Toolbars; 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace BrunoMikoski.SelectionHistory 10 | { 11 | public static class UnityMainToolbarUtility 12 | { 13 | public enum TargetContainer { Left, Right, Center } 14 | public enum Side { Left, Right } 15 | 16 | private static VisualElement MAIN_TOOLBAR_ROOT; 17 | private static VisualElement LEFT_ZONE_ROOT; 18 | private static VisualElement RIGHT_ZONE_ROOT; 19 | private static VisualElement rootVisualElement; 20 | private static Type ToolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar"); 21 | private static bool initialized; 22 | 23 | public static void AddCustom(TargetContainer container, Side side, VisualElement custom, int position) 24 | { 25 | if (!Initialize()) 26 | return; 27 | 28 | VisualElement containerElement; 29 | switch (container) 30 | { 31 | case TargetContainer.Left: 32 | containerElement = LEFT_ZONE_ROOT; 33 | break; 34 | case TargetContainer.Right: 35 | containerElement = RIGHT_ZONE_ROOT; 36 | break; 37 | case TargetContainer.Center: 38 | containerElement = rootVisualElement; 39 | break; 40 | default: 41 | throw new NotSupportedException(); 42 | } 43 | int index = Mathf.Clamp(position, 0, containerElement.childCount); 44 | containerElement.Insert(index, custom); 45 | } 46 | 47 | public static EditorToolbarButton AddButton(TargetContainer container, Side side, string name, Texture2D icon, Action callback) 48 | { 49 | if (!Initialize()) 50 | return null; 51 | 52 | EditorToolbarButton button = new EditorToolbarButton() { name = name, icon = icon }; 53 | VisualElement containerElement; 54 | switch (container) 55 | { 56 | case TargetContainer.Left: 57 | containerElement = LEFT_ZONE_ROOT; 58 | break; 59 | case TargetContainer.Right: 60 | containerElement = RIGHT_ZONE_ROOT; 61 | break; 62 | case TargetContainer.Center: 63 | containerElement = rootVisualElement; 64 | break; 65 | default: 66 | throw new NotSupportedException(); 67 | } 68 | containerElement.Add(button); 69 | button.clickable.clickedWithEventInfo += x => callback((MouseUpEvent)x); 70 | EditorToolbarUtility.SetupChildrenAsButtonStrip(containerElement); 71 | return button; 72 | } 73 | 74 | private static bool Initialize() 75 | { 76 | if (initialized) 77 | return true; 78 | Object[] toolbars = Resources.FindObjectsOfTypeAll(ToolbarType); 79 | if (toolbars.Length == 0) 80 | return false; 81 | 82 | Object toolbar = toolbars[0]; 83 | FieldInfo rootField = toolbar.GetType().GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance); 84 | rootVisualElement = rootField.GetValue(toolbar) as VisualElement; 85 | LEFT_ZONE_ROOT = rootVisualElement.Q("ToolbarZoneLeftAlign"); 86 | RIGHT_ZONE_ROOT = rootVisualElement.Q("ToolbarZoneRightAlign"); 87 | initialized = true; 88 | return true; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesContextMenu.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace BrunoMikoski.SelectionHistory 5 | { 6 | namespace BrunoMikoski.SelectionHistory 7 | { 8 | internal static class FavoritesContextMenu 9 | { 10 | private const string AssetsMenuPath = "Assets/Favorite"; 11 | private const string GameObjectMenuPath = "GameObject/Favorite"; 12 | private const string ContextMenuPath = "CONTEXT/Object/Favorite"; 13 | 14 | [MenuItem(AssetsMenuPath, false, 2000)] 15 | private static void ToggleFavoriteAssets() 16 | { 17 | Object[] objs = Selection.objects; 18 | if (objs == null || objs.Length == 0) 19 | return; 20 | bool allFav = FavoritesManager.AreAllManualFavorites(objs); 21 | for (int i = 0; i < objs.Length; i++) 22 | { 23 | Object o = objs[i]; 24 | if (o == null) 25 | continue; 26 | if (allFav) 27 | FavoritesManager.RemoveManualFavorite(o); 28 | else 29 | FavoritesManager.AddManualFavorite(o); 30 | } 31 | } 32 | 33 | [MenuItem(AssetsMenuPath, true)] 34 | private static bool ValidateToggleFavoriteAssets() 35 | { 36 | Object[] objs = Selection.objects; 37 | bool enabled = objs != null && objs.Length > 0; 38 | if (enabled) 39 | { 40 | bool allFav = FavoritesManager.AreAllManualFavorites(objs); 41 | Menu.SetChecked(AssetsMenuPath, allFav); 42 | } 43 | 44 | return enabled; 45 | } 46 | 47 | [MenuItem(GameObjectMenuPath, false, 2000)] 48 | private static void ToggleFavoriteGameObject() 49 | { 50 | GameObject[] objs = Selection.gameObjects; 51 | if (objs == null || objs.Length == 0) 52 | return; 53 | bool allFav = FavoritesManager.AreAllManualFavorites(objs); 54 | for (int i = 0; i < objs.Length; i++) 55 | { 56 | GameObject o = objs[i]; 57 | if (o == null) 58 | continue; 59 | if (allFav) 60 | FavoritesManager.RemoveManualFavorite(o); 61 | else 62 | FavoritesManager.AddManualFavorite(o); 63 | } 64 | } 65 | 66 | [MenuItem(GameObjectMenuPath, true)] 67 | private static bool ValidateToggleFavoriteGameObject() 68 | { 69 | GameObject[] objs = Selection.gameObjects; 70 | bool enabled = objs != null && objs.Length > 0; 71 | if (enabled) 72 | { 73 | bool allFav = FavoritesManager.AreAllManualFavorites(objs); 74 | Menu.SetChecked(GameObjectMenuPath, allFav); 75 | } 76 | 77 | return enabled; 78 | } 79 | 80 | [MenuItem(ContextMenuPath, false, 2000)] 81 | private static void ToggleFavoriteContext(MenuCommand command) 82 | { 83 | Object obj = command.context; 84 | if (obj == null) 85 | return; 86 | FavoritesManager.ToggleManualFavorite(obj); 87 | } 88 | 89 | [MenuItem(ContextMenuPath, true)] 90 | private static bool ValidateToggleFavoriteContext(MenuCommand command) 91 | { 92 | Object obj = command.context; 93 | if (obj == null) 94 | return false; 95 | bool isFav = FavoritesManager.IsManualFavorite(obj); 96 | Menu.SetChecked(ContextMenuPath, isFav); 97 | return true; 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/SelectionHistoryToolbar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEditor.UIElements; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | namespace BrunoMikoski.SelectionHistory 8 | { 9 | [InitializeOnLoad] 10 | internal class SelectionHistoryToolbar 11 | { 12 | private static ToolbarMenu HISTORY_SELECTION_MENU; 13 | 14 | private static readonly string HISTORY_STORAGE_KEY = Application.productName + "EditorHistoryKey"; 15 | private const string MAX_HISTORY_ITEMS_KEY = "MaxHistoryItemsKey"; 16 | 17 | private static SelectionHistoryData CACHED_HISTORY; 18 | private static SelectionHistoryData History 19 | { 20 | get 21 | { 22 | if (CACHED_HISTORY != null) 23 | return CACHED_HISTORY; 24 | 25 | CACHED_HISTORY = new SelectionHistoryData(); 26 | string historyJson = SessionState.GetString(HISTORY_STORAGE_KEY, string.Empty); 27 | if (!string.IsNullOrEmpty(historyJson)) 28 | EditorJsonUtility.FromJsonOverwrite(historyJson, CACHED_HISTORY); 29 | return CACHED_HISTORY; 30 | } 31 | } 32 | 33 | 34 | private static int? CACHED_MAXIMUM_HISTORY_ITEMS; 35 | 36 | public static int MaximumHistoryItems 37 | { 38 | get 39 | { 40 | if (CACHED_MAXIMUM_HISTORY_ITEMS.HasValue) 41 | return CACHED_MAXIMUM_HISTORY_ITEMS.Value; 42 | CACHED_MAXIMUM_HISTORY_ITEMS = EditorPrefs.GetInt(MAX_HISTORY_ITEMS_KEY, 30); 43 | return CACHED_MAXIMUM_HISTORY_ITEMS.Value; 44 | } 45 | } 46 | 47 | private static VisualElement backButton; 48 | private static VisualElement forwardButton; 49 | 50 | 51 | static SelectionHistoryToolbar() 52 | { 53 | if (Application.isBatchMode) 54 | return; 55 | 56 | EditorApplication.delayCall += Initialize; 57 | } 58 | 59 | private static void Initialize() 60 | { 61 | VisualElement parent = new VisualElement() 62 | { 63 | style = 64 | { 65 | flexGrow = 0, 66 | flexDirection = FlexDirection.Row, 67 | }, 68 | }; 69 | 70 | parent.Add(new VisualElement() 71 | { 72 | style = 73 | { 74 | flexGrow = 1, 75 | }, 76 | }); 77 | 78 | 79 | HISTORY_SELECTION_MENU = new ToolbarMenu 80 | { 81 | visible = false, 82 | }; 83 | 84 | HISTORY_SELECTION_MENU.menu.AppendAction("Default is never shown", a => { }, 85 | a => DropdownMenuAction.Status.None); 86 | 87 | parent.Add(HISTORY_SELECTION_MENU); 88 | backButton = AddButton("d_tab_prev@2x", "Go Back in Selection History", GoBack, ShowBackwardsHistory); 89 | parent.Add(backButton); 90 | forwardButton = AddButton("d_tab_next@2x", "Go Forward in Selection History", GoForward, ShowForwardHistory); 91 | parent.Add(forwardButton); 92 | 93 | 94 | UnityMainToolbarUtility.AddCustom(UnityMainToolbarUtility.TargetContainer.Left, 95 | UnityMainToolbarUtility.Side.Right, parent, 3); 96 | 97 | EditorApplication.playModeStateChanged += EditorApplicationOnPlayModeStateChanged; 98 | AssemblyReloadEvents.beforeAssemblyReload += SaveHistory; 99 | Selection.selectionChanged += OnSelectionChanged; 100 | } 101 | 102 | private static void EditorApplicationOnPlayModeStateChanged(PlayModeStateChange obj) 103 | { 104 | if (obj != PlayModeStateChange.ExitingPlayMode && obj != PlayModeStateChange.ExitingEditMode) 105 | return; 106 | 107 | SaveHistory(); 108 | } 109 | 110 | private static void SaveHistory() 111 | { 112 | if (CACHED_HISTORY == null) 113 | return; 114 | 115 | string json = EditorJsonUtility.ToJson(CACHED_HISTORY); 116 | SessionState.SetString(HISTORY_STORAGE_KEY, json); 117 | } 118 | 119 | private static void OnSelectionChanged() 120 | { 121 | History.AddToHistory(Selection.objects); 122 | UpdateButtonsVisibility(); 123 | } 124 | 125 | private static void UpdateButtonsVisibility() 126 | { 127 | backButton.SetEnabled(History.SelectionData.Count > 1 && History.PointInTime > 0); 128 | forwardButton.SetEnabled(History.SelectionData.Count > 1 && History.PointInTime < History.SelectionData.Count - 1); 129 | } 130 | 131 | [MenuItem("Tools/Selection History/Go Back")] 132 | public static void GoBack() 133 | { 134 | History.Back(); 135 | } 136 | 137 | [MenuItem("Tools/Selection History/Go Forward")] 138 | public static void GoForward() 139 | { 140 | History.Forward(); 141 | } 142 | 143 | private static void ClearHistory() 144 | { 145 | EditorPrefs.DeleteKey(HISTORY_STORAGE_KEY); 146 | CACHED_HISTORY = new SelectionHistoryData(); 147 | UpdateButtonsVisibility(); 148 | } 149 | 150 | private static void SetPointInTime(int itemIndex) 151 | { 152 | History.SetPointInTime(itemIndex); 153 | } 154 | 155 | private static void ShowBackwardsHistory() 156 | { 157 | HISTORY_SELECTION_MENU.menu.MenuItems().Clear(); 158 | 159 | for (int i = History.PointInTime-1; i >= 0; i--) 160 | { 161 | SelectionData selectionData = History.SelectionData[i]; 162 | 163 | if (!selectionData.IsValid) 164 | continue; 165 | 166 | int targetIndex = i; 167 | HISTORY_SELECTION_MENU.menu.AppendAction(selectionData.DisplayName, a => 168 | { 169 | SetPointInTime(targetIndex); 170 | }); 171 | } 172 | 173 | 174 | HISTORY_SELECTION_MENU.menu.AppendSeparator(); 175 | HISTORY_SELECTION_MENU.menu.AppendAction("Clear History", a => 176 | { 177 | ClearHistory(); 178 | }, a => DropdownMenuAction.Status.Normal); 179 | 180 | HISTORY_SELECTION_MENU.ShowMenu(); 181 | } 182 | 183 | private static void ShowForwardHistory() 184 | { 185 | HISTORY_SELECTION_MENU.menu.MenuItems().Clear(); 186 | 187 | for (int i = History.PointInTime+1; i < History.SelectionData.Count; i++) 188 | { 189 | SelectionData selectionData = History.SelectionData[i]; 190 | 191 | if (!selectionData.IsValid) 192 | continue; 193 | 194 | 195 | int targetIndex = i; 196 | HISTORY_SELECTION_MENU.menu.AppendAction(selectionData.DisplayName, a => 197 | { 198 | SetPointInTime(targetIndex); 199 | }); 200 | } 201 | 202 | HISTORY_SELECTION_MENU.menu.AppendSeparator(); 203 | HISTORY_SELECTION_MENU.menu.AppendAction("Clear History", a => 204 | { 205 | ClearHistory(); 206 | }, a => DropdownMenuAction.Status.Normal); 207 | 208 | HISTORY_SELECTION_MENU.ShowMenu(); 209 | } 210 | 211 | 212 | #region UI Elements visuals 213 | 214 | private static VisualElement AddButton(string iconName, string tooltip, Action leftMouseClickCallback, 215 | Action rightMouseClickCallback = null) 216 | { 217 | Button button = new Button() 218 | { 219 | tooltip = tooltip 220 | }; 221 | button.clickable.activators.Clear(); 222 | button.RegisterCallback(e => 223 | { 224 | if (e.button == 1 && rightMouseClickCallback != null) 225 | rightMouseClickCallback(); 226 | else 227 | leftMouseClickCallback(); 228 | }); 229 | 230 | FitChildrenStyle(button); 231 | 232 | VisualElement icon = new VisualElement(); 233 | icon.AddToClassList("unity-editor-toolbar-element__icon"); 234 | icon.style.backgroundImage = 235 | Background.FromTexture2D((Texture2D) EditorGUIUtility.IconContent(iconName).image); 236 | icon.style.height = 12; 237 | icon.style.width = 12; 238 | icon.style.alignSelf = Align.Center; 239 | button.Add(icon); 240 | 241 | return button; 242 | } 243 | 244 | 245 | private static void FitChildrenStyle(VisualElement element) 246 | { 247 | element.AddToClassList("unity-toolbar-button"); 248 | element.AddToClassList("unity-editor-toolbar-element"); 249 | element.RemoveFromClassList("unity-button"); 250 | element.style.paddingRight = 8; 251 | element.style.paddingLeft = 8; 252 | element.style.justifyContent = Justify.Center; 253 | element.style.display = DisplayStyle.Flex; 254 | element.style.borderTopLeftRadius = 2; 255 | element.style.borderTopRightRadius = 2; 256 | element.style.borderBottomLeftRadius = 2; 257 | element.style.borderBottomRightRadius = 2; 258 | element.style.height = 19; 259 | 260 | element.style.marginRight = 1; 261 | element.style.marginLeft = 1; 262 | } 263 | 264 | #endregion 265 | } 266 | } -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesAdvancedDropdown.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace BrunoMikoski.SelectionHistory 9 | { 10 | internal class FavoritesAdvancedDropdownItem : AdvancedDropdownItem 11 | { 12 | public readonly string GlobalId; 13 | public readonly string ContainerPath; 14 | public readonly string PreLabel; 15 | public readonly Object IconSource; 16 | 17 | public FavoritesAdvancedDropdownItem(string label, string globalId, string containerPath, string preLabel, Object iconSource) : base(label) 18 | { 19 | GlobalId = globalId; 20 | ContainerPath = containerPath; 21 | PreLabel = preLabel; 22 | IconSource = iconSource; 23 | if (iconSource != null) 24 | icon = (Texture2D)EditorGUIUtility.ObjectContent(iconSource, iconSource.GetType()).image; 25 | } 26 | } 27 | 28 | internal class FavoritesAdvancedDropdown : AdvancedDropdown 29 | { 30 | private List manualEntries; 31 | private List learnedFavorites; 32 | private List learnedIds; 33 | 34 | public FavoritesAdvancedDropdown(AdvancedDropdownState state) : base(state) 35 | { 36 | minimumSize = new Vector2(360, 420); 37 | } 38 | 39 | protected override AdvancedDropdownItem BuildRoot() 40 | { 41 | AdvancedDropdownItem root = new AdvancedDropdownItem("Favorites"); 42 | 43 | if (manualEntries != null && manualEntries.Count > 0) 44 | { 45 | for (int i = 0; i < manualEntries.Count; i++) 46 | { 47 | FavoriteEntry e = manualEntries[i]; 48 | Object resolved = TryResolve(e.globalId); 49 | string label = !string.IsNullOrEmpty(e.label) ? e.label : BuildDisplayLabel(e, resolved); 50 | root.AddChild(new FavoritesAdvancedDropdownItem(label, e.globalId, e.containerPath, e.label, resolved)); 51 | } 52 | } 53 | 54 | if (learnedFavorites != null && learnedFavorites.Count > 0) 55 | { 56 | if (manualEntries != null && manualEntries.Count > 0) 57 | root.AddSeparator(); 58 | 59 | for (int i = 0; i < learnedFavorites.Count; i++) 60 | { 61 | Object asset = learnedFavorites[i]; 62 | string gid = learnedIds != null && i < learnedIds.Count ? learnedIds[i] : (asset != null ? GlobalObjectId.GetGlobalObjectIdSlow(asset).ToString() : string.Empty); 63 | string label = BuildDisplayLabel(new FavoriteEntry { globalId = gid, containerPath = ComputeContainerPathFromGlobal(gid), objectPath = string.Empty, componentType = string.Empty }, asset); 64 | root.AddChild(new FavoritesAdvancedDropdownItem(label, gid, ComputeContainerPathFromGlobal(gid), label, asset)); 65 | } 66 | } 67 | 68 | return root; 69 | } 70 | 71 | public void SetFavorites(List manual, List learned) 72 | { 73 | manualEntries = manual ?? new List(); 74 | 75 | learnedFavorites = new List(); 76 | learnedIds = FavoritesManager.GetLearnedFavoriteIds(); 77 | 78 | HashSet manualIDs = new HashSet(); 79 | for (int i = 0; i < manualEntries.Count; i++) 80 | { 81 | Object resolved = TryResolve(manualEntries[i].globalId); 82 | if (resolved != null) 83 | manualIDs.Add(resolved.GetInstanceID()); 84 | } 85 | 86 | for (int i = 0; i < learned.Count; i++) 87 | { 88 | Object l = learned[i]; 89 | if (l != null && !manualIDs.Contains(l.GetInstanceID())) 90 | learnedFavorites.Add(l); 91 | } 92 | } 93 | 94 | protected override void ItemSelected(AdvancedDropdownItem item) 95 | { 96 | FavoritesAdvancedDropdownItem fav = item as FavoritesAdvancedDropdownItem; 97 | if (fav == null) 98 | return; 99 | 100 | bool shift = Event.current != null && Event.current.shift; 101 | 102 | Object target = TryResolve(fav.GlobalId); 103 | if (target == null) 104 | { 105 | string dynamicPath = ComputeContainerPathFromGlobal(fav.GlobalId); 106 | string pathToOpen = !string.IsNullOrEmpty(dynamicPath) ? dynamicPath : fav.ContainerPath; 107 | EnsureContainerLoaded(pathToOpen, shift); 108 | target = TryResolve(fav.GlobalId); 109 | } 110 | 111 | if (target == null) 112 | { 113 | // Clean up invalid manual favorite entries when the referenced object can no longer be resolved 114 | FavoritesManager.RemoveManualFavoriteByGlobalId(fav.GlobalId); 115 | return; 116 | } 117 | 118 | if (shift) 119 | OpenContainerForObject(target); 120 | 121 | Selection.activeObject = target; 122 | EditorGUIUtility.PingObject(target); 123 | } 124 | 125 | private static Object TryResolve(string globalId) 126 | { 127 | if (string.IsNullOrEmpty(globalId)) 128 | return null; 129 | GlobalObjectId gid; 130 | if (!GlobalObjectId.TryParse(globalId, out gid)) 131 | return null; 132 | Object[] objs = new Object[1]; 133 | GlobalObjectId[] gids = new GlobalObjectId[1]; 134 | gids[0] = gid; 135 | GlobalObjectId.GlobalObjectIdentifiersToObjectsSlow(gids, objs); 136 | return objs[0]; 137 | } 138 | 139 | private static void EnsureContainerLoaded(string path, bool openSingle) 140 | { 141 | if (string.IsNullOrEmpty(path)) 142 | return; 143 | 144 | if (path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase)) 145 | { 146 | // Verify the scene asset exists before attempting to open it to avoid ArgumentException 147 | SceneAsset sceneAsset = AssetDatabase.LoadAssetAtPath(path); 148 | if (sceneAsset == null) 149 | return; 150 | 151 | if (openSingle) 152 | UnityEditor.SceneManagement.EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); 153 | UnityEditor.SceneManagement.OpenSceneMode mode = openSingle ? UnityEditor.SceneManagement.OpenSceneMode.Single : UnityEditor.SceneManagement.OpenSceneMode.Additive; 154 | UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, mode); 155 | return; 156 | } 157 | 158 | if (path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) 159 | { 160 | Object main = AssetDatabase.LoadMainAssetAtPath(path); 161 | if (main != null) 162 | AssetDatabase.OpenAsset(main); 163 | return; 164 | } 165 | 166 | AssetDatabase.LoadMainAssetAtPath(path); 167 | } 168 | 169 | private static void OpenContainerForObject(Object obj) 170 | { 171 | if (obj == null) 172 | return; 173 | 174 | SceneAsset scene = obj as SceneAsset; 175 | if (scene != null) 176 | { 177 | string path = AssetDatabase.GetAssetPath(scene); 178 | if (!string.IsNullOrEmpty(path)) 179 | { 180 | UnityEditor.SceneManagement.EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); 181 | UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, UnityEditor.SceneManagement.OpenSceneMode.Single); 182 | } 183 | 184 | return; 185 | } 186 | 187 | if (PrefabUtility.IsPartOfPrefabAsset(obj)) 188 | { 189 | AssetDatabase.OpenAsset(obj); 190 | } 191 | } 192 | 193 | private static string BuildDisplayLabel(FavoriteEntry entry, Object resolved) 194 | { 195 | string containerName = string.IsNullOrEmpty(entry.containerPath) 196 | ? string.Empty 197 | : System.IO.Path.GetFileNameWithoutExtension(entry.containerPath); 198 | 199 | if (resolved != null) 200 | { 201 | if (resolved is GameObject go) 202 | { 203 | string path = GetTransformPath(go); 204 | if (!string.IsNullOrEmpty(entry.componentType)) 205 | return $"{containerName} -> {path} [{entry.componentType}]"; 206 | return $"{containerName} -> {path}"; 207 | } 208 | 209 | return resolved.name; 210 | } 211 | 212 | if (!string.IsNullOrEmpty(entry.objectPath)) 213 | { 214 | if (!string.IsNullOrEmpty(entry.componentType)) 215 | return $"{containerName} -> {entry.objectPath} [{entry.componentType}]"; 216 | return $"{containerName} -> {entry.objectPath}"; 217 | } 218 | 219 | return containerName.Length > 0 ? containerName : ""; 220 | } 221 | 222 | private static string GetTransformPath(GameObject go) 223 | { 224 | if (go == null) 225 | return string.Empty; 226 | string path = go.name; 227 | Transform current = go.transform.parent; 228 | while (current != null) 229 | { 230 | path = current.name + "/" + path; 231 | current = current.parent; 232 | } 233 | 234 | return path; 235 | } 236 | 237 | private static string ComputeContainerPathFromGlobal(string globalId) 238 | { 239 | if (!GlobalObjectId.TryParse(globalId, out GlobalObjectId gid)) 240 | return string.Empty; 241 | 242 | return AssetDatabase.GUIDToAssetPath(gid.assetGUID.ToString()); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Scripts/Editor/Core/FavoritesManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace BrunoMikoski.SelectionHistory 9 | { 10 | [Serializable] 11 | internal class LearnedFavorite 12 | { 13 | public string globalId; 14 | public int selectionCount; 15 | public long lastSelectedTicks; 16 | } 17 | 18 | [Serializable] 19 | internal class FavoriteEntry 20 | { 21 | public string globalId; 22 | public string containerPath; 23 | public string objectPath; 24 | public string componentType; 25 | public string label; 26 | } 27 | 28 | [Serializable] 29 | internal class FavoritesManualData 30 | { 31 | public FavoriteEntry[] entries; 32 | } 33 | 34 | [Serializable] 35 | internal class FavoritesLearnedData 36 | { 37 | public LearnedFavorite[] favorites; 38 | } 39 | 40 | internal static class FavoritesManager 41 | { 42 | private static string ManualKey = "FavoritesManual"; 43 | private static string LearnedKey = "FavoritesLearned"; 44 | 45 | private static List manualEntries = new List(); 46 | private static Dictionary learnedFavorites = new Dictionary(); 47 | 48 | private static bool manualFavoritesLoaded = false; 49 | private static bool learnedFavoritesLoaded = false; 50 | 51 | private static bool learnedDirty = false; 52 | private static double lastLearnedSaveTime = 0.0; 53 | private const double AUTO_SAVE_INTERVAL_SECONDS = 10.0; 54 | 55 | static FavoritesManager() 56 | { 57 | Selection.selectionChanged += OnSelectionChanged; 58 | EditorApplication.playModeStateChanged += OnPlayModeStateChanged; 59 | EditorApplication.quitting += OnEditorQuitting; 60 | EditorApplication.update += OnEditorUpdate; 61 | } 62 | 63 | private static void EnsureManualFavoritesLoaded() 64 | { 65 | if (!manualFavoritesLoaded) 66 | { 67 | LoadManualFavorites(); 68 | manualFavoritesLoaded = true; 69 | } 70 | } 71 | 72 | private static void EnsureLearnedFavoritesLoaded() 73 | { 74 | if (!learnedFavoritesLoaded) 75 | { 76 | LoadLearnedFavorites(); 77 | learnedFavoritesLoaded = true; 78 | } 79 | } 80 | 81 | private static void OnSelectionChanged() 82 | { 83 | EnsureLearnedFavoritesLoaded(); 84 | 85 | Object current = Selection.activeObject; 86 | if (current == null) 87 | return; 88 | 89 | string currentId = GlobalObjectId.GetGlobalObjectIdSlow(current).ToString(); 90 | if (!learnedFavorites.TryGetValue(currentId, out LearnedFavorite lf)) 91 | { 92 | lf = new LearnedFavorite 93 | { 94 | globalId = currentId, 95 | selectionCount = 0, 96 | lastSelectedTicks = 0 97 | }; 98 | learnedFavorites[currentId] = lf; 99 | } 100 | 101 | lf.selectionCount++; 102 | lf.lastSelectedTicks = DateTime.UtcNow.Ticks; 103 | 104 | learnedDirty = true; 105 | } 106 | 107 | private static void OnEditorUpdate() 108 | { 109 | if (!learnedDirty) 110 | return; 111 | 112 | double now = EditorApplication.timeSinceStartup; 113 | if (now - lastLearnedSaveTime >= AUTO_SAVE_INTERVAL_SECONDS) 114 | SaveLearnedFavorites(); 115 | } 116 | 117 | private static void OnPlayModeStateChanged(PlayModeStateChange state) 118 | { 119 | if (state == PlayModeStateChange.ExitingPlayMode || state == PlayModeStateChange.ExitingEditMode) 120 | SaveLearnedFavorites(); 121 | } 122 | 123 | private static void OnEditorQuitting() 124 | { 125 | SaveManualFavorites(); 126 | SaveLearnedFavorites(); 127 | } 128 | 129 | public static void ToggleManualFavorite(Object obj) 130 | { 131 | if (obj == null) 132 | return; 133 | if (IsManualFavorite(obj)) 134 | RemoveManualFavorite(obj); 135 | else 136 | AddManualFavorite(obj); 137 | } 138 | 139 | public static void AddManualFavorite(Object obj) 140 | { 141 | if (obj == null) 142 | return; 143 | 144 | EnsureManualFavoritesLoaded(); 145 | 146 | string id = GlobalObjectId.GetGlobalObjectIdSlow(obj).ToString(); 147 | if (IndexOfManual(id) >= 0) 148 | return; 149 | 150 | FavoriteEntry entry = new FavoriteEntry(); 151 | entry.globalId = id; 152 | entry.containerPath = ComputeContainerPath(id); 153 | entry.objectPath = ComputeObjectPath(obj); 154 | entry.componentType = GetComponentTypeName(obj); 155 | entry.label = BuildLabel(entry, obj); 156 | 157 | manualEntries.Add(entry); 158 | SaveManualFavorites(); 159 | } 160 | 161 | public static void RemoveManualFavorite(Object obj) 162 | { 163 | if (obj == null) 164 | return; 165 | 166 | EnsureManualFavoritesLoaded(); 167 | 168 | string id = GlobalObjectId.GetGlobalObjectIdSlow(obj).ToString(); 169 | int idx = IndexOfManual(id); 170 | if (idx >= 0) 171 | { 172 | manualEntries.RemoveAt(idx); 173 | SaveManualFavorites(); 174 | } 175 | } 176 | 177 | public static bool RemoveManualFavoriteByGlobalId(string globalId) 178 | { 179 | if (string.IsNullOrEmpty(globalId)) 180 | return false; 181 | 182 | EnsureManualFavoritesLoaded(); 183 | 184 | int idx = IndexOfManual(globalId); 185 | if (idx >= 0) 186 | { 187 | manualEntries.RemoveAt(idx); 188 | SaveManualFavorites(); 189 | return true; 190 | } 191 | 192 | return false; 193 | } 194 | 195 | public static bool IsManualFavorite(Object obj) 196 | { 197 | if (obj == null) 198 | return false; 199 | 200 | EnsureManualFavoritesLoaded(); 201 | string id = GlobalObjectId.GetGlobalObjectIdSlow(obj).ToString(); 202 | return IndexOfManual(id) >= 0; 203 | } 204 | 205 | public static bool AreAllManualFavorites(Object[] objs) 206 | { 207 | EnsureManualFavoritesLoaded(); 208 | if (objs == null || objs.Length == 0) 209 | return false; 210 | for (int i = 0; i < objs.Length; i++) 211 | { 212 | Object o = objs[i]; 213 | if (o == null) 214 | return false; 215 | if (!IsManualFavorite(o)) 216 | return false; 217 | } 218 | 219 | return true; 220 | } 221 | 222 | public static List GetManualFavoriteEntries() 223 | { 224 | EnsureManualFavoritesLoaded(); 225 | return new List(manualEntries); 226 | } 227 | 228 | public static List GetLearnedFavorites() 229 | { 230 | EnsureLearnedFavoritesLoaded(); 231 | 232 | List list = new List(learnedFavorites.Values); 233 | list.Sort((a, b) => 234 | { 235 | int byCount = b.selectionCount.CompareTo(a.selectionCount); 236 | if (byCount != 0) 237 | return byCount; 238 | return b.lastSelectedTicks.CompareTo(a.lastSelectedTicks); 239 | }); 240 | 241 | if (list.Count > 20) 242 | list = list.GetRange(0, 20); 243 | 244 | List result = new List(); 245 | GlobalObjectId[] gids = new GlobalObjectId[list.Count]; 246 | for (int i = 0; i < list.Count; i++) 247 | GlobalObjectId.TryParse(list[i].globalId, out gids[i]); 248 | 249 | Object[] objs = new Object[list.Count]; 250 | GlobalObjectId.GlobalObjectIdentifiersToObjectsSlow(gids, objs); 251 | 252 | for (int i = 0; i < objs.Length; i++) 253 | { 254 | Object o = objs[i]; 255 | if (o != null) 256 | result.Add(o); 257 | } 258 | 259 | return result; 260 | } 261 | 262 | public static List GetLearnedFavoriteIds() 263 | { 264 | EnsureLearnedFavoritesLoaded(); 265 | 266 | List list = new List(learnedFavorites.Values); 267 | list.Sort((a, b) => 268 | { 269 | int byCount = b.selectionCount.CompareTo(a.selectionCount); 270 | if (byCount != 0) 271 | return byCount; 272 | return b.lastSelectedTicks.CompareTo(a.lastSelectedTicks); 273 | }); 274 | 275 | List ids = new List(list.Count); 276 | for (int i = 0; i < list.Count; i++) 277 | ids.Add(list[i].globalId); 278 | return ids; 279 | } 280 | 281 | // ---------- Helpers ---------- 282 | private static int IndexOfManual(string globalId) 283 | { 284 | for (int i = 0; i < manualEntries.Count; i++) 285 | if (manualEntries[i].globalId == globalId) 286 | return i; 287 | return -1; 288 | } 289 | 290 | private static string ComputeContainerPath(string globalId) 291 | { 292 | GlobalObjectId gid; 293 | if (!GlobalObjectId.TryParse(globalId, out gid)) 294 | return string.Empty; 295 | return AssetDatabase.GUIDToAssetPath(gid.assetGUID.ToString()); 296 | } 297 | 298 | private static string ComputeObjectPath(Object obj) 299 | { 300 | Component comp = obj as Component; 301 | GameObject go = comp != null ? comp.gameObject : obj as GameObject; 302 | if (go == null) 303 | return string.Empty; 304 | 305 | StringBuilder sb = new StringBuilder(); 306 | Transform t = go.transform; 307 | List parts = new List(); 308 | while (t != null) 309 | { 310 | parts.Add(t.name); 311 | t = t.parent; 312 | } 313 | 314 | for (int i = parts.Count - 1; i >= 0; i--) 315 | { 316 | sb.Append(parts[i]); 317 | if (i > 0) 318 | sb.Append('/'); 319 | } 320 | 321 | return sb.ToString(); 322 | } 323 | 324 | private static string GetComponentTypeName(Object obj) 325 | { 326 | Component c = obj as Component; 327 | return c != null ? c.GetType().Name : string.Empty; 328 | } 329 | 330 | private static string BuildLabel(FavoriteEntry entry, Object liveObj) 331 | { 332 | string container = System.IO.Path.GetFileNameWithoutExtension(entry.containerPath); 333 | if (liveObj is Component) 334 | { 335 | return container + " -> " + entry.objectPath + " [" + entry.componentType + "]"; 336 | } 337 | 338 | if (liveObj is GameObject) 339 | { 340 | return container + " -> " + entry.objectPath; 341 | } 342 | 343 | if (liveObj != null) 344 | { 345 | return liveObj.name; 346 | } 347 | 348 | if (!string.IsNullOrEmpty(entry.objectPath)) 349 | return container + " -> " + entry.objectPath + (string.IsNullOrEmpty(entry.componentType) ? string.Empty : " [" + entry.componentType + "]"); 350 | if (!string.IsNullOrEmpty(container)) 351 | return container; 352 | return ""; 353 | } 354 | 355 | private static void SaveManualFavorites() 356 | { 357 | FavoritesManualData data = new FavoritesManualData(); 358 | data.entries = manualEntries.ToArray(); 359 | string json = JsonUtility.ToJson(data); 360 | EditorUserSettings.SetConfigValue(ManualKey, json); 361 | } 362 | 363 | private static void LoadManualFavorites() 364 | { 365 | string json = EditorUserSettings.GetConfigValue(ManualKey); 366 | if (string.IsNullOrEmpty(json)) 367 | return; 368 | 369 | FavoritesManualData data = JsonUtility.FromJson(json); 370 | if (data == null) 371 | return; 372 | 373 | if (data.entries != null && data.entries.Length > 0) 374 | { 375 | manualEntries = new List(data.entries); 376 | } 377 | } 378 | 379 | private static void SaveLearnedFavorites() 380 | { 381 | EnsureLearnedFavoritesLoaded(); 382 | FavoritesLearnedData data = new FavoritesLearnedData(); 383 | data.favorites = new LearnedFavorite[learnedFavorites.Values.Count]; 384 | int i = 0; 385 | foreach (KeyValuePair kvp in learnedFavorites) 386 | data.favorites[i++] = kvp.Value; 387 | string json = JsonUtility.ToJson(data); 388 | EditorUserSettings.SetConfigValue(LearnedKey, json); 389 | learnedDirty = false; 390 | lastLearnedSaveTime = EditorApplication.timeSinceStartup; 391 | } 392 | 393 | private static void LoadLearnedFavorites() 394 | { 395 | string json = EditorUserSettings.GetConfigValue(LearnedKey); 396 | if (string.IsNullOrEmpty(json)) 397 | return; 398 | FavoritesLearnedData data = JsonUtility.FromJson(json); 399 | if (data == null || data.favorites == null) 400 | return; 401 | learnedFavorites = new Dictionary(); 402 | for (int i = 0; i < data.favorites.Length; i++) 403 | { 404 | LearnedFavorite lf = data.favorites[i]; 405 | if (lf != null && !string.IsNullOrEmpty(lf.globalId)) 406 | learnedFavorites[lf.globalId] = lf; 407 | } 408 | } 409 | } 410 | } 411 | --------------------------------------------------------------------------------