#
PropertyGrid
PropertyGrid in UIToolbox shows the public fields and properties of Target and edits them. Each row is an editor for the member type. A search box filters the rows. The "A-Z" button changes between category foldouts and one alphabetical list. The panel at the bottom shows the description of the selected row.
<tb:PropertyGrid name="grid" sort-mode="Categorized" show-search="true" show-description="true" />
class Settings
{
[Category("Text"), Description("The text in the box.")] public string caption = "Hello";
[Category("Size"), Range(40, 200)] public int width = 120;
public Color fill = Color.blue;
public int Edits { get; internal set; } // no public setter: read-only row
[DisplayName("Health")] public int MaxHealth { get; set; }
[Choices(nameof(sizes))] public string Size = "M"; // a dropdown of the texts of sizes()
public bool Outline;
[EnabledIf(nameof(Outline))] public float OutlineWidth = 1;
private string[] sizes() => new[] { "S", "M", "L" };
}
grid.Target = new Settings();
grid.PropertyValueChanged += (item, old, now) => Debug.Log($"{item.Name}: {old} > {now}");
grid.RegisterEditor(typeof(Color), item => myColorPicker(item)); // call item.SetValue(v) to write
| Attribute |
Effect |
[Category] |
The foldout of the row. The default is "Misc". |
[Description], [Tooltip] |
The row tooltip and the description panel text. |
[DisplayName] (properties only), [InspectorName] |
The row label. Otherwise the member name, nicified: maxHealth is "Max Health". C# does not allow [DisplayName] on a field; use [InspectorName] there. |
[Choices(member)] |
A dropdown for a string or an int. member is a field, a property or a method without parameters of the same object, of any access, that returns a collection. A string takes the chosen text, an int takes its index. The grid reads the list again after each change. |
[EnabledIf(member, values)] |
The row is enabled only while member is true, or equals one of values. The grid checks again after each change. |
[Range] |
A slider for int, float, double, short, ushort, byte, sbyte, long, uint and ulong. |
[Multiline], [TextArea] |
A multiline text field for string. |
[ReadOnly(true)] |
A disabled row. readonly fields and properties without a public setter are disabled too. |
[Browsable(false)], [HideInInspector], [Obsolete] |
The member has no row. |
| Type |
Editor |
int, float |
IntegerField, FloatField, or SliderInt, Slider with [Range] |
long, double, string |
LongField, DoubleField, TextField (delayed: the value changes on Enter or blur). A double with [Range] gets a Slider; a long with [Range] gets a SliderInt, or a long slider past the int limits. |
uint, ulong |
UnsignedIntegerField, UnsignedLongField, or SliderInt with [Range], or a long slider past the int limits. |
short, ushort, byte, sbyte |
IntegerField, or SliderInt with [Range]. The value is clamped to the type limits. |
bool |
Toggle |
enum |
EnumField; a [Flags] enum gets one toggle per flag |
Vector2 to Vector4, Vector2Int, Vector3Int, Rect, RectInt, Bounds, BoundsInt |
The matching UI Toolkit field |
Color |
A compact ColorPicker: a swatch in the row. A click on the swatch opens the full picker in a popup. |
DateTime, TimeSpan |
DatePicker, TimeSpanPicker |
| A class or struct of your own |
A foldout with a row for each member. A null object shows "null" until code sets it and calls Refresh(). |
| Other types |
A read-only text field with ToString(). A collection shows its type and count, such as "List (3)". |
| Member |
Description |
Target |
The object to edit. A set builds the rows and clears the undo history. |
SortMode (sort-mode) |
Categorized or Alphabetical. |
DeclaredCategoryOrder (declared-category-order) |
True: the categories show in the order of their first member in the code. False (the default): A to Z. |
ShowSearch, ShowDescription (show-search, show-description) |
The toolbar and the description panel. |
SearchText |
The search text. It matches the label and the member name. |
ShowClearButton (show-clear-button) |
On (the default): the search box shows a "x" button while it has text, and a click clears the text. Escape in the search box also clears the text. See Clear button. |
Items, GetItem(path), GetEditor(path), SelectedItem |
The rows. A path is a member name or a dot path to a nested member, such as "stats.health". An element path has an index, such as "stops[2]" or "legs[0].health". PropertyGridItem has Name, Path, Parent, Children, DisplayName, Category, Description, ValueType, IsReadOnly, Range, Index (-1 for a member), GetValue() and SetValue(v). |
InsertElement(index, value), RemoveElement(index) |
On the item of an array or list: insert or remove an element. value null gives a new element of the element type. |
MoveElement(from, to) |
On the item of an array or list: move an element to another index; the other elements shift. One move is one undo step. It returns false for the same index, an index outside the collection, or a read-only list. A read-only array member reorders in place. Each element row has a "▲" and a "▼" button; the first row cannot move up and the last row cannot move down. |
RegisterEditor(type, factory) |
Replaces the editor for a type and the types assignable to it. The first registered match wins. |
Refresh() |
Reads the values again, for changes made outside the grid. |
Undo(), Redo(), CanUndo, CanRedo |
The history. Ctrl+Z undoes; Ctrl+Y and Ctrl+Shift+Z redo. |
PropertyValueChanged(item, old, new) |
Sent after an edit, an undo and a redo. |
- The grid reads the value again after a write, so a setter that clamps shows the clamped value. A write that does not change the value is not recorded.
- Changes to one row within 1 second merge into one undo step, so a slider drag is one step.
- A setter that throws logs the exception and the row shows the old value.
- Members of UnityEngine base classes (for example
MonoBehaviour) are left out, unless the target type is itself a UnityEngine type.
- The rows follow the declaration order: base class members first, fields before properties.
- In the UI Builder, a grid without a target edits a sample object in a dashed outline. The sample does not show in Play mode or in a build, and the UI Builder does not save it to the UXML file.
- Nested objects expand up to
PropertyGrid.MAX_DEPTH (4) levels. A type that is already in the chain from the target does not expand again, so a cycle stops. .NET types, Unity types, collections and delegates do not expand. The grid keeps the expanded foldouts when it builds the rows again.
- An edit in a nested struct sets the struct copy back in its owner, up the chain. The fields of a read-only struct member are read-only; the members of a read-only class member are not.
- The filter matches the members of the target, not nested members.
- An array or
IList<T> member shows in a foldout, "Name (count)". Each element is a row "Element n" with a "-" button, and "+ Add" is under the rows. An element of a class or struct type expands like a nested object. [Range] on the member applies to its elements.
- A list changes in place. An array is replaced by a new array, so a read-only array member does not resize; its elements are still editable, and the move buttons reorder them in place. A read-only list has no buttons and read-only rows. A null collection shows as a text row.
- Undo covers inserts and removes. For those,
PropertyValueChanged gives the old and new elements as object[] snapshots.
- The
[Range] slider of a long, uint or ulong is a SliderInt when the range fits in an int. Past the int limits it is a long slider: a float Slider to drag and a LongField or UnsignedLongField for the exact value, because a float holds only 24 bits. The grid cuts the range to the type limits (0 for an unsigned type). A value outside the range shows at the nearest end, and an edit writes a value in the range.
[Range] takes float limits, so a limit past 2^24 is rounded to 7 digits: [Range(0, 1e12f)] gives 1 000 000 000 000.
#Styling
| Class |
Element |
tb-property-grid |
The grid (a border; it grows). |
tb-property-grid__toolbar |
The row with the search box and the sort button. |
tb-property-grid__search |
The search box. |
tb-property-grid__sort, tb-property-grid__sort--active |
The "A-Z" button, and the button in alphabetical sort (accent). |
tb-property-grid__body |
The scroll view with the rows. |
tb-property-grid__category |
A category foldout (bold header on the alternate surface). |
tb-property-grid__row |
A row: the editor field. The label is 40 % of the row. |
tb-property-grid__row--selected |
The selected row (hover surface, accent left border). |
tb-property-grid__long-input |
The exact number field in a long slider (110 px, no shrink). |
tb-property-grid__nested |
The foldout of a nested object. Its content is indented by the medium spacing. |
tb-property-grid__collection |
The foldout of an array or list. It has tb-property-grid__nested too. |
tb-property-grid__element |
A line with an element row and its remove button. |
tb-property-grid__move-up, tb-property-grid__move-down |
The "▲" and "▼" buttons of an element (20 x 18 px). They are disabled at the ends of the collection. |
tb-property-grid__remove |
The "-" button of an element (20 x 18 px). |
tb-property-grid__add |
The "+ Add" button under the elements. |
tb-property-grid__flags |
The toggles of a [Flags] enum (a wrapped row). |
tb-property-grid__empty |
The "No object" and "No properties" text (muted). |
tb-property-grid__description, __description-title, __description-text |
The description panel (48 px high at least), its bold title and its muted text. |