594 lines
25 KiB
C#
594 lines
25 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEditor;
|
||
using UnityEditor.UIElements;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
using Ashwild.Building;
|
||
using Ashwild.Inventory;
|
||
|
||
namespace Ashwild.EditorTools
|
||
{
|
||
/// <summary>
|
||
/// The custom editor for a BuildableData shown in the right pane of the Ashwild Database. Replaces
|
||
/// the default ScriptableObject inspector with a hero header (large clickable icon, editable name,
|
||
/// "Buildable" badge) and cards for the identity (description), the ghost/built prefabs, the resource
|
||
/// cost, and the placed structure's health. The cost card reuses the recipe editor's interactive chip
|
||
/// strip — one clickable item card (with a −/+ quantity stepper and a remove button) per cost line,
|
||
/// joined by "+", followed by a dashed add tile — so authoring a price feels identical to authoring a
|
||
/// recipe. All fields bind live to the asset; changing the name or icon notifies the list so the row
|
||
/// updates without a full rebuild.
|
||
/// </summary>
|
||
public class BuildableEditorView
|
||
{
|
||
#region State
|
||
|
||
public VisualElement Root { get; }
|
||
|
||
private const int IconPickerControlId = 0x41534842;
|
||
private const int CostPickerControlId = 0x41534843;
|
||
|
||
private readonly BuildableData buildable;
|
||
private readonly SerializedObject so;
|
||
private readonly SerializedProperty costProp;
|
||
private readonly Action onMetaChanged;
|
||
private readonly Action onAssetChanged;
|
||
|
||
private VisualElement iconPreview;
|
||
private VisualElement costStrip;
|
||
private Action<ItemData> costPickHandler;
|
||
private bool costPickCommitsOnCloseOnly;
|
||
|
||
private GameObject generationSource;
|
||
private BuildSnapLayout generationLayout = BuildSnapLayout.None;
|
||
|
||
#endregion
|
||
|
||
#region Construction
|
||
|
||
/// <summary>
|
||
/// Builds the full editor tree for a buildable. <paramref name="onMetaChanged"/> is raised when
|
||
/// the display name or icon changes so the list can refresh that row;
|
||
/// <paramref name="onAssetChanged"/> re-renders the whole view after a structural change (prefab
|
||
/// generation, asset rename) that the live bindings alone cannot reflect.
|
||
/// </summary>
|
||
public BuildableEditorView(BuildableData buildable, Action onMetaChanged, Action onAssetChanged)
|
||
{
|
||
this.buildable = buildable;
|
||
this.onMetaChanged = onMetaChanged;
|
||
this.onAssetChanged = onAssetChanged;
|
||
so = new SerializedObject(buildable);
|
||
costProp = so.FindProperty("cost");
|
||
|
||
Root = new VisualElement();
|
||
Root.Add(BuildHero());
|
||
Root.Add(BuildIdentityCard());
|
||
Root.Add(BuildPrefabsCard());
|
||
Root.Add(BuildCostCard());
|
||
Root.Add(BuildPlacementCard());
|
||
Root.Add(BuildHealthCard());
|
||
Root.Add(BuildPickerProxy());
|
||
|
||
RefreshCost();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Hero
|
||
|
||
/// <summary>
|
||
/// Builds the hero header: a large clickable icon preview, the editable display name and the
|
||
/// "Buildable" badge. Name/icon changes notify the list so its row follows immediately.
|
||
/// </summary>
|
||
private VisualElement BuildHero()
|
||
{
|
||
VisualElement hero = new VisualElement();
|
||
hero.AddToClassList("ash-hero");
|
||
|
||
iconPreview = new VisualElement();
|
||
iconPreview.AddToClassList("ash-hero__icon");
|
||
iconPreview.AddToClassList("ash-hero__icon--clickable");
|
||
iconPreview.tooltip = "Click to change the buildable icon";
|
||
iconPreview.RegisterCallback<ClickEvent>(_ => OpenIconPicker());
|
||
UpdateIconPreview(buildable.Icon);
|
||
hero.Add(iconPreview);
|
||
|
||
IMGUIContainer iconPickerProxy = new IMGUIContainer(HandleIconPickerCommands);
|
||
iconPickerProxy.style.position = Position.Absolute;
|
||
iconPickerProxy.style.width = 1;
|
||
iconPickerProxy.style.height = 1;
|
||
hero.Add(iconPickerProxy);
|
||
|
||
VisualElement info = new VisualElement();
|
||
info.AddToClassList("ash-hero__info");
|
||
|
||
TextField nameField = new TextField { value = buildable.DisplayName };
|
||
nameField.AddToClassList("ash-hero__name");
|
||
nameField.BindProperty(so.FindProperty("displayName"));
|
||
nameField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke());
|
||
nameField.RegisterCallback<FocusOutEvent>(_ => SyncAssetName());
|
||
info.Add(AshwildUI.EditableNameRow(nameField));
|
||
|
||
VisualElement typeRow = new VisualElement();
|
||
typeRow.AddToClassList("ash-hero__typerow");
|
||
typeRow.Add(AshwildUI.Badge("Buildable", AshwildUI.BuildableColor));
|
||
info.Add(typeRow);
|
||
|
||
hero.Add(info);
|
||
return hero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Renames the asset file to match the display name once the designer leaves the name field, so a
|
||
/// buildable titled "Wood Wall" stops living on disk as "NewBuildable 2". Deliberately fired on
|
||
/// focus-out rather than on every keystroke — renaming per character would spam the AssetDatabase
|
||
/// and leave a trail of half-typed file names. Re-renders the view so the header reflects the new
|
||
/// asset identity.
|
||
/// </summary>
|
||
private void SyncAssetName()
|
||
{
|
||
if (BuildableAssetFactory.RenameAssetToDisplayName(buildable))
|
||
onAssetChanged?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Shows the buildable's icon in the hero preview, or a neutral placeholder when none is set.
|
||
/// </summary>
|
||
private void UpdateIconPreview(Sprite sprite)
|
||
{
|
||
iconPreview.style.backgroundImage = sprite != null ? new StyleBackground(sprite) : new StyleBackground();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Opens Unity's sprite object picker, seeded with the current icon, so the designer can change
|
||
/// the buildable icon by clicking the hero preview directly.
|
||
/// </summary>
|
||
private void OpenIconPicker()
|
||
{
|
||
EditorGUIUtility.ShowObjectPicker<Sprite>(buildable.Icon, false, string.Empty, IconPickerControlId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Listens (through the hidden IMGUI proxy) for the picker selecting/closing on our control id
|
||
/// and applies the chosen sprite live, so the hero preview and the list row update immediately.
|
||
/// </summary>
|
||
private void HandleIconPickerCommands()
|
||
{
|
||
Event evt = Event.current;
|
||
if (evt == null || evt.type != EventType.ExecuteCommand) return;
|
||
if (EditorGUIUtility.GetObjectPickerControlID() != IconPickerControlId) return;
|
||
if (evt.commandName != "ObjectSelectorUpdated" && evt.commandName != "ObjectSelectorClosed") return;
|
||
|
||
AssignIcon(EditorGUIUtility.GetObjectPickerObject() as Sprite);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Writes the picked icon onto the asset (no-op when unchanged) and refreshes the hero preview
|
||
/// and the list row.
|
||
/// </summary>
|
||
private void AssignIcon(Sprite sprite)
|
||
{
|
||
SerializedProperty iconProp = so.FindProperty("icon");
|
||
if (iconProp.objectReferenceValue == sprite) return;
|
||
|
||
iconProp.objectReferenceValue = sprite;
|
||
so.ApplyModifiedProperties();
|
||
UpdateIconPreview(sprite);
|
||
onMetaChanged?.Invoke();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Identity & Prefab Cards
|
||
|
||
/// <summary>
|
||
/// Identity card: the description shown on the menu card, plus the icon field (kept in sync
|
||
/// with the hero preview and the list row when edited here).
|
||
/// </summary>
|
||
private VisualElement BuildIdentityCard()
|
||
{
|
||
VisualElement card = AshwildUI.Card("Identity");
|
||
|
||
TextField description = new TextField("Description") { multiline = true };
|
||
description.AddToClassList("ash-description");
|
||
description.BindProperty(so.FindProperty("description"));
|
||
card.Add(description);
|
||
|
||
ObjectField icon = new ObjectField("Icon") { objectType = typeof(Sprite), allowSceneObjects = false };
|
||
icon.BindProperty(so.FindProperty("icon"));
|
||
icon.RegisterValueChangedCallback(evt =>
|
||
{
|
||
UpdateIconPreview(evt.newValue as Sprite);
|
||
onMetaChanged?.Invoke();
|
||
});
|
||
card.Add(icon);
|
||
|
||
return card;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Prefabs card: the semi-transparent ghost spawned while positioning and the real structure
|
||
/// spawned once placement is confirmed. When either slot is still empty the card also offers the
|
||
/// one-click generator below the fields, so the common path (drop a mesh in, press Generate) never
|
||
/// leaves the window. The generator's visibility is resolved when the view is built rather than
|
||
/// bound to the two fields: BindProperty raises a change event on its initial bind, so re-rendering
|
||
/// from those callbacks would loop the view rebuild endlessly. Generating re-renders explicitly,
|
||
/// and a hand-assigned prefab is picked up the next time the buildable is selected.
|
||
/// </summary>
|
||
private VisualElement BuildPrefabsCard()
|
||
{
|
||
VisualElement card = AshwildUI.Card("Prefabs");
|
||
|
||
ObjectField ghost = new ObjectField("Ghost Prefab") { objectType = typeof(GameObject), allowSceneObjects = false };
|
||
ghost.BindProperty(so.FindProperty("ghostPrefab"));
|
||
card.Add(ghost);
|
||
|
||
ObjectField built = new ObjectField("Built Prefab") { objectType = typeof(GameObject), allowSceneObjects = false };
|
||
built.BindProperty(so.FindProperty("builtPrefab"));
|
||
card.Add(built);
|
||
|
||
if (buildable.GhostPrefab == null || buildable.BuiltPrefab == null)
|
||
card.Add(BuildGeneratorBlock());
|
||
|
||
return card;
|
||
}
|
||
|
||
/// <summary>
|
||
/// The generator shown while a prefab slot is empty: pick a source model or prefab, tick which
|
||
/// socket sets the piece should offer, and press the button to author the missing prefab(s) —
|
||
/// collider, gameplay components, networking and snap sockets included. Only the empty slots are
|
||
/// generated, so regenerating a ghost never clobbers a built prefab that has already been hand-tuned.
|
||
///
|
||
/// The layout is a mask rather than a single choice because one piece usually offers several kinds
|
||
/// of connection: a floor both chains to other floors and hosts walls. Choosing a source pre-ticks
|
||
/// the set inferred from the mesh proportions, which the designer is free to change.
|
||
/// </summary>
|
||
private VisualElement BuildGeneratorBlock()
|
||
{
|
||
VisualElement block = new VisualElement();
|
||
block.AddToClassList("ash-generator");
|
||
|
||
Label heading = new Label("Generate from a source model");
|
||
heading.AddToClassList("ash-generator__title");
|
||
block.Add(heading);
|
||
|
||
MaskField layoutField = null;
|
||
|
||
ObjectField sourceField = new ObjectField("Source Mesh / Prefab")
|
||
{
|
||
objectType = typeof(GameObject),
|
||
allowSceneObjects = false,
|
||
tooltip = "The authored model (FBX) or prefab to wrap. It is nested as a child, never flattened, so you can keep editing it."
|
||
};
|
||
sourceField.RegisterValueChangedCallback(evt =>
|
||
{
|
||
generationSource = evt.newValue as GameObject;
|
||
generationLayout = BuildableAssetFactory.InferLayout(generationSource);
|
||
layoutField?.SetValueWithoutNotify((int)generationLayout);
|
||
});
|
||
block.Add(sourceField);
|
||
|
||
layoutField = new MaskField(
|
||
"Snap Layout",
|
||
new List<string> { "Floor Edges", "Wall Mounts", "Wall Body", "Roof Mount", "Roof Body", "Pillar Caps" },
|
||
(int)generationLayout)
|
||
{
|
||
tooltip = "Which sockets to place — combinable. A floor slab usually wants Floor Edges (chain to other slabs) AND Wall Mounts (walls stand on its edges); a wall piece wants Wall Body."
|
||
};
|
||
layoutField.RegisterValueChangedCallback(evt => generationLayout = (BuildSnapLayout)evt.newValue);
|
||
block.Add(layoutField);
|
||
|
||
Button generate = new Button(GenerateMissingPrefabs) { text = DescribeGeneration() };
|
||
generate.AddToClassList("ash-btn");
|
||
generate.AddToClassList("ash-btn--primary");
|
||
block.Add(generate);
|
||
|
||
Label hint = new Label("Sockets are placed from the mesh bounds: floor edges face outward, wall mounts face up, a wall's foot faces down, and the roof mount is a single socket centred on the top face. Treat the layout as a starting point — nudge them in the prefab afterwards.");
|
||
hint.AddToClassList("ash-generator__hint");
|
||
block.Add(hint);
|
||
|
||
return block;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Labels the generate button with exactly what it will create, so the designer can tell at a
|
||
/// glance whether pressing it touches one slot or both.
|
||
/// </summary>
|
||
private string DescribeGeneration()
|
||
{
|
||
bool needsBuilt = buildable.BuiltPrefab == null;
|
||
bool needsGhost = buildable.GhostPrefab == null;
|
||
|
||
if (needsBuilt && needsGhost) return "Generate Built + Ghost Prefabs";
|
||
return needsBuilt ? "Generate Built Prefab" : "Generate Ghost Prefab";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Runs the generation for whichever slots are empty and re-renders the view so the new prefabs
|
||
/// appear in their fields and the generator collapses away.
|
||
/// </summary>
|
||
private void GenerateMissingPrefabs()
|
||
{
|
||
bool generated = BuildableAssetFactory.GeneratePrefabs(
|
||
buildable,
|
||
generationSource,
|
||
generationLayout,
|
||
buildable.BuiltPrefab == null,
|
||
buildable.GhostPrefab == null);
|
||
|
||
if (generated) onAssetChanged?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Health card: the hit points the placed structure starts with, edited as a plain float field.
|
||
/// An empty cost makes the build free; likewise this drives the proportional demolition refund.
|
||
/// </summary>
|
||
private VisualElement BuildHealthCard()
|
||
{
|
||
VisualElement card = AshwildUI.Card("Health");
|
||
|
||
FloatField maxHealth = new FloatField("Max Health");
|
||
maxHealth.BindProperty(so.FindProperty("maxHealth"));
|
||
card.Add(maxHealth);
|
||
|
||
return card;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Placement card: what this structure may rest on. Generating the prefabs sets it from the snap
|
||
/// layout, so this is the override for a piece the layout cannot classify — or for a buildable
|
||
/// authored before the rule existed, which defaults to the permissive Ground Or Snap.
|
||
/// </summary>
|
||
private VisualElement BuildPlacementCard()
|
||
{
|
||
VisualElement card = AshwildUI.Card("Placement");
|
||
|
||
EnumField support = new EnumField("Support")
|
||
{
|
||
tooltip = "Ground Or Snap: free placement anywhere the aim lands, or connected. Snap Only: valid solely while connected to a matching socket — use it for walls, ceilings and roofs so they cannot be dropped in mid-air."
|
||
};
|
||
support.BindProperty(so.FindProperty("support"));
|
||
card.Add(support);
|
||
|
||
return card;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Cost Card
|
||
|
||
/// <summary>
|
||
/// Cost card: an interactive chip strip mirroring the recipe editor. Each cost line is a clickable
|
||
/// item card with a −/+ quantity stepper and a remove button, the lines are joined by "+", and a
|
||
/// dashed add tile appends a new line. An empty strip means a free build.
|
||
/// </summary>
|
||
private VisualElement BuildCostCard()
|
||
{
|
||
VisualElement card = AshwildUI.Card("Cost");
|
||
|
||
costStrip = new VisualElement();
|
||
costStrip.AddToClassList("ash-equation");
|
||
card.Add(costStrip);
|
||
|
||
return card;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Repaints the whole cost strip: one interactive card per cost line (joined by "+"), then the
|
||
/// dashed add tile. Shows only the add tile when the build is free.
|
||
/// </summary>
|
||
private void RefreshCost()
|
||
{
|
||
costStrip.Clear();
|
||
|
||
int count = costProp.arraySize;
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
if (i > 0) costStrip.Add(Operator("+"));
|
||
costStrip.Add(BuildCostCardChip(i));
|
||
}
|
||
|
||
if (count > 0) costStrip.Add(Operator("+"));
|
||
costStrip.Add(BuildAddTile());
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds the interactive card for the cost line at <paramref name="index"/>: a clickable icon
|
||
/// (change item), the name, a −/+ quantity stepper, and a remove button.
|
||
/// </summary>
|
||
private VisualElement BuildCostCardChip(int index)
|
||
{
|
||
SerializedProperty element = costProp.GetArrayElementAtIndex(index);
|
||
ItemData item = element.FindPropertyRelative("item").objectReferenceValue as ItemData;
|
||
int quantity = element.FindPropertyRelative("quantity").intValue;
|
||
|
||
VisualElement card = MakeCard(item, () => OpenItemPicker(item, true, picked => SetCostItem(index, picked)));
|
||
card.Add(Stepper(quantity, delta => AdjustCostQuantity(index, delta)));
|
||
|
||
Button remove = new Button(() => RemoveCost(index)) { text = "✕" };
|
||
remove.AddToClassList("ash-rchip__remove");
|
||
card.Add(remove);
|
||
|
||
return card;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds the dashed "+" tile that appends a new cost line once an item is picked.
|
||
/// </summary>
|
||
private VisualElement BuildAddTile()
|
||
{
|
||
VisualElement tile = new VisualElement();
|
||
tile.AddToClassList("ash-rchip");
|
||
tile.AddToClassList("ash-rchip--add");
|
||
tile.tooltip = "Add a cost line";
|
||
tile.RegisterCallback<ClickEvent>(_ => OpenItemPicker(null, false, AddCost));
|
||
|
||
Label plus = new Label("+");
|
||
plus.AddToClassList("ash-rchip__plus");
|
||
tile.Add(plus);
|
||
|
||
return tile;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds the shared card body (icon + name) for an item, with the icon wired to open a picker.
|
||
/// The icon shows the item's sprite, or a neutral placeholder when unset.
|
||
/// </summary>
|
||
private VisualElement MakeCard(ItemData item, Action onIconClicked)
|
||
{
|
||
VisualElement card = new VisualElement();
|
||
card.AddToClassList("ash-rchip");
|
||
|
||
VisualElement icon = new VisualElement();
|
||
icon.AddToClassList("ash-rchip__icon");
|
||
icon.tooltip = "Click to choose an item";
|
||
if (item != null && item.Icon != null) icon.style.backgroundImage = new StyleBackground(item.Icon);
|
||
icon.RegisterCallback<ClickEvent>(_ => onIconClicked());
|
||
card.Add(icon);
|
||
|
||
Label name = new Label(item != null ? item.ItemName : "Choose…");
|
||
name.AddToClassList("ash-rchip__name");
|
||
card.Add(name);
|
||
|
||
return card;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds a −/+ quantity stepper showing the current value; <paramref name="onDelta"/> receives
|
||
/// -1 or +1.
|
||
/// </summary>
|
||
private VisualElement Stepper(int quantity, Action<int> onDelta)
|
||
{
|
||
VisualElement stepper = new VisualElement();
|
||
stepper.AddToClassList("ash-rchip__stepper");
|
||
|
||
Button minus = new Button(() => onDelta(-1)) { text = "−" };
|
||
minus.AddToClassList("ash-rchip__step");
|
||
stepper.Add(minus);
|
||
|
||
Label value = new Label(quantity.ToString());
|
||
value.AddToClassList("ash-rchip__qty");
|
||
stepper.Add(value);
|
||
|
||
Button plus = new Button(() => onDelta(1)) { text = "+" };
|
||
plus.AddToClassList("ash-rchip__step");
|
||
stepper.Add(plus);
|
||
|
||
return stepper;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds a large "+" operator glyph between cost cards.
|
||
/// </summary>
|
||
private static Label Operator(string glyph)
|
||
{
|
||
Label op = new Label(glyph);
|
||
op.AddToClassList("ash-equation__op");
|
||
return op;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Cost Mutations
|
||
|
||
/// <summary>
|
||
/// Sets the item of an existing cost line and repaints.
|
||
/// </summary>
|
||
private void SetCostItem(int index, ItemData item)
|
||
{
|
||
if (index < 0 || index >= costProp.arraySize) return;
|
||
costProp.GetArrayElementAtIndex(index).FindPropertyRelative("item").objectReferenceValue = item;
|
||
so.ApplyModifiedProperties();
|
||
RefreshCost();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Changes a cost line's quantity by a delta, clamped to a minimum of one, and repaints.
|
||
/// </summary>
|
||
private void AdjustCostQuantity(int index, int delta)
|
||
{
|
||
if (index < 0 || index >= costProp.arraySize) return;
|
||
SerializedProperty quantity = costProp.GetArrayElementAtIndex(index).FindPropertyRelative("quantity");
|
||
quantity.intValue = Mathf.Max(1, quantity.intValue + delta);
|
||
so.ApplyModifiedProperties();
|
||
RefreshCost();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Appends a new cost line (quantity 1) for the picked item and repaints; ignores a null pick
|
||
/// (e.g. the picker was cancelled).
|
||
/// </summary>
|
||
private void AddCost(ItemData item)
|
||
{
|
||
if (item == null) return;
|
||
|
||
int index = costProp.arraySize;
|
||
costProp.arraySize++;
|
||
SerializedProperty element = costProp.GetArrayElementAtIndex(index);
|
||
element.FindPropertyRelative("item").objectReferenceValue = item;
|
||
element.FindPropertyRelative("quantity").intValue = 1;
|
||
so.ApplyModifiedProperties();
|
||
RefreshCost();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes the cost line at the given index and repaints.
|
||
/// </summary>
|
||
private void RemoveCost(int index)
|
||
{
|
||
if (index < 0 || index >= costProp.arraySize) return;
|
||
costProp.DeleteArrayElementAtIndex(index);
|
||
so.ApplyModifiedProperties();
|
||
RefreshCost();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Item Picker
|
||
|
||
/// <summary>
|
||
/// Builds the hidden IMGUI proxy that relays Unity's object-picker commands to the active cost
|
||
/// pick handler (the editor window is UI Toolkit, which can't receive those commands directly).
|
||
/// </summary>
|
||
private VisualElement BuildPickerProxy()
|
||
{
|
||
IMGUIContainer proxy = new IMGUIContainer(HandleItemPickerCommands);
|
||
proxy.style.position = Position.Absolute;
|
||
proxy.style.width = 1;
|
||
proxy.style.height = 1;
|
||
return proxy;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Opens Unity's item picker seeded with the current item. When <paramref name="commitLive"/>
|
||
/// is true the pick applies on every highlight (live preview, e.g. changing an existing line);
|
||
/// otherwise it applies only when the picker closes (e.g. adding a new line — commit once).
|
||
/// </summary>
|
||
private void OpenItemPicker(ItemData seed, bool commitLive, Action<ItemData> onPicked)
|
||
{
|
||
costPickHandler = onPicked;
|
||
costPickCommitsOnCloseOnly = !commitLive;
|
||
EditorGUIUtility.ShowObjectPicker<ItemData>(seed, false, string.Empty, CostPickerControlId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Forwards the picker's selection to the active handler, honouring the live-vs-on-close policy.
|
||
/// </summary>
|
||
private void HandleItemPickerCommands()
|
||
{
|
||
Event evt = Event.current;
|
||
if (evt == null || evt.type != EventType.ExecuteCommand) return;
|
||
if (EditorGUIUtility.GetObjectPickerControlID() != CostPickerControlId) return;
|
||
|
||
bool updated = evt.commandName == "ObjectSelectorUpdated";
|
||
bool closed = evt.commandName == "ObjectSelectorClosed";
|
||
if (!updated && !closed) return;
|
||
if (updated && costPickCommitsOnCloseOnly) return;
|
||
|
||
costPickHandler?.Invoke(EditorGUIUtility.GetObjectPickerObject() as ItemData);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|