Files
2025-06-09 23:23:13 +08:00

371 lines
15 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.Linq;
using System;
namespace JBooth.MicroVerseCore
{
/// <summary>
/// A replacement for Unity's selection grid.
/// This one supports an active checkbox and multiple selection
/// </summary>
public class SelectionGrid
{
/// <summary>
/// Data structure which is used in the selection grid
/// </summary>
public class Selectable
{
public string text;
public string tooltip;
public Texture image;
public bool active;
/// <summary>
/// In multiselection mode this classifies the one that was last selected
/// </summary>
public bool focused;
}
/// <summary>
/// Show the selection grid in the inspector
/// </summary>
/// <param name="selectedIndexes">The selected indexes</param>
/// <param name="selectables">Objects used for the cells of the selection grid</param>
/// <param name="cellSize">The size of the cells</param>
/// <param name="dynamicResize">Whether the cells should become smaller with increasing items</param>
/// <param name="title">Optional title</param>
/// <returns></returns>
public static bool ShowSelectionGrid(List<int> selectedIndexes, Selectable[] selectables, int cellSize, bool dynamicResize = false, string title = null, bool missingDataTextVisible = false)
{
// title
if (!string.IsNullOrEmpty(title))
{
GUIContent terrainLayers = EditorGUIUtility.TrTextContent(title);
GUILayout.Label(terrainLayers, EditorStyles.boldLabel);
}
if (selectables.Length > 0)
{
EditorGUILayout.HelpBox("Selection: Click = Single, Control+Click = Add, Shift+Click = Range", MessageType.None);
}
bool changed = false;
if (selectables.Length == 0)
{
if (missingDataTextVisible)
{
EditorGUILayout.HelpBox("No objects found", MessageType.Info);
}
return changed;
}
// preselect first item
if (selectedIndexes.Count == 0 && selectables.Length > 0)
{
selectedIndexes.Add(0);
changed = true;
}
if ( dynamicResize)
{
// with more than 10 textures the texture size is reduced by a given percentage
// ie the more items, the smaller the thumbnails
if (selectables.Length > 10)
{
cellSize = (int)(cellSize * ( 1 - 1f/cellSize));
}
}
// calculate number of columns and rows
int columns = (int)(EditorGUIUtility.currentViewWidth - cellSize) / cellSize + 1;
int rows = (int)Mathf.Ceil((selectables.Length + columns - 2) / (float)columns);
int selectedIndex = -1;
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.Label("Active:");
if(GUILayout.Button( "All", EditorStyles.miniButton))
{
foreach (Selectable selectable in selectables)
selectable.active = true;
changed = true;
}
if (GUILayout.Button("None", EditorStyles.miniButton))
{
foreach (Selectable selectable in selectables)
selectable.active = false;
changed = true;
}
if (GUILayout.Button("Invert", EditorStyles.miniButton))
{
foreach (Selectable selectable in selectables)
selectable.active = !selectable.active;
changed = true;
}
EditorGUILayout.EndHorizontal();
for (int row = 0; row < rows; row++)
{
EditorGUILayout.BeginHorizontal();
{
for (int col = 0; col < columns; col++)
{
selectedIndex++;
if (selectedIndex < selectables.Length)
{
Texture previewTexture = selectables[selectedIndex].image;
string label = selectables[selectedIndex].text;
bool active = selectables[selectedIndex].active;
// check if the cell is selected
bool isCellSelected = selectedIndexes.Contains(selectedIndex);
// visualization style
GUIStyle style;
if (isCellSelected)
style = GUIStylesSelectionGrid.TextureSelectionStyleInclude;
else
style = GUIStylesSelectionGrid.TextureSelectionStyleUnselected;
// show background image
GUILayout.Label(previewTexture, style, GUILayout.Width(cellSize), GUILayout.Height(cellSize));
// get clickable area of cell
Rect previewTextureRect = GUILayoutUtility.GetLastRect();
bool previewTextureClicked = Event.current.rawType == EventType.MouseDown && previewTextureRect.Contains(Event.current.mousePosition);
if (previewTextureClicked)
{
// add selection to list
// multi edit mode is activated with shift key: you can select multiple cells this way
bool isMultiEditMode = Event.current.control || Event.current.shift;
// add selection to list
if (isMultiEditMode)
{
// toggle single selection
if (Event.current.control)
{
if (selectedIndexes.Contains(selectedIndex) && selectedIndexes.Count > 1)
selectedIndexes.Remove(selectedIndex);
else
selectedIndexes.Add(selectedIndex);
}
// toggle range selection
if (Event.current.shift)
{
int startIndex = selectedIndex;
int endIndex = Array.IndexOf(selectables, Array.Find(selectables, x => x.focused));
selectedIndexes.Clear();
// flip range if necessary
if (startIndex > endIndex)
{
int tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
// set selection range
selectedIndexes.Clear();
for (int i = startIndex; i <= endIndex; i++)
{
selectedIndexes.Add(i);
}
}
}
else
{
selectedIndexes.Clear();
selectedIndexes.Add(selectedIndex);
}
// invalidate focused on all
Array.ForEach(selectables, x => x.focused = false);
// set new focused
selectables[selectedIndex].focused = true;
changed = true;
}
// margin for the selection
int margin = 3;
// get image rect
Rect labelRect = GUILayoutUtility.GetLastRect();
// reduce image rect by margin
labelRect.x += margin;
labelRect.y += margin;
labelRect.width -= margin * 2;
labelRect.height -= margin * 2;
// rect of the toggle, depends on the label rect
Rect toggleRect = new Rect(labelRect);
// label rect
labelRect.height = GUIStylesSelectionGrid.SelectionElementLabelStyle.CalcHeight(new GUIContent(label), labelRect.width);
// label background
GUI.DrawTexture(labelRect, GUIStylesSelectionGrid.LabelBackgroundTexture, ScaleMode.StretchToFill);
GUI.Box(labelRect, label, GUIStylesSelectionGrid.SelectionElementLabelStyle);
// unselected: make the cell appear grey-ish, ie inaccessible
if (!active)
{
Color overlay = Color.grey;
overlay.a = 0.5f;
EditorGUI.DrawRect(previewTextureRect, overlay);
}
// active toggle
float lineHeight = GUI.skin.label.lineHeight;
// reduce the checkbox area, otherwise outside of the box we'd get a mouse cursor change on hovering where a label would be
toggleRect.x += margin;
toggleRect.y += toggleRect.height - lineHeight - margin;
toggleRect.height = lineHeight;
toggleRect.width = GUI.skin.toggle.CalcHeight( GUIContent.none, toggleRect.width);
selectables[selectedIndex].active = GUI.Toggle(toggleRect, active, "");
// detect change
if (active != selectables[selectedIndex].active)
{
changed = true;
}
}
}
}
EditorGUILayout.EndHorizontal();
}
return changed;
}
}
public class GUIStylesSelectionGrid
{
/// <summary>
/// Used for include and unselected texture border
/// </summary>
private static GUIStyle _textureSelectionStyleUnselected;
public static GUIStyle TextureSelectionStyleUnselected
{
get
{
if (_textureSelectionStyleUnselected == null || _textureSelectionStyleUnselected.normal.background == null) // background check because it's null when a scene reloads
{
_textureSelectionStyleUnselected = new GUIStyle(GUI.skin.label);
_textureSelectionStyleUnselected.normal.background = GUI.skin.box.normal.background;
_textureSelectionStyleUnselected.stretchWidth = true;
_textureSelectionStyleUnselected.border = new RectOffset(0, 0, 0, 0);
_textureSelectionStyleUnselected.margin = new RectOffset(2, 2, 2, 2);
_textureSelectionStyleUnselected.padding = new RectOffset(2, 2, 2, 2);
}
return _textureSelectionStyleUnselected;
}
}
private static GUIStyle _textureSelectionStyleInclude;
public static GUIStyle TextureSelectionStyleInclude
{
get
{
if (_textureSelectionStyleInclude == null || _textureSelectionStyleInclude.normal.background == null) // background check because it's null when a scene reloads
{
_textureSelectionStyleInclude = new GUIStyle(TextureSelectionStyleUnselected);
_textureSelectionStyleInclude.normal.background = CreateColorPixel(Color.green * 0.8f); // or use MV color new Color(0, 0, 128)
}
return _textureSelectionStyleInclude;
}
}
private static GUIStyle _textureSelectionStyleExclude;
public static GUIStyle TextureSelectionStyleExclude
{
get
{
if (_textureSelectionStyleExclude == null || _textureSelectionStyleExclude.normal.background == null) // background check because it's null when a scene reloads
{
_textureSelectionStyleExclude = new GUIStyle(TextureSelectionStyleUnselected);
_textureSelectionStyleExclude.normal.background = CreateColorPixel(Color.red);
}
return _textureSelectionStyleExclude;
}
}
static Texture2D _labelBackgroundTexture;
public static Texture2D LabelBackgroundTexture
{
get
{
if (_labelBackgroundTexture == null)
{
_labelBackgroundTexture = new Texture2D(1, 1);
_labelBackgroundTexture.SetPixel(0, 0, new Color(0.0f, 0.0f, 0.0f, 0.5f));
_labelBackgroundTexture.Apply();
}
return _labelBackgroundTexture;
}
}
static GUIStyle _selectionElementLabelStyle;
public static GUIStyle SelectionElementLabelStyle
{
get
{
if (_selectionElementLabelStyle == null)
{
_selectionElementLabelStyle = new GUIStyle(EditorStyles.wordWrappedMiniLabel);
_selectionElementLabelStyle.normal.textColor = EditorGUIUtility.isProSkin ? Color.white : Color.black;
_selectionElementLabelStyle.fontStyle = FontStyle.Bold;
_selectionElementLabelStyle.alignment = TextAnchor.UpperCenter;
}
return _selectionElementLabelStyle;
}
}
/// <summary>
/// Creates a 1x1 texture
/// </summary>
/// <param name="Background">Color of the texture</param>
/// <returns></returns>
public static Texture2D CreateColorPixel(Color color)
{
Texture2D texture = new Texture2D(1, 1);
texture.SetPixel(0, 0, color);
texture.Apply();
return texture;
}
}
}