├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Editor.meta ├── Editor ├── ProjectWindowHistory.cs ├── ProjectWindowHistory.cs.meta ├── ProjectWindowHistoryHolder.cs ├── ProjectWindowHistoryHolder.cs.meta ├── ProjectWindowHistoryManager.cs ├── ProjectWindowHistoryManager.cs.meta ├── ProjectWindowHistoryRecord.cs ├── ProjectWindowHistoryRecord.cs.meta ├── ProjectWindowHistoryView.cs ├── ProjectWindowHistoryView.cs.meta ├── ProjectWindowReflectionUtility.cs ├── ProjectWindowReflectionUtility.cs.meta ├── YujiAp.ProjectWindowHistory.Editor.asmdef └── YujiAp.ProjectWindowHistory.Editor.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── package.json └── package.json.meta /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## [1.0.0] - 2023-12-02 4 | ### first release 5 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a9defd240d87d4c05a60cee0179513df 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 928cab63c0c9344f6ae976278e3e2746 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/ProjectWindowHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace ProjectWindowHistory 7 | { 8 | /// 9 | /// ProjectWindowの履歴を保持するクラス 10 | /// 11 | [Serializable] 12 | public class ProjectWindowHistory 13 | { 14 | [SerializeField] private List _records; 15 | [SerializeField] private int _currentRecordIndex; 16 | 17 | public ProjectWindowHistoryRecord CurrentRecord => 18 | _currentRecordIndex >= 0 && _currentRecordIndex < _records.Count ? _records[_currentRecordIndex] : null; 19 | 20 | public bool CanUndo => _currentRecordIndex > 0; 21 | public bool CanRedo => _currentRecordIndex < _records.Count - 1; 22 | 23 | // 履歴レコードの最大数 24 | private const int MaxRecordCount = 50; 25 | 26 | public ProjectWindowHistory() 27 | { 28 | _records = new List(MaxRecordCount); 29 | _currentRecordIndex = -1; 30 | } 31 | 32 | /// 33 | /// 現在の状態をセット 34 | /// 35 | /// 36 | public void SetCurrentRecord(ProjectWindowHistoryRecord record) 37 | { 38 | // Redo側のレコードを削除 39 | if (_currentRecordIndex < _records.Count - 1) 40 | { 41 | _records.RemoveRange(_currentRecordIndex + 1, _records.Count - _currentRecordIndex - 1); 42 | } 43 | 44 | // レコードが最大数を超えていたら古いものから削除 45 | if (_records.Count >= MaxRecordCount) 46 | { 47 | var overCount = _records.Count - MaxRecordCount + 1; 48 | _records.RemoveRange(0, overCount); 49 | _currentRecordIndex -= overCount; 50 | } 51 | 52 | _records.Add(record); 53 | _currentRecordIndex++; 54 | } 55 | 56 | /// 57 | /// Undo操作 58 | /// 59 | /// 60 | public ProjectWindowHistoryRecord Undo() 61 | { 62 | RemoveInvalidRecords(); 63 | 64 | if (!CanUndo) 65 | { 66 | return null; 67 | } 68 | 69 | _currentRecordIndex--; 70 | return CurrentRecord; 71 | } 72 | 73 | /// 74 | /// Redo操作 75 | /// 76 | /// 77 | public ProjectWindowHistoryRecord Redo() 78 | { 79 | RemoveInvalidRecords(); 80 | 81 | if (!CanRedo) 82 | { 83 | return null; 84 | } 85 | 86 | _currentRecordIndex++; 87 | return CurrentRecord; 88 | } 89 | 90 | /// 91 | /// 複数回Undoをまとめて行う 92 | /// 93 | /// 94 | /// 95 | public ProjectWindowHistoryRecord UndoMultiple(int count) 96 | { 97 | RemoveInvalidRecords(); 98 | 99 | for (var i = 0; i < count; i++) 100 | { 101 | if (CanUndo) 102 | { 103 | Undo(); 104 | } 105 | } 106 | 107 | return CurrentRecord; 108 | } 109 | 110 | /// 111 | /// 複数回Redoをまとめて行う 112 | /// 113 | /// 114 | /// 115 | public ProjectWindowHistoryRecord RedoMultiple(int count) 116 | { 117 | RemoveInvalidRecords(); 118 | 119 | for (var i = 0; i < count; i++) 120 | { 121 | if (CanRedo) 122 | { 123 | Redo(); 124 | } 125 | } 126 | 127 | return CurrentRecord; 128 | } 129 | 130 | /// 131 | /// Undoレコード一覧を返す 132 | /// 133 | /// 134 | public IEnumerable GetUndoHistoryRecordList() 135 | { 136 | RemoveInvalidRecords(); 137 | return CanUndo ? _records.Take(_currentRecordIndex) : new List(); 138 | } 139 | 140 | /// 141 | /// Redoレコード一覧を返す 142 | /// 143 | /// 144 | public IEnumerable GetRedoHistoryRecordList() 145 | { 146 | RemoveInvalidRecords(); 147 | return CanRedo ? _records.Skip(_currentRecordIndex + 1) : new List(); 148 | } 149 | 150 | /// 151 | /// Invalidなレコードを削除する 152 | /// 153 | private void RemoveInvalidRecords() 154 | { 155 | for (var i = _records.Count - 1; i >= 0; i--) 156 | { 157 | var record = _records[i]; 158 | if (record.IsValid()) 159 | { 160 | continue; 161 | } 162 | 163 | _records.RemoveAt(i); 164 | if (i <= _currentRecordIndex) 165 | { 166 | _currentRecordIndex--; 167 | } 168 | } 169 | } 170 | 171 | /// 172 | /// 履歴をクリアする 173 | /// 174 | public void Clear() 175 | { 176 | _records.Clear(); 177 | _currentRecordIndex = -1; 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /Editor/ProjectWindowHistory.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10bda852e25b34bcfa8ba36200ec6ccf 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryHolder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace ProjectWindowHistory 8 | { 9 | /// 10 | /// ProjectWindowとHistoryのペアを保持しておくScriptableSingleton 11 | /// アセットとして保存はしてないので、Unityエディタ再起動時には履歴情報は消える 12 | /// 13 | public class ProjectWindowHistoryHolder : ScriptableSingleton 14 | { 15 | [SerializeField] private List _saveDataList = new(); 16 | 17 | public ProjectWindowHistory GetHistory(EditorWindow targetWindow) 18 | { 19 | return _saveDataList.FirstOrDefault(data => data.WindowInstanceId == targetWindow.GetInstanceID())?.History; 20 | } 21 | 22 | public void Add(EditorWindow targetWindow, ProjectWindowHistory history) 23 | { 24 | var saveData = new ProjectWindowHistorySaveData(targetWindow, history); 25 | _saveDataList.Add(saveData); 26 | } 27 | } 28 | 29 | [Serializable] 30 | public class ProjectWindowHistorySaveData 31 | { 32 | [SerializeField] private int _windowInstanceId; 33 | [SerializeField] private ProjectWindowHistory _history; 34 | 35 | public int WindowInstanceId => _windowInstanceId; 36 | public ProjectWindowHistory History => _history; 37 | 38 | public ProjectWindowHistorySaveData(EditorWindow window, ProjectWindowHistory history) 39 | { 40 | _windowInstanceId = window.GetInstanceID(); 41 | _history = history; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryHolder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e713151264734644824da386f9d8d245 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEditor; 4 | 5 | namespace ProjectWindowHistory 6 | { 7 | /// 8 | /// 各ProjectWindowHistoryViewを管理するクラス 9 | /// 10 | [InitializeOnLoad] 11 | public static class ProjectWindowHistoryManager 12 | { 13 | private static readonly Dictionary _historyViews = new(); 14 | private static bool _isInitialized; 15 | 16 | static ProjectWindowHistoryManager() 17 | { 18 | _isInitialized = false; 19 | EditorApplication.update += OnUpdate; 20 | } 21 | 22 | private static void AllProjectWindowUpdate() 23 | { 24 | var windows = ProjectWindowReflectionUtility.GetAllProjectWindows(); 25 | foreach (var window in windows) 26 | { 27 | UpdateProjectWindow(window); 28 | } 29 | } 30 | 31 | private static void OnUpdate() 32 | { 33 | // 1フレーム目で初期化 34 | if (!_isInitialized) 35 | { 36 | AllProjectWindowUpdate(); 37 | _isInitialized = true; 38 | } 39 | 40 | // 既に閉じられたProjectWindowがあればDictionaryから消す 41 | var closedPairs = _historyViews.Where(pair => pair.Key == null).ToList(); 42 | foreach (var (window, view) in closedPairs) 43 | { 44 | view.Destroy(); 45 | _historyViews.Remove(window); 46 | } 47 | 48 | // 最後に操作したProjectWindowを取得 49 | var lastProjectWindow = ProjectWindowReflectionUtility.GetLastProjectWindow(); 50 | if (lastProjectWindow == null) 51 | { 52 | return; 53 | } 54 | 55 | UpdateProjectWindow(lastProjectWindow); 56 | } 57 | 58 | private static void UpdateProjectWindow(EditorWindow projectWindow) 59 | { 60 | // 保存している履歴を取得、なければ新規作成 61 | var history = ProjectWindowHistoryHolder.instance.GetHistory(projectWindow); 62 | if (history == null) 63 | { 64 | history = new ProjectWindowHistory(); 65 | ProjectWindowHistoryHolder.instance.Add(projectWindow, history); 66 | } 67 | 68 | // 新規ProjectWindowならViewを作成してDictionaryに追加 69 | if (!_historyViews.ContainsKey(projectWindow)) 70 | { 71 | var view = new ProjectWindowHistoryView(projectWindow, history); 72 | _historyViews.Add(projectWindow, view); 73 | } 74 | 75 | // Viewの更新処理を呼ぶ 76 | _historyViews[projectWindow].OnUpdate(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 408a1a238ecb45d5acc30166f346377e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using SearchViewState = ProjectWindowHistory.ProjectWindowReflectionUtility.SearchViewState; 7 | 8 | namespace ProjectWindowHistory 9 | { 10 | /// 11 | /// ProjectWindowの履歴レコード 12 | /// 13 | [Serializable] 14 | public class ProjectWindowHistoryRecord 15 | { 16 | [SerializeField] private int[] _selectedFolderInstanceIds; 17 | [SerializeField] private string _searchedText; 18 | [SerializeField] private SearchViewState _searchViewState; 19 | 20 | public int[] SelectedFolderInstanceIDs => _selectedFolderInstanceIds; 21 | public string SearchedText => _searchedText; 22 | public SearchViewState SearchViewState => _searchViewState; 23 | 24 | public ProjectWindowHistoryRecord(int[] selectedFolderInstanceIds, string searchedText, SearchViewState searchViewState) 25 | { 26 | _selectedFolderInstanceIds = selectedFolderInstanceIds; 27 | _searchedText = searchedText; 28 | _searchViewState = searchViewState; 29 | } 30 | 31 | public void ChangeSearchViewState(SearchViewState searchViewState) 32 | { 33 | _searchViewState = searchViewState; 34 | } 35 | 36 | public bool IsValid() 37 | { 38 | // フォルダが何かしら削除されていた場合は無効にしておく 39 | return (_selectedFolderInstanceIds?.Any() ?? false) 40 | && _selectedFolderInstanceIds.All(instanceId => EditorUtility.InstanceIDToObject(instanceId) != null); 41 | } 42 | 43 | /// 44 | /// 表示用テキストを生成して返す 45 | /// 46 | /// 47 | public string ToLabelText() 48 | { 49 | // 検索文字列がない場合は選択フォルダ名 50 | if (string.IsNullOrEmpty(_searchedText)) 51 | { 52 | return SelectedFolderToLabelText(); 53 | } 54 | 55 | // 検索文字列がある場合は検索文字列と検索範囲 56 | var labelText = $"\"{_searchedText}\" [{_searchViewState}]"; 57 | 58 | // 検索範囲が選択フォルダ内の場合は選択フォルダ名も追記 59 | if (_searchViewState == SearchViewState.SubFolders) 60 | { 61 | labelText = $"{SelectedFolderToLabelText()} : {labelText}"; 62 | } 63 | 64 | return labelText; 65 | 66 | string SelectedFolderToLabelText() 67 | { 68 | const int displayFolderCountMax = 3; // 表示は最大3件 69 | var targetFolderNames = _selectedFolderInstanceIds 70 | .Take(displayFolderCountMax) 71 | .Select(id => 72 | { 73 | var path = AssetDatabase.GetAssetPath(id); 74 | return Path.GetFileName(path); 75 | }); 76 | 77 | var suffix = _selectedFolderInstanceIds.Length > displayFolderCountMax ? "+" : string.Empty; 78 | return string.Join(",", targetFolderNames) + suffix; 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryRecord.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2ca4524e080e4bf88756c536a78ea446 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryView.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using UnityEngine.UIElements; 5 | using SearchViewState = ProjectWindowHistory.ProjectWindowReflectionUtility.SearchViewState; 6 | 7 | namespace ProjectWindowHistory 8 | { 9 | /// 10 | /// ProjectWindowにUndo/Redoボタンを追加し、押下時に履歴を辿る処理を呼び出す 11 | /// 選択フォルダと検索結果の変化を検知し、履歴を追加する 12 | /// 13 | public class ProjectWindowHistoryView 14 | { 15 | private readonly EditorWindow _projectWindow; 16 | private readonly ProjectWindowHistory _history; 17 | private Button _undoButton; 18 | private Button _redoButton; 19 | 20 | private bool _isOneColumnViewMode; // 1カラムビューか 21 | private float _timeAddToHistoryForSearchedText; // 現在の検索文字列が入力完了してからの経過時間 22 | private string _lastSearchedText; // 前フレームの検索文字列 23 | private const float DurationForInputCompleted = 2f; // 入力完了と判断する秒数 24 | 25 | public ProjectWindowHistoryView(EditorWindow projectWindow, ProjectWindowHistory history) 26 | { 27 | _projectWindow = projectWindow; 28 | _history = history; 29 | 30 | CreateButton(); 31 | RefreshButtons(); 32 | } 33 | 34 | /// 35 | /// Undo/Redoボタンを作成する 36 | /// 37 | private void CreateButton() 38 | { 39 | const float buttonWidth = 20f; 40 | 41 | #if UNITY_2022_2_OR_NEWER 42 | // Unity2022.2以降ではSearchByImportLogTypeボタンが増えたため、その分ボタンの位置を左にずらす 43 | const float buttonMarginRight = 470f; 44 | #else 45 | const float buttonMarginRight = 440f; 46 | #endif 47 | 48 | _undoButton = new Button(Undo) 49 | { 50 | text = "<", 51 | focusable = false, 52 | style = 53 | { 54 | width = buttonWidth, 55 | position = new StyleEnum(Position.Absolute), 56 | right = buttonMarginRight + buttonWidth 57 | } 58 | }; 59 | _redoButton = new Button(Redo) 60 | { 61 | text = ">", 62 | focusable = false, 63 | style = 64 | { 65 | width = buttonWidth, 66 | position = new StyleEnum(Position.Absolute), 67 | right = buttonMarginRight 68 | } 69 | }; 70 | 71 | // 右クリックで履歴一覧を表示 72 | _undoButton.RegisterCallback(evt => 73 | { 74 | if (evt.button == 1) 75 | { 76 | ShowHistoryRecordListMenu(true); 77 | } 78 | }); 79 | _redoButton.RegisterCallback(evt => 80 | { 81 | if (evt.button == 1) 82 | { 83 | ShowHistoryRecordListMenu(false); 84 | } 85 | }); 86 | 87 | _projectWindow.rootVisualElement.Add(_undoButton); 88 | _projectWindow.rootVisualElement.Add(_redoButton); 89 | } 90 | 91 | private void Undo() 92 | { 93 | _isOneColumnViewMode = ProjectWindowReflectionUtility.IsOneColumnViewMode(_projectWindow); 94 | if (_isOneColumnViewMode) 95 | { 96 | return; 97 | } 98 | 99 | var record = _history.Undo(); 100 | if (record != null) 101 | { 102 | ApplyHistoryRecord(record); 103 | } 104 | } 105 | 106 | private void Redo() 107 | { 108 | _isOneColumnViewMode = ProjectWindowReflectionUtility.IsOneColumnViewMode(_projectWindow); 109 | if (_isOneColumnViewMode) 110 | { 111 | return; 112 | } 113 | 114 | var record = _history.Redo(); 115 | if (record != null) 116 | { 117 | ApplyHistoryRecord(record); 118 | } 119 | } 120 | 121 | /// 122 | /// 履歴情報をProjectWindowに反映する 123 | /// 124 | /// 125 | private void ApplyHistoryRecord(ProjectWindowHistoryRecord record) 126 | { 127 | ProjectWindowReflectionUtility.SetFolderSelection(_projectWindow, record.SelectedFolderInstanceIDs); 128 | ProjectWindowReflectionUtility.SetSearch(_projectWindow, record.SearchedText, record.SelectedFolderInstanceIDs); 129 | if (record.SearchViewState > 0) 130 | { 131 | ProjectWindowReflectionUtility.SetSearchViewState(_projectWindow, record.SearchViewState); 132 | } 133 | 134 | RefreshButtons(); 135 | } 136 | 137 | /// 138 | /// Undo/Redoボタンの状態を更新する 139 | /// 140 | private void RefreshButtons() 141 | { 142 | _undoButton.SetEnabled(!_isOneColumnViewMode && _history.CanUndo); 143 | _redoButton.SetEnabled(!_isOneColumnViewMode && _history.CanRedo); 144 | } 145 | 146 | public void OnUpdate() 147 | { 148 | // 1カラムビューかを取得 149 | _isOneColumnViewMode = ProjectWindowReflectionUtility.IsOneColumnViewMode(_projectWindow); 150 | if (_isOneColumnViewMode) 151 | { 152 | // 現状1カラムビューは未対応なので、ボタン状態の更新だけして終了 153 | RefreshButtons(); 154 | return; 155 | } 156 | 157 | CheckSearchedText(); 158 | CheckSelectedFolder(); 159 | } 160 | 161 | /// 162 | /// 検索文字列を履歴に追加するかチェック 163 | /// 164 | private void CheckSearchedText() 165 | { 166 | var searchedText = ProjectWindowReflectionUtility.GetSearchedText(_projectWindow); // 現在の検索文字列 167 | var realtimeSinceStartup = Time.realtimeSinceStartup; 168 | 169 | // 検索文字列の入力完了からの経過時間を更新 170 | if (searchedText != _lastSearchedText) 171 | { 172 | _timeAddToHistoryForSearchedText = realtimeSinceStartup + DurationForInputCompleted; 173 | _lastSearchedText = searchedText; 174 | } 175 | 176 | // 入力完了からの経過時間が閾値を超えていなければ、入力中と判断して何もしない 177 | if (realtimeSinceStartup < _timeAddToHistoryForSearchedText) 178 | { 179 | return; 180 | } 181 | 182 | // 検索文字列が空なら何もしない 183 | if (string.IsNullOrEmpty(searchedText)) 184 | { 185 | return; 186 | } 187 | 188 | // 検索範囲が指定されていない(SearchViewState.NotSearching)なら何もしない 189 | var searchViewState = ProjectWindowReflectionUtility.GetSearchViewState(_projectWindow); 190 | if (searchViewState == SearchViewState.NotSearching) 191 | { 192 | return; 193 | } 194 | 195 | var currentRecord = _history.CurrentRecord; 196 | 197 | // 検索文字列が最新履歴と変わったら履歴に追加 198 | var isSearchedTextChanged = searchedText != currentRecord?.SearchedText; 199 | if (isSearchedTextChanged) 200 | { 201 | var record = new ProjectWindowHistoryRecord(currentRecord?.SelectedFolderInstanceIDs, searchedText, searchViewState); 202 | _history.SetCurrentRecord(record); 203 | RefreshButtons(); 204 | } 205 | 206 | // 検索範囲が変わっただけなら、最新履歴の検索範囲を更新 207 | var isSearchViewStateChanged = searchViewState != currentRecord?.SearchViewState; 208 | if (isSearchViewStateChanged) 209 | { 210 | currentRecord?.ChangeSearchViewState(searchViewState); 211 | } 212 | } 213 | 214 | /// 215 | /// 選択フォルダを履歴に追加するかチェック 216 | /// 217 | private void CheckSelectedFolder() 218 | { 219 | var selectedFolderInstanceIds = ProjectWindowReflectionUtility.GetLastFolderInstanceIds(_projectWindow); 220 | var isFolderSelected = selectedFolderInstanceIds != null && selectedFolderInstanceIds.Any(); 221 | 222 | // ツリービューでフォルダが選択されていなければ何もしない 223 | if (!isFolderSelected) 224 | { 225 | return; 226 | } 227 | 228 | selectedFolderInstanceIds = selectedFolderInstanceIds 229 | .Where(instanceId => AssetDatabase.IsValidFolder(AssetDatabase.GetAssetPath(instanceId))) 230 | .ToArray(); 231 | var lastRecord = _history.CurrentRecord; 232 | var lastSelectedFolderInstanceIds = lastRecord?.SelectedFolderInstanceIDs; 233 | var isFirstFolderSelected = lastSelectedFolderInstanceIds == null; 234 | 235 | // 初めてフォルダを選択した、もしくは選択フォルダが最新履歴と変わったら履歴に追加 236 | if (isFirstFolderSelected || !selectedFolderInstanceIds.SequenceEqual(lastSelectedFolderInstanceIds)) 237 | { 238 | // 検索範囲が選択フォルダ内なら検索を維持しつつフォルダ選択され、それ以外の検索範囲なら検索はリセットされる 239 | var isSearchedSubFolders = lastRecord?.SearchViewState == SearchViewState.SubFolders; 240 | var searchedText = isSearchedSubFolders ? lastRecord.SearchedText : null; 241 | var searchViewState = isSearchedSubFolders ? SearchViewState.SubFolders : SearchViewState.NotSearching; 242 | 243 | // 履歴に追加する 244 | var record = new ProjectWindowHistoryRecord(selectedFolderInstanceIds, searchedText, searchViewState); 245 | _history.SetCurrentRecord(record); 246 | RefreshButtons(); 247 | } 248 | } 249 | 250 | /// 251 | /// 履歴一覧を表示 252 | /// 253 | /// 254 | private void ShowHistoryRecordListMenu(bool isUndo) 255 | { 256 | var menu = new GenericMenu(); 257 | var recordList = isUndo ? _history.GetUndoHistoryRecordList().Reverse().ToList() : _history.GetRedoHistoryRecordList().ToList(); 258 | for (var i = 0; i < recordList.Count; i++) 259 | { 260 | var labelText = recordList[i].ToLabelText(); 261 | 262 | var operationCount = i + 1; 263 | menu.AddItem(new GUIContent(labelText), false, () => 264 | { 265 | var record = isUndo ? _history.UndoMultiple(operationCount) : _history.RedoMultiple(operationCount); 266 | ApplyHistoryRecord(record); 267 | }); 268 | } 269 | 270 | menu.AddSeparator(""); 271 | menu.AddItem(new GUIContent("UndoRedo履歴を全削除"), false, () => 272 | { 273 | _history.Clear(); 274 | RefreshButtons(); 275 | }); 276 | 277 | menu.ShowAsContext(); 278 | } 279 | 280 | public void Destroy() 281 | { 282 | _undoButton.RemoveFromHierarchy(); 283 | _redoButton.RemoveFromHierarchy(); 284 | } 285 | } 286 | } -------------------------------------------------------------------------------- /Editor/ProjectWindowHistoryView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c853a699abc3142bea2f0aef99eddadd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectWindowReflectionUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | 7 | namespace ProjectWindowHistory 8 | { 9 | /// 10 | /// ProjectWindowのリフレクションUtilityクラス 11 | /// 12 | public static class ProjectWindowReflectionUtility 13 | { 14 | // ====== ProjectBrowserの型情報 ====== 15 | private static Type _projectBrowserType; 16 | private static Type ProjectBrowserType => _projectBrowserType ??= Type.GetType("UnityEditor.ProjectBrowser,UnityEditor"); 17 | 18 | private static Type _projectBrowserListType; 19 | private static Type ProjectBrowserListType => _projectBrowserListType ??= typeof(List<>).MakeGenericType(ProjectBrowserType); 20 | 21 | // ====== ProjectBrowserのフィールド ====== 22 | private static FieldInfo _lastInteractedProjectBrowserField; 23 | private static FieldInfo LastInteractedProjectBrowserField => _lastInteractedProjectBrowserField 24 | ??= ProjectBrowserType.GetField("s_LastInteractedProjectBrowser", BindingFlags.Public | BindingFlags.Static); 25 | 26 | private static FieldInfo _viewModeField; 27 | private static FieldInfo ViewModeField => _viewModeField 28 | ??= ProjectBrowserType.GetField("m_ViewMode", BindingFlags.NonPublic | BindingFlags.Instance); 29 | 30 | private static FieldInfo _lastFoldersField; 31 | private static FieldInfo LastFoldersField => _lastFoldersField 32 | ??= ProjectBrowserType.GetField("m_LastFolders", BindingFlags.NonPublic | BindingFlags.Instance); 33 | 34 | private static FieldInfo _searchFieldTextField; 35 | private static FieldInfo SearchFieldTextField => _searchFieldTextField 36 | ??= ProjectBrowserType.GetField("m_SearchFieldText", BindingFlags.NonPublic | BindingFlags.Instance); 37 | 38 | // ====== ProjectBrowserのメソッド ====== 39 | private static MethodInfo _getAllProjectBrowsersMethod; 40 | private static MethodInfo GetAllProjectBrowsersMethod => _getAllProjectBrowsersMethod 41 | ??= ProjectBrowserType.GetMethod("GetAllProjectBrowsers", BindingFlags.Public | BindingFlags.Static); 42 | 43 | private static MethodInfo _getFolderInstanceIDsMethod; 44 | private static MethodInfo GetFolderInstanceIDsMethod => _getFolderInstanceIDsMethod 45 | ??= ProjectBrowserType.GetMethod("GetFolderInstanceIDs", BindingFlags.NonPublic | BindingFlags.Static); 46 | 47 | private static MethodInfo _setFolderSelectionMethod; 48 | // オーバーロードがあるので引数2つのメソッドの方を探して呼ぶ 49 | private static MethodInfo SetFolderSelectionMethod => _setFolderSelectionMethod 50 | ??= ProjectBrowserType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) 51 | .FirstOrDefault(method => method.Name == "SetFolderSelection" && method.GetParameters().Length == 2); 52 | 53 | private static MethodInfo _setSearchMethod; 54 | // オーバーロードがあるのでSearchFilter型引数のメソッドの方を探して呼ぶ 55 | private static MethodInfo SetSearchMethod => _setSearchMethod 56 | ??= ProjectBrowserType.GetMethods(BindingFlags.Public | BindingFlags.Instance) 57 | .FirstOrDefault(method => method.Name == "SetSearch" && method.GetParameters().First().ParameterType == SearchFilterType); 58 | 59 | private static MethodInfo _getSearchViewStateMethod; 60 | private static MethodInfo GetSearchViewStateMethod => _getSearchViewStateMethod 61 | ??= ProjectBrowserType.GetMethod("GetSearchViewState", BindingFlags.NonPublic | BindingFlags.Instance); 62 | 63 | private static MethodInfo _setSearchViewStateMethod; 64 | private static MethodInfo SetSearchViewStateMethod => _setSearchViewStateMethod 65 | ??= ProjectBrowserType.GetMethod("SetSearchViewState", BindingFlags.NonPublic | BindingFlags.Instance); 66 | 67 | // ====== SearchFilterの型情報 ====== 68 | private const string SearchFilterTypeName = "UnityEditor.SearchFilter,UnityEditor"; 69 | private static Type _searchFilterType; 70 | private static Type SearchFilterType => _searchFilterType ??= Type.GetType(SearchFilterTypeName); 71 | 72 | // ====== SearchFilterのフィールド ====== 73 | private static FieldInfo _searchFilterFoldersField; 74 | private static FieldInfo SearchFilterFoldersField => _searchFilterFoldersField 75 | ??= SearchFilterType.GetField("m_Folders", BindingFlags.NonPublic | BindingFlags.Instance); 76 | 77 | // ====== SearchFilterのメソッド ====== 78 | private static MethodInfo _createSearchFilterFromStringMethod; 79 | private static MethodInfo CreateSearchFilterFromStringMethod => _createSearchFilterFromStringMethod 80 | ??= SearchFilterType.GetMethod("CreateSearchFilterFromString", BindingFlags.NonPublic | BindingFlags.Static); 81 | 82 | /// 83 | /// 現在エディタ上に存在する全てのProjectビューを取得する 84 | /// 85 | /// 86 | public static List GetAllProjectWindows() 87 | { 88 | var projectWindowsObject = GetAllProjectBrowsersMethod.Invoke(null, null); 89 | 90 | // 以下object型をList型に変換する処理 91 | var countProperty = ProjectBrowserListType.GetProperty("Count"); 92 | var indexer = ProjectBrowserListType.GetProperty("Item"); 93 | 94 | if (countProperty == null || indexer == null) 95 | { 96 | return new List(); 97 | } 98 | 99 | var projectWindowCount = (int) countProperty.GetValue(projectWindowsObject, null); 100 | var projectWindows = new List(); 101 | for (var i = 0; i < projectWindowCount; i++) 102 | { 103 | var projectWindow = (EditorWindow) indexer.GetValue(projectWindowsObject, new object[] { i }); 104 | projectWindows.Add(projectWindow); 105 | } 106 | 107 | return projectWindows; 108 | } 109 | 110 | /// 111 | /// 開いているProjectビューを取得する 112 | /// 113 | /// 114 | public static EditorWindow GetLastProjectWindow() 115 | { 116 | return (EditorWindow) LastInteractedProjectBrowserField.GetValue(null); 117 | } 118 | 119 | /// 120 | /// ProjectWindowが1カラムビューかどうか 121 | /// 122 | /// 123 | public static bool IsOneColumnViewMode(EditorWindow targetProjectWindow) 124 | { 125 | // OneColumn=0, TwoColumns=1 126 | return ((int) ViewModeField.GetValue(targetProjectWindow)) == 0; 127 | } 128 | 129 | /// 130 | /// 左側のツリーで選択したフォルダのインスタンスIDを取得する 131 | /// 132 | /// 133 | public static int[] GetLastFolderInstanceIds(EditorWindow targetProjectWindow) 134 | { 135 | // 選択中のフォルダのパス配列を取得 136 | var lastFolderPaths = LastFoldersField.GetValue(targetProjectWindow) ?? Array.Empty(); 137 | 138 | // インスタンスID配列にして返す 139 | return (int[]) GetFolderInstanceIDsMethod.Invoke(null, new[] { lastFolderPaths }); 140 | } 141 | 142 | /// 143 | /// 指定したインスタンスIDのフォルダを選択状態にする 144 | /// 145 | /// 146 | /// 147 | public static void SetFolderSelection(EditorWindow targetProjectWindow, int[] selectedFolderInstanceIds) 148 | { 149 | SetFolderSelectionMethod.Invoke(targetProjectWindow, new object[] { selectedFolderInstanceIds, false }); 150 | } 151 | 152 | /// 153 | /// 検索フィールドの文字列を取得する 154 | /// 155 | /// 156 | /// 157 | public static string GetSearchedText(EditorWindow targetProjectWindow) 158 | { 159 | return (string) SearchFieldTextField.GetValue(targetProjectWindow); 160 | } 161 | 162 | /// 163 | /// 検索を設定する 164 | /// 165 | /// 166 | /// 167 | /// 168 | public static void SetSearch(EditorWindow targetProjectWindow, string searchedText, int[] selectedFolderInstanceIds) 169 | { 170 | // 検索文字列からsearchFilterを生成する 171 | var searchFilter = CreateSearchFilterFromStringMethod.Invoke(null, new object[] { searchedText }); 172 | 173 | // searchFilterに選択中のフォルダを設定する 174 | var selectedFolderPathList = selectedFolderInstanceIds.Select(AssetDatabase.GetAssetPath).ToArray(); 175 | SearchFilterFoldersField.SetValue(searchFilter, selectedFolderPathList); 176 | 177 | // targetProjectWindowにsearchFilterを設定する 178 | SetSearchMethod.Invoke(targetProjectWindow, new[] { searchFilter }); 179 | } 180 | 181 | /// 182 | /// 検索範囲(SearchViewState)を取得する 183 | /// 184 | /// 185 | /// 186 | public static SearchViewState GetSearchViewState(EditorWindow targetProjectWindow) 187 | { 188 | return (SearchViewState) GetSearchViewStateMethod.Invoke(targetProjectWindow, null); 189 | } 190 | 191 | /// 192 | /// 検索範囲(SearchViewState)を設定する 193 | /// 194 | /// 195 | /// 196 | public static void SetSearchViewState(EditorWindow targetProjectWindow, SearchViewState searchViewState) 197 | { 198 | SetSearchViewStateMethod.Invoke(targetProjectWindow, new object[] { (int) searchViewState }); 199 | } 200 | 201 | /// 202 | /// internalクラスのProjectBrowser内で定義されたSearchViewStateを複製したもの 203 | /// 204 | public enum SearchViewState 205 | { 206 | NotSearching, 207 | AllAssets, 208 | InAssetsOnly, 209 | InPackagesOnly, 210 | SubFolders, 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /Editor/ProjectWindowReflectionUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 08ae52954074944f09e4a6032b382a87 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/YujiAp.ProjectWindowHistory.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YujiAp.ProjectWindowHistory.Editor", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Editor/YujiAp.ProjectWindowHistory.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cae170857ec24fdba984c925ee128db 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yusuke Nakajima 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 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3d2a637b5ce654598851877e20e7d4e3 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ProjectWindowHistory](https://github.com/Yusuke57/ProjectWindowHistory/assets/27964732/676088db-2202-40d9-a44b-a4db8cb38d9c) 2 | 3 | ## Overview 4 | Editor extension that allows Undo/Redo on Unity ProjectWindow. 5 | 6 | ![projectWindowHistory_demo](https://github.com/Yusuke57/ProjectWindowHistory/assets/27964732/9bd46aff-500c-4bc8-8087-e2f9010c2a43) 7 | 8 | ## Features 9 | - You can trace the ProjectWindow display folder and search history by left-clicking the button. 10 | - You can check the history list by right-clicking the button, and jump to that history by selecting the item. 11 | - If you have multiple ProjectWindows open, store the history for each ProjectWindow. 12 | 13 | ## Getting started 14 | 1. Open Window > PackageManager 15 | 2. Click the `+` button and select `Add package from git URL...` 16 | 3. Enter the URL below and click `Add` button 17 | 18 | ``` 19 | https://github.com/Yusuke57/ProjectWindowHistory.git 20 | ``` 21 | 22 | image 23 | 24 | ## Author 25 | @yuji_ap: [https://twitter.com/yuji_ap](https://twitter.com/yuji_ap) 26 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 70e131e51f7f346af81f3b5a35a62bfe 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.yujiap.project-window-history", 3 | "displayName": "ProjectWindowHistory", 4 | "version": "1.0.1", 5 | "unity": "2021.3", 6 | "description": "ProjectWindow display history can be saved and restored using the Undo/Redo button.", 7 | "author": { 8 | "name": "yuji_ap", 9 | "url": "https://twitter.com/yuji_ap" 10 | }, 11 | 12 | "keywords": [ 13 | "editor", 14 | "projectwindow", 15 | "history" 16 | ], 17 | 18 | "dependencies": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff92cd970bff34121856bb213398e39e 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------