[Unity, Editor] SOManagementWindow 제작 일기 Part.2

2025. 4. 25. 10:57·Editor

지난번에 발생했던 문제들을 해결하는 방법입니다.

저는 이 문제를 해결하기 위해서 VisualElement로 Tab을 구현하고 ScrollView 안쪽에 넣어서 관리 했습니다.

 

 

때문에 다음과 같은 함수들이 새로 추가 되었습니다.

 

TabVisualMake

/// <summary>
/// Tab의 Visual를 만드어주는 함수
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
private VisualElement TabVisualMake(string name)
{
    VisualElement tabVisual = new VisualElement();

    tabVisual.AddToClassList(_tabClassName);
    tabVisual.name = name;
    tabVisual.tooltip = GUID.Generate().ToString();
    
    // Mouse Down Event
    tabVisual.RegisterCallback<PointerDownEvent>(OnPointerDownInTabEvent);
    
    Label label = new Label();
    label.name = "label";
    label.AddToClassList(_tabLabelClassName);
    label.text = name;
    
    tabVisual.Add(label);

    return tabVisual;
}

Tab을 만들고 ScrollView에 넣어줌

/// <summary>
/// Tab 위에 마우스가 클릭 되었을 때 발생되는 이벤트
/// 현재 Tab을 변경시켜주고 Select USSClass 넣어준다.
/// </summary>
/// <param name="evt"></param>
private void OnPointerDownInTabEvent(PointerDownEvent evt)
{
    TabInfo tab = _tabList.Find(x => x.tabElement.GetHashCode()
                                     == evt.currentTarget.GetHashCode());
    _currentTab = tab;
    VisualElement tabElement = tab.tabElement;
    
    if (_currentSOView != null)
    {
        _currentSelectTab.RemoveFromClassList(_tabSelectClassName);
        _currentSelectTab.AddToClassList(_tabClassName);
        _rootElement.Remove(_currentSOView);
    }
    
    _currentSOView = _soInfoViewDict[tab];
    _currentSelectTab = tabElement;
    _rootElement.Add(_currentSOView);
    
    _currentSelectTab.RemoveFromClassList(_tabClassName);
    _currentSelectTab.AddToClassList(_tabSelectClassName);
    
    _fileNameField = _currentSOView.Q<TextField>("FileNameField");
    _selectedLabel = _currentSOView.Q<Label>("NameLabel");
}

Tab을 클릭했을 때 이벤트

 

이외에는 로직은 같고 최적화만 진행했습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Editors.SO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public struct TabInfo
{
    public VisualElement tabElement;
    public Type tabType;
    public Dictionary<VisualElement ,ScriptableObject> getSODict;
}

public class TestWindow : EditorWindow
{
    #region PathORUSSClass

    private readonly string _tabSelectClassName = "tab-select";
    private readonly string _tabClassName = "tab";
    private readonly string _tabLabelClassName = "tab-label";
    private readonly string _itemVisual = "item-visual";
    private readonly string _itemLabel = "item-label";
    private readonly string _itemVisualSelect = "item-visual-select";
    
    #endregion
    
    #region MyElements
    
    private ScrollView _tabScrollView;
    private VisualElement _rootElement;
    private VisualElement _currentSOView;
    private VisualElement _currentSelectTab;
    private IMGUIContainer _cachedGUI;
    private Label _selectedLabel;
    private VisualElement _selected;
    private TextField _fileNameField;
    
    #endregion
    
    #region UxmlTemplate
    
    [SerializeField]
    private VisualTreeAsset m_VisualTreeAsset = default;
    [SerializeField] private VisualTreeAsset _tabSplitView;
    
    #endregion
    
    /// <summary>
    /// 생성할 SO의 Type들을 모은 Table
    /// </summary>
    [SerializeField] private SOTypeTable _soTypeTable;

    [SerializeField] private SOCreator _soCreator; 

    private List<TabInfo> _tabList = new();
    private Dictionary<TabInfo, VisualElement> _soInfoViewDict = new();
    /// <summary>
    /// 생성된 Cached Editor을 담고 있는 Dict
    /// </summary>
    private Dictionary<ScriptableObject, Editor> _cachedEditorDict = new();
    /// <summary>
    /// SO의 실제 경로를 담고 있는 Dict
    /// </summary>
    private Dictionary<ScriptableObject, string> _soPathDict = new();
    /// <summary>
    /// 현재 SO Data
    /// </summary>
    private ScriptableObject _currentData;
    /// <summary>
    /// 현재 선택된 TabInfo
    /// </summary>
    private TabInfo _currentTab;

