Files
chameleon/Assets/Scripts/PaintHud.Library.cs
T
adminandClaude Opus 4.8 44cea8dd15 Load paintings instantly and allow overwriting saves in the paint HUD
Load now applies the selected slot immediately instead of going through a
confirmation dialog. Save keeps its OK button enabled even when the name
already exists (the warning text stays, purely informational) and writes
through with overwrite, so re-saving under the same name replaces the slot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:47:16 +02:00

290 lines
11 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;
if (_nameInput) _nameInput.text = "";
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();
}
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 emission intensity + camo knobs onto the painter (v3 slots) ->
// push them into the sliders so the HUD reflects the loaded look, exactly like a pipette pick does.
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();
}
}