How do I save changes to a custom window? I want to create a window with 3 or more tabs and I want the window to remember the changes I make.
At the moment I am creating a ScriptableObject for each tab to store data and a custom editor for each to display as I want. I also have another scriptable object for the window to remember which window was selected between sessions. And I use the InspectorElement to draw the inspector of a tab.
I am not sure if this is the best way to handle it.
There is also the ScriptableSingleton, but it is very tedious to work with.
Any better solutions?
How is it tedious? The docs on it even show to use it to have persistent editor data: Unity - Scripting API: ScriptableSingleton<T0>
It’s pretty quick and straight forward to implement.
I don’t see an issue using a ScriptableObject to store state data for your EditorWindow when it should persist. Though when you just have transient data, just make sure your fields are public and serializable and they should survive an assembly reload. But of course a restart or closing / reopening of the window would loose any data. Keep in mind that an EditorWindow itself IS a ScriptableObject and Unity will serialize it during assembly reload like everything else.
If you just have general settings you want to preserve, Unity provides the EditorPrefs which are specifically meant to store preferences for editor tools. Though only primitive data can be stored here. If you need to store asset references and they should persist, a ScriptableObject would be the way to go.
When using EditorPrefs, keep in mind that those are shared with every bit of editor code, so make sure you use unique names. Best practise would be to use a specific prefix in the name to avoid name collisions. Personally whenever I used the EditorPrefs, I usually store a single json string with the necessary configuration, but it’s up to you. Over here I made a script to store and handle scene view camera presets for example. (This was the associated thread). Over here I made a custom sceneview camera control (at a time when the default controls were more limited as they are now). There I used the EditorPrefs in a “classical” way
I find it tedious because of the Save(), you have to call it every time to make sure you save everything, unlike ScriptableObjects where it is autonomous.
Maybe it’s because I’m not using it correctly. But for my example, I have a window that has to store the index of the selected tab. Then I have 3 different tabs, each doing something different. So I need to create a ScriptableObject for each tab, make a custom editor for each, and use the InspectorElement on each to show the appropriate interface.
o.O? Why? Since you introduced “tabs” inside your own window, those three tabs should belong together. Why do you need to separate the data then? If the 3 tabs work independent from each other, they probably should be 3 separate EditorWindows so the user can dock the actual tabs the way he wants. Things like the current tab index can easily be stored in EditorPrefs unless you plan to have multiple instances of the same window open which work independently of each other. But in this case you would also run into issues with scriptable objects.
Currently I’m not sure what data Unity actually stores in the window layout when you close the Unity editor. Certainly the type, position and dock state are saved. Though I can’t remember if Unity actually serializes the editor window instances themselfs. Of course in the case of the sceneview, it should save them individually since you can open multiple scene views.
I thought so too. My concern with this is the following. My window servers to create a neural network. It is not my code, but Sebastian Lague’s. I will have 3 tabs, one to create the model file (load all the images and compress them into a binary file), the second to create variants of the images with noise and the third to train the neural network.
So I have 3 classes, ModelCreator, ImageProcessor and NeuralNetworkTrainer. I also have another one which is the NeuralNetworkWindowData to store the data related to the window. These are all scriptable objects. I wanted to make the 3 previous classes as pure C# class and serialise them inside the window data. My problem is using PropertyDrawer instead of CustomEditor which is not as good. I can’t access the functions of my pure C# class and even getting the data from it is more complex than using EditorWindow, but maybe I’m not using it correctly.
Just call it in OnDisable. That’s all I ever do and it works fine.
2 Likes
Oh … right, I did not think to that. So when you leave the window, it saves?
No in the scriptable singleton itself.
Oh ok … what does it correspond to? I mean, when is it disable?
OnDisable and OnEnable get called on all scriptable objects during a domain reload. Basically, it will be called before the data will be lost.
1 Like
Is this correct? I find it a bit complicated to just add a button to call a function. And also adding my PathField.
public class ModelCreatorData
{
[SerializeField] string imagesPath;
[SerializeField] string savePath;
[SerializeField] string saveName;
#region Getters & Setters
public string ImagesPath => imagesPath;
public string SavePath => savePath;
public string SaveName => saveName;
#endregion
}
[CustomPropertyDrawer(typeof(ModelCreatorData))]
public class ModelCreatorEditor : PropertyDrawer
{
SerializedProperty property;
public string ImagesPath => property.FindPropertyRelative("imagesPath").stringValue;
public string SavePath => property.FindPropertyRelative("savePath").stringValue;
public string SaveName => property.FindPropertyRelative("saveName").stringValue;
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
this.property = property;
VisualElement root = new VisualElement();
PathField imagesPath = new PathField("Images Path");
imagesPath.bindingPath = "imagesPath";
root.Add(imagesPath);
PathField savePath = new PathField("Save Path");
savePath.bindingPath = "savePath";
root.Add(savePath);
TextField saveNameField = new TextField("Save Name");
saveNameField.bindingPath = "saveName";
root.Add(saveNameField);
Button createBytesModels = new Button();
createBytesModels.text = "Create Bytes Models";
createBytesModels.clicked += CreateBytesModels;
root.Add(createBytesModels);
return root;
}
void CreateBytesModels()
{
Image[] images = ImageHelper.LoadImages(ImagesPath);
int numLabels = ImageHelper.GetNumLabels(images);
ImageFile imageFile = new ImageFile(images, numLabels);
string path = Path.Combine(SavePath, SaveName);
FileHelper.SaveFile(path, imageFile);
Debug.Log("The model was <color=green>successfully created</color>.");
}
}
Here is the solution came to:
public class WindowTab : VisualElement
{
}
public class WindowTab<T> : WindowTab
{
protected T data;
protected SerializedProperty dataProperty;
public WindowTab(SerializedProperty dataProperty, T data)
{
this.dataProperty = dataProperty;
this.data = data;
}
protected PropertyField CreatePropertyField(string name)
{
SerializedProperty property = dataProperty.FindPropertyRelative(name);
PropertyField filed = new PropertyField(property, property.displayName);
filed.BindProperty(property);
return filed;
}
protected PropertyField CreatePropertyField()
{
PropertyField filed = new PropertyField(dataProperty, dataProperty.displayName);
filed.BindProperty(dataProperty);
return filed;
}
}
[System.Serializable]
public class ModelCreatorData
{
[PathString] public string imagesPath;
[PathString] public string savePath;
public string saveName;
}
public class ModelCreatorTab : WindowTab<ModelCreatorData>
{
public ModelCreatorTab(SerializedProperty dataProperty, ModelCreatorData data) : base(dataProperty, data)
{
CreateUI();
}
void CreateUI()
{
Add(CreatePropertyField());
Button button = new Button(SaveImagesFile);
button.text = "Save Images File";
Add(button);
}
void SaveImagesFile()
{
Image[] images = ImageHelper.LoadImages(data.imagesPath);
int numLabels = ImageHelper.GetNumLabels(images);
ImageFile imageFile = new ImageFile(images, numLabels);
string path = Path.Combine(data.savePath, data.saveName);
FileHelper.SaveFile(path, imageFile);
Debug.Log($"{data.saveName} was <color=green>successfully created</color> to '{data.savePath}'");
}
}
public class MyWindow : EditorWindow
{
[SerializeField] kWindowData data;
SerializedObject serializedObject;
WindowTab[] tabs;
[MenuItem("Tools/Neural Network Window")]
public static void Open()
{
MyWindow window = GetWindow<MyWindow >("My Window");
}
void OnEnable()
{
data = Resources.Load<WindowData>("Data/My Window");
serializedObject = new SerializedObject(data);
}
void OnDisable()
{
serializedObject.Dispose();
}
public void CreateGUI()
{
VisualElement root = rootVisualElement;
tabs = new WindowTab[]
{
new ModelCreatorTab(serializedObject.FindProperty(nameof(data.modelCreatorData)), data.modelCreatorData),
new ImageProcessorTab(serializedObject.FindProperty(nameof(data.imageProcessorData)), data.imageProcessorData),
new NeuralNetworkTrainerTab(serializedObject.FindProperty(nameof(data.neuralNetworkTrainerData)), data.neuralNetworkTrainerData)
};
foreach (var tab in tabs)
root.Add(tab);
}
}