    [MenuItem("Editor/LJS/SoManagementWindow")]
    public static void ShowExample()
    {
        TestWindow wnd = GetWindow<TestWindow>();
        wnd.titleContent = new GUIContent("TestWindow");
        wnd.minSize = new Vector2(900, 600);
        wnd.maxSize = new Vector2(900, 600);
    }

    /// <summary>
    /// 기본 초기화 함수
    /// 기본적인 세팅과 Tab 생성 Button 이벤트 구독 ViewElement 생성 등 여러가지 작업을 한다.
    /// </summary>
    public void CreateGUI()
    {
        VisualElement root = rootVisualElement;
        root.style.flexGrow = 1;
        VisualElement labelFromUXML = m_VisualTreeAsset.Instantiate();
        root.Add(labelFromUXML);

        _tabScrollView = root.Q<ScrollView>("TabScrollView");
        _rootElement = root.Q<VisualElement>("SoManagement");

        foreach (var type in _soTypeTable._typeList)
        {
            TabInfo tab = new TabInfo();
            tab.tabType = type;
            tab.getSODict = new();
            
            var template = _tabSplitView.Instantiate().Q<VisualElement>();
            VisualElement itemVisualList = template.Q<VisualElement>("ItemVisualList"); 
            
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.Append("t:");
            strBuilder.Append(type);
            string[] soPathArray = AssetDatabase.FindAssets(strBuilder.ToString());
            string path = "";
            for (int i = 0; i < soPathArray.Length; ++i)
            {
                path = AssetDatabase.GUIDToAssetPath(soPathArray[i]);
                ScriptableObject so = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
                VisualElement view = ViewElementMake(so);
                tab.getSODict.Add(view, so);
                _soPathDict.Add(so, path);
                itemVisualList.Add(view);
            }

            VisualElement tabElement = TabVisualMake(type.Name);
            
            tab.tabElement = tabElement;
            
            _tabList.Add(tab);
            _tabScrollView.Add(tabElement);
            _soInfoViewDict.Add(tab, template);            
            template.Q<Button>("MakeBtn").clicked += HandleMakeBtnClickEvent;
            template.Q<Button>("DeleteBtn").clicked += HandleDeleteBtnClickEvent;
            template.Q<Button>("RenameBtn").clicked += HandleRenameBtnClickEvent;
        }
    }

    /// <summary>
    /// Tab의 Visual를 만드어주는 함수
    /// </summary>
    /// <param name="name"></param>
    /// <returns></returns>
    private VisualElement TabVisualMake(string name)
    {
        VisualElement tabVisual = new VisualElement();

        tabVisual.AddToClassList(_tabClassName);
        tabVisual.name = name;
        tabVisual.tooltip = GUID.Generate().ToString();
        
        // Mouse Down Event
        tabVisual.RegisterCallback<PointerDownEvent>(OnPointerDownInTabEvent);
        
        Label label = new Label();
        label.name = "label";
        label.AddToClassList(_tabLabelClassName);
        label.text = name;
        
        tabVisual.Add(label);

        return tabVisual;
    }

    /// <summary>
    /// 실제 보이는 ViewElement를 만들어주는 함수
    /// </summary>
    /// <param name="soData"> 만들 View의 Data </param>
    /// <returns></returns>
    private VisualElement ViewElementMake(ScriptableObject soData)
    {
        VisualElement element = new VisualElement();
        element.AddToClassList(_itemVisual);
        element.name = soData.name;
        element.tooltip = GUID.Generate().ToString();
        
        // Mouse Down Event
        element.RegisterCallback<PointerDownEvent>(OnPointerDownInElementEvent);
        
        Label label = new Label();
        label.name = "label";
        label.AddToClassList(_itemLabel);
        label.text = soData.name;
        
        element.Add(label);

        return element;
    }

    #region EventMethod
    
