2026-01-17 11:35:49 -05:00
using LunaWolfStudiosEditor.ScriptableSheets.Importers ;
using LunaWolfStudiosEditor.ScriptableSheets.Layout ;
using LunaWolfStudiosEditor.ScriptableSheets.Scanning ;
using Newtonsoft.Json ;
using System.Collections.Generic ;
using System.Linq ;
using UnityEditor ;
using UnityEngine ;
namespace LunaWolfStudiosEditor.ScriptableSheets.Settings
{
#if UNITY_2020_1_OR_NEWER
// We cannot persist a ScriptableSingleton in Unity versions prior to 2020 because FilePathAttribute is internal.
// https://forum.unity.com/threads/missing-documentation-for-scriptable-singleton.292754/
[FilePath("UserSettings/ScriptableSheetsSettings.asset", FilePathAttribute.Location.ProjectFolder)]
#endif
public class ScriptableSheetsSettings : ScriptableSingleton < ScriptableSheetsSettings >
{
[SerializeField]
private DataTransferSettings m_DataTransfer ;
public DataTransferSettings DataTransfer = > m_DataTransfer ;
[SerializeField]
private ObjectManagementSettings m_ObjectManagement ;
public ObjectManagementSettings ObjectManagement = > m_ObjectManagement ;
[SerializeField]
private UserInterfaceSettings m_UserInterface ;
public UserInterfaceSettings UserInterface = > m_UserInterface ;
[SerializeField]
private WorkloadSettings m_Workload ;
public WorkloadSettings Workload = > m_Workload ;
[SerializeField]
private ExperimentalSettings m_Experimental ;
public ExperimentalSettings Experimental = > m_Experimental ;
[SerializeField]
private List < GoogleSheetsImporter > m_GoogleSheetsImporters = new List < GoogleSheetsImporter > ( ) ;
public List < GoogleSheetsImporter > GoogleSheetsImporters = > m_GoogleSheetsImporters ;
[SerializeField]
private string m_WindowSessionStates ;
private bool m_IsQuitting ;
private bool m_WasWindowDestroyedThisUpdate ;
private ScanOption m_PreviousScanOption ;
private string [ ] m_PreviousScanPaths ;
private bool m_PreviousRootPrefabsOnly ;
private bool m_PreviousCaseSensitive ;
private bool m_PreviousStartsWith ;
private float m_PreviousHiglightAlpha ;
private bool m_PreviousHighlightSelectedRow ;
private bool m_PreviousHighlightSelectedColumn ;
private HeaderFormat m_PreviousHeaderFormat ;
private bool m_PreviousLockNames ;
private int m_PreviousRowLineHeight ;
private bool m_PreviousShowAssetPreviews ;
private ScaleMode m_PreviousPreviewScaleMode ;
private bool m_PreviousShowRowIndex ;
private bool m_PreviousShowColumnIndex ;
private bool m_PreviousShowChildren ;
private bool m_PreviousShowArrays ;
private bool m_PreviousOverrideArraySize ;
2026-03-20 12:07:44 -04:00
private bool m_PreviousBestFitArraySize ;
2026-01-17 11:35:49 -05:00
private int m_PreviousArraySize ;
private bool m_PreviousShowAssetPath ;
private bool m_PreviousShowGuids ;
private bool m_PreviousShowReadOnly ;
private bool m_PreviousSubAssetFilters ;
private int m_PreviousMaxIterations ;
private int m_PreviousRowsPerPage ;
private int m_PreviousVisibleColumnLimit ;
#if UNITY_2020_1_OR_NEWER
private string m_FilePath ;
private string m_FolderPath ;
private void Awake ( )
{
if ( ! System . IO . File . Exists ( GetFilePath ( ) ) )
{
ResetDefaultsAndSave ( ) ;
}
m_FilePath = System . IO . Path . Combine ( Application . dataPath , ".." , GetFilePath ( ) ) ;
m_FolderPath = System . IO . Path . GetDirectoryName ( m_FilePath ) ;
}
#endif
private void OnEnable ( )
{
CacheReactiveSettings ( ) ;
EditorApplication . wantsToQuit + = OnEditorApplicationWantsToQuit ;
EditorApplication . update + = OnEditorApplicationUpdate ;
}
private void OnDisable ( )
{
// Save window session state when assembly reloads.
if ( ! m_IsQuitting )
{
SaveWindowSessions ( ) ;
}
}
private bool OnEditorApplicationWantsToQuit ( )
{
try
{
m_IsQuitting = true ;
SaveWindowSessions ( ) ;
#if UNITY_2020_1_OR_NEWER
Save ( true ) ;
#endif
}
catch ( System . Exception ex )
{
Debug . LogError ( $"An error occured while trying to save {nameof(ScriptableSheetsSettings)}.\n{ex.Message}" ) ;
}
return true ;
}
private void OnEditorApplicationUpdate ( )
{
m_WasWindowDestroyedThisUpdate = false ;
}
private void SaveWindowSessions ( )
{
if ( ScriptableSheetsEditorWindow . Instances . Count > 0 )
{
var windowSessionStates = GetWindowSessionStates ( ) ;
if ( windowSessionStates = = null )
{
windowSessionStates = new HashSet < WindowSessionState > ( ) ;
}
foreach ( var window in ScriptableSheetsEditorWindow . Instances )
{
windowSessionStates . Add ( window . GetWindowSessionState ( ) ) ;
}
// Remove duplicate session caches that have the same instance id. Take the last group which should be the newest if renamed.
windowSessionStates = new HashSet < WindowSessionState > ( windowSessionStates . GroupBy ( s = > s . InstanceId ) . Select ( g = > g . Last ( ) ) ) ;
m_WindowSessionStates = JsonConvert . SerializeObject ( windowSessionStates ) ;
}
}
public void WindowDestroyed ( )
{
// When the user maximizes an editor window then other docked editor windows are destroyed.
// https://forum.unity.com/threads/how-do-i-prevent-docked-editorwindows-being-destroyed-when-pressing-play-with-maximize-on-play-on.276970/
if ( ! m_IsQuitting & & ! m_WasWindowDestroyedThisUpdate )
{
// Save the session states for each window that was destroyed this update so they can be restored when unmaximized.
SaveWindowSessions ( ) ;
m_WasWindowDestroyedThisUpdate = true ;
}
}
public HashSet < WindowSessionState > GetWindowSessionStates ( )
{
if ( ! string . IsNullOrWhiteSpace ( m_WindowSessionStates ) )
{
try
{
var windowSessionStates = JsonConvert . DeserializeObject < HashSet < WindowSessionState > > ( m_WindowSessionStates ) ;
return windowSessionStates ;
}
catch ( System . Exception ex )
{
Debug . LogWarning ( $"Failed to deserialize {nameof(WindowSessionState)} JSON '{m_WindowSessionStates}'. {ex.Message}." ) ;
}
}
return null ;
}
public HashSet < WindowSessionState > SaveAndGetWindowSessionStates ( )
{
SaveWindowSessions ( ) ;
return GetWindowSessionStates ( ) ;
}
public void DeleteWindowSessionState ( WindowSessionState windowSessionState )
{
var windowSessionStates = GetWindowSessionStates ( ) ;
if ( windowSessionStates = = null | | windowSessionStates . Count < = 0 )
{
Debug . LogWarning ( $"Cannot delete {nameof(WindowSessionState)} {windowSessionState.Title}. {nameof(windowSessionStates)} was null or empty." ) ;
return ;
}
windowSessionStates . RemoveWhere ( s = > s . InstanceId = = windowSessionState . InstanceId ) ;
m_WindowSessionStates = JsonConvert . SerializeObject ( windowSessionStates ) ;
}
public WindowSessionState LoadWindowSessionStateFromWindow ( ScriptableSheetsEditorWindow window )
{
var windowSessionStates = GetWindowSessionStates ( ) ;
WindowSessionState windowSessionState = null ;
if ( windowSessionStates ! = null & & windowSessionStates . Count > 0 )
{
var windowPosition = window . position . ToString ( ) ;
var instanceId = window . GetInstanceID ( ) ;
// First search by instance id and position. We include position here as well because Unity's instance id can be unreliable and change.
windowSessionState = windowSessionStates . FirstOrDefault ( s = > s . InstanceId = = instanceId & & s . Position = = windowPosition ) ;
if ( windowSessionState = = null )
{
// If no valid instance ids were found then look for a matching position.
windowSessionState = windowSessionStates . FirstOrDefault ( s = > s . Position = = windowPosition ) ;
if ( windowSessionState = = null )
{
// Find the first window session state that isn't already in use.
var activeInstanceIds = new HashSet < int > ( ScriptableSheetsEditorWindow . Instances . Select ( i = > i . GetInstanceID ( ) ) ) ;
windowSessionState = windowSessionStates . FirstOrDefault ( s = > ! activeInstanceIds . Contains ( s . InstanceId ) ) ;
}
}
}
return LoadWindowSessionState ( windowSessionState , windowSessionStates ) ;
}
public WindowSessionState LoadWindowSessionState ( WindowSessionState windowSessionState , HashSet < WindowSessionState > windowSessionStates = null )
{
if ( windowSessionStates = = null )
{
windowSessionStates = GetWindowSessionStates ( ) ;
}
// Use default state if window session state is null.
if ( windowSessionState = = null )
{
windowSessionState = new WindowSessionState
{
SelectableSheetAssets = SheetAsset . Default ,
SelectedSheetAsset = SheetAsset . ScriptableObject ,
SelectedTypeIndex = 0 ,
NewAmount = 1 ,
SearchInput = string . Empty
} ;
}
if ( windowSessionState . PinnedIndexSets = = null )
{
windowSessionState . PinnedIndexSets = new Dictionary < SheetAsset , HashSet < int > > ( ) ;
}
// Initialize any new or null SheetAsset sets.
foreach ( SheetAsset sheetAsset in System . Enum . GetValues ( typeof ( SheetAsset ) ) )
{
// The set can become null if the file was edited manually.
if ( ! windowSessionState . PinnedIndexSets . TryGetValue ( sheetAsset , out var set ) | | set = = null )
{
windowSessionState . PinnedIndexSets [ sheetAsset ] = new HashSet < int > ( ) ;
}
}
// Create TableLayout Dictionary for previous Window Session States.
if ( windowSessionState . TableLayouts = = null )
{
windowSessionState . TableLayouts = new Dictionary < string , TableLayout > ( ) ;
}
// Remove the found window session state so it's not loaded by another docked window at the same position and so the instance id is overwritten next save.
if ( windowSessionStates ! = null & & windowSessionStates . Count > 0 )
{
windowSessionStates . Remove ( windowSessionState ) ;
m_WindowSessionStates = JsonConvert . SerializeObject ( windowSessionStates ) ;
}
return windowSessionState ;
}
public void DrawGUI ( bool isSeparateWindow )
{
Undo . RecordObject ( this , $"{nameof(ScriptableSheetsSettings)}" ) ;
var serializedObject = new SerializedObject ( this ) ;
m_DataTransfer . DrawGUI ( serializedObject ) ;
m_ObjectManagement . DrawGUI ( serializedObject ) ;
m_UserInterface . DrawGUI ( serializedObject ) ;
m_Workload . DrawGUI ( serializedObject ) ;
m_Experimental . DrawGUI ( serializedObject ) ;
SheetLayout . DrawHorizontalLine ( ) ;
EditorGUILayout . BeginHorizontal ( ) ;
var googleSheetsImportersProperty = serializedObject . FindProperty ( nameof ( m_GoogleSheetsImporters ) ) ;
EditorGUILayout . PropertyField ( googleSheetsImportersProperty , SettingsContent . Label . GoogleSheetsImporters , true ) ;
if ( GUILayout . Button ( SettingsContent . Button . ScanImporters , SheetLayout . InlineButton ) )
{
m_GoogleSheetsImporters . Clear ( ) ;
var guids = AssetDatabase . FindAssets ( $"t:{nameof(GoogleSheetsImporter)}" ) ;
foreach ( var guid in guids )
{
var path = AssetDatabase . GUIDToAssetPath ( guid ) ;
var asset = AssetDatabase . LoadAssetAtPath < GoogleSheetsImporter > ( path ) ;
if ( asset ! = null )
{
m_GoogleSheetsImporters . Add ( asset ) ;
}
}
}
EditorGUILayout . EndHorizontal ( ) ;
serializedObject . ApplyModifiedProperties ( ) ;
SheetLayout . DrawHorizontalLine ( ) ;
#if UNITY_2020_1_OR_NEWER
if ( m_Workload . AutoSave | | GUILayout . Button ( SettingsContent . Button . SaveChangesToDisk ) )
{
Save ( true ) ;
}
if ( GUILayout . Button ( SettingsContent . Button . OpenFile ) )
{
Application . OpenURL ( m_FilePath ) ;
}
if ( GUILayout . Button ( SettingsContent . Button . OpenFolder ) )
{
Application . OpenURL ( m_FolderPath ) ;
}
#endif
if ( GUILayout . Button ( SettingsContent . Button . ResetDefaults ) )
{
if ( EditorUtility . DisplayDialog ( "Scriptable Sheets" , "Reset settings to default values?" , "Confirm" , "Cancel" ) )
{
ResetDefaultsAndSave ( ) ;
}
}
if ( ! isSeparateWindow & & GUILayout . Button ( SettingsContent . Button . OpenWindow ) )
{
ScriptableSheetsSettingsEditorWindow . ShowWindow ( ) ;
}
// Repaint and refresh column layouts immediately as reactive settings change.
var hasScanOptionChanged = m_PreviousScanOption ! = m_ObjectManagement . Scan . Option ;
var hasScanPathChanged = ! m_PreviousScanPaths . SequenceEqual ( m_ObjectManagement . Scan . GetScanPaths ( ) ) ;
var hasRootPrefabsOnlyChanged = m_PreviousRootPrefabsOnly ! = m_ObjectManagement . Scan . RootPrefabsOnly ;
var hasHeaderFormatChanged = m_PreviousHeaderFormat ! = m_UserInterface . HeaderFormat ;
var hasShowRowIndexChanged = m_PreviousShowRowIndex ! = m_UserInterface . ShowRowIndex ;
var hasShowColumnIndexChanged = m_PreviousShowColumnIndex ! = m_UserInterface . ShowColumnIndex ;
var hasShowChildrenChanged = m_PreviousShowChildren ! = m_UserInterface . ShowChildren ;
var hasShowArraysChanged = m_PreviousShowArrays ! = m_UserInterface . ShowArrays ;
var hasOverrideArraySizeChanged = m_PreviousOverrideArraySize ! = m_UserInterface . OverrideArraySize ;
2026-03-20 12:07:44 -04:00
var hasBestFitArraySizeChanged = m_PreviousBestFitArraySize ! = m_UserInterface . BestFitArraySize ;
2026-01-17 11:35:49 -05:00
var hasArraySizeChanged = m_PreviousArraySize ! = m_UserInterface . ArraySize ;
var hasShowAssetPathChanged = m_PreviousShowAssetPath ! = m_UserInterface . ShowAssetPath ;
var hasShowGuidChanged = m_PreviousShowGuids ! = m_UserInterface . ShowGuid ;
var hasShowReadOnlyChanged = m_PreviousShowReadOnly ! = m_UserInterface . ShowReadOnly ;
var hasMaxIterationsChanged = m_PreviousMaxIterations ! = m_Workload . MaxIterations ;
var hasVisibleColumnLimitChanged = m_PreviousVisibleColumnLimit ! = m_Workload . VisibleColumnLimit ;
var needsColumnRefresh = hasScanOptionChanged | | hasScanPathChanged
| | hasHeaderFormatChanged | | hasShowRowIndexChanged
| | hasShowColumnIndexChanged | | hasShowChildrenChanged
2026-03-20 12:07:44 -04:00
| | hasShowArraysChanged | | hasOverrideArraySizeChanged | | hasArraySizeChanged | | hasBestFitArraySizeChanged
2026-01-17 11:35:49 -05:00
| | hasShowGuidChanged | | hasShowAssetPathChanged
| | hasShowReadOnlyChanged | | hasMaxIterationsChanged | | hasVisibleColumnLimitChanged ;
var hasCaseSensitiveChanged = m_PreviousCaseSensitive ! = m_ObjectManagement . Search . CaseSensitive ;
var hasStartsWithChanged = m_PreviousStartsWith ! = m_ObjectManagement . Search . StartsWith ;
var hasHighlightAlphaChanged = m_PreviousHiglightAlpha ! = m_UserInterface . TableNav . HighlightAlpha ;
var hasHighlightSelectedRowChanged = m_PreviousHighlightSelectedRow ! = m_UserInterface . TableNav . HighlightSelectedRow ;
var hasHighlightSelectedColumnChanged = m_PreviousHighlightSelectedColumn ! = m_UserInterface . TableNav . HighlightSelectedColumn ;
var hasLockNamesChanged = m_PreviousLockNames ! = m_UserInterface . LockNames ;
var hasRowLineHeightChanged = m_PreviousRowLineHeight ! = m_UserInterface . RowLineHeight ;
var hasShowAssetPreviewsChanged = m_PreviousShowAssetPreviews ! = m_UserInterface . AssetPreview . Show ;
var hasPreviewScaleModeChanged = m_PreviousPreviewScaleMode ! = m_UserInterface . AssetPreview . ScaleMode ;
var hasRowsPerPageChanged = m_PreviousRowsPerPage ! = m_Workload . RowsPerPage ;
var hasSubAssetFiltersChanged = m_PreviousSubAssetFilters ! = m_UserInterface . SubAssetFilters ;
var needsRepaint = needsColumnRefresh | | hasRootPrefabsOnlyChanged | | hasCaseSensitiveChanged
| | hasStartsWithChanged | | hasHighlightAlphaChanged
| | hasHighlightSelectedRowChanged | | hasHighlightSelectedColumnChanged | | hasLockNamesChanged
| | hasRowLineHeightChanged | | hasShowAssetPreviewsChanged | | hasPreviewScaleModeChanged
| | hasRowsPerPageChanged | | hasSubAssetFiltersChanged ;
if ( needsRepaint )
{
foreach ( var window in ScriptableSheetsEditorWindow . Instances )
{
if ( hasScanPathChanged )
{
if ( m_ObjectManagement . Scan . Option ! = ScanOption . Assembly )
{
window . ResetSelectedType ( ) ;
}
window . ScanObjects ( ) ;
}
else if ( hasScanOptionChanged | | hasRootPrefabsOnlyChanged )
{
window . ScanObjects ( ) ;
}
if ( needsColumnRefresh )
{
window . ForceRefreshColumnLayout ( ) ;
}
window . Repaint ( ) ;
}
}
CacheReactiveSettings ( ) ;
}
private void CacheReactiveSettings ( )
{
m_PreviousScanOption = m_ObjectManagement . Scan . Option ;
m_PreviousScanPaths = m_ObjectManagement . Scan . GetScanPaths ( ) ;
m_PreviousRootPrefabsOnly = m_ObjectManagement . Scan . RootPrefabsOnly ;
m_PreviousCaseSensitive = m_ObjectManagement . Search . CaseSensitive ;
m_PreviousStartsWith = m_ObjectManagement . Search . StartsWith ;
m_PreviousHiglightAlpha = m_UserInterface . TableNav . HighlightAlpha ;
m_PreviousHighlightSelectedRow = m_UserInterface . TableNav . HighlightSelectedRow ;
m_PreviousHighlightSelectedColumn = m_UserInterface . TableNav . HighlightSelectedColumn ;
m_PreviousHeaderFormat = m_UserInterface . HeaderFormat ;
m_PreviousLockNames = m_UserInterface . LockNames ;
m_PreviousRowLineHeight = m_UserInterface . RowLineHeight ;
m_PreviousShowAssetPreviews = m_UserInterface . AssetPreview . Show ;
m_PreviousPreviewScaleMode = m_UserInterface . AssetPreview . ScaleMode ;
m_PreviousShowRowIndex = m_UserInterface . ShowRowIndex ;
m_PreviousShowColumnIndex = m_UserInterface . ShowColumnIndex ;
m_PreviousShowChildren = m_UserInterface . ShowChildren ;
m_PreviousShowArrays = m_UserInterface . ShowArrays ;
m_PreviousOverrideArraySize = m_UserInterface . OverrideArraySize ;
2026-03-20 12:07:44 -04:00
m_PreviousBestFitArraySize = m_UserInterface . BestFitArraySize ;
2026-01-17 11:35:49 -05:00
m_PreviousArraySize = m_UserInterface . ArraySize ;
m_PreviousShowAssetPath = m_UserInterface . ShowAssetPath ;
m_PreviousShowGuids = m_UserInterface . ShowGuid ;
m_PreviousShowReadOnly = m_UserInterface . ShowReadOnly ;
m_PreviousMaxIterations = m_Workload . MaxIterations ;
m_PreviousRowsPerPage = m_Workload . RowsPerPage ;
m_PreviousVisibleColumnLimit = m_Workload . VisibleColumnLimit ;
}
private void ResetDefaultsAndSave ( )
{
m_DataTransfer = new DataTransferSettings ( ) ;
m_ObjectManagement = new ObjectManagementSettings ( ) ;
m_UserInterface = new UserInterfaceSettings ( ) ;
m_Workload = new WorkloadSettings ( ) ;
m_Experimental = new ExperimentalSettings ( ) ;
m_GoogleSheetsImporters = new List < GoogleSheetsImporter > ( ) ;
#if UNITY_2020_1_OR_NEWER
Save ( true ) ;
#endif
}
}
}