first comit
This commit is contained in:
18
Editor/Jovian.TagSystem.Editor.asmdef
Normal file
18
Editor/Jovian.TagSystem.Editor.asmdef
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Jovian.TagSystem.Editor",
|
||||
"rootNamespace": "Jovian.TagSystem.Editor",
|
||||
"references": [
|
||||
"Jovian.TagSystem"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Editor/Jovian.TagSystem.Editor.asmdef.meta
Normal file
7
Editor/Jovian.TagSystem.Editor.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 941a740b30b4595478b5e69393ffa045
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Editor/JovianTagsEditorHistory.cs
Normal file
46
Editor/JovianTagsEditorHistory.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
|
||||
[InitializeOnLoad]
|
||||
public class JovianTagsEditorHistory {
|
||||
private readonly int numRecentTags = 20;
|
||||
private readonly string saveKey = "EditorSave";
|
||||
private readonly string saveDelimiter = "%£&";
|
||||
|
||||
public static JovianTagsEditorHistory Instance { get; }
|
||||
|
||||
public List<string> RecentTags { get; private set; } = new();
|
||||
|
||||
static JovianTagsEditorHistory() {
|
||||
Instance = new JovianTagsEditorHistory();
|
||||
}
|
||||
|
||||
private JovianTagsEditorHistory() {
|
||||
EditorApplication.delayCall += Load;
|
||||
}
|
||||
|
||||
public void AddRecentTag(string tag) {
|
||||
RecentTags.Insert(0, tag);
|
||||
while(RecentTags.Count > numRecentTags) {
|
||||
RecentTags.RemoveAt(RecentTags.Count - 1);
|
||||
}
|
||||
Save();
|
||||
}
|
||||
|
||||
private void Load() {
|
||||
var saves = EditorPrefs.GetString(Application.productName + saveKey);
|
||||
RecentTags = saves.Split(saveDelimiter).ToList();
|
||||
if(RecentTags == null) {
|
||||
RecentTags = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Save() {
|
||||
EditorPrefs.SetString(Application.productName + saveKey, string.Join(saveDelimiter, RecentTags));
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsEditorHistory.cs.meta
Normal file
2
Editor/JovianTagsEditorHistory.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45efc4b96d295b84498b0fed0c4130d1
|
||||
24
Editor/JovianTagsEditorUtility.cs
Normal file
24
Editor/JovianTagsEditorUtility.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
public static class JovianTagsEditorUtility {
|
||||
public static JovianTagsSettings[] GetSettings() {
|
||||
var settingsPaths = AssetDatabase.FindAssets("t:JovianTagsSettings");
|
||||
if(settingsPaths.Length == 0) {
|
||||
return Array.Empty<JovianTagsSettings>();
|
||||
}
|
||||
|
||||
var tagSettings = new List<JovianTagsSettings>();
|
||||
for(int i = 0, n = settingsPaths.Length; i < n; i++) {
|
||||
var guid = settingsPaths[i];
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var setting = AssetDatabase.LoadAssetAtPath<JovianTagsSettings>(path);
|
||||
tagSettings.Add(setting);
|
||||
}
|
||||
|
||||
return tagSettings.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsEditorUtility.cs.meta
Normal file
2
Editor/JovianTagsEditorUtility.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2a32b341a059664bb365498df0d6bdf
|
||||
630
Editor/JovianTagsEditorWindow.cs
Normal file
630
Editor/JovianTagsEditorWindow.cs
Normal file
@@ -0,0 +1,630 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
|
||||
public class JovianTagsEditorWindow : EditorWindow {
|
||||
|
||||
// --- Colors ---
|
||||
private static readonly Color DividerColor = new(1f, 1f, 1f, 0.12f);
|
||||
private static readonly Color GreenButton = new(0.3f, 0.8f, 0.3f);
|
||||
private static readonly Color HeaderBg = new(0.18f, 0.18f, 0.18f, 1f);
|
||||
private static readonly Color RowEven = new(0f, 0f, 0f, 0f);
|
||||
private static readonly Color RowOdd = new(1f, 1f, 1f, 0.03f);
|
||||
private static readonly Color RowHover = new(0.3f, 0.5f, 0.8f, 0.12f);
|
||||
private static readonly Color ChildIndentLine = new(1f, 1f, 1f, 0.08f);
|
||||
|
||||
// --- Constants ---
|
||||
private const int RowHeight = 22;
|
||||
private const int HeaderHeight = 26;
|
||||
private const int DepthIndent = 20;
|
||||
|
||||
// --- Cached layout options ---
|
||||
private static readonly GUILayoutOption RowHeightOpt = GUILayout.Height(RowHeight);
|
||||
private static readonly GUILayoutOption HeaderHeightOpt = GUILayout.Height(HeaderHeight);
|
||||
private static readonly GUILayoutOption ExpandWidth = GUILayout.ExpandWidth(true);
|
||||
private static readonly GUILayoutOption SmallBtnWidth = GUILayout.Width(22);
|
||||
private static readonly GUILayoutOption SmallBtnHeight = GUILayout.Height(18);
|
||||
|
||||
// --- State ---
|
||||
private Vector2 scrollPosition;
|
||||
private string searchFilter = "";
|
||||
private string newTagName = "";
|
||||
private string outputPath;
|
||||
private readonly Dictionary<string, bool> foldoutState = new();
|
||||
private bool scrollToBottom;
|
||||
private JovianTagsSettings[] allSettings;
|
||||
private int rowIndex;
|
||||
|
||||
// --- Styles (lazy init) ---
|
||||
private GUIStyle _tagStyle;
|
||||
|
||||
private GUIStyle TagStyle => _tagStyle ??= new GUIStyle(EditorStyles.label) {
|
||||
fontSize = 12, fixedHeight = RowHeight, padding = new RectOffset(2, 2, 2, 2)
|
||||
};
|
||||
|
||||
private GUIStyle _headerStyle;
|
||||
|
||||
private GUIStyle HeaderStyle => _headerStyle ??= new GUIStyle(EditorStyles.boldLabel) {
|
||||
fontSize = 12, fixedHeight = HeaderHeight, padding = new RectOffset(2, 2, 4, 4)
|
||||
};
|
||||
|
||||
private GUIStyle _pathStyle;
|
||||
|
||||
private GUIStyle PathStyle => _pathStyle ??= new GUIStyle(EditorStyles.miniLabel) {
|
||||
fontSize = 10, fixedHeight = RowHeight,
|
||||
normal = { textColor = new Color(1f, 1f, 1f, 0.3f) },
|
||||
padding = new RectOffset(4, 4, 4, 2)
|
||||
};
|
||||
|
||||
// --- Tree structure ---
|
||||
private class TagTreeNode {
|
||||
public string Name; // segment name (e.g., "Fire")
|
||||
public string FullPath; // full dotted path (e.g., "Damage.Fire")
|
||||
public bool IsRegistered; // exists in GameTagSettings
|
||||
public List<TagTreeNode> Children = new();
|
||||
}
|
||||
|
||||
[MenuItem("Fidelit&y/Tag System/Tag Editor...")]
|
||||
public static void ShowWindow() {
|
||||
var window = GetWindow<JovianTagsEditorWindow>(true, "Tag Editor");
|
||||
window.minSize = new Vector2(480, 380);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable() { RefreshData(); }
|
||||
private void OnFocus() { RefreshData(); }
|
||||
|
||||
private void RefreshData() {
|
||||
allSettings = JovianTagsEditorUtility.GetSettings();
|
||||
outputPath = JovianTagsGenerator.FindOrDefaultOutputPath();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.BeginVertical();
|
||||
GUILayout.Space(8);
|
||||
|
||||
DrawToolbar();
|
||||
|
||||
if(scrollToBottom) {
|
||||
scrollPosition.y = float.MaxValue;
|
||||
scrollToBottom = false;
|
||||
}
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUIStyle.none, GUI.skin.verticalScrollbar);
|
||||
DrawContent();
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
DrawDivider();
|
||||
DrawAddTagRow();
|
||||
GUILayout.Space(4);
|
||||
DrawFooter();
|
||||
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndVertical();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ===== Toolbar =====
|
||||
|
||||
private void DrawToolbar() {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Output:", GUILayout.Width(50));
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.TextField(outputPath);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
if(GUILayout.Button("...", GUILayout.Width(26), GUILayout.Height(18)))
|
||||
ChangeOutputPath();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(2);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Search:", GUILayout.Width(50));
|
||||
searchFilter = EditorGUILayout.TextField(searchFilter);
|
||||
if(!string.IsNullOrEmpty(searchFilter) && GUILayout.Button("✕", GUILayout.Width(20), GUILayout.Height(18))) {
|
||||
searchFilter = "";
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(4);
|
||||
DrawDivider();
|
||||
}
|
||||
|
||||
// ===== Content =====
|
||||
|
||||
private void DrawContent() {
|
||||
if(allSettings.Length == 0) {
|
||||
GUILayout.Space(40);
|
||||
DrawCenteredMessage("No Tag Settings asset found.");
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("Create Tag Settings", GUILayout.Height(28), GUILayout.Width(180))) {
|
||||
CreateSettingsAsset();
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
return;
|
||||
}
|
||||
|
||||
var allTags = CollectAllTags();
|
||||
if(allTags.Count == 0) {
|
||||
GUILayout.Space(40);
|
||||
DrawCenteredMessage("No tags defined.\nAdd tags using the field below.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build tree
|
||||
var roots = BuildTree(allTags);
|
||||
|
||||
// Filter
|
||||
if(!string.IsNullOrEmpty(searchFilter)) {
|
||||
roots = FilterTree(roots, searchFilter);
|
||||
}
|
||||
|
||||
rowIndex = 0;
|
||||
foreach(var root in roots.OrderBy(r => r.Name, System.StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawTreeNode(root, 0);
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
var totalTags = allSettings.Sum(s => s.gameTags.Length);
|
||||
GUILayout.Label($"{totalTags} tags total", EditorStyles.centeredGreyMiniLabel);
|
||||
}
|
||||
|
||||
private void DrawTreeNode(TagTreeNode node, int depth) {
|
||||
var hasChildren = node.Children.Count > 0;
|
||||
var isRoot = depth == 0;
|
||||
|
||||
// --- Row ---
|
||||
if(isRoot) {
|
||||
// Root header
|
||||
var headerRect = EditorGUILayout.BeginHorizontal(HeaderHeightOpt);
|
||||
EditorGUI.DrawRect(headerRect, HeaderBg);
|
||||
GUILayout.Space(6);
|
||||
|
||||
if(hasChildren) {
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
foldoutState[node.FullPath] = EditorGUILayout.Foldout(
|
||||
foldoutState[node.FullPath], "", true, EditorStyles.foldout);
|
||||
GUILayout.Space(-4);
|
||||
GUILayout.Label(node.Name, HeaderStyle, ExpandWidth);
|
||||
GUILayout.Label($"{CountDescendants(node)}", new GUIStyle(EditorStyles.miniLabel) {
|
||||
alignment = TextAnchor.MiddleRight,
|
||||
normal = { textColor = new Color(1, 1, 1, 0.4f) }
|
||||
}, GUILayout.Width(30));
|
||||
}
|
||||
else {
|
||||
GUILayout.Space(14);
|
||||
GUILayout.Label(node.Name, HeaderStyle, ExpandWidth);
|
||||
}
|
||||
|
||||
DrawAddChildButton(node);
|
||||
DrawDeleteButton(node.FullPath);
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
else {
|
||||
// Child row
|
||||
var bgColor = rowIndex % 2 == 0 ? RowEven : RowOdd;
|
||||
var rect = EditorGUILayout.BeginHorizontal(RowHeightOpt);
|
||||
if(rect.Contains(Event.current.mousePosition)) bgColor = RowHover;
|
||||
if(bgColor.a > 0) EditorGUI.DrawRect(rect, bgColor);
|
||||
|
||||
// Indent with vertical guide lines
|
||||
GUILayout.Space(6 + depth * DepthIndent);
|
||||
if(Event.current.type == EventType.Repaint) {
|
||||
for(int d = 1; d <= depth; d++) {
|
||||
var lineX = 6 + d * DepthIndent - 10;
|
||||
EditorGUI.DrawRect(new Rect(lineX, rect.y, 1, rect.height), ChildIndentLine);
|
||||
}
|
||||
}
|
||||
|
||||
if(hasChildren) {
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
foldoutState[node.FullPath] = EditorGUILayout.Foldout(
|
||||
foldoutState[node.FullPath], "", true, EditorStyles.foldout);
|
||||
GUILayout.Space(-4);
|
||||
}
|
||||
else {
|
||||
GUILayout.Space(14);
|
||||
}
|
||||
|
||||
GUILayout.Label(node.Name, TagStyle, ExpandWidth);
|
||||
|
||||
// Full path hint for deep tags
|
||||
if(depth > 1) {
|
||||
GUILayout.Label(node.FullPath, PathStyle,
|
||||
GUILayout.Width(Mathf.Min(250, position.width * 0.3f)));
|
||||
}
|
||||
|
||||
DrawAddChildButton(node);
|
||||
DrawDeleteButton(node.FullPath);
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
// --- Children ---
|
||||
if(!hasChildren) return;
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
if(!foldoutState[node.FullPath]) return;
|
||||
|
||||
foreach(var child in node.Children.OrderBy(c => c.Name, System.StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawTreeNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAddChildButton(TagTreeNode parent) {
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("+", SmallBtnWidth, SmallBtnHeight)) {
|
||||
// Open a small input for the child name
|
||||
AddChildTagPopup.Show(parent.FullPath, childName => {
|
||||
var fullTag = parent.FullPath + "." + childName;
|
||||
AddTag(fullTag);
|
||||
});
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
}
|
||||
|
||||
private void DrawDeleteButton(string tag) {
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f);
|
||||
if(GUILayout.Button("✕", SmallBtnWidth, SmallBtnHeight)) {
|
||||
DeleteTag(tag);
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
}
|
||||
|
||||
// ===== Add Tag Row =====
|
||||
|
||||
private void DrawAddTagRow() {
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("New Tag:", GUILayout.Width(60));
|
||||
newTagName = EditorGUILayout.TextField(newTagName, GUILayout.Height(20));
|
||||
|
||||
string validationError = null;
|
||||
if(!string.IsNullOrWhiteSpace(newTagName)) {
|
||||
validationError = JovianTagsGenerator.ValidateTagName(newTagName.Trim());
|
||||
if(validationError == null && CollectAllTags().Contains(newTagName.Trim()))
|
||||
validationError = $"Tag '{newTagName.Trim()}' already exists.";
|
||||
}
|
||||
var canAdd = !string.IsNullOrWhiteSpace(newTagName) && validationError == null;
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!canAdd);
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("+ Add", GUILayout.Width(60), GUILayout.Height(20))) {
|
||||
AddTag(newTagName.Trim());
|
||||
newTagName = "";
|
||||
GUI.FocusControl(null);
|
||||
scrollToBottom = true;
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(newTagName) && validationError != null)
|
||||
EditorGUILayout.HelpBox(validationError, MessageType.Error);
|
||||
if(canAdd && newTagName.Contains(JovianTagsHandler.tagDelimiter))
|
||||
EditorGUILayout.HelpBox("Parent tags are created automatically.", MessageType.Info);
|
||||
|
||||
GUILayout.Space(4);
|
||||
}
|
||||
|
||||
// ===== Footer =====
|
||||
|
||||
private void DrawFooter() {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = GreenButton;
|
||||
if(GUILayout.Button("Save & Generate", GUILayout.Height(28), GUILayout.MinWidth(150)))
|
||||
SaveAndGenerate();
|
||||
GUI.backgroundColor = prevBg;
|
||||
if(GUILayout.Button("Clean Deleted In Assets", GUILayout.Height(28))) {
|
||||
CleanStaleTagsFromAllAssets();
|
||||
}
|
||||
if(GUILayout.Button("Refresh", GUILayout.Height(28), GUILayout.Width(70))) {
|
||||
RefreshData();
|
||||
Repaint();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ===== Tree Building =====
|
||||
|
||||
private List<TagTreeNode> BuildTree(List<string> allTags) {
|
||||
var rootNodes = new Dictionary<string, TagTreeNode>();
|
||||
var allNodes = new Dictionary<string, TagTreeNode>();
|
||||
|
||||
foreach(var tag in allTags) {
|
||||
var parts = tag.Split(JovianTagsHandler.tagDelimiter);
|
||||
var accumulated = "";
|
||||
TagTreeNode parent = null;
|
||||
|
||||
for(int i = 0; i < parts.Length; i++) {
|
||||
accumulated = i == 0 ? parts[i] : accumulated + "." + parts[i];
|
||||
|
||||
if(!allNodes.TryGetValue(accumulated, out var node)) {
|
||||
node = new TagTreeNode {
|
||||
Name = parts[i],
|
||||
FullPath = accumulated,
|
||||
IsRegistered = allTags.Contains(accumulated)
|
||||
};
|
||||
allNodes[accumulated] = node;
|
||||
|
||||
if(parent != null)
|
||||
parent.Children.Add(node);
|
||||
else
|
||||
rootNodes[accumulated] = node;
|
||||
}
|
||||
parent = node;
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes.Values.ToList();
|
||||
}
|
||||
|
||||
private List<TagTreeNode> FilterTree(List<TagTreeNode> roots, string filter) {
|
||||
var result = new List<TagTreeNode>();
|
||||
foreach(var root in roots) {
|
||||
var filtered = FilterNode(root, filter);
|
||||
if(filtered != null) result.Add(filtered);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private TagTreeNode FilterNode(TagTreeNode node, string filter) {
|
||||
var matchesSelf = node.FullPath.IndexOf(filter, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
var filteredChildren = new List<TagTreeNode>();
|
||||
|
||||
foreach(var child in node.Children) {
|
||||
var fc = FilterNode(child, filter);
|
||||
if(fc != null) filteredChildren.Add(fc);
|
||||
}
|
||||
|
||||
if(!matchesSelf && filteredChildren.Count == 0) return null;
|
||||
|
||||
return new TagTreeNode {
|
||||
Name = node.Name,
|
||||
FullPath = node.FullPath,
|
||||
IsRegistered = node.IsRegistered,
|
||||
Children = matchesSelf ? node.Children : filteredChildren
|
||||
};
|
||||
}
|
||||
|
||||
private int CountDescendants(TagTreeNode node) {
|
||||
int count = 0;
|
||||
foreach(var child in node.Children) {
|
||||
count++;
|
||||
count += CountDescendants(child);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ===== Actions =====
|
||||
|
||||
private void AddTag(string tagName) {
|
||||
if(allSettings.Length == 0) {
|
||||
EditorUtility.DisplayDialog("No Settings", "Create a JovianTagsSettings asset first.", "OK");
|
||||
return;
|
||||
}
|
||||
Undo.RecordObject(allSettings[0], "Add Tag");
|
||||
allSettings[0].AddGameTag(tagName);
|
||||
}
|
||||
|
||||
private void DeleteTag(string tag) {
|
||||
foreach(var s in allSettings) {
|
||||
if(!s.gameTags.Any(t => t.tag == tag)) continue;
|
||||
var hasChildren = s.gameTags.Any(t => t.tag != tag && t.tag.StartsWith(tag + "."));
|
||||
var message = hasChildren ? $"Delete '{tag}' and all children?" : $"Delete '{tag}'?";
|
||||
if(EditorUtility.DisplayDialog("Delete Tag", message, "Delete", "Cancel")) {
|
||||
Undo.RecordObject(s, "Remove Tag");
|
||||
s.RemoveTag(tag);
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSettingsAsset() {
|
||||
var folder = EditorUtility.OpenFolderPanel("Choose folder for Tag Settings", "Assets", "");
|
||||
if(string.IsNullOrEmpty(folder)) return;
|
||||
|
||||
var dataPath = Application.dataPath.Replace('\\', '/');
|
||||
folder = folder.Replace('\\', '/');
|
||||
if(folder.StartsWith(dataPath))
|
||||
folder = "Assets" + folder.Substring(dataPath.Length);
|
||||
|
||||
var asset = ScriptableObject.CreateInstance<JovianTagsSettings>();
|
||||
var path = $"{folder}/JovianTagsSettings.asset";
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.FocusProjectWindow();
|
||||
Selection.activeObject = asset;
|
||||
Debug.Log($"[Tag System] Created Tag Settings at {path}");
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void CleanStaleTagsFromAllAssets() {
|
||||
var validTags = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
foreach(var s in allSettings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
validTags.Add(t.tag);
|
||||
|
||||
int assetsScanned = 0;
|
||||
int assetsCleaned = 0;
|
||||
int tagsRemoved = 0;
|
||||
|
||||
// Scan all prefabs and ScriptableObjects
|
||||
var guids = AssetDatabase.FindAssets("t:GameObject t:ScriptableObject");
|
||||
// FindAssets with multiple types uses OR, but let's do them separately for clarity
|
||||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
||||
var soGuids = AssetDatabase.FindAssets("t:ScriptableObject");
|
||||
var allGuids = new HashSet<string>(prefabGuids);
|
||||
foreach(var g in soGuids) allGuids.Add(g);
|
||||
|
||||
foreach(var guid in allGuids) {
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
assetsScanned++;
|
||||
|
||||
foreach(var asset in assets) {
|
||||
if(asset == null) continue;
|
||||
var so = new SerializedObject(asset);
|
||||
var cleaned = CleanSerializedObject(so, validTags);
|
||||
if(cleaned > 0) {
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(asset);
|
||||
assetsCleaned++;
|
||||
tagsRemoved += cleaned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
EditorUtility.DisplayDialog("Clean Complete",
|
||||
$"Scanned {assetsScanned} assets.\n" +
|
||||
$"Cleaned {assetsCleaned} assets.\n" +
|
||||
$"Removed {tagsRemoved} stale tag references.",
|
||||
"OK");
|
||||
}
|
||||
|
||||
private static int CleanSerializedObject(SerializedObject so, HashSet<string> validTags) {
|
||||
int totalRemoved = 0;
|
||||
var prop = so.GetIterator();
|
||||
|
||||
while(prop.NextVisible(true)) {
|
||||
// Look for string arrays named "tags" inside JovianTagsGroup structs
|
||||
if(prop.isArray && prop.propertyType == SerializedPropertyType.String) continue;
|
||||
if(prop.propertyType != SerializedPropertyType.Generic) continue;
|
||||
if(!prop.type.Contains("JovianTagsGroup")) continue;
|
||||
|
||||
var tagsProp = prop.FindPropertyRelative("tags");
|
||||
if(tagsProp == null || !tagsProp.isArray) continue;
|
||||
|
||||
for(int i = tagsProp.arraySize - 1; i >= 0; i--) {
|
||||
var val = tagsProp.GetArrayElementAtIndex(i).stringValue;
|
||||
if(!string.IsNullOrEmpty(val) && !validTags.Contains(val)) {
|
||||
tagsProp.DeleteArrayElementAtIndex(i);
|
||||
totalRemoved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalRemoved;
|
||||
}
|
||||
|
||||
private void SaveAndGenerate() {
|
||||
foreach(var s in allSettings) EditorUtility.SetDirty(s);
|
||||
AssetDatabase.SaveAssets();
|
||||
JovianTagsGenerator.Regenerate(outputPath);
|
||||
}
|
||||
|
||||
private void ChangeOutputPath() {
|
||||
var newPath = EditorUtility.SaveFilePanel("Choose Generated File Location",
|
||||
System.IO.Path.GetDirectoryName(outputPath) ?? "Assets",
|
||||
System.IO.Path.GetFileName(outputPath) ?? "GameTags.cs", "cs");
|
||||
if(string.IsNullOrEmpty(newPath)) return;
|
||||
var dataPath = Application.dataPath.Replace('\\', '/');
|
||||
newPath = newPath.Replace('\\', '/');
|
||||
if(newPath.StartsWith(dataPath))
|
||||
newPath = "Assets" + newPath.Substring(dataPath.Length);
|
||||
outputPath = newPath;
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
private List<string> CollectAllTags() {
|
||||
var tags = new SortedSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
||||
foreach(var s in allSettings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
tags.Add(t.tag);
|
||||
return tags.ToList();
|
||||
}
|
||||
|
||||
private static void DrawDivider() {
|
||||
var rect = EditorGUILayout.GetControlRect(false, 1);
|
||||
EditorGUI.DrawRect(rect, DividerColor);
|
||||
}
|
||||
|
||||
private static void DrawCenteredMessage(string message) {
|
||||
var style = new GUIStyle(EditorStyles.label) {
|
||||
alignment = TextAnchor.MiddleCenter, wordWrap = true, fontSize = 12,
|
||||
normal = { textColor = new Color(1, 1, 1, 0.4f) }
|
||||
};
|
||||
EditorGUILayout.LabelField(message, style, GUILayout.Height(60));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Add Child Tag Popup =====
|
||||
|
||||
public class AddChildTagPopup : EditorWindow {
|
||||
private string parentPath;
|
||||
private string childName = "";
|
||||
private System.Action<string> onConfirm;
|
||||
|
||||
public static void Show(string parentPath, System.Action<string> onConfirm) {
|
||||
var popup = CreateInstance<AddChildTagPopup>();
|
||||
popup.parentPath = parentPath;
|
||||
popup.onConfirm = onConfirm;
|
||||
popup.titleContent = new GUIContent("Add Child Tag");
|
||||
var size = new Vector2(300, 90);
|
||||
var mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
|
||||
popup.position = new Rect(mousePos.x - size.x * 0.5f, mousePos.y, size.x, size.y);
|
||||
popup.ShowUtility();
|
||||
popup.Focus();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label($"Parent: {parentPath}", EditorStyles.boldLabel);
|
||||
GUILayout.Space(4);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Name:", GUILayout.Width(42));
|
||||
GUI.SetNextControlName("ChildNameField");
|
||||
childName = EditorGUILayout.TextField(childName);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUI.FocusTextInControl("ChildNameField");
|
||||
|
||||
var error = string.IsNullOrWhiteSpace(childName)
|
||||
? null
|
||||
: JovianTagsGenerator.ValidateTagSegment(childName.Trim());
|
||||
var canAdd = !string.IsNullOrWhiteSpace(childName) && error == null;
|
||||
|
||||
GUILayout.Space(4);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUI.BeginDisabledGroup(!canAdd);
|
||||
if(GUILayout.Button("Add", GUILayout.Height(22)) || (canAdd && Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return)) {
|
||||
onConfirm?.Invoke(childName.Trim());
|
||||
Close();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
if(GUILayout.Button("Cancel", GUILayout.Height(22))) {
|
||||
Close();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if(Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsEditorWindow.cs.meta
Normal file
2
Editor/JovianTagsEditorWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9190068b05141d74aae0f630a8080f23
|
||||
300
Editor/JovianTagsGenerator.cs
Normal file
300
Editor/JovianTagsGenerator.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
/// <summary>
|
||||
/// Generates a C# file with static GameTag fields organized in a nested class hierarchy.
|
||||
/// Fields are resolved at runtime via JovianTagsHandler. Manual additions are preserved.
|
||||
/// </summary>
|
||||
public static class JovianTagsGenerator {
|
||||
|
||||
private const string DefaultClassName = "GameTags";
|
||||
private const string DefaultOutputPath = "Assets/GameTags.cs";
|
||||
private const string TagFieldPostfix = "_Tag";
|
||||
|
||||
/// <summary>
|
||||
/// Collects all tag names from all GameTagSettings assets in the project.
|
||||
/// </summary>
|
||||
public static List<string> CollectAllTags() {
|
||||
var allTags = new HashSet<string>(StringComparer.Ordinal);
|
||||
var settings = JovianTagsEditorUtility.GetSettings();
|
||||
foreach(var s in settings) {
|
||||
foreach(var t in s.gameTags) {
|
||||
if(!string.IsNullOrEmpty(t.tag)) {
|
||||
allTags.Add(t.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allTags.OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the C# source content for the strong-typed tag fields.
|
||||
/// </summary>
|
||||
public static string GenerateFileContent(List<string> tagNames, string className) {
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header
|
||||
sb.AppendLine("//------------------------------------------------------------------------------");
|
||||
sb.AppendLine("// Generated by Jovian Game Tag System");
|
||||
sb.AppendLine("// Manual edits are preserved — fields added here will be kept on regeneration.");
|
||||
sb.AppendLine($"// Last generated on {DateTime.Now:yyyy-MM-dd HH:mm}");
|
||||
sb.AppendLine("//------------------------------------------------------------------------------");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using Jovian.TagSystem;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"public static partial class {className} {{");
|
||||
|
||||
// Build tree structure from dot-delimited names
|
||||
var root = new TagNode("", "");
|
||||
foreach(var fullName in tagNames) {
|
||||
var parts = fullName.Split(JovianTagsHandler.tagDelimiter);
|
||||
var current = root;
|
||||
var accumulated = "";
|
||||
for(int i = 0; i < parts.Length; i++) {
|
||||
var part = parts[i];
|
||||
accumulated = i == 0 ? part : accumulated + "." + part;
|
||||
if(!current.Children.TryGetValue(part, out var child)) {
|
||||
child = new TagNode(part, accumulated);
|
||||
current.Children[part] = child;
|
||||
}
|
||||
current = child;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate nested classes from tree
|
||||
GenerateNode(sb, root, 1);
|
||||
|
||||
// Generate initializer methods
|
||||
var allNodes = FlattenNodes(root);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" private static readonly string[] AllTags = {");
|
||||
for(int i = 0; i < allNodes.Count; i++) {
|
||||
var comma = i < allNodes.Count - 1 ? "," : "";
|
||||
sb.AppendLine($" \"{allNodes[i].FullName}\"{comma}");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)]");
|
||||
sb.AppendLine(" private static void RuntimeInitialize() {");
|
||||
sb.AppendLine(" JovianTagsHandler.Initialize();");
|
||||
sb.AppendLine(" JovianTagsHandler.RegisterTags(AllTags);");
|
||||
foreach(var node in allNodes) {
|
||||
sb.AppendLine($" {node.FieldPath} = JovianTagsHandler.GetTag(\"{node.FullName}\");");
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#if UNITY_EDITOR");
|
||||
sb.AppendLine(" [UnityEditor.InitializeOnLoadMethod]");
|
||||
sb.AppendLine(" private static void EditorInitialize() {");
|
||||
sb.AppendLine(" RuntimeInitialize();");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("#endif");
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void GenerateNode(StringBuilder sb, TagNode node, int depth) {
|
||||
var indent = new string(' ', depth * 4);
|
||||
|
||||
foreach(var child in node.Children.Values.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)) {
|
||||
var safeName = SanitizeIdentifier(child.Name);
|
||||
|
||||
if(child.Children.Count > 0) {
|
||||
// Node with children → nested class + field
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"{indent}public static class {safeName} {{");
|
||||
sb.AppendLine($"{indent} public static JovianTag {safeName}{TagFieldPostfix};");
|
||||
GenerateNode(sb, child, depth + 1);
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
else {
|
||||
// Leaf → just a field
|
||||
sb.AppendLine($"{indent}public static JovianTag {safeName}{TagFieldPostfix};");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TagNode> FlattenNodes(TagNode root) {
|
||||
var result = new List<TagNode>();
|
||||
foreach(var child in root.Children.Values.OrderBy(c => c.FullName, StringComparer.OrdinalIgnoreCase)) {
|
||||
FlattenRecursive(child, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void FlattenRecursive(TagNode node, List<TagNode> result) {
|
||||
result.Add(node);
|
||||
foreach(var child in node.Children.Values.OrderBy(c => c.FullName, StringComparer.OrdinalIgnoreCase)) {
|
||||
FlattenRecursive(child, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the generated file and triggers AssetDatabase refresh.
|
||||
/// </summary>
|
||||
public static void WriteAndRefresh(string content, string outputPath) {
|
||||
outputPath = outputPath.Replace('\\', '/');
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
File.WriteAllText(outputPath, content);
|
||||
Debug.Log($"[Game Tag System] Generated tag file at: {outputPath}");
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full regeneration from current GameTagSettings.
|
||||
/// </summary>
|
||||
public static void Regenerate(string outputPath = null) {
|
||||
if(string.IsNullOrEmpty(outputPath)) {
|
||||
outputPath = FindOrDefaultOutputPath();
|
||||
}
|
||||
var className = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var tags = CollectAllTags();
|
||||
if(tags.Count == 0) {
|
||||
Debug.LogWarning("[Game Tag System] No tags found in any GameTagSettings asset.");
|
||||
return;
|
||||
}
|
||||
var content = GenerateFileContent(tags, className);
|
||||
WriteAndRefresh(content, outputPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for an existing generated file or returns a default path.
|
||||
/// </summary>
|
||||
public static string FindOrDefaultOutputPath() {
|
||||
var guids = AssetDatabase.FindAssets($"{DefaultClassName} t:MonoScript");
|
||||
if(guids.Length > 0) {
|
||||
return AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
}
|
||||
return DefaultOutputPath;
|
||||
}
|
||||
|
||||
[MenuItem("Fidelit&y/Tag System/Regenerate Tag Constants")]
|
||||
private static void MenuRegenerate() {
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> CSharpKeywords = new(StringComparer.Ordinal) {
|
||||
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char",
|
||||
"checked", "class", "const", "continue", "decimal", "default", "delegate", "do",
|
||||
"double", "else", "enum", "event", "explicit", "extern", "false", "finally",
|
||||
"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int",
|
||||
"interface", "internal", "is", "lock", "long", "namespace", "new", "null",
|
||||
"object", "operator", "out", "override", "params", "private", "protected",
|
||||
"public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof",
|
||||
"stackalloc", "static", "string", "struct", "switch", "this", "throw", "true",
|
||||
"try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
|
||||
"virtual", "void", "volatile", "while"
|
||||
};
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex ValidIdentifierRegex =
|
||||
new(@"^[A-Za-z][A-Za-z0-9_]*$");
|
||||
|
||||
/// <summary>
|
||||
/// Validates a single tag segment (one part between dots).
|
||||
/// Returns null if valid, or an error message if invalid.
|
||||
/// </summary>
|
||||
public static string ValidateTagSegment(string segment) {
|
||||
if(string.IsNullOrWhiteSpace(segment))
|
||||
return "Tag name cannot be empty.";
|
||||
|
||||
if(!ValidIdentifierRegex.IsMatch(segment))
|
||||
return $"'{segment}' must start with a letter and contain only letters, digits, or underscores.";
|
||||
|
||||
if(CSharpKeywords.Contains(segment))
|
||||
return $"'{segment}' is a C# reserved keyword.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a full dot-delimited tag name. Checks each segment.
|
||||
/// Returns null if valid, or an error message for the first invalid segment.
|
||||
/// </summary>
|
||||
public static string ValidateTagName(string tagName) {
|
||||
if(string.IsNullOrWhiteSpace(tagName))
|
||||
return "Tag name cannot be empty.";
|
||||
|
||||
var segments = tagName.Split(JovianTagsHandler.tagDelimiter);
|
||||
foreach(var segment in segments) {
|
||||
var error = ValidateTagSegment(segment);
|
||||
if(error != null) return error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string SanitizeIdentifier(string name) {
|
||||
if(string.IsNullOrWhiteSpace(name)) return "_Empty";
|
||||
var sanitized = System.Text.RegularExpressions.Regex.Replace(name.Trim(), @"[^A-Za-z0-9_]", "_");
|
||||
if(char.IsDigit(sanitized[0])) sanitized = "_" + sanitized;
|
||||
if(CSharpKeywords.Contains(sanitized)) sanitized = "_" + sanitized;
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private class TagNode {
|
||||
public string Name;
|
||||
public string FullName;
|
||||
public Dictionary<string, TagNode> Children = new(StringComparer.Ordinal);
|
||||
|
||||
public TagNode(string name, string fullName) {
|
||||
Name = name;
|
||||
FullName = fullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the fully qualified C# field path for this node's tag field.
|
||||
/// Nodes with children are nested classes, so the path includes the class hierarchy.
|
||||
/// Leaf nodes at root level are just field names.
|
||||
/// E.g., "Character.Player" → "Character.Player.Player_Tag"
|
||||
/// E.g., "big" (leaf, no children) → "big_Tag"
|
||||
/// </summary>
|
||||
public string FieldPath {
|
||||
get {
|
||||
var parts = FullName.Split(JovianTagsHandler.tagDelimiter);
|
||||
var safeParts = parts.Select(p =>
|
||||
System.Text.RegularExpressions.Regex.Replace(p.Trim(), @"[^A-Za-z0-9_]", "_")).ToArray();
|
||||
var lastSafe = safeParts[^1];
|
||||
|
||||
// Nodes with children become classes; their _Tag field lives inside the class.
|
||||
// Leaf nodes are just fields inside their parent class.
|
||||
//
|
||||
// Examples (→ = generates):
|
||||
// "big" (root leaf) → big_Tag
|
||||
// "Damage" (root with children) → Damage.Damage_Tag
|
||||
// "Damage.Fire" (child leaf) → Damage.Fire_Tag
|
||||
// "Damage.Fire.AOE" (deep leaf) → Damage.Fire.AOE_Tag
|
||||
// "Damage.Fire" (with children) → Damage.Fire.Fire_Tag
|
||||
|
||||
if(Children.Count > 0) {
|
||||
// This node is a class — its _Tag field is inside itself
|
||||
// Class path = all segments joined, field = lastSegment_Tag
|
||||
return string.Join(".", safeParts) + "." + lastSafe + TagFieldPostfix;
|
||||
}
|
||||
|
||||
if(safeParts.Length == 1) {
|
||||
// Root leaf — field directly in outer class
|
||||
return lastSafe + TagFieldPostfix;
|
||||
}
|
||||
|
||||
// Non-root leaf — field inside parent class
|
||||
// Parent class path = all segments except last
|
||||
var parentPath = string.Join(".", safeParts.Take(safeParts.Length - 1));
|
||||
return parentPath + "." + lastSafe + TagFieldPostfix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsGenerator.cs.meta
Normal file
2
Editor/JovianTagsGenerator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 126771008dabad6429c6a5408b5f204d
|
||||
287
Editor/JovianTagsPickerPopup.cs
Normal file
287
Editor/JovianTagsPickerPopup.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
|
||||
public class JovianTagsPickerPopup : EditorWindow {
|
||||
|
||||
private static readonly Color DividerColor = new(1f, 1f, 1f, 0.12f);
|
||||
private static readonly Color RowOdd = new(1f, 1f, 1f, 0.03f);
|
||||
private static readonly Color RowHover = new(0.3f, 0.5f, 0.8f, 0.12f);
|
||||
private static readonly Color HeaderBg = new(0.18f, 0.18f, 0.18f, 1f);
|
||||
private static readonly Color ChildIndentLine = new(1f, 1f, 1f, 0.08f);
|
||||
|
||||
private const int RowHeight = 22;
|
||||
private const int DepthIndent = 20;
|
||||
|
||||
private HashSet<string> selectedTags = new();
|
||||
private Action<HashSet<string>> onConfirm;
|
||||
private string searchFilter = "";
|
||||
private Vector2 scrollPosition;
|
||||
private List<string> allTags = new();
|
||||
private readonly Dictionary<string, bool> foldoutState = new();
|
||||
private int rowIndex;
|
||||
|
||||
private GUIStyle _tagStyle;
|
||||
|
||||
private GUIStyle TagStyle => _tagStyle ??= new GUIStyle(EditorStyles.label) {
|
||||
fontSize = 12, fixedHeight = RowHeight
|
||||
};
|
||||
|
||||
private GUIStyle _headerStyle;
|
||||
|
||||
private GUIStyle HeaderStyle => _headerStyle ??= new GUIStyle(EditorStyles.boldLabel) {
|
||||
fontSize = 12, fixedHeight = 24
|
||||
};
|
||||
|
||||
private class PickerNode {
|
||||
public string Name;
|
||||
public string FullPath;
|
||||
public List<PickerNode> Children = new();
|
||||
}
|
||||
|
||||
public static void Show(HashSet<string> currentTags, Action<HashSet<string>> onConfirm) {
|
||||
var window = CreateInstance<JovianTagsPickerPopup>();
|
||||
window.selectedTags = new HashSet<string>(currentTags);
|
||||
window.onConfirm = onConfirm;
|
||||
window.titleContent = new GUIContent("Select Tags");
|
||||
window.RefreshTags();
|
||||
|
||||
var size = new Vector2(380, 440);
|
||||
var mousePos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
|
||||
window.position = new Rect(mousePos.x - size.x * 0.5f, mousePos.y, size.x, size.y);
|
||||
window.minSize = new Vector2(300, 300);
|
||||
window.ShowUtility();
|
||||
}
|
||||
|
||||
private void RefreshTags() {
|
||||
allTags.Clear();
|
||||
var settings = JovianTagsEditorUtility.GetSettings();
|
||||
var tagSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach(var s in settings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
tagSet.Add(t.tag);
|
||||
allTags = tagSet.ToList();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
EditorGUILayout.BeginVertical();
|
||||
GUILayout.Space(6);
|
||||
|
||||
// Search
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label("Search:", GUILayout.Width(50));
|
||||
searchFilter = EditorGUILayout.TextField(searchFilter);
|
||||
if(!string.IsNullOrEmpty(searchFilter) && GUILayout.Button("✕", GUILayout.Width(20), GUILayout.Height(18))) {
|
||||
searchFilter = "";
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(4);
|
||||
|
||||
// Selection count
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label($"{selectedTags.Count} selected", EditorStyles.centeredGreyMiniLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
if(selectedTags.Count > 0 && GUILayout.Button("Clear All", EditorStyles.miniButton, GUILayout.Width(60)))
|
||||
selectedTags.Clear();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(2);
|
||||
DrawDivider();
|
||||
|
||||
// Tree
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
DrawTagTree();
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
DrawDivider();
|
||||
GUILayout.Space(4);
|
||||
|
||||
// Buttons
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(8);
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
|
||||
if(GUILayout.Button("Confirm", GUILayout.Height(26))) {
|
||||
onConfirm?.Invoke(selectedTags);
|
||||
Close();
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
if(GUILayout.Button("Cancel", GUILayout.Height(26)))
|
||||
Close();
|
||||
if(GUILayout.Button("Edit Tags", GUILayout.Height(26), GUILayout.Width(70)))
|
||||
JovianTagsEditorWindow.ShowWindow();
|
||||
GUILayout.Space(8);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(6);
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private void DrawTagTree() {
|
||||
if(allTags.Count == 0) {
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("No tags defined.", EditorStyles.centeredGreyMiniLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
var roots = BuildTree();
|
||||
|
||||
// Filter
|
||||
if(!string.IsNullOrEmpty(searchFilter)) {
|
||||
roots = FilterTree(roots, searchFilter);
|
||||
}
|
||||
|
||||
rowIndex = 0;
|
||||
foreach(var root in roots.OrderBy(r => r.Name, StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawPickerNode(root, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPickerNode(PickerNode node, int depth) {
|
||||
var hasChildren = node.Children.Count > 0;
|
||||
var isSelected = selectedTags.Contains(node.FullPath);
|
||||
|
||||
var bgColor = rowIndex % 2 != 0 ? RowOdd : Color.clear;
|
||||
var rect = EditorGUILayout.BeginHorizontal(GUILayout.Height(RowHeight));
|
||||
if(rect.Contains(Event.current.mousePosition)) bgColor = RowHover;
|
||||
if(depth == 0) bgColor = HeaderBg;
|
||||
if(bgColor.a > 0) EditorGUI.DrawRect(rect, bgColor);
|
||||
|
||||
// Indent + guide lines
|
||||
GUILayout.Space(6 + depth * DepthIndent);
|
||||
if(depth > 0 && Event.current.type == EventType.Repaint) {
|
||||
for(int d = 1; d <= depth; d++) {
|
||||
var lineX = 6 + d * DepthIndent - 10;
|
||||
EditorGUI.DrawRect(new Rect(lineX, rect.y, 1, rect.height), ChildIndentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Checkbox
|
||||
var newSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Width(16), GUILayout.Height(RowHeight));
|
||||
if(newSelected != isSelected) {
|
||||
if(newSelected) selectedTags.Add(node.FullPath);
|
||||
else selectedTags.Remove(node.FullPath);
|
||||
}
|
||||
|
||||
// Foldout for parents
|
||||
if(hasChildren) {
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
foldoutState[node.FullPath] = EditorGUILayout.Foldout(
|
||||
foldoutState[node.FullPath], "", true, EditorStyles.foldout);
|
||||
GUILayout.Space(-4);
|
||||
}
|
||||
else {
|
||||
GUILayout.Space(14);
|
||||
}
|
||||
|
||||
// Label
|
||||
var style = depth == 0 ? HeaderStyle : TagStyle;
|
||||
GUILayout.Label(node.Name, style, GUILayout.ExpandWidth(true));
|
||||
|
||||
// Select all children button
|
||||
if(hasChildren) {
|
||||
var allChildTags = CollectAllPaths(node);
|
||||
var allChildrenSelected = allChildTags.All(t => selectedTags.Contains(t));
|
||||
var prevColor = GUI.color;
|
||||
GUI.color = new Color(1, 1, 1, 0.5f);
|
||||
if(GUILayout.Button(allChildrenSelected ? "−all" : "+all", EditorStyles.miniButton,
|
||||
GUILayout.Width(32), GUILayout.Height(16))) {
|
||||
foreach(var t in allChildTags) {
|
||||
if(allChildrenSelected) selectedTags.Remove(t);
|
||||
else selectedTags.Add(t);
|
||||
}
|
||||
}
|
||||
GUI.color = prevColor;
|
||||
}
|
||||
|
||||
GUILayout.Space(4);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
rowIndex++;
|
||||
|
||||
// Children
|
||||
if(!hasChildren) return;
|
||||
foldoutState.TryAdd(node.FullPath, true);
|
||||
if(!foldoutState[node.FullPath]) return;
|
||||
|
||||
foreach(var child in node.Children.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)) {
|
||||
DrawPickerNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Tree helpers =====
|
||||
|
||||
private List<PickerNode> BuildTree() {
|
||||
var rootNodes = new Dictionary<string, PickerNode>();
|
||||
var allNodes = new Dictionary<string, PickerNode>();
|
||||
|
||||
foreach(var tag in allTags) {
|
||||
var parts = tag.Split(JovianTagsHandler.tagDelimiter);
|
||||
PickerNode parent = null;
|
||||
var accumulated = "";
|
||||
|
||||
for(int i = 0; i < parts.Length; i++) {
|
||||
accumulated = i == 0 ? parts[i] : accumulated + "." + parts[i];
|
||||
if(!allNodes.TryGetValue(accumulated, out var node)) {
|
||||
node = new PickerNode { Name = parts[i], FullPath = accumulated };
|
||||
allNodes[accumulated] = node;
|
||||
if(parent != null) parent.Children.Add(node);
|
||||
else rootNodes[accumulated] = node;
|
||||
}
|
||||
parent = node;
|
||||
}
|
||||
}
|
||||
return rootNodes.Values.ToList();
|
||||
}
|
||||
|
||||
private List<PickerNode> FilterTree(List<PickerNode> roots, string filter) {
|
||||
var result = new List<PickerNode>();
|
||||
foreach(var root in roots) {
|
||||
var filtered = FilterNode(root, filter);
|
||||
if(filtered != null) result.Add(filtered);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private PickerNode FilterNode(PickerNode node, string filter) {
|
||||
var matchesSelf = node.FullPath.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
var filteredChildren = new List<PickerNode>();
|
||||
foreach(var child in node.Children) {
|
||||
var fc = FilterNode(child, filter);
|
||||
if(fc != null) filteredChildren.Add(fc);
|
||||
}
|
||||
if(!matchesSelf && filteredChildren.Count == 0) return null;
|
||||
return new PickerNode {
|
||||
Name = node.Name,
|
||||
FullPath = node.FullPath,
|
||||
Children = matchesSelf ? node.Children : filteredChildren
|
||||
};
|
||||
}
|
||||
|
||||
private List<string> CollectAllPaths(PickerNode node) {
|
||||
var result = new List<string>();
|
||||
CollectPathsRecursive(node, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CollectPathsRecursive(PickerNode node, List<string> result) {
|
||||
result.Add(node.FullPath);
|
||||
foreach(var child in node.Children) CollectPathsRecursive(child, result);
|
||||
}
|
||||
|
||||
private static void DrawDivider() {
|
||||
var rect = EditorGUILayout.GetControlRect(false, 1);
|
||||
EditorGUI.DrawRect(rect, new Color(1f, 1f, 1f, 0.12f));
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsPickerPopup.cs.meta
Normal file
2
Editor/JovianTagsPickerPopup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 819384bf19a494342ba5e5f4319e34fd
|
||||
179
Editor/JovianTagsSelectionDrawer.cs
Normal file
179
Editor/JovianTagsSelectionDrawer.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
[CustomPropertyDrawer(typeof(JovianTagsGroup), true)]
|
||||
public class JovianTagsSelectionDrawer : PropertyDrawer {
|
||||
|
||||
private const float RowHeight = 20f;
|
||||
private const float Spacing = 2f;
|
||||
private const float ButtonHeight = 20f;
|
||||
private const float FoldoutHeight = 20f;
|
||||
|
||||
// Per-property foldout state keyed by property path
|
||||
private static readonly Dictionary<string, bool> FoldoutStates = new();
|
||||
|
||||
private static bool GetFoldout(SerializedProperty property) {
|
||||
FoldoutStates.TryAdd(property.propertyPath, true);
|
||||
return FoldoutStates[property.propertyPath];
|
||||
}
|
||||
|
||||
private static void SetFoldout(SerializedProperty property, bool value) {
|
||||
FoldoutStates[property.propertyPath] = value;
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
var tagsProp = property.FindPropertyRelative("tags");
|
||||
var count = tagsProp != null ? tagsProp.arraySize : 0;
|
||||
var expanded = GetFoldout(property);
|
||||
|
||||
// Foldout row (always)
|
||||
var height = FoldoutHeight + Spacing;
|
||||
|
||||
if(!expanded) return height;
|
||||
|
||||
// Tag rows
|
||||
height += count * (RowHeight + Spacing);
|
||||
// Bottom button (always)
|
||||
height += ButtonHeight + Spacing;
|
||||
height += 2; // padding
|
||||
return height;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
var tagsProp = property.FindPropertyRelative("tags");
|
||||
if(tagsProp == null) {
|
||||
EditorGUI.LabelField(position, label, new GUIContent("Error: 'tags' field not found"));
|
||||
EditorGUI.EndProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-remove stale tags that no longer exist in any settings
|
||||
CleanStaleTags(tagsProp, property);
|
||||
|
||||
var count = tagsProp.arraySize;
|
||||
var expanded = GetFoldout(property);
|
||||
|
||||
// Foldout row with tag count
|
||||
var y = position.y;
|
||||
var foldoutRect = new Rect(position.x, y, position.width, FoldoutHeight);
|
||||
var foldoutLabel = count > 0 ? $"{label.text} ({count})" : label.text;
|
||||
var newExpanded = EditorGUI.Foldout(foldoutRect, expanded, foldoutLabel, true);
|
||||
if(newExpanded != expanded) SetFoldout(property, newExpanded);
|
||||
y += FoldoutHeight + Spacing;
|
||||
|
||||
if(!newExpanded) {
|
||||
EditorGUI.EndProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
var indent = EditorGUI.indentLevel * 15f + 14f;
|
||||
var contentX = position.x + indent;
|
||||
var contentWidth = position.width - indent;
|
||||
|
||||
// Tag rows
|
||||
for(int i = 0; i < tagsProp.arraySize; i++) {
|
||||
var elementProp = tagsProp.GetArrayElementAtIndex(i);
|
||||
var tagValue = elementProp.stringValue;
|
||||
|
||||
var rowRect = new Rect(contentX, y, contentWidth, RowHeight);
|
||||
var tagRect = new Rect(rowRect.x + 4, rowRect.y, rowRect.width - 30, RowHeight);
|
||||
var removeBtnRect = new Rect(rowRect.xMax - 22, rowRect.y + 1, 20, RowHeight - 2);
|
||||
|
||||
// Row background
|
||||
var bgColor = i % 2 == 0 ? new Color(0, 0, 0, 0.08f) : Color.clear;
|
||||
EditorGUI.DrawRect(rowRect, bgColor);
|
||||
|
||||
// Tag label
|
||||
var displayName = string.IsNullOrEmpty(tagValue) ? "(empty)" : tagValue;
|
||||
EditorGUI.LabelField(tagRect, displayName, EditorStyles.label);
|
||||
|
||||
// Remove button
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.8f, 0.2f, 0.2f);
|
||||
if(GUI.Button(removeBtnRect, "✕", EditorStyles.miniButton)) {
|
||||
tagsProp.DeleteArrayElementAtIndex(i);
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
EditorGUI.EndProperty();
|
||||
return;
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
|
||||
y += RowHeight + Spacing;
|
||||
}
|
||||
|
||||
// Bottom Select Tags button (always)
|
||||
DrawAddButton(contentX, y, contentWidth, tagsProp, property);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
private float DrawAddButton(float x, float y, float width, SerializedProperty tagsProp, SerializedProperty property) {
|
||||
var gap = 4f;
|
||||
var editBtnWidth = 70f;
|
||||
var addBtnRect = new Rect(x, y, width - editBtnWidth - gap, ButtonHeight);
|
||||
var editBtnRect = new Rect(x + width - editBtnWidth, y, editBtnWidth, ButtonHeight);
|
||||
|
||||
// Edit Tags button — opens Tag Editor window
|
||||
if(GUI.Button(editBtnRect, "Edit Tags")) {
|
||||
JovianTagsEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.3f, 0.7f, 0.3f);
|
||||
if(GUI.Button(addBtnRect, "+ Select Tags")) {
|
||||
var currentTags = new HashSet<string>();
|
||||
for(int i = 0; i < tagsProp.arraySize; i++) {
|
||||
var val = tagsProp.GetArrayElementAtIndex(i).stringValue;
|
||||
if(!string.IsNullOrEmpty(val)) currentTags.Add(val);
|
||||
}
|
||||
|
||||
JovianTagsPickerPopup.Show(currentTags, selectedTags => {
|
||||
tagsProp.ClearArray();
|
||||
foreach(var tag in selectedTags.OrderBy(t => t)) {
|
||||
tagsProp.InsertArrayElementAtIndex(tagsProp.arraySize);
|
||||
tagsProp.GetArrayElementAtIndex(tagsProp.arraySize - 1).stringValue = tag;
|
||||
}
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
foreach(var tag in selectedTags)
|
||||
JovianTagsEditorHistory.Instance.AddRecentTag(tag);
|
||||
});
|
||||
}
|
||||
GUI.backgroundColor = prevBg;
|
||||
return y + ButtonHeight + Spacing;
|
||||
}
|
||||
private static HashSet<string> cachedValidTags;
|
||||
private static double lastCacheTime;
|
||||
|
||||
private static void CleanStaleTags(SerializedProperty tagsProp, SerializedProperty property) {
|
||||
// Refresh valid tags cache every 2 seconds to avoid scanning settings every frame
|
||||
if(cachedValidTags == null || EditorApplication.timeSinceStartup - lastCacheTime > 2.0) {
|
||||
cachedValidTags = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
var settings = JovianTagsEditorUtility.GetSettings();
|
||||
foreach(var s in settings)
|
||||
foreach(var t in s.gameTags)
|
||||
if(!string.IsNullOrEmpty(t.tag))
|
||||
cachedValidTags.Add(t.tag);
|
||||
lastCacheTime = EditorApplication.timeSinceStartup;
|
||||
}
|
||||
|
||||
bool removed = false;
|
||||
for(int i = tagsProp.arraySize - 1; i >= 0; i--) {
|
||||
var val = tagsProp.GetArrayElementAtIndex(i).stringValue;
|
||||
if(!string.IsNullOrEmpty(val) && !cachedValidTags.Contains(val)) {
|
||||
tagsProp.DeleteArrayElementAtIndex(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(removed) {
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsSelectionDrawer.cs.meta
Normal file
2
Editor/JovianTagsSelectionDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6c5df6e64308804287ab931e990df7b
|
||||
102
Editor/JovianTagsSettingsInspector.cs
Normal file
102
Editor/JovianTagsSettingsInspector.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.TagSystem.Editor {
|
||||
[CustomEditor(typeof(JovianTagsSettings))]
|
||||
public class JovianTagsSettingsInspector : UnityEditor.Editor {
|
||||
public override void OnInspectorGUI() {
|
||||
serializedObject.Update();
|
||||
DrawDefaultInspector();
|
||||
|
||||
var settings = (JovianTagsSettings)target;
|
||||
|
||||
// Validate and show errors
|
||||
var errors = ValidateTags(settings);
|
||||
if(errors.Count > 0) {
|
||||
GUILayout.Space(4);
|
||||
foreach(var error in errors) {
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
var prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
|
||||
if(GUILayout.Button("Save & Generate Tags", GUILayout.Height(28))) {
|
||||
CleanAndSave(settings);
|
||||
JovianTagsGenerator.Regenerate();
|
||||
}
|
||||
|
||||
GUI.backgroundColor = prevBg;
|
||||
|
||||
if(GUILayout.Button("Open Tag Editor", GUILayout.Height(28))) {
|
||||
JovianTagsEditorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private static List<string> ValidateTags(JovianTagsSettings settings) {
|
||||
var errors = new List<string>();
|
||||
var seen = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
|
||||
foreach(var t in settings.gameTags) {
|
||||
if(string.IsNullOrWhiteSpace(t.tag)) {
|
||||
errors.Add("Empty tag name found.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentError = JovianTagsGenerator.ValidateTagName(t.tag);
|
||||
if(segmentError != null) {
|
||||
errors.Add($"'{t.tag}': {segmentError}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!seen.Add(t.tag)) {
|
||||
errors.Add($"Duplicate tag '{t.tag}' — will be removed on Save & Generate.");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void CleanAndSave(JovianTagsSettings settings) {
|
||||
var seen = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
var cleaned = new List<RegisteredTag>();
|
||||
var removed = 0;
|
||||
|
||||
foreach(var t in settings.gameTags) {
|
||||
if(string.IsNullOrWhiteSpace(t.tag)) {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(JovianTagsGenerator.ValidateTagName(t.tag) != null) {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!seen.Add(t.tag)) {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
cleaned.Add(t);
|
||||
}
|
||||
|
||||
if(removed > 0) {
|
||||
Undo.RecordObject(settings, "Clean Game Tags");
|
||||
settings.gameTags = cleaned.ToArray();
|
||||
Debug.Log($"[GameTagSettings] Cleaned {removed} invalid/duplicate tag(s).");
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(settings);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/JovianTagsSettingsInspector.cs.meta
Normal file
2
Editor/JovianTagsSettingsInspector.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa5b2c3fc4ef68498c7852291a4e8a0
|
||||
Reference in New Issue
Block a user