        /// <summary>
        /// Tab 위에 마우스가 클릭 되었을 때 발생되는 이벤트
        /// 현재 Tab을 변경시켜주고 Select USSClass 넣어준다.
        /// </summary>
        /// <param name="evt"></param>
        private void OnPointerDownInTabEvent(PointerDownEvent evt)
        {
            TabInfo tab = _tabList.Find(x => x.tabElement.GetHashCode()
                                             == evt.currentTarget.GetHashCode());
            _currentTab = tab;
            VisualElement tabElement = tab.tabElement;
            
            if (_currentSOView != null)
            {
                _currentSelectTab.RemoveFromClassList(_tabSelectClassName);
                _currentSelectTab.AddToClassList(_tabClassName);
                _rootElement.Remove(_currentSOView);
            }
            
            _currentSOView = _soInfoViewDict[tab];
            _currentSelectTab = tabElement;
            _rootElement.Add(_currentSOView);
            
            _currentSelectTab.RemoveFromClassList(_tabClassName);
            _currentSelectTab.AddToClassList(_tabSelectClassName);
            
            _fileNameField = _currentSOView.Q<TextField>("FileNameField");
            _selectedLabel = _currentSOView.Q<Label>("NameLabel");
        }
        
        /// <summary>
        /// Tab 내에 항목을 클릭했을 때 발행되는 이벤트
        /// Cached Editor 연결해주고 Select 상태로 USSClass 넣어줌
        /// </summary>
        /// <param name="evt"></param>
        private void OnPointerDownInElementEvent(PointerDownEvent evt)
        {
            if (_selectedLabel == null)
                _selectedLabel = _currentSelectTab.Q<Label>("NameLabel");
        
            foreach(var item in _currentTab.getSODict){
                VisualElement element = _currentSOView.Q<VisualElement>(item.Value.name);
            
                _cachedGUI = _currentSOView.Q<IMGUIContainer>();
                List<string> classNames = element.GetClasses().ToList();
            
                // 모든 SelectEffect가 적용된 VisualElement에 Effect를 제거한다.
                foreach(string str in classNames){
                    if(str == _itemVisualSelect){
                        element.AddToClassList(_itemVisual);
                        element.RemoveFromClassList(_itemVisualSelect);
                    }
                }

                // 클릭한 위치의 VisualElement에 Effect를 부여하고
                // Toolbar Label의 Value를 바꾸어 준다.
                if (evt.currentTarget.GetHashCode() == element.GetHashCode())
                {
                    element.RemoveFromClassList(_itemVisual);
                    element.AddToClassList(_itemVisualSelect);
                    _selectedLabel.text = item.Value.name;
                    _selected = element;
                
                    ValueListBinding(item.Value);
                    _fileNameField.value = "";
                }
            }

        }
        
        /// <summary>
        /// 이름을 변경 버튼 클릭 시 발행하는 이벤트
        /// 경로를 찾고 추적해서 변경해줌 그리고 다시 그려줌
        /// </summary>
        private void HandleRenameBtnClickEvent()
        {
            if (_fileNameField.text.Length <= 0)
            {
                Debug.LogError("The name must be at least 1 character long, and if it is only 1 character, special characters are not allowed.");    
                return;
            }
            
            if(_selected == null) return;
            
            string path = _soPathDict[_currentData];
            ScriptableObject changeTarget = AssetDatabase.LoadAssetAtPath<ScriptableObject>(
                path);
            VisualElement viewTable = _currentSOView.Q<VisualElement>("ItemVisualList");
            
            AssetDatabase.RenameAsset(path, _fileNameField.text);
            changeTarget.name = _fileNameField.text;
            
            viewTable.Remove(_selected);
            viewTable.Add(ViewElementMake(changeTarget));
        }

        /// <summary>
        /// 삭제 버튼 클릭시 발행되는 이벤트
        /// 역으로 경로를 추척하고 그 경로에 있는 SO를 삭제 해준다.
        /// 그리고 현재 Tab에서도 삭제해주고 비주얼도 지워준다.
        /// </summary>
        private void HandleDeleteBtnClickEvent()
        {
            if (_selected == null)
            {
                Debug.LogError("Select None");
                return;
            }
        
            // 선택된 Item 삭제
            string path = _soPathDict[_currentData];
            Debug.Log(path);
            ScriptableObject changeTarget = AssetDatabase.LoadAssetAtPath<ScriptableObject>(
                path);
            VisualElement viewTable = _currentSOView.Q<VisualElement>("ItemVisualList");
            VisualElement deleteElement = viewTable.Q<VisualElement>(_selected.name);
            viewTable.Remove(deleteElement);

            _currentTab.getSODict.Remove(deleteElement);
        
            AssetDatabase.DeleteAsset(path);
            AssetDatabase.SaveAssets();

            Editor cachedEditor = _cachedEditorDict[changeTarget];
            if (cachedEditor != null)
            {
                _cachedEditorDict.Remove(changeTarget);
                DestroyImmediate(cachedEditor);
            }

            _selected = null;
            _selectedLabel.text = string.Empty;
        }

