1518 lines
62 KiB
C#
1518 lines
62 KiB
C#
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEditor.IMGUI.Controls;
|
||
using UnityEngine;
|
||
using WordsToolkit.Scripts.NLP;
|
||
|
||
using System;
|
||
using Object = UnityEngine.Object;
|
||
using WordsToolkit.Scripts.Levels.Editor.EditorWindows;
|
||
|
||
namespace WordsToolkit.Scripts.Levels.Editor
|
||
{
|
||
|
||
public class LevelHierarchyTreeView : TreeView
|
||
{
|
||
private IModelController ModelController => EditorScope.Resolve<IModelController>();
|
||
|
||
// Event for selection changes
|
||
public global::System.Action<LevelHierarchyItem> OnSelectionChanged;
|
||
|
||
// Event for delete request
|
||
public global::System.Action<LevelHierarchyItem> OnDeleteItem;
|
||
|
||
// Event for creating subgroups
|
||
public global::System.Action<LevelHierarchyItem> OnCreateSubgroup;
|
||
|
||
// Event for creating levels
|
||
public global::System.Action<LevelHierarchyItem> OnCreateLevel;
|
||
// Event fired when hierarchy changes (e.g., drag & drop)
|
||
public global::System.Action OnHierarchyChanged;
|
||
|
||
// Dictionary to map tree IDs to hierarchy items
|
||
private Dictionary<int, LevelHierarchyItem> m_IdToItem = new Dictionary<int, LevelHierarchyItem>();
|
||
|
||
// Root items for groups directly (removed the collection level)
|
||
private List<LevelHierarchyItem> m_RootItems = new List<LevelHierarchyItem>();
|
||
|
||
// Column headers
|
||
private enum ColumnId
|
||
{
|
||
Name
|
||
}
|
||
|
||
public LevelHierarchyTreeView(TreeViewState state)
|
||
: base(state)
|
||
{
|
||
// Single-click selection
|
||
showBorder = true;
|
||
showAlternatingRowBackgrounds = true;
|
||
|
||
// Configure columns
|
||
Reload();
|
||
}
|
||
|
||
public LevelHierarchyTreeView(TreeViewState state, MultiColumnHeader multiColumnHeader)
|
||
: base(state, multiColumnHeader)
|
||
{
|
||
// Single-click selection
|
||
showBorder = true;
|
||
showAlternatingRowBackgrounds = true;
|
||
|
||
// Configure columns
|
||
// multiColumnHeader.sortingChanged += OnSortingChanged;
|
||
|
||
// Show Name column only
|
||
multiColumnHeader.state.visibleColumns = new int[] { 0 };
|
||
|
||
Reload();
|
||
}
|
||
|
||
public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState()
|
||
{
|
||
var columns = new[]
|
||
{
|
||
new MultiColumnHeaderState.Column
|
||
{
|
||
headerContent = new GUIContent("Name"),
|
||
headerTextAlignment = TextAlignment.Left,
|
||
sortedAscending = true,
|
||
sortingArrowAlignment = TextAlignment.Right,
|
||
width = 100,
|
||
minWidth = 50,
|
||
autoResize = true,
|
||
allowToggleVisibility = false
|
||
}
|
||
};
|
||
|
||
return new MultiColumnHeaderState(columns);
|
||
}
|
||
|
||
protected override TreeViewItem BuildRoot()
|
||
{
|
||
// Clear existing data
|
||
m_IdToItem.Clear();
|
||
m_RootItems.Clear();
|
||
|
||
// Create root item (not visible in the tree)
|
||
var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
|
||
|
||
// Find all level groups and build the hierarchy
|
||
DiscoverLevelGroups();
|
||
|
||
// Only add top-level items to the root
|
||
var allItems = new List<TreeViewItem>();
|
||
|
||
// Add root items directly to the root as children
|
||
if (m_RootItems.Count > 0)
|
||
{
|
||
root.children = new List<TreeViewItem>(m_RootItems);
|
||
}
|
||
else
|
||
{
|
||
root.children = new List<TreeViewItem>(); // Empty list to avoid null ref
|
||
Debug.Log("BuildRoot: No root items found");
|
||
}
|
||
|
||
return root;
|
||
}
|
||
|
||
private void DiscoverLevelGroups()
|
||
{
|
||
// Clear existing data to prevent duplicates
|
||
m_IdToItem.Clear();
|
||
m_RootItems.Clear();
|
||
|
||
// Track processed levels to avoid duplicates
|
||
var processedLevelIds = new HashSet<int>();
|
||
int itemId = 1; // Start IDs from 1 (0 is used for invisible root)
|
||
|
||
// First: Find all LevelGroup assets and create their hierarchy items
|
||
string[] groupGuids = AssetDatabase.FindAssets("t:LevelGroup");
|
||
|
||
Dictionary<LevelGroup, LevelHierarchyItem> groupToItem = new Dictionary<LevelGroup, LevelHierarchyItem>();
|
||
|
||
// Create all group items
|
||
foreach (string groupGuid in groupGuids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(groupGuid);
|
||
LevelGroup group = AssetDatabase.LoadAssetAtPath<LevelGroup>(path);
|
||
|
||
if (group != null)
|
||
{
|
||
var groupItem = new LevelHierarchyItem
|
||
{
|
||
id = itemId++,
|
||
displayName = string.IsNullOrEmpty(group.groupName) ?
|
||
Path.GetFileNameWithoutExtension(path) : group.groupName,
|
||
depth = 0,
|
||
type = LevelHierarchyItem.ItemType.Group,
|
||
groupAsset = group,
|
||
assetPath = path,
|
||
icon = EditorGUIUtility.FindTexture("ScriptableObject Icon"),
|
||
children = new List<TreeViewItem>()
|
||
};
|
||
|
||
m_IdToItem[groupItem.id] = groupItem;
|
||
groupToItem[group] = groupItem;
|
||
}
|
||
}
|
||
|
||
// Setup group hierarchy
|
||
foreach (var pair in groupToItem)
|
||
{
|
||
LevelGroup group = pair.Key;
|
||
LevelHierarchyItem item = pair.Value;
|
||
|
||
if (group.parentGroup != null && groupToItem.TryGetValue(group.parentGroup, out LevelHierarchyItem parentItem))
|
||
{
|
||
if (parentItem.children == null)
|
||
parentItem.children = new List<TreeViewItem>();
|
||
parentItem.children.Add(item);
|
||
}
|
||
else
|
||
{
|
||
m_RootItems.Add(item);
|
||
}
|
||
}
|
||
|
||
// Set proper depth for groups
|
||
foreach (var rootItem in m_RootItems)
|
||
{
|
||
rootItem.depth = 0;
|
||
SetChildrenDepth(rootItem, 1);
|
||
}
|
||
|
||
// Add levels to their groups
|
||
foreach (var pair in groupToItem)
|
||
{
|
||
LevelGroup group = pair.Key;
|
||
LevelHierarchyItem groupItem = pair.Value;
|
||
|
||
if (group.levels != null && group.levels.Count > 0)
|
||
{
|
||
var validLevels = group.levels
|
||
.Where(l => l != null)
|
||
.OrderBy(l => l.number)
|
||
.ToList();
|
||
|
||
foreach (var level in validLevels)
|
||
{
|
||
int levelId = level.GetInstanceID();
|
||
if (processedLevelIds.Contains(levelId))
|
||
continue;
|
||
|
||
processedLevelIds.Add(levelId);
|
||
string levelPath = AssetDatabase.GetAssetPath(level);
|
||
|
||
var levelItem = new LevelHierarchyItem
|
||
{
|
||
id = itemId++,
|
||
displayName = $"Level {level.number}",
|
||
depth = groupItem.depth + 1,
|
||
type = LevelHierarchyItem.ItemType.Level,
|
||
levelAsset = level,
|
||
assetPath = levelPath,
|
||
icon = EditorGUIUtility.FindTexture("ScriptableObject Icon")
|
||
};
|
||
|
||
m_IdToItem[levelItem.id] = levelItem;
|
||
if (groupItem.children == null)
|
||
groupItem.children = new List<TreeViewItem>();
|
||
|
||
groupItem.children.Add(levelItem);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"No levels found in group: {group.groupName}");
|
||
}
|
||
}
|
||
|
||
// Update group display names to include level count
|
||
foreach (var pair in groupToItem)
|
||
{
|
||
LevelGroup group = pair.Key;
|
||
LevelHierarchyItem groupItem = pair.Value;
|
||
|
||
int levelCount = groupItem.children?.Count ?? 0;
|
||
string baseName = string.IsNullOrEmpty(group.groupName) ?
|
||
Path.GetFileNameWithoutExtension(groupItem.assetPath) : group.groupName;
|
||
|
||
groupItem.displayName = $"{baseName} ({levelCount})";
|
||
}
|
||
|
||
// Find and add standalone levels (levels not in any group)
|
||
string[] levelGuids = AssetDatabase.FindAssets("t:Level");
|
||
foreach (string levelGuid in levelGuids)
|
||
{
|
||
string levelPath = AssetDatabase.GUIDToAssetPath(levelGuid);
|
||
Level level = AssetDatabase.LoadAssetAtPath<Level>(levelPath);
|
||
|
||
if (level != null)
|
||
{
|
||
int levelId = level.GetInstanceID();
|
||
if (!processedLevelIds.Contains(levelId))
|
||
{
|
||
processedLevelIds.Add(levelId);
|
||
|
||
var levelItem = new LevelHierarchyItem
|
||
{
|
||
id = itemId++,
|
||
displayName = $"Level {level.number}",
|
||
depth = 0,
|
||
type = LevelHierarchyItem.ItemType.Level,
|
||
levelAsset = level,
|
||
assetPath = levelPath,
|
||
icon = EditorGUIUtility.FindTexture("ScriptableObject Icon")
|
||
};
|
||
|
||
m_IdToItem[levelItem.id] = levelItem;
|
||
m_RootItems.Add(levelItem);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort root items - groups first, then levels by number
|
||
m_RootItems = m_RootItems
|
||
.OrderBy(item => item is LevelHierarchyItem levelItem ? 0 : 1)
|
||
.ThenBy(item => item is LevelHierarchyItem levelItem && levelItem.levelAsset != null ?
|
||
levelItem.levelAsset.number : 0)
|
||
.ToList();
|
||
|
||
}
|
||
|
||
// Helper method to recursively set depth of child items
|
||
private void SetChildrenDepth(TreeViewItem item, int depth)
|
||
{
|
||
if (item.children == null)
|
||
return;
|
||
|
||
foreach (var child in item.children)
|
||
{
|
||
child.depth = depth;
|
||
SetChildrenDepth(child, depth + 1);
|
||
}
|
||
}
|
||
|
||
protected override void RowGUI(RowGUIArgs args)
|
||
{
|
||
// Check if we're in a drag operation to disable highlighting
|
||
bool isDragging = DragAndDrop.GetGenericData("LevelHierarchyItems") != null;
|
||
|
||
if (isDragging)
|
||
{
|
||
// Temporarily disable selection highlighting during drag
|
||
args.selected = false;
|
||
args.focused = false;
|
||
}
|
||
|
||
if (args.item is LevelHierarchyItem item)
|
||
{
|
||
// Calculate button rects
|
||
float buttonWidth = 20;
|
||
float buttonSpacing = 2;
|
||
float rightMargin = 4;
|
||
|
||
var adjustedRect = args.rowRect;
|
||
|
||
// Only show buttons for groups
|
||
if (item.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
// Place buttons at the right side of the row
|
||
var rowRect = args.rowRect;
|
||
var minusRect = new Rect(rowRect.xMax - buttonWidth - rightMargin, rowRect.y + 1, buttonWidth, rowRect.height - 2);
|
||
var plusRect = new Rect(minusRect.x - buttonWidth - buttonSpacing, rowRect.y + 1, buttonWidth, rowRect.height - 2);
|
||
|
||
// Adjust the content rect to not overlap with buttons
|
||
adjustedRect.xMax = plusRect.x - buttonSpacing;
|
||
|
||
// Draw + button for creating subgroups
|
||
var plusStyle = new GUIStyle(EditorStyles.miniButton) { fontSize = 10, alignment = TextAnchor.MiddleCenter };
|
||
if (UnityEngine.GUI.Button(minusRect, "+", plusStyle))
|
||
{
|
||
OnCreateLevel?.Invoke(item);
|
||
Event.current.Use();
|
||
}
|
||
|
||
// // Draw - button for deleting groups
|
||
// var minusStyle = new GUIStyle(EditorStyles.miniButton) { fontSize = 10, alignment = TextAnchor.MiddleCenter };
|
||
// if (UnityEngine.GUI.Button(minusRect, "−", minusStyle)) // Using minus sign (U+2212) for better appearance
|
||
// {
|
||
// bool confirm = EditorUtility.DisplayDialog(
|
||
// "Delete Group",
|
||
// "Are you sure you want to delete this group?",
|
||
// "Delete",
|
||
// "Cancel"
|
||
// );
|
||
//
|
||
// if (confirm)
|
||
// {
|
||
// OnDeleteItem?.Invoke(item);
|
||
// Event.current.Use();
|
||
// }
|
||
// }
|
||
}
|
||
|
||
args.rowRect = adjustedRect;
|
||
|
||
// Draw columns with adjusted rect
|
||
for (int i = 0; i < args.GetNumVisibleColumns(); ++i)
|
||
{
|
||
CellGUI(args.GetCellRect(i), item, (ColumnId)args.GetColumn(i), ref args);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
base.RowGUI(args);
|
||
}
|
||
}
|
||
|
||
private void CellGUI(Rect cellRect, LevelHierarchyItem item, ColumnId column, ref RowGUIArgs args)
|
||
{
|
||
// Only have Name column which shows the tree structure
|
||
base.RowGUI(args);
|
||
}
|
||
|
||
protected override void SelectionChanged(IList<int> selectedIds)
|
||
{
|
||
// Don't change selection during drag operations to prevent highlighting
|
||
if (DragAndDrop.GetGenericData("LevelHierarchyItems") != null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (selectedIds != null && selectedIds.Count > 0 && m_IdToItem.TryGetValue(selectedIds[0], out LevelHierarchyItem item))
|
||
{
|
||
OnSelectionChanged?.Invoke(item);
|
||
|
||
// Automatically open CrosswordGridWindow when a level is selected
|
||
if (item.type == LevelHierarchyItem.ItemType.Level && item.levelAsset != null)
|
||
{
|
||
string languageCode = LevelEditorUtility.GetLanguageCodeForLevel(item.levelAsset);
|
||
if (!string.IsNullOrEmpty(languageCode))
|
||
{
|
||
WordsToolkit.Scripts.Levels.Editor.CrosswordGridWindow.ShowWindow(item.levelAsset, languageCode);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
OnSelectionChanged?.Invoke(null);
|
||
}
|
||
}
|
||
|
||
protected override void DoubleClickedItem(int id)
|
||
{
|
||
if (m_IdToItem.TryGetValue(id, out LevelHierarchyItem item))
|
||
{
|
||
// When double-clicking an item, ping it in the Project window
|
||
if (item.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
EditorGUIUtility.PingObject(item.groupAsset);
|
||
}
|
||
else if (item.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
EditorGUIUtility.PingObject(item.levelAsset);
|
||
}
|
||
}
|
||
}
|
||
|
||
protected override bool CanRename(TreeViewItem item)
|
||
{
|
||
return false; // Disable renaming in the tree view
|
||
}
|
||
|
||
protected override void RenameEnded(RenameEndedArgs args)
|
||
{
|
||
// Let base class handle UI aspects of renaming
|
||
base.RenameEnded(args);
|
||
|
||
// If renaming was cancelled or name didn't change, do nothing
|
||
if (args.acceptedRename == false || string.IsNullOrEmpty(args.newName))
|
||
return;
|
||
|
||
// Find the item being renamed
|
||
var item = FindItem(args.itemID, rootItem) as LevelHierarchyItem;
|
||
if (item == null)
|
||
return;
|
||
|
||
// Handle renaming based on item type
|
||
switch (item.type)
|
||
{
|
||
case LevelHierarchyItem.ItemType.Group:
|
||
if (item.groupAsset != null)
|
||
{
|
||
// Update group name
|
||
Undo.RecordObject(item.groupAsset, "Rename Group");
|
||
item.groupAsset.groupName = args.newName;
|
||
item.displayName = args.newName;
|
||
EditorUtility.SetDirty(item.groupAsset);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
break;
|
||
|
||
case LevelHierarchyItem.ItemType.Level:
|
||
if (item.levelAsset != null)
|
||
{
|
||
// Update level display name if there's a custom name property
|
||
Undo.RecordObject(item.levelAsset, "Rename Level");
|
||
// If your Level class has a name property:
|
||
// item.levelAsset.levelName = args.newName;
|
||
item.displayName = args.newName;
|
||
EditorUtility.SetDirty(item.levelAsset);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
break;
|
||
|
||
case LevelHierarchyItem.ItemType.Collection:
|
||
// Renaming collections would involve renaming folders on disk,
|
||
// which is more complex. For now, we'll just update the display name.
|
||
item.displayName = args.newName;
|
||
break;
|
||
}
|
||
|
||
// Refresh the tree to show updated names
|
||
Reload();
|
||
|
||
// If you have events for rename operations:
|
||
// if (OnItemRenamed != null)
|
||
// OnItemRenamed(item, args.newName);
|
||
}
|
||
|
||
protected override void KeyEvent()
|
||
{
|
||
base.KeyEvent();
|
||
|
||
// Handle escape key press to cancel drag operations
|
||
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
||
{
|
||
// Check if there's an active drag operation
|
||
if (DragAndDrop.GetGenericData("LevelHierarchyItems") != null)
|
||
{
|
||
// Cancel the drag operation
|
||
DragAndDrop.PrepareStartDrag();
|
||
DragAndDrop.SetGenericData("LevelHierarchyItems", null);
|
||
Event.current.Use();
|
||
Repaint();
|
||
}
|
||
}
|
||
|
||
// Handle delete key press
|
||
if (Event.current.type == EventType.KeyDown &&
|
||
(Event.current.keyCode == KeyCode.Delete || Event.current.keyCode == KeyCode.Backspace))
|
||
{
|
||
var selection = GetSelection();
|
||
if (selection.Count > 0)
|
||
{
|
||
var selectedItems = selection
|
||
.Select(id => m_IdToItem.TryGetValue(id, out LevelHierarchyItem item) ? item : null)
|
||
.Where(item => item != null)
|
||
.ToList();
|
||
|
||
if (selectedItems.Count > 0)
|
||
{
|
||
string itemTypes = selectedItems.Count == 1
|
||
? selectedItems[0].type.ToString()
|
||
: "items";
|
||
|
||
bool confirm = EditorUtility.DisplayDialog(
|
||
$"Delete {itemTypes}",
|
||
$"Are you sure you want to delete {selectedItems.Count} selected {itemTypes}?",
|
||
"Delete",
|
||
"Cancel"
|
||
);
|
||
|
||
if (confirm)
|
||
{
|
||
foreach (var item in selectedItems)
|
||
{
|
||
OnDeleteItem?.Invoke(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
Event.current.Use();
|
||
}
|
||
}
|
||
}
|
||
|
||
public void SelectAsset(Object asset)
|
||
{
|
||
if (asset == null) return;
|
||
|
||
// Find the item representing this asset
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
var item = entry.Value;
|
||
if ((item.type == LevelHierarchyItem.ItemType.Group && item.groupAsset == asset) ||
|
||
(item.type == LevelHierarchyItem.ItemType.Level && item.levelAsset == asset))
|
||
{
|
||
// Select this item
|
||
SetSelection(new List<int> { item.id }, TreeViewSelectionOptions.RevealAndFrame);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
public new void ExpandAll()
|
||
{
|
||
// Expand all items
|
||
SetExpanded(GetRows().Select(r => r.id).ToList());
|
||
}
|
||
|
||
public new void CollapseAll()
|
||
{
|
||
// With no collections level, we can just collapse everything
|
||
SetExpanded(new List<int>());
|
||
}
|
||
|
||
public List<LevelHierarchyItem> GetAllItems()
|
||
{
|
||
return m_IdToItem.Values.ToList();
|
||
}
|
||
|
||
public LevelHierarchyItem CreateSubGroup(LevelHierarchyItem parentItem)
|
||
{
|
||
// Create a new level group asset
|
||
LevelGroup newGroup = ScriptableObject.CreateInstance<LevelGroup>();
|
||
|
||
// Set default name
|
||
newGroup.groupName = "New Group";
|
||
|
||
// Set parent to be the same as the current group's parent
|
||
if (parentItem != null && parentItem.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
newGroup.parentGroup = parentItem.groupAsset.parentGroup;
|
||
}
|
||
|
||
// Initialize localized texts with all languages from configuration
|
||
// with 'en' as the default language
|
||
string[] configGuids = AssetDatabase.FindAssets("t:LanguageConfiguration");
|
||
if (configGuids.Length > 0)
|
||
{
|
||
string configPath = AssetDatabase.GUIDToAssetPath(configGuids[0]);
|
||
var config = AssetDatabase.LoadAssetAtPath<LanguageConfiguration>(configPath);
|
||
if (config != null && config.languages != null && config.languages.Count > 0)
|
||
{
|
||
foreach (var langInfo in config.languages)
|
||
{
|
||
// Add empty localized text entries for each language
|
||
newGroup.localizedTexts.Add(new LocalizedTextGroup
|
||
{
|
||
language = langInfo.code,
|
||
text = ""
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Determine save path - always save in Resources/Groups folder
|
||
string defaultFolder = "Assets/WordConnectGameToolkit/Resources/Groups";
|
||
if (!Directory.Exists(defaultFolder))
|
||
{
|
||
Directory.CreateDirectory(defaultFolder);
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
// Add a timestamp to ensure the new group appears at the bottom when sorted alphabetically
|
||
string timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
|
||
string path = AssetDatabase.GenerateUniqueAssetPath($"{defaultFolder}/ZZZ_NewGroup_{timestamp}.asset");
|
||
|
||
// Create the asset file
|
||
AssetDatabase.CreateAsset(newGroup, path);
|
||
AssetDatabase.SaveAssets();
|
||
|
||
// Reload to see the new group
|
||
Reload();
|
||
|
||
// Find the newly created item
|
||
LevelHierarchyItem newItem = null;
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
if (entry.Value.groupAsset == newGroup)
|
||
{
|
||
newItem = entry.Value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Select the new item after a frame to ensure the tree is updated
|
||
if (newItem != null)
|
||
{
|
||
EditorApplication.delayCall += () => {
|
||
// Set selection in tree
|
||
SetSelection(new List<int> { newItem.id }, TreeViewSelectionOptions.RevealAndFrame);
|
||
SetExpanded(newItem.id, true);
|
||
|
||
// Update selection state
|
||
OnSelectionChanged?.Invoke(newItem);
|
||
Selection.activeObject = newGroup;
|
||
EditorGUIUtility.PingObject(newGroup);
|
||
|
||
// Create a level in the new group without selecting it
|
||
var createdLevel = CreateLevel(newItem);
|
||
|
||
// Reset selection back to the group
|
||
EditorApplication.delayCall += () => {
|
||
SetSelection(new List<int> { newItem.id }, TreeViewSelectionOptions.RevealAndFrame);
|
||
OnSelectionChanged?.Invoke(newItem);
|
||
Selection.activeObject = newGroup;
|
||
|
||
// Wait one more frame and rename the asset to remove the ZZZ_ prefix
|
||
EditorApplication.delayCall += () => {
|
||
string newPath = path.Replace("ZZZ_", "");
|
||
AssetDatabase.MoveAsset(path, newPath);
|
||
AssetDatabase.SaveAssets();
|
||
};
|
||
};
|
||
};
|
||
}
|
||
|
||
return newItem;
|
||
}
|
||
|
||
public LevelHierarchyItem CreateLevel(LevelHierarchyItem parentItem)
|
||
{
|
||
// We need a parent group to create a level
|
||
if (parentItem == null || parentItem.type != LevelHierarchyItem.ItemType.Group || parentItem.groupAsset == null)
|
||
{
|
||
Debug.LogError("Cannot create level: Invalid parent group");
|
||
return null;
|
||
}
|
||
|
||
// Create a new level data asset
|
||
Level newLevel = ScriptableObject.CreateInstance<Level>();
|
||
newLevel.letters = EditorPrefs.GetInt("WordsToolkit_LettersAmount", 5);
|
||
|
||
// Set default properties - find highest level number across all levels and add 1
|
||
int highestNumber = LevelHierarchyUtility.GetHighestLevelNumber();
|
||
newLevel.number = highestNumber + 1;
|
||
|
||
// Ensure Resources/Levels folder exists
|
||
string levelsFolder = "Assets/WordConnectGameToolkit/Resources/Levels";
|
||
if (!Directory.Exists(levelsFolder))
|
||
{
|
||
// Create Resources folder if it doesn't exist
|
||
string resourcesFolder = "Assets/WordConnectGameToolkit/Resources";
|
||
if (!Directory.Exists(resourcesFolder))
|
||
{
|
||
Directory.CreateDirectory(resourcesFolder);
|
||
}
|
||
Directory.CreateDirectory(levelsFolder);
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
string path = $"{levelsFolder}/level_{newLevel.GetHashCode()}.asset";
|
||
path = AssetDatabase.GenerateUniqueAssetPath(path);
|
||
|
||
// Add enabled languages from configuration
|
||
string[] configGuids = AssetDatabase.FindAssets("t:LanguageConfiguration");
|
||
|
||
if (configGuids.Length > 0)
|
||
{
|
||
string configPath = AssetDatabase.GUIDToAssetPath(configGuids[0]);
|
||
var config = AssetDatabase.LoadAssetAtPath<LanguageConfiguration>(configPath);
|
||
|
||
if (config != null)
|
||
{
|
||
var enabledLanguages = config.GetEnabledLanguages();
|
||
|
||
foreach (var langInfo in enabledLanguages)
|
||
{
|
||
var langData = newLevel.AddLanguage(langInfo.code);
|
||
langData.wordsAmount = newLevel.words;
|
||
|
||
// Initialize empty arrays and crossword data
|
||
langData.words = new string[0];
|
||
var columns = EditorPrefs.GetInt("WordsToolkit_grid_x", CrosswordPreviewHandler.defaultGridColumns);
|
||
var rows = EditorPrefs.GetInt("WordsToolkit_grid_y", CrosswordPreviewHandler.defaultGridRows);
|
||
langData.crosswordData = new SerializableCrosswordData {
|
||
columns = columns,
|
||
rows = rows,
|
||
grid = new char[columns, rows],
|
||
minBounds = Vector2Int.zero,
|
||
maxBounds = new Vector2Int(CrosswordPreviewHandler.defaultGridColumns - 1, CrosswordPreviewHandler.defaultGridRows - 1),
|
||
placements = new List<SerializableWordPlacement>()
|
||
};
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("Failed to load language configuration asset");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// No configuration found, add English as default
|
||
var langData = newLevel.AddLanguage("en");
|
||
langData.wordsAmount = newLevel.words;
|
||
// Initialize empty arrays and crossword data
|
||
langData.words = new string[0];
|
||
langData.crosswordData = new SerializableCrosswordData {
|
||
columns = CrosswordPreviewHandler.defaultGridColumns,
|
||
rows = CrosswordPreviewHandler.defaultGridRows,
|
||
grid = new char[CrosswordPreviewHandler.defaultGridColumns, CrosswordPreviewHandler.defaultGridRows],
|
||
minBounds = Vector2Int.zero,
|
||
maxBounds = new Vector2Int(CrosswordPreviewHandler.defaultGridColumns - 1, CrosswordPreviewHandler.defaultGridRows - 1),
|
||
placements = new List<SerializableWordPlacement>()
|
||
};
|
||
}
|
||
|
||
// Inherit background and colorsTile from parent group
|
||
if (parentItem.groupAsset.background != null)
|
||
{
|
||
newLevel.background = parentItem.groupAsset.background;
|
||
}
|
||
|
||
if (parentItem.groupAsset.colorsTile != null)
|
||
{
|
||
newLevel.colorsTile = parentItem.groupAsset.colorsTile;
|
||
}
|
||
|
||
// Create the asset file
|
||
AssetDatabase.CreateAsset(newLevel, path);
|
||
|
||
// Add the level to the parent group
|
||
if (parentItem.groupAsset.levels == null)
|
||
{
|
||
parentItem.groupAsset.levels = new List<Level>();
|
||
}
|
||
|
||
parentItem.groupAsset.levels.Add(newLevel);
|
||
|
||
EditorUtility.SetDirty(parentItem.groupAsset);
|
||
AssetDatabase.SaveAssets();
|
||
|
||
// Reload to show the new level
|
||
Reload();
|
||
|
||
// Find the newly created item
|
||
LevelHierarchyItem newItem = null;
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
if (entry.Value.levelAsset == newLevel)
|
||
{
|
||
newItem = entry.Value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (newItem == null)
|
||
{
|
||
Debug.LogError("Failed to find newly created level item after reload");
|
||
}
|
||
else
|
||
{
|
||
EditorUtility.SetDirty(newLevel);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
LevelEditorUtility.RefreshHierarchy(this);
|
||
return newItem;
|
||
}
|
||
|
||
// Handle right-click context menu
|
||
protected override void ContextClickedItem(int id)
|
||
{
|
||
if (m_IdToItem.TryGetValue(id, out LevelHierarchyItem item))
|
||
{
|
||
GenericMenu menu = new GenericMenu();
|
||
|
||
// Context menu based on item type
|
||
if (item.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
menu.AddItem(new GUIContent("Create Group"), false, () => {
|
||
OnCreateSubgroup?.Invoke(item);
|
||
});
|
||
|
||
menu.AddItem(new GUIContent("Create Level"), false, () => {
|
||
OnCreateLevel?.Invoke(item);
|
||
});
|
||
|
||
menu.AddSeparator("");
|
||
|
||
menu.AddItem(new GUIContent("Delete Group"), false, () => {
|
||
OnDeleteItem?.Invoke(item);
|
||
});
|
||
}
|
||
else if (item.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
// Find parent group to allow creating sibling levels
|
||
LevelHierarchyItem parentItem = FindParentGroupForLevel(item);
|
||
if (parentItem != null)
|
||
{
|
||
menu.AddItem(new GUIContent("Create Level"), false, () => {
|
||
OnCreateLevel?.Invoke(parentItem);
|
||
});
|
||
}
|
||
|
||
menu.AddSeparator("");
|
||
|
||
menu.AddItem(new GUIContent("Delete Level"), false, () => {
|
||
OnDeleteItem?.Invoke(item);
|
||
});
|
||
}
|
||
|
||
menu.AddSeparator("");
|
||
|
||
menu.AddItem(new GUIContent("Locate in Project Window"), false, () => {
|
||
if (item.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
EditorGUIUtility.PingObject(item.groupAsset);
|
||
}
|
||
else if (item.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
EditorGUIUtility.PingObject(item.levelAsset);
|
||
}
|
||
});
|
||
|
||
menu.ShowAsContext();
|
||
Event.current.Use(); // This ensures the event is consumed and doesn't propagate
|
||
}
|
||
else
|
||
{
|
||
// Right-click on empty space or invalid item
|
||
ContextClicked();
|
||
}
|
||
}
|
||
|
||
// Handle right-click in empty area
|
||
protected override void ContextClicked()
|
||
{
|
||
GenericMenu menu = new GenericMenu();
|
||
menu.AddItem(new GUIContent("Create Group"), false, () => {
|
||
OnCreateSubgroup?.Invoke(null);
|
||
});
|
||
menu.ShowAsContext();
|
||
}
|
||
|
||
// Find parent group for a level item
|
||
private LevelHierarchyItem FindParentGroupForLevel(LevelHierarchyItem levelItem)
|
||
{
|
||
if (levelItem == null || levelItem.type != LevelHierarchyItem.ItemType.Level || levelItem.levelAsset == null)
|
||
return null;
|
||
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
if (entry.Value.type == LevelHierarchyItem.ItemType.Group &&
|
||
entry.Value.groupAsset != null &&
|
||
entry.Value.groupAsset.levels != null &&
|
||
entry.Value.groupAsset.levels.Contains(levelItem.levelAsset))
|
||
{
|
||
return entry.Value;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// Enable dragging operations
|
||
protected override bool CanStartDrag(CanStartDragArgs args)
|
||
{
|
||
// Make sure at least one item is selected
|
||
if (args.draggedItemIDs == null || args.draggedItemIDs.Count == 0)
|
||
return false;
|
||
|
||
// Select the dragged item
|
||
SetSelection(new[] { args.draggedItemIDs[0] }, TreeViewSelectionOptions.FireSelectionChanged);
|
||
|
||
// Allow dragging both group and level items
|
||
return true;
|
||
}
|
||
|
||
// Set up the drag operation
|
||
protected override void SetupDragAndDrop(SetupDragAndDropArgs args)
|
||
{
|
||
// Get selected items for dragging
|
||
List<LevelHierarchyItem> draggedItems = new List<LevelHierarchyItem>();
|
||
foreach (int id in args.draggedItemIDs)
|
||
{
|
||
if (m_IdToItem.TryGetValue(id, out LevelHierarchyItem item))
|
||
{
|
||
draggedItems.Add(item);
|
||
// Ensure the item is selected during drag
|
||
SetSelection(new[] { id }, TreeViewSelectionOptions.FireSelectionChanged);
|
||
}
|
||
}
|
||
|
||
if (draggedItems.Count == 0)
|
||
return;
|
||
|
||
// Set drag data
|
||
DragAndDrop.PrepareStartDrag();
|
||
DragAndDrop.SetGenericData("LevelHierarchyItems", draggedItems);
|
||
DragAndDrop.StartDrag("Dragging Level Items");
|
||
}
|
||
|
||
// Override to provide visual feedback during drag operations
|
||
protected override void BeforeRowsGUI()
|
||
{
|
||
base.BeforeRowsGUI();
|
||
|
||
// Check if drag operation has ended
|
||
bool isDragging = DragAndDrop.GetGenericData("LevelHierarchyItems") != null;
|
||
if (!isDragging && Event.current.type == EventType.Repaint)
|
||
{
|
||
// Drag operation has ended, force a repaint to restore normal highlighting
|
||
// Repaint();
|
||
}
|
||
|
||
// Draw insertion line during drag operations
|
||
if (isDragging)
|
||
{
|
||
var draggedItems = DragAndDrop.GetGenericData("LevelHierarchyItems") as List<LevelHierarchyItem>;
|
||
if (draggedItems != null && draggedItems.Count == 1 && draggedItems[0].type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
DrawInsertionLine();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Override to disable default drag highlighting and target highlighting
|
||
protected override bool CanChangeExpandedState(TreeViewItem item)
|
||
{
|
||
// During drag operations, prevent default TreeView highlighting and targeting
|
||
if (DragAndDrop.GetGenericData("LevelHierarchyItems") != null)
|
||
{
|
||
return false;
|
||
}
|
||
return base.CanChangeExpandedState(item);
|
||
}
|
||
|
||
private void DrawInsertionLine()
|
||
{
|
||
if (Event.current.type != EventType.Repaint)
|
||
return;
|
||
|
||
var mousePos = Event.current.mousePosition;
|
||
var rows = GetRows();
|
||
|
||
bool lineDrawn = false;
|
||
|
||
// First, try to find a row that the mouse is directly over
|
||
for (int i = 0; i < rows.Count; i++)
|
||
{
|
||
var rowRect = GetRowRect(i);
|
||
|
||
// Check if mouse is over this row
|
||
if (rowRect.Contains(mousePos))
|
||
{
|
||
var item = rows[i] as LevelHierarchyItem;
|
||
if (item?.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
// Determine if we should draw line above or below based on mouse position
|
||
float lineY;
|
||
bool drawAbove = (mousePos.y - rowRect.y) < (rowRect.height * 0.5f);
|
||
|
||
if (drawAbove)
|
||
{
|
||
lineY = rowRect.y;
|
||
}
|
||
else
|
||
{
|
||
lineY = rowRect.yMax;
|
||
}
|
||
|
||
// Draw the insertion line
|
||
var lineRect = new Rect(rowRect.x + GetInsertionLineIndent(item), lineY - 1, rowRect.width - GetInsertionLineIndent(item), 2);
|
||
EditorGUI.DrawRect(lineRect, new Color(0.3f, 0.7f, 1f, 0.8f)); // Blue insertion line
|
||
lineDrawn = true;
|
||
break;
|
||
}
|
||
else if (item?.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
// Draw line at the end of the group (where levels would be added)
|
||
var lineRect = new Rect(rowRect.x + GetInsertionLineIndent(item) + 20, rowRect.yMax - 1, rowRect.width - GetInsertionLineIndent(item) - 20, 2);
|
||
EditorGUI.DrawRect(lineRect, new Color(0.3f, 0.7f, 1f, 0.8f)); // Blue insertion line
|
||
lineDrawn = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// If no line was drawn and mouse is below all rows, draw at the end
|
||
if (!lineDrawn && rows.Count > 0)
|
||
{
|
||
var lastRowRect = GetRowRect(rows.Count - 1);
|
||
if (mousePos.y > lastRowRect.yMax)
|
||
{
|
||
// Find the last group to determine proper indentation
|
||
for (int i = rows.Count - 1; i >= 0; i--)
|
||
{
|
||
var item = rows[i] as LevelHierarchyItem;
|
||
if (item?.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
var lineRect = new Rect(lastRowRect.x + GetInsertionLineIndent(item) + 20, lastRowRect.yMax + 2, lastRowRect.width - GetInsertionLineIndent(item) - 20, 2);
|
||
EditorGUI.DrawRect(lineRect, new Color(0.3f, 0.7f, 1f, 0.8f)); // Blue insertion line
|
||
break;
|
||
}
|
||
else if (item?.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
var lineRect = new Rect(lastRowRect.x + GetInsertionLineIndent(item), lastRowRect.yMax + 2, lastRowRect.width - GetInsertionLineIndent(item), 2);
|
||
EditorGUI.DrawRect(lineRect, new Color(0.3f, 0.7f, 1f, 0.8f)); // Blue insertion line
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Override drag handling methods
|
||
protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args)
|
||
{
|
||
// Check if we have valid drag data
|
||
List<LevelHierarchyItem> draggedItems = DragAndDrop.GetGenericData("LevelHierarchyItems") as List<LevelHierarchyItem>;
|
||
if (draggedItems == null || draggedItems.Count == 0)
|
||
return DragAndDropVisualMode.None;
|
||
|
||
|
||
// Only handle single level drags for reordering
|
||
if (draggedItems.Count != 1 || draggedItems[0].type != LevelHierarchyItem.ItemType.Level)
|
||
return DragAndDropVisualMode.None;
|
||
|
||
var draggedLevel = draggedItems[0];
|
||
|
||
// Force no target highlighting by completely disabling TreeView's built-in target system
|
||
args.parentItem = null;
|
||
args.insertAtIndex = -1;
|
||
|
||
// Use mouse position to determine drop target and position
|
||
var mousePos = Event.current.mousePosition;
|
||
var rows = GetRows();
|
||
|
||
LevelGroup targetGroup = null;
|
||
int insertIndex = -1;
|
||
|
||
// Find the row under the mouse
|
||
for (int i = 0; i < rows.Count; i++)
|
||
{
|
||
var rowRect = GetRowRect(i);
|
||
|
||
if (rowRect.Contains(mousePos))
|
||
{
|
||
var targetItem = rows[i] as LevelHierarchyItem;
|
||
if (targetItem?.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
targetGroup = GetLevelGroup(targetItem);
|
||
if (targetGroup != null)
|
||
{
|
||
int targetIndex = targetGroup.levels.IndexOf(targetItem.levelAsset);
|
||
if (targetIndex != -1)
|
||
{
|
||
// Determine if we should insert before or after based on mouse position
|
||
bool insertBefore = (mousePos.y - rowRect.y) < (rowRect.height * 0.5f);
|
||
insertIndex = insertBefore ? targetIndex : targetIndex + 1;
|
||
}
|
||
}
|
||
}
|
||
else if (targetItem?.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
// Dropping on a group - add to the end of the group
|
||
targetGroup = targetItem.groupAsset;
|
||
insertIndex = targetGroup?.levels.Count ?? 0;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
var sourceGroup = GetLevelGroup(draggedLevel);
|
||
|
||
// If no specific target found, check if we're in an area where we can still drop
|
||
if (targetGroup == null || insertIndex == -1)
|
||
{
|
||
// Check if we're below all rows - allow dropping at the end of the last group
|
||
if (rows.Count > 0)
|
||
{
|
||
var lastRowRect = GetRowRect(rows.Count - 1);
|
||
if (mousePos.y > lastRowRect.yMax)
|
||
{
|
||
// Find the last group in the hierarchy
|
||
for (int i = rows.Count - 1; i >= 0; i--)
|
||
{
|
||
var item = rows[i] as LevelHierarchyItem;
|
||
if (item?.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
targetGroup = item.groupAsset;
|
||
insertIndex = targetGroup?.levels.Count ?? 0;
|
||
break;
|
||
}
|
||
else if (item?.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
var group = GetLevelGroup(item);
|
||
if (group != null)
|
||
{
|
||
targetGroup = group;
|
||
insertIndex = group.levels.Count;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle both reordering within the same group and moving between groups
|
||
if (targetGroup != null && insertIndex != -1)
|
||
{
|
||
// Handle the actual drop
|
||
if (args.performDrop)
|
||
{
|
||
// Show progress bar during the move operation
|
||
string progressTitle = sourceGroup == targetGroup ? "Reordering Level..." : "Moving Level...";
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Preparing level move operation...", 0.1f);
|
||
|
||
try
|
||
{
|
||
if (sourceGroup == targetGroup)
|
||
{
|
||
// Reordering within the same group
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Reordering level within group...", 0.5f);
|
||
ReorderLevelInGroup(draggedLevel, insertIndex, targetGroup);
|
||
}
|
||
else
|
||
{
|
||
// Moving to a different group
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Recording undo operations...", 0.2f);
|
||
Undo.RecordObject(sourceGroup, "Move Level - Remove from source");
|
||
Undo.RecordObject(targetGroup, "Move Level - Add to target");
|
||
Undo.RecordObject(draggedLevel.levelAsset, "Update Level Background and ColorsTile");
|
||
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Removing level from source group...", 0.3f);
|
||
// Remove from source group
|
||
sourceGroup.levels.Remove(draggedLevel.levelAsset);
|
||
EditorUtility.SetDirty(sourceGroup);
|
||
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Adding level to target group...", 0.5f);
|
||
// Add to target group at specific position
|
||
if (targetGroup.levels == null)
|
||
targetGroup.levels = new List<Level>();
|
||
|
||
targetGroup.levels.Insert(insertIndex, draggedLevel.levelAsset);
|
||
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Updating level properties...", 0.7f);
|
||
// Update level's background and colorsTile from new group
|
||
if (targetGroup.background != null)
|
||
draggedLevel.levelAsset.background = targetGroup.background;
|
||
if (targetGroup.colorsTile != null)
|
||
draggedLevel.levelAsset.colorsTile = targetGroup.colorsTile;
|
||
|
||
EditorUtility.SetDirty(targetGroup);
|
||
EditorUtility.SetDirty(draggedLevel.levelAsset);
|
||
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Updating level numbers...", 0.8f);
|
||
// Update level numbers in both groups
|
||
UpdateLevelNumbers(sourceGroup);
|
||
UpdateLevelNumbers(targetGroup);
|
||
}
|
||
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Reloading hierarchy...", 0.9f);
|
||
Reload();
|
||
|
||
// Find and select the level at its new position after reload
|
||
foreach (var item in m_IdToItem.Values)
|
||
{
|
||
if (item.type == LevelHierarchyItem.ItemType.Level &&
|
||
item.levelAsset == draggedLevel.levelAsset)
|
||
{
|
||
SetSelection(new[] { item.id }, TreeViewSelectionOptions.RevealAndFrame);
|
||
break;
|
||
}
|
||
}
|
||
|
||
EditorUtility.DisplayProgressBar(progressTitle, "Saving assets...", 0.95f);
|
||
AssetDatabase.SaveAssets();
|
||
OnHierarchyChanged?.Invoke();
|
||
|
||
// Clear drag data to ensure highlighting is restored
|
||
EditorApplication.delayCall += () => {
|
||
DragAndDrop.PrepareStartDrag(); // This clears the drag data
|
||
Repaint(); // Force repaint to restore normal highlighting
|
||
};
|
||
}
|
||
finally
|
||
{
|
||
// Always clear the progress bar, even if an exception occurs
|
||
EditorUtility.ClearProgressBar();
|
||
}
|
||
}
|
||
|
||
return DragAndDropVisualMode.Move;
|
||
}
|
||
|
||
return DragAndDropVisualMode.None;
|
||
}
|
||
|
||
// Helper method to get content indent for drawing insertion line
|
||
private float GetInsertionLineIndent(TreeViewItem item)
|
||
{
|
||
return GetContentIndent(item) + extraSpaceBeforeIconAndLabel;
|
||
}
|
||
|
||
// Helper method to check for circular references
|
||
private bool WouldCreateCircularReference(LevelHierarchyItem draggedItem, LevelHierarchyItem newParentItem)
|
||
{
|
||
// If dragging to root, no circular reference is possible
|
||
if (newParentItem == null)
|
||
return false;
|
||
|
||
// Check if target is trying to become child of itself or its child
|
||
LevelHierarchyItem current = newParentItem;
|
||
while (current != null)
|
||
{
|
||
if (current.id == draggedItem.id)
|
||
return true;
|
||
|
||
// Find the parent of current item
|
||
current = GetParentItem(current);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// Helper method to find the parent item of a group
|
||
private LevelHierarchyItem GetParentItem(LevelHierarchyItem item)
|
||
{
|
||
if (item == null || item.type != LevelHierarchyItem.ItemType.Group ||
|
||
item.groupAsset == null || item.groupAsset.parentGroup == null)
|
||
return null;
|
||
|
||
// Find the hierarchy item for this parent group
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
if (entry.Value.type == LevelHierarchyItem.ItemType.Group &&
|
||
entry.Value.groupAsset == item.groupAsset.parentGroup)
|
||
{
|
||
return entry.Value;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// Helper method to update the parent of a group
|
||
private void UpdateGroupParent(LevelHierarchyItem groupItem, LevelHierarchyItem newParentItem)
|
||
{
|
||
if (groupItem == null || groupItem.type != LevelHierarchyItem.ItemType.Group || groupItem.groupAsset == null)
|
||
return;
|
||
|
||
// Prepare for undo
|
||
Undo.RecordObject(groupItem.groupAsset, "Change Group Parent");
|
||
|
||
// Update parent reference
|
||
if (newParentItem != null && newParentItem.type == LevelHierarchyItem.ItemType.Group)
|
||
{
|
||
groupItem.groupAsset.parentGroup = newParentItem.groupAsset;
|
||
}
|
||
else
|
||
{
|
||
// Dragging to root (no parent)
|
||
groupItem.groupAsset.parentGroup = null;
|
||
}
|
||
|
||
// Save changes to asset
|
||
EditorUtility.SetDirty(groupItem.groupAsset);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
|
||
// Helper method to update the parent of a level
|
||
private void UpdateLevelParent(LevelHierarchyItem levelItem, LevelHierarchyItem newParentItem)
|
||
{
|
||
if (levelItem == null || levelItem.type != LevelHierarchyItem.ItemType.Level ||
|
||
levelItem.levelAsset == null || newParentItem == null ||
|
||
newParentItem.type != LevelHierarchyItem.ItemType.Group ||
|
||
newParentItem.groupAsset == null)
|
||
return;
|
||
|
||
// Find the current parent group
|
||
LevelGroup currentGroup = null;
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
if (entry.Value.type == LevelHierarchyItem.ItemType.Group &&
|
||
entry.Value.groupAsset != null &&
|
||
entry.Value.groupAsset.levels != null)
|
||
{
|
||
if (entry.Value.groupAsset.levels.Contains(levelItem.levelAsset))
|
||
{
|
||
currentGroup = entry.Value.groupAsset;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// If the current parent is the same as the new parent, do nothing
|
||
if (currentGroup == newParentItem.groupAsset)
|
||
return;
|
||
|
||
// Prepare for undo
|
||
if (currentGroup != null)
|
||
Undo.RecordObject(currentGroup, "Change Level Parent - Remove from old");
|
||
|
||
Undo.RecordObject(newParentItem.groupAsset, "Change Level Parent - Add to new");
|
||
Undo.RecordObject(levelItem.levelAsset, "Update Level Background and ColorsTile");
|
||
|
||
// Remove from old parent
|
||
if (currentGroup != null && currentGroup.levels != null)
|
||
{
|
||
currentGroup.levels.Remove(levelItem.levelAsset);
|
||
EditorUtility.SetDirty(currentGroup);
|
||
}
|
||
|
||
// Add to new parent
|
||
if (newParentItem.groupAsset.levels == null)
|
||
newParentItem.groupAsset.levels = new List<Level>();
|
||
|
||
if (!newParentItem.groupAsset.levels.Contains(levelItem.levelAsset))
|
||
{
|
||
newParentItem.groupAsset.levels.Add(levelItem.levelAsset);
|
||
EditorUtility.SetDirty(newParentItem.groupAsset);
|
||
}
|
||
|
||
// Update level's background and colorsTile from new parent group
|
||
if (newParentItem.groupAsset.background != null)
|
||
{
|
||
levelItem.levelAsset.background = newParentItem.groupAsset.background;
|
||
}
|
||
|
||
if (newParentItem.groupAsset.colorsTile != null)
|
||
{
|
||
levelItem.levelAsset.colorsTile = newParentItem.groupAsset.colorsTile;
|
||
}
|
||
|
||
// Mark level as dirty to save the changes
|
||
EditorUtility.SetDirty(levelItem.levelAsset);
|
||
|
||
// Save changes to assets
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
|
||
// Helper method to get the group containing a level item
|
||
private LevelGroup GetLevelGroup(LevelHierarchyItem item)
|
||
{
|
||
if (item == null)
|
||
return null;
|
||
|
||
if (item.type == LevelHierarchyItem.ItemType.Group)
|
||
return item.groupAsset;
|
||
|
||
if (item.type == LevelHierarchyItem.ItemType.Level)
|
||
{
|
||
// Find the group that contains this level
|
||
foreach (var entry in m_IdToItem)
|
||
{
|
||
if (entry.Value.type == LevelHierarchyItem.ItemType.Group &&
|
||
entry.Value.groupAsset != null &&
|
||
entry.Value.groupAsset.levels != null &&
|
||
entry.Value.groupAsset.levels.Contains(item.levelAsset))
|
||
{
|
||
return entry.Value.groupAsset;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// Helper method to reorder a level within its group
|
||
private void ReorderLevelInGroup(LevelHierarchyItem draggedLevel, int insertAtIndex, LevelGroup group)
|
||
{
|
||
if (draggedLevel?.levelAsset == null || group?.levels == null)
|
||
return;
|
||
|
||
// Prepare for undo
|
||
Undo.RecordObject(group, "Reorder Level");
|
||
|
||
// Find current index of dragged level
|
||
int currentIndex = group.levels.IndexOf(draggedLevel.levelAsset);
|
||
if (currentIndex == -1)
|
||
return;
|
||
|
||
// Clamp insert index to valid range
|
||
int newIndex = Mathf.Clamp(insertAtIndex, 0, group.levels.Count);
|
||
|
||
// If dragging to the same position, do nothing
|
||
if (currentIndex == newIndex || (currentIndex == newIndex - 1 && newIndex < group.levels.Count))
|
||
return;
|
||
|
||
// Remove from current position
|
||
var levelToMove = group.levels[currentIndex];
|
||
group.levels.RemoveAt(currentIndex);
|
||
|
||
// Adjust new index if we removed an item before the target position
|
||
if (currentIndex < newIndex)
|
||
newIndex--;
|
||
|
||
// Ensure we don't exceed the list bounds after removal
|
||
newIndex = Mathf.Clamp(newIndex, 0, group.levels.Count);
|
||
|
||
// Insert at new position
|
||
group.levels.Insert(newIndex, levelToMove);
|
||
|
||
// Update level numbers based on new positions
|
||
UpdateLevelNumbers(group);
|
||
|
||
// Mark as dirty
|
||
EditorUtility.SetDirty(group);
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
|
||
// Helper method to update level numbers based on their position in the group
|
||
private void UpdateLevelNumbers(LevelGroup group)
|
||
{
|
||
if (group?.levels == null)
|
||
return;
|
||
|
||
for (int i = 0; i < group.levels.Count; i++)
|
||
{
|
||
if (group.levels[i] != null)
|
||
{
|
||
// Prepare for undo
|
||
Undo.RecordObject(group.levels[i], "Update Level Number");
|
||
|
||
// Set level number based on position (1-indexed)
|
||
int oldNumber = group.levels[i].number;
|
||
group.levels[i].number = i + 1;
|
||
|
||
// Mark as dirty
|
||
EditorUtility.SetDirty(group.levels[i]);
|
||
}
|
||
}
|
||
}
|
||
|
||
public TreeViewItem FindItemWrapper(int id)
|
||
{
|
||
return FindItem(id, rootItem);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new root-level LevelGroup asset via save panel and reloads the tree.
|
||
/// </summary>
|
||
public void CreateRootGroup()
|
||
{
|
||
OnCreateSubgroup?.Invoke(null);
|
||
}
|
||
|
||
public LevelHierarchyItem GetSelectedItem()
|
||
{
|
||
var selection = GetSelection();
|
||
if (m_IdToItem != null && m_IdToItem.Count > 0)
|
||
{
|
||
return selection is { Count: > 0 } ? m_IdToItem[selection[0]] : null;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Enable single selection for better drag and drop experience
|
||
protected override bool CanMultiSelect(TreeViewItem item)
|
||
{
|
||
// Allow multi-select for groups but prefer single selection for levels to make reordering easier
|
||
if (item is LevelHierarchyItem hierarchyItem)
|
||
{
|
||
return hierarchyItem.type == LevelHierarchyItem.ItemType.Group;
|
||
}
|
||
return base.CanMultiSelect(item);
|
||
}
|
||
}
|
||
}
|