Aetheria/Assets/Scripts/CultCache/Editor/DatabaseView.cs
Reinier Vens 526a737a4d Modular Database Tools
Extracted all inspector code for database inspector into a single class per supported field type and wrote reflection code to invoke them automatically
Removed all Aetheria-specific code from database tools
Database View editor is now abstract and supports arbitrary grouping of entry types; specific users must implement their own database view
2021-03-31 04:07:11 +01:00

340 lines
13 KiB
C#

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using JsonKnownTypes;
using RethinkDb.Driver;
using RethinkDb.Driver.Net;
using UnityEditor;
using UnityEngine;
using static UnityEditor.EditorGUILayout;
public abstract class DatabaseListView : EditorWindow
{
public static GUIStyle ListStyleOdd;
public static GUIStyle ListStyleEven;
public Guid SelectedItem;
public GUIStyle SelectedStyle;
public static RethinkDB R = RethinkDB.R;
protected CultCache cultCache;
private static DatabaseInspector _inspector;
private Type[] _entryTypes;
private bool[] _typeFoldouts;
private string _connectionString;
private Vector2 _view;
private RethinkQueryStatus _queryStatus;
private HashSet<int> _groupFoldouts = new HashSet<int>();
protected abstract string DatabaseName { get; }
protected abstract string FilePath { get; }
protected virtual DatabaseEntryGroup[] Groupers => new DatabaseEntryGroup[] { };
protected class DatabaseEntryGroup<T, K> : DatabaseEntryGroup where T : DatabaseEntry
{
public Func<T,K> GroupSelector { get; }
public Func<K,string> GroupLabel { get; }
public Action<T, K> Activator { get; }
public DatabaseEntryGroup(Func<T, K> groupSelector, Func<K, string> groupLabel, Action<T, K> activator)
{
GroupSelector = groupSelector;
GroupLabel = groupLabel;
Activator = activator;
}
public override bool CanGroup(Type type)
{
return typeof(T) == type;
}
public override object SelectGroup(DatabaseEntry o)
{
if (o is T t) return GroupSelector(t);
return null;
}
public override string GetGroupName(object o)
{
if (o is K k) return GroupLabel(k);
return "ERROR";
}
public override int HashGroup(object o)
{
if (o is K k) return k.GetHashCode();
return 0;
}
public override void Activate(DatabaseEntry o, object g)
{
if(o is T t && g is K k) Activator(t, k);
throw new ArgumentException(
$"Entry group activation failed! Expected: object type {typeof(T)}, group type {typeof(K)}; received: object type {o.GetType()}, group type {g.GetType()}");
}
}
protected abstract class DatabaseEntryGroup
{
public abstract bool CanGroup(Type type);
public abstract object SelectGroup(DatabaseEntry o);
public abstract string GetGroupName(object o);
public abstract int HashGroup(object o);
public abstract void Activate(DatabaseEntry o, object g);
}
public Color LabelColor => EditorGUIUtility.isProSkin ? Color.white : Color.black;
private bool _listItemStyle;
public GUIStyle ListItemStyle =>
(_listItemStyle = !_listItemStyle) ? ListStyleEven : ListStyleOdd;
void OnEnable()
{
ListStyleOdd = new GUIStyle
{
normal = {background = EditorGUIUtility.isProSkin ? XKCDColors.DarkGrey.ToTexture() : XKCDColors.LightGrey.ToTexture()},
margin = new RectOffset(0, 0, 0, 0)
};
ListStyleEven = new GUIStyle
{
normal = {background = EditorGUIUtility.isProSkin ? Color.Lerp(XKCDColors.DarkGrey,Color.black,.5f).ToTexture() : Color.Lerp(XKCDColors.LightGrey,Color.white,.5f).ToTexture()},
margin = new RectOffset(0, 0, 0, 0)
};
cultCache = new CultCache(FilePath);
cultCache.OnDataUpdateLocal += _ => Repaint();
cultCache.OnDataInsertLocal += _ => Repaint();
cultCache.OnDataDeleteLocal += _ => Repaint();
cultCache.OnDataUpdateRemote += _ => EditorDispatcher.Dispatch(Repaint);
cultCache.OnDataInsertRemote += _ => EditorDispatcher.Dispatch(Repaint);
cultCache.OnDataDeleteRemote += _ => EditorDispatcher.Dispatch(Repaint);
DatabaseInspector.CultCache = cultCache;
_inspector = DatabaseInspector.Instance;
_inspector.Show();
_entryTypes = typeof(DatabaseEntry).GetAllChildClasses()
.Where(t => t.GetCustomAttribute<InspectableAttribute>() != null).ToArray();
_typeFoldouts = new bool[_entryTypes.Length];
if (EditorPrefs.HasKey("RethinkDB.URL"))
_connectionString = EditorPrefs.GetString("RethinkDB.URL");
else
_connectionString = "Enter DB URL";
InitStyles();
}
private void InitStyles()
{
SelectedStyle = new GUIStyle
{
normal = {background = EditorGUIUtility.isProSkin ? XKCDColors.DarkGreyBlue.ToTexture() : XKCDColors.Greyblue.ToTexture()},
margin = new RectOffset(0, 0, 0, 0)
};
}
public void OnGUI()
{
Event currentEvent = Event.current;
EventType currentEventType = currentEvent.type;
if (currentEventType == EventType.DragUpdated)
// Indicate that we don't accept drags ourselves
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
using (var h = new HorizontalScope())
{
_connectionString = TextField(_connectionString);
if (GUILayout.Button("Connect"))
{
EditorPrefs.SetString("RethinkDB.URL", _connectionString);
JsonKnownTypesSettingsManager.RegisterTypeAssembly<ItemData>();
_queryStatus = RethinkConnection.RethinkConnect(cultCache, _connectionString, DatabaseName);
}
// if (GUILayout.Button("Connect All"))
// {
// EditorPrefs.SetString("RethinkDB.URL", _connectionString);
// _queryStatus = RethinkConnection.RethinkConnect(_databaseCache, _connectionString, true, false);
// }
}
using (var h = new HorizontalScope())
{
//_fileName = TextField(_fileName);
if (GUILayout.Button("Save"))
{
cultCache.Save();
Debug.Log("Local DB Cache Saved!");
}
if (GUILayout.Button("Load"))
{
cultCache.Load();
Debug.Log("Loaded DB From Local Cache!");
}
}
if (_queryStatus != null && _queryStatus.RetrievedEntries < _queryStatus.TotalEntries)
{
var progressRect = GetControlRect(false, 20);
EditorGUI.ProgressBar(progressRect, (float)_queryStatus.RetrievedEntries/_queryStatus.TotalEntries, "Sync Progress");
}
GUILayout.Space(5);
_view = BeginScrollView(
_view,
false,
false,
GUIStyle.none,
GUI.skin.verticalScrollbar,
GUI.skin.scrollView,
GUILayout.Width(EditorGUIUtility.currentViewWidth),
GUILayout.ExpandHeight(true));
for (var i = 0; i < _entryTypes.Length; i++)
{
using (var h = new HorizontalScope(ListItemStyle))
{
_typeFoldouts[i] = Foldout(_typeFoldouts[i],
((NameAttribute) _entryTypes[i].GetCustomAttribute(typeof(NameAttribute)))?.Name ?? FormatTypeName(_entryTypes[i].Name),
true);
}
if (_typeFoldouts[i])
{
// TODO: NESTED GROUPS!
var entries = cultCache.GetAll(_entryTypes[i])
.OrderBy(entry => entry is INamedEntry namedEntry ? namedEntry.EntryName : entry.ID.ToString());
var grouper = Groupers.FirstOrDefault(g => g.CanGroup(_entryTypes[i]));
if (grouper != null)
{
var groups = entries.GroupBy(o => grouper.SelectGroup(o));
foreach (var group in groups)
{
var groupHash = grouper.HashGroup(group.Key);
var groupName = grouper.GetGroupName(group.Key);
using (var h = new HorizontalScope(ListItemStyle))
{
GUILayout.Space(10);
if (Foldout(_groupFoldouts.Contains(groupHash), FormatTypeName(groupName), true))
_groupFoldouts.Add(groupHash);
else _groupFoldouts.Remove(groupHash);
}
if (_groupFoldouts.Contains(groupHash))
{
int index = 0;
foreach (var entry in group)
{
index++;
var style = ListItemStyle;
var selected = SelectedItem == entry.ID;
using (new HorizontalScope(selected?SelectedStyle:style))
{
using (var h = new HorizontalScope())
{
if (h.rect.Contains(currentEvent.mousePosition))
{
if (currentEventType == EventType.MouseDrag)
{
DragAndDrop.PrepareStartDrag();
DragAndDrop.SetGenericData("Item", entry.ID);
DragAndDrop.StartDrag("Database Item");
}
else if (currentEventType == EventType.MouseUp)
Select(entry);
}
GUILayout.Space(20);
GUILayout.Label((entry as INamedEntry)?.EntryName ?? index.ToString(), selected ? EditorStyles.whiteLabel : GUI.skin.label);
}
}
}
using (var h = new HorizontalScope(ListItemStyle))
{
if(GUI.Button(h.rect, GUIContent.none, GUIStyle.none))
CreateItem(_entryTypes[i], entry => grouper.Activate(entry, group.Key));
GUILayout.Space(20);
GUILayout.Label($"New {groupName}");
}
}
}
}
else
{
int index = 0;
foreach (var entry in entries)
{
index++;
var style = ListItemStyle;
var selected = SelectedItem == entry.ID;
using (var h = new HorizontalScope(selected?SelectedStyle:style))
{
if (h.rect.Contains(currentEvent.mousePosition))
{
if (currentEventType == EventType.MouseDrag)
{
DragAndDrop.PrepareStartDrag();
DragAndDrop.SetGenericData("Item", entry.ID);
DragAndDrop.StartDrag("Database Item");
}
else if (currentEventType == EventType.MouseUp)
Select(entry);
}
GUILayout.Space(10);
GUILayout.Label((entry as INamedEntry)?.EntryName ?? index.ToString(), selected ? EditorStyles.whiteLabel : GUI.skin.label);
}
}
}
using (var h = new HorizontalScope(ListItemStyle))
{
if(GUI.Button(h.rect, GUIContent.none, GUIStyle.none))
CreateItem(_entryTypes[i]);
GUILayout.Space(10);
GUILayout.Label($"New {_entryTypes[i].Name}");
}
}
}
EndScrollView();
}
private string FormatTypeName(string typeName)
{
return (typeName.EndsWith("Data")
? typeName.Substring(0, typeName.Length - 4)
: typeName).SplitCamelCase();
}
void Select(DatabaseEntry item)
{
SelectedItem = item.ID;
Repaint();
_inspector.entry = cultCache.Get(SelectedItem);
_inspector.Repaint();
}
private DatabaseEntry CreateItem(Type type, Action<DatabaseEntry> onCreate = null)
{
var newEntry = (DatabaseEntry) Activator.CreateInstance(type);
if(newEntry is INamedEntry entry)
entry.EntryName = $"New {FormatTypeName(type.Name)}";
onCreate?.Invoke(newEntry);
cultCache.Add(newEntry);
Select(newEntry);
return newEntry;
}
}