        /// <summary>
        /// 만들기 버튼을 클릭 시 발행되는 이벤트
        /// SOCreator를 열어주고 기본 정보들을 전달함.
        /// </summary>
        private void HandleMakeBtnClickEvent()
        {
            Type soType = _currentTab.tabType;
            string path = _soTypeTable.ReturnPath(soType);

            _soCreator = GetWindow<SOCreator>();
            _soCreator.titleContent = new GUIContent("SOCreator");
            _soCreator.minSize = new Vector2(300f, 75f);
            _soCreator.maxSize = new Vector2(300f, 75f);
            _soCreator.SettingInfo(soType, path, HandleCreateSOEvent);
        }

        /// <summary>
        /// 새로운 SO를 생성할 때 호출되는 Method
        /// SO Data Dict에 넣어주고 비주얼을 그려줌
        /// </summary>
        /// <param name="item"></param>
        private void HandleCreateSOEvent(ScriptableObject item)
        {
            VisualElement newView = ViewElementMake(item);
            _currentSOView.Q<VisualElement>("ItemVisualList").Add(newView);
            _currentTab.getSODict.Add(newView, item);
            _soPathDict.Add(item, AssetDatabase.GetAssetPath(item));
        }

        #endregion
    
    private void ValueListBinding(ScriptableObject item)
    {
        Editor cachedEditor = null;
        _cachedGUI.onGUIHandler = () =>
        {
            if (item != null)
            {
                Editor.CreateCachedEditor(item, null, ref cachedEditor);
                if (cachedEditor != null)
                {
                    _currentData = item;
                    cachedEditor.OnInspectorGUI();
                    _cachedEditorDict.TryAdd(item, cachedEditor);
                }
            }
        };
    }

    private void OnDisable()
    {
        foreach (var item in _soInfoViewDict)
        {
            item.Value.Q<Button>("MakeBtn").clicked -= HandleMakeBtnClickEvent;
            item.Value.Q<Button>("DeleteBtn").clicked -= HandleDeleteBtnClickEvent;
            item.Value.Q<Button>("RenameBtn").clicked -= HandleRenameBtnClickEvent;
        }

        foreach (var item in _cachedEditorDict)
        {
            DestroyImmediate(item.Value);
        }
    }
}

https://github.com/ljs1206/GraduationWork_Script/tree/main/Scripts/Editor

 

GraduationWork_Script/Scripts/Editor at main · ljs1206/GraduationWork_Script

Contribute to ljs1206/GraduationWork_Script development by creating an account on GitHub.

github.com

여기서 TestWindow 부분을 확인하시면 됩니다. 그럼 USS와 UMXL도 받아볼 수 있습니다.

결과물

 

추가 해야 할 것

사소한 버그들 고치고 최적화도 해보고 검색 기능만 구현해주면 툴 제작은 마무리 될 것 같다.

'Editor' 카테고리의 다른 글

[Unity,Editor] SOManagementWindow 제작 일기 Part.4  (0) 2025.05.20
[Unity, Editor] SOManagementWindow 제작 일기 Part.3  (0) 2025.05.15
[Unity, Editor] SOManagementWindow 제작 일기 Part.1  (0) 2025.04.25
[졸업 작품, Unity]Item EditorWindow로 관리하기  (0) 2025.03.18
[Editor] 적 생성 Editor Window 코드 부분 제작  (0) 2024.10.10
'Editor' 카테고리의 다른 글
  • [Unity,Editor] SOManagementWindow 제작 일기 Part.4
  • [Unity, Editor] SOManagementWindow 제작 일기 Part.3
  • [Unity, Editor] SOManagementWindow 제작 일기 Part.1
  • [졸업 작품, Unity]Item EditorWindow로 관리하기
HK1206
HK1206
고3 게임 개발자의 개발 일지
  • HK1206
    GGM-LJS
    HK1206
  • 전체
    오늘
    어제
    • 분류 전체보기 (25) N
      • Unity (16)
      • Shader (1)
      • Editor (8) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
HK1206
[Unity, Editor] SOManagementWindow 제작 일기 Part.2
상단으로

티스토리툴바