using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.TestTools; namespace BracerLib.Editor.Utility { /// /// A set-up for a component that can be a searchable list. Deprecated but fun to create. /// Doesn't work for entry adds. /// /// [ExcludeFromCoverage] [Obsolete] public class SearchableScrollView { public delegate void DataDelegate(int index, string originalValue, string newValue); public delegate bool ValidationDelegate(string entry); private static class Styles { public static readonly GUIStyle SELECTED; static Styles() { var t = new Texture2D(1, 1); t.SetPixel(0, 0, new Color(0.4f, 0.4f, 0.4f)); t.Apply(); SELECTED = new GUIStyle(EditorStyles.label); SELECTED.normal.textColor = Color.white; SELECTED.normal.background = t; } } private static readonly string EDIT_FIELD = "selectedTextField"; private readonly EditorSearchField searchField; private IList data; private IList dataLabels = new List(); private IList searchResults; private readonly IDictionary filteredLabelsToData = new Dictionary(); private EventType prevEventType; private int selectedIndex = -1; private bool isEditing; private bool isNewEntry; private bool cancelEdit; private bool editingFinished; private string originalEntryText; private string editedEntryText; private Vector2 scrollPosition; public float MaxWidth { get; set; } public IList Data { set { if (value == null) return; data = value; searchField.SearchString = string.Empty; dataLabels.Clear(); dataLabels = data.Select(d => d.ToString()).ToList(); filteredLabelsToData.Clear(); } } /// /// The modified index of the selection in the list with the search results taken into account. /// public int SelectedIndex => filteredLabelsToData.ContainsKey(selectedIndex) ? filteredLabelsToData[selectedIndex] : selectedIndex; public ValidationDelegate ValidateEntry { get; set; } public DataDelegate DataEntryAdded { get; set; } public DataDelegate DataEntryEdited { get; set; } public DataDelegate DataEntryRemoved { get; set; } private IList TargetData => (searchResults?.Count ?? 0) > 0 ? searchResults : dataLabels; public SearchableScrollView() { searchField = new EditorSearchField(); searchField.OnInputChanged += SearchFieldInputChanged; } public void OnGUI() { HandleEditing(); GUILayout.BeginVertical(); GUILayout.BeginVertical(EditorStyles.helpBox); searchField.OnGUI(); scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MaxWidth(MaxWidth)); GUILayout.BeginVertical(); // Output all keys in the list, with some being modified in style or interact-ability by way of flags var newEntryIndex = -1; var targetData = TargetData; for (var i = 0; i < (targetData?.Count ?? 0); i++) { var target = targetData![i]; var selected = selectedIndex == i; if (isEditing && selected) { if (originalEntryText == null) { originalEntryText = target; editedEntryText = target; } GUI.SetNextControlName(EDIT_FIELD); editedEntryText = GUILayout.TextField(editedEntryText, Styles.SELECTED); GUI.FocusControl(EDIT_FIELD); } else if (cancelEdit && selected) { cancelEdit = false; if (isNewEntry) newEntryIndex = i; else targetData[i] = originalEntryText; originalEntryText = null; editedEntryText = null; } else if (editingFinished && selected) { editingFinished = false; UpdateListValue(i, originalEntryText, editedEntryText); originalEntryText = null; editedEntryText = null; } else if (GUILayout.Button(target, selected ? Styles.SELECTED : EditorStyles.label) && !isEditing) selectedIndex = selected ? -1 : i; } GUILayout.FlexibleSpace(); GUILayout.EndVertical(); GUILayout.EndScrollView(); GUILayout.EndVertical(); GUILayout.Label("F2 = Edit entry\nEnter = Accept and apply changes\nEscape = Cancel entry edit", EditorStyles.helpBox); GUILayout.EndVertical(); } public void AddNewEntry() { searchField.SearchString = string.Empty; dataLabels.Add(string.Empty); selectedIndex = dataLabels.Count - 1; isEditing = true; isNewEntry = true; } public void DeleteEntry() { if (selectedIndex < 0) return; dataLabels.RemoveAt(SelectedIndex); DataEntryRemoved?.Invoke(SelectedIndex, null, null); searchField.SearchString = string.Empty; } private void FinalizeNewEntry() { if (!isEditing) return; isEditing = false; editingFinished = true; } private void HandleEditing() { var eventType = Event.current.type; var isKeyDown = eventType == EventType.KeyDown; if (selectedIndex < 0 || !isKeyDown || eventType == prevEventType) { if (!isKeyDown) prevEventType = eventType; return; } prevEventType = eventType; var keyCode = Event.current.keyCode; switch (keyCode) { case KeyCode.Escape: if (!isEditing) return; isEditing = false; cancelEdit = true; break; case KeyCode.Return: case KeyCode.KeypadEnter: FinalizeNewEntry(); break; case KeyCode.F2: isEditing = true; break; } } private void UpdateListValue(int index, string origText, string editedText) { if (editedText == origText || (ValidateEntry != null && !ValidateEntry(editedText))) { if (isNewEntry) { isNewEntry = false; dataLabels.RemoveAt(dataLabels.Count - 1); } return; } if (filteredLabelsToData.TryGetValue(index, out var origIndex)) { searchResults[index] = editedText; dataLabels[origIndex] = editedText; index = origIndex; } else dataLabels[index] = editedText; if (isNewEntry) { isNewEntry = false; DataEntryAdded?.Invoke(index, null, editedText); } else DataEntryEdited?.Invoke(index, origText, editedText); } private void SearchFieldInputChanged(string searchString) { searchResults ??= new List(); searchResults.Clear(); filteredLabelsToData.Clear(); if (searchString != null) { var newIndex = 0; searchResults = dataLabels.Where((label, index) => { var result = label.IndexOf(searchString, StringComparison.OrdinalIgnoreCase) >= 0; if (result) filteredLabelsToData.Add(newIndex++, index); return result; }).ToList(); } else searchResults = dataLabels; if (selectedIndex >= searchResults.Count) selectedIndex = -1; } } }