Files
chameleon/Assets/Scripts/PaintHud.Library.cs
T
adminandClaude Opus 4.8 86d187eb96 Select pre-filled name in paint save dialog
Select the whole pre-filled painting name one frame after activating the
input field so the first keystroke replaces it. Done explicitly so it works
regardless of the field's authored "On Focus Select All" toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 22:11:28 +02:00

309 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Binder for the "Paintings" library panel. The panel's visual layout (Panel_Paintings, the scrollable
/// list, an entry template, and the Save / Confirm dialogs) is authored inside the PaintHudPanel layout
/// prefab - this code only finds those objects by their stable names and wires the behaviour, exactly like
/// <see cref="WirePanel"/> does for the brush widgets. The list is populated by cloning the authored
/// Lib_EntryTemplate per saved slot. Save opens a name dialog (an existing name is overwritten, with a
/// warning shown while typing); Load applies the selected slot immediately; Delete goes through a
/// confirmation dialog. Loading replaces the current painting and, when networked, re-broadcasts the
/// loaded state to every other peer.
/// </summary>
public partial class PaintHud
{
// Stable widget names, matching the objects authored in the PaintHudPanel prefab.
private const string LibListContent = "Lib_ListContent";
private const string LibEntryTemplate = "Lib_EntryTemplate";
private const string LibSaveButton = "Lib_Save";
private const string LibLoadButton = "Lib_Load";
private const string LibDeleteButton = "Lib_Delete";
private const string LibResetButton = "Lib_ResetHud";
private const string LibSaveDialog = "Lib_SaveDialog";
private const string LibNameInput = "Lib_NameInput";
private const string LibSaveInfo = "Lib_SaveInfo";
private const string LibSaveOk = "Lib_SaveOk";
private const string LibSaveCancel = "Lib_SaveCancel";
private const string LibConfirmDialog = "Lib_ConfirmDialog";
private const string LibConfirmText = "Lib_ConfirmText";
private const string LibConfirmOk = "Lib_ConfirmOk";
private const string LibConfirmCancel = "Lib_ConfirmCancel";
private bool _libraryBound;
private RectTransform _listContent;
private GameObject _entryTemplate;
private Button _saveButton, _loadButton, _deleteButton, _resetButton;
private string _selectedPainting;
// Save dialog
private GameObject _saveDialog;
private TMP_InputField _nameInput;
private TMP_Text _saveInfo;
private Button _saveOkButton;
// Confirmation dialog (reused for Load + Delete)
private GameObject _confirmDialog;
private TMP_Text _confirmText;
private Button _confirmButton;
private Action _confirmAction;
private struct EntryRef { public string name; public Image bg; }
private readonly List<EntryRef> _entries = new List<EntryRef>();
private static readonly Color EntryNormal = new Color(0.18f, 0.18f, 0.22f, 1f);
private static readonly Color EntrySelected = new Color(0.20f, 0.45f, 0.80f, 1f);
private static readonly Color OkDanger = new Color(0.62f, 0.26f, 0.26f, 1f);
// --- Bind (find authored objects, hook listeners) ---
/// <summary>
/// Binds the authored library panel under <paramref name="root"/> (the instantiated layout prefab).
/// Idempotent: binds exactly once. No-op if the prefab carries no library objects.
/// </summary>
private void BindLibrary(Transform root)
{
if (_libraryBound || !root) return;
_listContent = FindDeep(root, LibListContent) as RectTransform;
var template = FindDeep(root, LibEntryTemplate);
_entryTemplate = template ? template.gameObject : null;
if (!_listContent || !_entryTemplate) return; // panel not present in this layout
_entryTemplate.SetActive(false);
_saveButton = Find<Button>(root, LibSaveButton);
_loadButton = Find<Button>(root, LibLoadButton);
_deleteButton = Find<Button>(root, LibDeleteButton);
_resetButton = Find<Button>(root, LibResetButton); // optional: present only if authored in the layout
Hook(_saveButton, OnSaveClicked);
Hook(_loadButton, OnLoadClicked);
Hook(_deleteButton, OnDeleteClicked);
Hook(_resetButton, OnResetClicked);
var sd = FindDeep(root, LibSaveDialog);
_saveDialog = sd ? sd.gameObject : null;
_nameInput = Find<TMP_InputField>(root, LibNameInput);
_saveInfo = Find<TMP_Text>(root, LibSaveInfo);
_saveOkButton = Find<Button>(root, LibSaveOk);
Hook(_saveOkButton, ConfirmSave);
Hook(Find<Button>(root, LibSaveCancel), () => { if (_saveDialog) _saveDialog.SetActive(false); });
if (_nameInput) _nameInput.onValueChanged.AddListener(_ => ValidateSaveName());
var cd = FindDeep(root, LibConfirmDialog);
_confirmDialog = cd ? cd.gameObject : null;
_confirmText = Find<TMP_Text>(root, LibConfirmText);
_confirmButton = Find<Button>(root, LibConfirmOk);
Hook(_confirmButton, OnConfirmClicked);
Hook(Find<Button>(root, LibConfirmCancel), () => { if (_confirmDialog) _confirmDialog.SetActive(false); });
HideDialogs();
_libraryBound = true;
UpdateButtonStates();
}
private static void Hook(Button button, Action onClick)
{
if (!button || onClick == null) return;
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => onClick());
}
// --- List ---
private void RefreshList()
{
if (!_listContent || !_entryTemplate) return;
for (int i = _listContent.childCount - 1; i >= 0; i--)
{
var child = _listContent.GetChild(i).gameObject;
if (child != _entryTemplate) Destroy(child);
}
_entries.Clear();
var names = MeshPainter.ListSavedPaintings();
if (!string.IsNullOrEmpty(_selectedPainting) && !names.Contains(_selectedPainting))
_selectedPainting = null;
foreach (var name in names)
{
var go = Instantiate(_entryTemplate, _listContent);
go.name = "Lib_Entry";
go.SetActive(true);
var bg = go.GetComponent<Image>();
if (bg) bg.color = name == _selectedPainting ? EntrySelected : EntryNormal;
var label = go.GetComponentInChildren<TMP_Text>(true);
if (label) label.text = name;
var btn = go.GetComponent<Button>();
if (btn)
{
string captured = name;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => SelectPainting(captured));
}
_entries.Add(new EntryRef { name = name, bg = bg });
}
UpdateButtonStates();
}
private void SelectPainting(string name)
{
_selectedPainting = name;
RefreshHighlights();
UpdateButtonStates();
}
private void RefreshHighlights()
{
foreach (var e in _entries)
if (e.bg) e.bg.color = e.name == _selectedPainting ? EntrySelected : EntryNormal;
}
private void UpdateButtonStates()
{
bool hasPainter = painter;
bool hasSel = !string.IsNullOrEmpty(_selectedPainting);
if (_saveButton) _saveButton.interactable = hasPainter;
if (_loadButton) _loadButton.interactable = hasPainter && hasSel;
if (_deleteButton) _deleteButton.interactable = hasSel;
}
private void HideDialogs()
{
if (_saveDialog) _saveDialog.SetActive(false);
if (_confirmDialog) _confirmDialog.SetActive(false);
}
// --- Actions ---
private void OnSaveClicked()
{
if (!painter || !_saveDialog) return;
// Pre-fill the currently selected painting's name so "Save" defaults to overwriting it (empty when
// nothing is selected) and select that text so the first keystroke replaces it for a fresh name.
if (_nameInput) _nameInput.text = _selectedPainting ?? "";
if (_saveInfo) _saveInfo.text = "";
_saveDialog.SetActive(true);
_saveDialog.transform.SetAsLastSibling(); // a dragged panel may have moved itself in front; stay on top
ValidateSaveName();
if (_nameInput)
{
_nameInput.ActivateInputField();
StartCoroutine(SelectNameFieldNextFrame());
}
}
// ActivateInputField only takes focus on a later update and resets the caret, so selecting the text this
// frame would be cleared. Wait one frame, then select the whole pre-filled name - done explicitly here so
// it works regardless of the field's authored "On Focus Select All" toggle.
private System.Collections.IEnumerator SelectNameFieldNextFrame()
{
yield return null;
if (!_nameInput) yield break;
_nameInput.selectionStringAnchorPosition = _nameInput.text.Length;
_nameInput.selectionStringFocusPosition = 0;
_nameInput.ForceLabelUpdate();
}
private void ValidateSaveName()
{
string name = _nameInput ? _nameInput.text.Trim() : "";
bool empty = string.IsNullOrEmpty(name);
bool exists = !empty && MeshPainter.PaintExists(name);
// Overwrite is allowed -> the OK button stays enabled even when the name exists; the warning is
// purely informational so you know you are about to replace an existing slot.
if (_saveOkButton) _saveOkButton.interactable = !empty && painter;
if (_saveInfo) _saveInfo.text = exists ? "A painting with this name already exists and will be overwritten." : "";
}
private void ConfirmSave()
{
if (!painter || !_nameInput) return;
string name = _nameInput.text.Trim();
if (string.IsNullOrEmpty(name)) { ValidateSaveName(); return; }
if (painter.SavePaintAs(name, overwrite: true))
{
_selectedPainting = name;
if (_saveDialog) _saveDialog.SetActive(false);
RefreshList();
}
else if (_saveInfo)
{
_saveInfo.text = "Save failed.";
}
}
private void OnLoadClicked()
{
if (!painter || string.IsNullOrEmpty(_selectedPainting)) return;
DoLoad(_selectedPainting); // load immediately, no confirmation
}
private void OnDeleteClicked()
{
if (string.IsNullOrEmpty(_selectedPainting)) return;
string name = _selectedPainting;
ShowConfirm($"Delete \"{name}\"?", OkDanger, () => DoDelete(name));
}
/// <summary>Reset the panels + brush values to their authored defaults (confirmed; the dialog reuses the
/// library's confirm path). Falls back to an immediate reset if no confirm dialog is authored.</summary>
private void OnResetClicked()
{
if (_confirmDialog)
ShowConfirm("Reset the HUD panels and brush values to their defaults?", OkDanger, ResetHudToDefaults);
else
ResetHudToDefaults();
}
private void DoLoad(string name)
{
if (!painter || !painter.LoadPaintByName(name)) return;
// The load also restored the slot's material scalars onto the painter (emission + camo from v3,
// the painted-height knobs from v4) -> push them into the sliders so the HUD reflects the loaded
// look, exactly like a pipette pick does. SyncCamoSliders covers the camo + height sliders.
SyncFromPainter();
SyncCamoSliders();
// Re-sync the loaded baseline to every other peer (no-op offline / single-player). The restored
// emission/camo replicate on their own via PlayerPaintSync's owner-authoritative NetworkVariables.
var sync = painter.GetComponentInParent<PlayerPaintSync>();
if (sync) sync.BroadcastLoadedPaint();
}
private void DoDelete(string name)
{
MeshPainter.DeletePaint(name);
if (_selectedPainting == name) _selectedPainting = null;
RefreshList();
}
private void ShowConfirm(string message, Color okColor, Action onConfirm)
{
if (!_confirmDialog) return;
if (_confirmText) _confirmText.text = message;
_confirmAction = onConfirm;
if (_confirmButton && _confirmButton.targetGraphic is Image okImg) okImg.color = okColor;
_confirmDialog.SetActive(true);
_confirmDialog.transform.SetAsLastSibling(); // a dragged panel may have moved itself in front; stay on top
}
private void OnConfirmClicked()
{
var act = _confirmAction;
_confirmAction = null;
if (_confirmDialog) _confirmDialog.SetActive(false);
act?.Invoke();
}
}