Editor/IconGridItem.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace GeneralGame.Editor;
/// <summary>
/// A single item in the icon grid - displays file/folder with large icon
/// </summary>
public class IconGridItem : Widget
{
public string FilePath { get; }
public string FileName { get; }
public string Extension { get; }
public bool IsFolder { get; }
public Asset Asset { get; private set; }
/// <summary>
/// Called when item is double-clicked (open file/folder)
/// </summary>
public Action<string> OnActivated;
/// <summary>
/// Called when folder is double-clicked (for navigation in grid)
/// </summary>
public Action<string> OnFolderNavigate;
private bool _isSelected;
private Pixmap _thumbnail;
public const float ItemWidth = 80;
public const float ItemHeight = 100;
private const float IconSize = 64;
private static readonly Dictionary<string, string> ExtensionIcons = new()
{
{ ".vmdl", "view_in_ar" },
{ ".fbx", "view_in_ar" },
{ ".obj", "view_in_ar" },
{ ".gltf", "view_in_ar" },
{ ".glb", "view_in_ar" },
{ ".png", "image" },
{ ".jpg", "image" },
{ ".jpeg", "image" },
{ ".tga", "image" },
{ ".vtex", "image" },
{ ".psd", "image" },
{ ".vmat", "texture" },
{ ".wav", "audiotrack" },
{ ".mp3", "audiotrack" },
{ ".ogg", "audiotrack" },
{ ".vsnd", "audiotrack" },
{ ".cs", "code" },
{ ".razor", "code" },
{ ".scss", "style" },
{ ".css", "style" },
{ ".shader", "gradient" },
{ ".shdrgrph", "gradient" },
{ ".prefab", "inventory_2" },
{ ".scene", "landscape" },
{ ".json", "data_object" },
{ ".xml", "data_object" },
{ ".txt", "description" },
{ ".md", "description" },
{ ".item", "category" },
{ ".clothing", "checkroom" },
{ ".weapon", "sports_martial_arts" },
{ ".particle", "blur_on" },
{ ".vanmgrph", "animation" },
{ ".vpost", "auto_fix_high" }
};
private static readonly Dictionary<string, Color> ExtensionColors = new()
{
{ ".cs", new Color(0.4f, 0.7f, 1.0f) },
{ ".razor", new Color(0.6f, 0.4f, 0.9f) },
{ ".shader", new Color(0.3f, 0.9f, 0.5f) },
{ ".vmdl", new Color(0.9f, 0.6f, 0.3f) },
{ ".vmat", new Color(0.9f, 0.4f, 0.6f) },
{ ".prefab", new Color(0.3f, 0.8f, 0.9f) },
{ ".scene", new Color(0.9f, 0.9f, 0.3f) },
};
public IconGridItem(Widget parent, string path, bool isFolder) : base(parent)
{
FilePath = Path.GetFullPath(path);
FileName = Path.GetFileName(path);
Extension = isFolder ? "" : Path.GetExtension(path).ToLowerInvariant();
IsFolder = isFolder;
Size = new Vector2(ItemWidth, ItemHeight);
MinimumSize = new Vector2(ItemWidth, ItemHeight);
MaximumSize = new Vector2(ItemWidth, ItemHeight);
if (!isFolder)
{
Asset = AssetSystem.FindByPath(path);
if (Asset != null)
{
_thumbnail = Asset.GetAssetThumb();
}
}
ToolTip = BuildTooltip();
// Force accept mouse events
AcceptDrops = false;
}
private string BuildTooltip()
{
var tip = FilePath;
if (!IsFolder && Asset != null)
{
tip += $"\nType: {Asset.AssetType?.FriendlyName ?? "Unknown"}";
}
if (!IsFolder)
{
var fileInfo = new FileInfo(FilePath);
if (fileInfo.Exists)
{
tip += $"\nSize: {FormatFileSize(fileInfo.Length)}";
tip += $"\nModified: {fileInfo.LastWriteTime:g}";
}
}
return tip;
}
protected override void OnPaint()
{
var rect = LocalRect;
// Background
if (_isSelected)
{
Paint.SetBrush(Theme.Primary.WithAlpha(0.3f));
Paint.DrawRect(rect, 4);
}
// Icon area
var iconRect = new Rect(
rect.Left + (rect.Width - IconSize) / 2,
rect.Top + 4,
IconSize,
IconSize
);
// Draw thumbnail or icon
if (_thumbnail != null)
{
Paint.Draw(iconRect, _thumbnail);
}
else
{
var icon = GetIcon();
var iconColor = GetIconColor();
Paint.SetPen(iconColor);
Paint.DrawIcon(iconRect, icon, IconSize, TextFlag.Center);
}
// Filename
var textRect = new Rect(
rect.Left + 2,
iconRect.Bottom + 2,
rect.Width - 4,
rect.Height - iconRect.Height - 8
);
Paint.SetPen(Theme.Text);
Paint.SetDefaultFont(8, 400);
var displayName = FileName;
if (displayName.Length > 12)
displayName = displayName.Substring(0, 10) + "..";
Paint.DrawText(textRect, displayName, TextFlag.Center);
}
private string GetIcon()
{
if (IsFolder)
return "folder";
if (ExtensionIcons.TryGetValue(Extension, out var icon))
return icon;
return "description";
}
private Color GetIconColor()
{
if (IsFolder)
return Theme.Yellow;
if (Asset?.AssetType?.Color != null && Asset.AssetType.Color != default)
return Asset.AssetType.Color;
if (ExtensionColors.TryGetValue(Extension, out var color))
return color;
return Theme.Text.WithAlpha(0.7f);
}
private Vector2 _dragStartPos;
private bool _isDragging;
protected override void OnMousePress(MouseEvent e)
{
base.OnMousePress(e);
if (e.LeftMouseButton)
{
_isSelected = true;
_dragStartPos = e.LocalPosition;
_isDragging = false;
Update();
if (!IsFolder && Asset != null)
{
EditorUtility.InspectorObject = Asset;
}
}
}
protected override void OnMouseMove(MouseEvent e)
{
base.OnMouseMove(e);
if (e.ButtonState.HasFlag(MouseButtons.Left) && !_isDragging)
{
var delta = e.LocalPosition - _dragStartPos;
if (delta.Length > 5) // Start drag after moving 5 pixels
{
_isDragging = true;
StartDrag();
}
}
}
private void StartDrag()
{
var drag = new Drag(this);
if (IsFolder)
{
drag.Data.Url = new Uri("file:///" + FilePath);
}
else if (Asset != null)
{
drag.Data.Text = Asset.RelativePath;
drag.Data.Url = new Uri("file:///" + Asset.AbsolutePath);
}
else
{
drag.Data.Text = FilePath;
drag.Data.Url = new Uri("file:///" + FilePath);
}
drag.Execute();
}
protected override void OnDoubleClick(MouseEvent e)
{
base.OnDoubleClick(e);
if (e.LeftMouseButton)
{
if (IsFolder)
{
OnFolderNavigate?.Invoke(FilePath);
}
else
{
if (Asset != null)
Asset.OpenInEditor();
else
EditorUtility.OpenFolder(FilePath);
}
}
}
protected override void OnContextMenu(ContextMenuEvent e)
{
var menu = new ContextMenu(this);
if (IsFolder)
{
menu.AddOption("Open", "folder_open", () => OnFolderNavigate?.Invoke(FilePath));
menu.AddOption("Open in Explorer", "launch", () => EditorUtility.OpenFolder(FilePath));
menu.AddSeparator();
menu.AddOption("Copy Path", "content_copy", () => EditorUtility.Clipboard.Copy(FilePath));
menu.AddSeparator();
// Create submenu
var createMenu = menu.AddMenu("Create", "add");
AssetCreator.AddOptions(createMenu, FilePath);
menu.AddSeparator();
menu.AddOption("Delete", "delete", () => DeleteFolder());
}
else
{
// Open options
if (Asset != null)
{
menu.AddOption("Open in Editor", "edit", () => Asset.OpenInEditor());
}
else
{
menu.AddOption("Open", "open_in_new", () => EditorUtility.OpenFolder(FilePath));
}
menu.AddOption("Show in Explorer", "folder_open", () => EditorUtility.OpenFileFolder(FilePath));
menu.AddSeparator();
// Copy options
if (Asset != null)
{
menu.AddOption("Copy Relative Path", "content_paste_go", () => EditorUtility.Clipboard.Copy(Asset.RelativePath));
}
menu.AddOption("Copy Absolute Path", "content_paste", () => EditorUtility.Clipboard.Copy(FilePath));
// Asset-type specific options (Create Material, Create Texture, etc.)
AssetContextMenuHelper.AddAssetTypeOptions(menu, Asset);
menu.AddSeparator();
// Edit options
menu.AddOption("Rename", "edit", () => ShowRenameDialog());
menu.AddOption("Duplicate", "file_copy", () => DuplicateFile());
menu.AddSeparator();
// Create submenu for quick asset creation in same folder
var parentFolder = Path.GetDirectoryName(FilePath);
if (!string.IsNullOrEmpty(parentFolder))
{
var createMenu = menu.AddMenu("Create", "add");
AssetCreator.AddOptions(createMenu, parentFolder);
menu.AddSeparator();
}
menu.AddOption("Delete", "delete", () => DeleteFile());
}
menu.OpenAtCursor();
e.Accepted = true;
}
private void ShowRenameDialog()
{
var dialog = new RenameDialog("Rename File", Path.GetFileNameWithoutExtension(FileName));
dialog.OnConfirm = (newName) =>
{
if (string.IsNullOrWhiteSpace(newName))
return;
// Preserve extension if not provided
if (!Path.HasExtension(newName))
{
newName += Extension;
}
if (newName == FileName)
return;
var newPath = Path.Combine(Path.GetDirectoryName(FilePath), newName);
try
{
File.Move(FilePath, newPath);
}
catch (Exception ex)
{
Log.Error($"Failed to rename file: {ex.Message}");
}
};
dialog.Show();
}
private void DuplicateFile()
{
try
{
var directory = Path.GetDirectoryName(FilePath);
var nameWithoutExt = Path.GetFileNameWithoutExtension(FileName);
var newName = $"{nameWithoutExt}_copy{Extension}";
var newPath = Path.Combine(directory, newName);
var counter = 1;
while (File.Exists(newPath))
{
newName = $"{nameWithoutExt}_copy{counter++}{Extension}";
newPath = Path.Combine(directory, newName);
}
File.Copy(FilePath, newPath);
}
catch (Exception ex)
{
Log.Error($"Failed to duplicate file: {ex.Message}");
}
}
private void DeleteFile()
{
var confirm = new PopupWindow(
"Delete File",
$"Are you sure you want to delete '{FileName}'?",
"Cancel",
new Dictionary<string, Action>()
{
{ "Delete", () =>
{
try
{
File.Delete(FilePath);
}
catch (Exception ex)
{
Log.Error($"Failed to delete file: {ex.Message}");
}
}
}
}
);
confirm.Show();
}
private void DeleteFolder()
{
var confirm = new PopupWindow(
"Delete Folder",
$"Are you sure you want to delete '{FileName}'?\nAll contents will be deleted.",
"Cancel",
new Dictionary<string, Action>()
{
{ "Delete", () =>
{
try
{
Directory.Delete(FilePath, recursive: true);
}
catch (Exception ex)
{
Log.Error($"Failed to delete folder: {ex.Message}");
}
}
}
}
);
confirm.Show();
}
private static string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
int order = 0;
double size = bytes;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size /= 1024;
}
return $"{size:0.##} {sizes[order]}";
}
}