您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
456 行
18 KiB
456 行
18 KiB
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.Perception.GroundTruth;
|
|
using UnityEngine.UIElements;
|
|
using UnityEditor.UIElements;
|
|
|
|
namespace UnityEditor.Perception.GroundTruth
|
|
{
|
|
abstract class LabelConfigEditor<T> : Editor where T : ILabelEntry
|
|
{
|
|
private string m_UxmlDir = "Packages/com.unity.perception/Editor/GroundTruth/Uxml/";
|
|
private string m_UxmlPath;
|
|
|
|
private int m_AddedLabelsItemHeight = 37;
|
|
private int m_OtherLabelsItemHeight = 27;
|
|
|
|
private List<string> m_AddedLabels = new List<string>();
|
|
|
|
protected SerializedProperty m_SerializedLabelsArray;
|
|
|
|
private static HashSet<string> allLabelsInProject = new HashSet<string>();
|
|
private List<string> m_LabelsNotPresentInConfig = new List<string>();
|
|
private bool m_UiInitialized;
|
|
private bool m_EditorHasUi;
|
|
|
|
private VisualElement m_Root;
|
|
|
|
private Button m_SaveButton;
|
|
private Button m_AddNewLabelButton;
|
|
private Button m_RemoveAllButton;
|
|
private Button m_AddAllButton;
|
|
private Button m_ImportFromFileButton;
|
|
private Button m_ExportToFileButton;
|
|
private ListView m_NonPresentLabelsListView;
|
|
|
|
protected ListView m_LabelListView;
|
|
protected Button m_MoveUpButton;
|
|
protected Button m_MoveDownButton;
|
|
protected VisualElement m_MoveButtons;
|
|
protected VisualElement m_IdSpecificUi;
|
|
protected EnumField m_StartingIdEnumField;
|
|
protected Toggle m_AutoIdToggle;
|
|
|
|
|
|
public void OnEnable()
|
|
{
|
|
m_SerializedLabelsArray = serializedObject.FindProperty(IdLabelConfig.labelEntriesFieldName);
|
|
m_UiInitialized = false;
|
|
ChangesHappeningInForeground = true;
|
|
RefreshListDataAndPresentation();
|
|
}
|
|
|
|
private int m_PreviousLabelsArraySize = -1;
|
|
/// <summary>
|
|
/// This boolean is used to signify when changes in the model are triggered directly from the inspector UI by the user.
|
|
/// In these cases, the scheduled model checker does not need to update the UI again.
|
|
/// </summary>
|
|
public bool ChangesHappeningInForeground { get; set; }
|
|
private void CheckForModelChanges()
|
|
{
|
|
if (ChangesHappeningInForeground)
|
|
{
|
|
ChangesHappeningInForeground = false;
|
|
m_PreviousLabelsArraySize = m_SerializedLabelsArray.arraySize;
|
|
return;
|
|
}
|
|
|
|
if (m_SerializedLabelsArray.arraySize != m_PreviousLabelsArraySize)
|
|
{
|
|
RefreshListDataAndPresentation();
|
|
m_PreviousLabelsArraySize = m_SerializedLabelsArray.arraySize;
|
|
}
|
|
}
|
|
|
|
protected abstract void InitUiExtended();
|
|
|
|
public abstract void PostRemoveOperations();
|
|
|
|
public override VisualElement CreateInspectorGUI()
|
|
{
|
|
if (!m_UiInitialized)
|
|
{
|
|
InitUi();
|
|
m_UiInitialized = true;
|
|
}
|
|
serializedObject.Update();
|
|
RefreshListDataAndPresentation();
|
|
return m_Root;
|
|
}
|
|
|
|
private void InitUi()
|
|
{
|
|
m_EditorHasUi = true;
|
|
|
|
m_UxmlPath = m_UxmlDir + "LabelConfig_Main.uxml";
|
|
m_Root = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(m_UxmlPath).CloneTree();
|
|
m_LabelListView = m_Root.Q<ListView>("labels-listview");
|
|
|
|
m_NonPresentLabelsListView = m_Root.Q<ListView>("labels-in-project-listview");
|
|
m_SaveButton = m_Root.Q<Button>("save-button");
|
|
m_AddNewLabelButton = m_Root.Q<Button>("add-label");
|
|
m_RemoveAllButton = m_Root.Q<Button>("remove-all-labels");
|
|
m_MoveUpButton = m_Root.Q<Button>("move-up-button");
|
|
m_MoveDownButton = m_Root.Q<Button>("move-down-button");
|
|
m_MoveButtons = m_Root.Q<VisualElement>("move-buttons");
|
|
m_ImportFromFileButton = m_Root.Q<Button>("import-file-button");
|
|
m_ExportToFileButton = m_Root.Q<Button>("export-file-button");
|
|
m_AddAllButton = m_Root.Q<Button>("add-all-labels-in-project");
|
|
m_StartingIdEnumField = m_Root.Q<EnumField>("starting-id-dropdown");
|
|
m_AutoIdToggle = m_Root.Q<Toggle>("auto-id-toggle");
|
|
m_IdSpecificUi = m_Root.Q<VisualElement>("id-specific-ui");
|
|
|
|
m_SaveButton.SetEnabled(false);
|
|
|
|
SetupPresentLabelsListView();
|
|
RefreshLabelsMasterList();
|
|
RefreshNonPresentLabels();
|
|
SetupNonPresentLabelsListView();
|
|
|
|
InitUiExtended();
|
|
UpdateMoveButtonState(null);
|
|
|
|
m_AddNewLabelButton.clicked += () => { AddNewLabel(m_AddedLabels); };
|
|
|
|
m_LabelListView.onSelectionChanged += UpdateMoveButtonState;
|
|
|
|
m_RemoveAllButton.clicked += () =>
|
|
{
|
|
m_SerializedLabelsArray.ClearArray();
|
|
serializedObject.ApplyModifiedProperties();
|
|
ChangesHappeningInForeground = true;
|
|
RefreshListDataAndPresentation();
|
|
};
|
|
|
|
m_AddAllButton.clicked += () =>
|
|
{
|
|
foreach (var label in m_LabelsNotPresentInConfig)
|
|
{
|
|
AppendLabelEntryToSerializedArray(m_SerializedLabelsArray, CreateLabelEntryFromLabelString(m_SerializedLabelsArray, label));
|
|
}
|
|
serializedObject.ApplyModifiedProperties();
|
|
ChangesHappeningInForeground = true;
|
|
RefreshListDataAndPresentation();
|
|
};
|
|
|
|
m_ImportFromFileButton.clicked += () =>
|
|
{
|
|
var path = EditorUtility.OpenFilePanel("Import label configuration from file", "", "json");
|
|
if (path.Length != 0)
|
|
{
|
|
var fileContent = File.ReadAllText(path);
|
|
var jsonObj = JObject.Parse(fileContent);
|
|
ImportFromJson(jsonObj);
|
|
}
|
|
};
|
|
|
|
m_ExportToFileButton.clicked += () =>
|
|
{
|
|
var path = EditorUtility.SaveFilePanel("Export label configuration to file", "", this.name, "json");
|
|
if (path.Length != 0)
|
|
{
|
|
string fileContents = ExportToJson();
|
|
var writer = File.CreateText(path);
|
|
|
|
writer.Write(fileContents);
|
|
writer.Flush();
|
|
writer.Close();
|
|
}
|
|
};
|
|
|
|
m_Root.schedule.Execute(CheckForModelChanges).Every(30);
|
|
}
|
|
|
|
static void RefreshLabelsMasterList()
|
|
{
|
|
allLabelsInProject.Clear();
|
|
|
|
var allPrefabPaths = GetAllPrefabsInProject();
|
|
foreach (var path in allPrefabPaths)
|
|
{
|
|
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|
var labeling = asset.GetComponent<Labeling>();
|
|
if (labeling)
|
|
{
|
|
allLabelsInProject.UnionWith(labeling.labels);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RefreshNonPresentLabels()
|
|
{
|
|
m_LabelsNotPresentInConfig.Clear();
|
|
m_LabelsNotPresentInConfig.AddRange(allLabelsInProject);
|
|
m_LabelsNotPresentInConfig.RemoveAll(label => m_AddedLabels.Contains(label));
|
|
}
|
|
|
|
private static IEnumerable<string> GetAllPrefabsInProject()
|
|
{
|
|
var allPaths = AssetDatabase.GetAllAssetPaths();
|
|
return allPaths.Where(path => path.EndsWith(".prefab")).ToList();
|
|
}
|
|
|
|
private void UpdateMoveButtonState(IEnumerable<object> objectList)
|
|
{
|
|
var selectedIndex = m_LabelListView.selectedIndex;
|
|
m_MoveDownButton.SetEnabled(selectedIndex < m_LabelListView.itemsSource.Count - 1);
|
|
m_MoveUpButton.SetEnabled(selectedIndex > 0);
|
|
}
|
|
|
|
public void RefreshListDataAndPresentation()
|
|
{
|
|
serializedObject.Update();
|
|
RefreshAddedLabels();
|
|
if (m_EditorHasUi && m_UiInitialized)
|
|
{
|
|
RefreshNonPresentLabels();
|
|
m_NonPresentLabelsListView.Refresh();
|
|
RefreshListViewHeight();
|
|
m_LabelListView.Refresh();
|
|
}
|
|
}
|
|
|
|
private void ScrollToBottomAndSelectLastItem()
|
|
{
|
|
m_LabelListView.selectedIndex = m_LabelListView.itemsSource.Count - 1;
|
|
UpdateMoveButtonState(null);
|
|
|
|
m_Root.schedule.Execute(() => { m_LabelListView.ScrollToItem(-1); })
|
|
.StartingIn(
|
|
10); //to circumvent the delay in listview's internal scrollview updating its geometry (when new items are added).
|
|
}
|
|
|
|
protected void RefreshAddedLabels()
|
|
{
|
|
m_AddedLabels.Clear();
|
|
m_SerializedLabelsArray = serializedObject.FindProperty(IdLabelConfig.labelEntriesFieldName);
|
|
|
|
for (int i = 0; i < m_SerializedLabelsArray.arraySize; i++)
|
|
{
|
|
m_AddedLabels.Add(m_SerializedLabelsArray.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(ILabelEntry.label)).stringValue);
|
|
}
|
|
}
|
|
|
|
protected virtual void SetupPresentLabelsListView()
|
|
{
|
|
m_LabelListView.itemsSource = m_AddedLabels;
|
|
m_LabelListView.itemHeight = m_AddedLabelsItemHeight;
|
|
m_LabelListView.selectionType = SelectionType.Single;
|
|
|
|
m_LabelListView.RegisterCallback<AttachToPanelEvent>(evt => { RefreshListViewHeight(); });
|
|
}
|
|
|
|
private void SetupNonPresentLabelsListView()
|
|
{
|
|
m_NonPresentLabelsListView.itemsSource = m_LabelsNotPresentInConfig;
|
|
|
|
VisualElement MakeItem()
|
|
{
|
|
var element = new NonPresentLabelElement<T>(this);
|
|
return element;
|
|
}
|
|
|
|
void BindItem(VisualElement e, int i)
|
|
{
|
|
if (e is NonPresentLabelElement<T> nonPresentLabel)
|
|
{
|
|
nonPresentLabel.m_Label.text = m_LabelsNotPresentInConfig[i];
|
|
}
|
|
}
|
|
|
|
m_NonPresentLabelsListView.bindItem = BindItem;
|
|
m_NonPresentLabelsListView.makeItem = MakeItem;
|
|
m_NonPresentLabelsListView.itemHeight = m_OtherLabelsItemHeight;
|
|
m_NonPresentLabelsListView.selectionType = SelectionType.None;
|
|
}
|
|
|
|
protected void RefreshListViewHeight()
|
|
{
|
|
m_LabelListView.style.minHeight =
|
|
Mathf.Clamp(m_LabelListView.itemsSource.Count * m_LabelListView.itemHeight, 300, 600);
|
|
}
|
|
|
|
string FindNewLabelString(List<string> labels)
|
|
{
|
|
string baseLabel = "New Label";
|
|
string label = baseLabel;
|
|
int count = 1;
|
|
while (labels.Contains(label))
|
|
{
|
|
label = baseLabel + "_" + count++;
|
|
}
|
|
|
|
return label;
|
|
}
|
|
|
|
private void AddNewLabel(List<string> presentLabels)
|
|
{
|
|
AddLabel(FindNewLabelString(presentLabels));
|
|
}
|
|
|
|
public void AddLabel(string labelToAdd)
|
|
{
|
|
if (m_AddedLabels.Contains(labelToAdd)) //label has already been added, cannot add again
|
|
return;
|
|
|
|
AppendLabelEntryToSerializedArray(m_SerializedLabelsArray, CreateLabelEntryFromLabelString(m_SerializedLabelsArray, labelToAdd));
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
RefreshListDataAndPresentation();
|
|
if (m_EditorHasUi)
|
|
ScrollToBottomAndSelectLastItem();
|
|
}
|
|
|
|
public void RemoveLabel(string labelToRemove)
|
|
{
|
|
var index = IndexOfStringLabelInSerializedLabelsArray(labelToRemove);
|
|
if (index >= 0)
|
|
{
|
|
m_SerializedLabelsArray.DeleteArrayElementAtIndex(index);
|
|
}
|
|
serializedObject.ApplyModifiedProperties();
|
|
RefreshListDataAndPresentation();
|
|
if (m_EditorHasUi)
|
|
ScrollToBottomAndSelectLastItem();
|
|
}
|
|
|
|
protected abstract T CreateLabelEntryFromLabelString(SerializedProperty serializedArray, string labelToAdd);
|
|
|
|
protected abstract void AppendLabelEntryToSerializedArray(SerializedProperty serializedArray, T labelEntry);
|
|
|
|
void ImportFromJson(JObject jsonObj)
|
|
{
|
|
Undo.RegisterCompleteObjectUndo(serializedObject.targetObject, "Import new label config");
|
|
JsonUtility.FromJsonOverwrite(jsonObj.ToString(), serializedObject.targetObject);
|
|
ChangesHappeningInForeground = true;
|
|
RefreshListDataAndPresentation();
|
|
|
|
}
|
|
|
|
private string ExportToJson()
|
|
{
|
|
return JsonUtility.ToJson(serializedObject.targetObject);
|
|
}
|
|
|
|
public int IndexOfStringLabelInSerializedLabelsArray(string label)
|
|
{
|
|
for (int i = 0; i < m_SerializedLabelsArray.arraySize; i++)
|
|
{
|
|
var element = m_SerializedLabelsArray.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(ILabelEntry.label));
|
|
if (element.stringValue == label)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
internal abstract class LabelElementInLabelConfig<T> : VisualElement where T : ILabelEntry
|
|
{
|
|
protected const string UxmlDir = "Packages/com.unity.perception/Editor/GroundTruth/Uxml/";
|
|
protected abstract string UxmlPath { get; }
|
|
|
|
private Button m_RemoveButton;
|
|
|
|
public TextField m_LabelTextField;
|
|
|
|
public int m_IndexInList;
|
|
|
|
protected SerializedProperty m_LabelsArray;
|
|
|
|
protected LabelConfigEditor<T> m_LabelConfigEditor;
|
|
|
|
protected LabelElementInLabelConfig(LabelConfigEditor<T> editor, SerializedProperty labelsArray)
|
|
{
|
|
m_LabelConfigEditor = editor;
|
|
m_LabelsArray = labelsArray;
|
|
|
|
Init();
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree(this);
|
|
m_LabelTextField = this.Q<TextField>("label-value");
|
|
m_RemoveButton = this.Q<Button>("remove-button");
|
|
|
|
m_LabelTextField.isDelayed = true;
|
|
|
|
InitExtended();
|
|
|
|
m_LabelTextField.RegisterValueChangedCallback((cEvent) =>
|
|
{
|
|
int index = m_LabelConfigEditor.IndexOfStringLabelInSerializedLabelsArray(cEvent.newValue);
|
|
|
|
if (index != -1 && index != m_IndexInList)
|
|
{
|
|
//The listview recycles child visual elements and that causes the RegisterValueChangedCallback event to be called when scrolling.
|
|
//Therefore, we need to make sure we are not in this code block just because of scrolling, but because the user is actively changing one of the labels.
|
|
//The index check is for this purpose.
|
|
|
|
Debug.LogError("A label with the string " + cEvent.newValue + " has already been added to this label configuration.");
|
|
m_LabelsArray.GetArrayElementAtIndex(m_IndexInList).FindPropertyRelative(nameof(ILabelEntry.label))
|
|
.stringValue = cEvent.previousValue; //since the textfield is bound to this property, it has already changed the property, so we need to revert the proprty.
|
|
m_LabelsArray.serializedObject.ApplyModifiedProperties();
|
|
m_LabelConfigEditor.ChangesHappeningInForeground = true;
|
|
m_LabelConfigEditor.RefreshListDataAndPresentation();
|
|
return;
|
|
}
|
|
|
|
|
|
//even though the textfield is already bound to the relevant property, we need to explicitly set the
|
|
//property here too in order to make "hasModifiedProperties" return the right value in the next line. Otherwise it will always be false.
|
|
m_LabelsArray.GetArrayElementAtIndex(m_IndexInList).FindPropertyRelative(nameof(ILabelEntry.label))
|
|
.stringValue = cEvent.newValue;
|
|
if (m_LabelsArray.serializedObject.hasModifiedProperties)
|
|
{
|
|
//the value change event is called even when the listview recycles its child elements for re-use during scrolling, therefore, we should check to make sure there are modified properties, otherwise we would be doing the refresh for no reason (reduces scrolling performance)
|
|
m_LabelsArray.serializedObject.ApplyModifiedProperties();
|
|
m_LabelConfigEditor.ChangesHappeningInForeground = true;
|
|
m_LabelConfigEditor.RefreshListDataAndPresentation();
|
|
}
|
|
});
|
|
|
|
m_RemoveButton.clicked += () =>
|
|
{
|
|
m_LabelsArray.DeleteArrayElementAtIndex(m_IndexInList);
|
|
m_LabelConfigEditor.PostRemoveOperations();
|
|
m_LabelConfigEditor.serializedObject.ApplyModifiedProperties();
|
|
m_LabelConfigEditor.ChangesHappeningInForeground = true;
|
|
m_LabelConfigEditor.RefreshListDataAndPresentation();
|
|
};
|
|
}
|
|
|
|
protected abstract void InitExtended();
|
|
}
|
|
|
|
class NonPresentLabelElement<T> : VisualElement where T : ILabelEntry
|
|
{
|
|
private string m_UxmlDir = "Packages/com.unity.perception/Editor/GroundTruth/Uxml/";
|
|
public Label m_Label;
|
|
|
|
public NonPresentLabelElement(LabelConfigEditor<T> editor)
|
|
{
|
|
var uxmlPath = m_UxmlDir + "SuggestedLabelElement.uxml";
|
|
AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath).CloneTree(this);
|
|
m_Label = this.Q<Label>("label-value");
|
|
var addButton = this.Q<Button>("add-button");
|
|
|
|
addButton.clicked += () => { editor.AddLabel(m_Label.text); };
|
|
}
|
|
}
|
|
}
|