优化
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.RhythmGame;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public class DynamicUISlider : DynamicUIElement
|
||||
{
|
||||
public Slider slider;
|
||||
public TMP_InputField valueInputField;
|
||||
public TMP_InputField minInputField;
|
||||
public TMP_InputField maxInputField;
|
||||
|
||||
private UnityAction<float> customAction;
|
||||
private bool updatingInternally; // 防止输入框和滑块互相递归更新
|
||||
|
||||
#region [初始化] Initialize
|
||||
|
||||
public override void Initialize(IBaseElement baseElement, string title, string parameterName)
|
||||
{
|
||||
Initialize(baseElement, title, parameterName, 0f, 1f, false);
|
||||
}
|
||||
|
||||
public void Initialize(IBaseElement baseElement, string title, string parameterName,
|
||||
float min, float max, bool wholeNumbers = false)
|
||||
{
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.wholeNumbers = wholeNumbers;
|
||||
|
||||
// [对象池安全] 精准解绑业务代理
|
||||
slider.onValueChanged.RemoveListener(OnSliderValueChanged);
|
||||
valueInputField?.onEndEdit.RemoveListener(OnValueInputEndEdit);
|
||||
minInputField?.onEndEdit.RemoveListener(OnMinInputEndEdit);
|
||||
maxInputField?.onEndEdit.RemoveListener(OnMaxInputEndEdit);
|
||||
customAction = null;
|
||||
|
||||
base.Initialize(baseElement, title, parameterName);
|
||||
|
||||
if (parameterName != string.Empty)
|
||||
{
|
||||
var val = ReflectionHelper.GetDeepValue(connectedBaseElement, parameterName);
|
||||
if (val != null)
|
||||
{
|
||||
slider.SetValueWithoutNotify(System.Convert.ToSingle(val));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[DynamicUI] 尝试绑定 {title} ({parameterName}) 失败,由于其值或路径无效。");
|
||||
}
|
||||
slider.onValueChanged.AddListener(OnSliderValueChanged);
|
||||
}
|
||||
else
|
||||
{
|
||||
slider.onValueChanged.AddListener(OnSliderValueChanged);
|
||||
}
|
||||
|
||||
SyncAllInputFields();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region [滑块变化回调] Slider Callback
|
||||
|
||||
private void OnSliderValueChanged(float value)
|
||||
{
|
||||
if (updatingInternally) return;
|
||||
updatingInternally = true;
|
||||
|
||||
UpdateValueInputText(value);
|
||||
if (parameterName != string.Empty)
|
||||
{
|
||||
ApplyParameters(value);
|
||||
}
|
||||
customAction?.Invoke(value);
|
||||
|
||||
updatingInternally = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region [输入框回调] Input Field Callbacks
|
||||
|
||||
private void OnValueInputEndEdit(string text)
|
||||
{
|
||||
if (updatingInternally) return;
|
||||
if (!float.TryParse(text, out float val)) return;
|
||||
|
||||
val = Mathf.Clamp(val, slider.minValue, slider.maxValue);
|
||||
|
||||
updatingInternally = true;
|
||||
|
||||
slider.SetValueWithoutNotify(val);
|
||||
UpdateValueInputText(val);
|
||||
if (parameterName != string.Empty)
|
||||
{
|
||||
ApplyParameters(val);
|
||||
}
|
||||
customAction?.Invoke(val);
|
||||
|
||||
updatingInternally = false;
|
||||
}
|
||||
|
||||
private void OnMinInputEndEdit(string text)
|
||||
{
|
||||
if (updatingInternally) return;
|
||||
if (!float.TryParse(text, out float val)) return;
|
||||
|
||||
if (val >= slider.maxValue) val = slider.maxValue - 0.0001f;
|
||||
slider.minValue = val;
|
||||
|
||||
if (slider.value < val)
|
||||
{
|
||||
slider.value = val;
|
||||
}
|
||||
|
||||
UpdateMinMaxInputTexts();
|
||||
UpdateValueInputText(slider.value);
|
||||
}
|
||||
|
||||
private void OnMaxInputEndEdit(string text)
|
||||
{
|
||||
if (updatingInternally) return;
|
||||
if (!float.TryParse(text, out float val)) return;
|
||||
|
||||
if (val <= slider.minValue) val = slider.minValue + 0.0001f;
|
||||
slider.maxValue = val;
|
||||
|
||||
if (slider.value > val)
|
||||
{
|
||||
slider.value = val;
|
||||
}
|
||||
|
||||
UpdateMinMaxInputTexts();
|
||||
UpdateValueInputText(slider.value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region [参数写入] Apply Parameters
|
||||
|
||||
private void ApplyParameters(float value)
|
||||
{
|
||||
object convertedValue;
|
||||
if (slider.wholeNumbers)
|
||||
{
|
||||
convertedValue = Mathf.RoundToInt(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedValue = value;
|
||||
}
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(
|
||||
new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, convertedValue));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region [UI 同步] UI Sync Helpers
|
||||
|
||||
private void SyncAllInputFields()
|
||||
{
|
||||
UpdateValueInputText(slider.value);
|
||||
UpdateMinMaxInputTexts();
|
||||
}
|
||||
|
||||
private void UpdateValueInputText(float value)
|
||||
{
|
||||
if (valueInputField != null)
|
||||
{
|
||||
valueInputField.SetTextWithoutNotify(slider.wholeNumbers
|
||||
? Mathf.RoundToInt(value).ToString()
|
||||
: value.ToString("F3"));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMinMaxInputTexts()
|
||||
{
|
||||
string FormatRangeVal(float v)
|
||||
{
|
||||
return slider.wholeNumbers ? Mathf.RoundToInt(v).ToString() : v.ToString("F3");
|
||||
}
|
||||
|
||||
if (minInputField != null)
|
||||
{
|
||||
minInputField.SetTextWithoutNotify(FormatRangeVal(slider.minValue));
|
||||
}
|
||||
if (maxInputField != null)
|
||||
{
|
||||
maxInputField.SetTextWithoutNotify(FormatRangeVal(slider.maxValue));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region [Public API]
|
||||
|
||||
public void SetRange(float min, float max)
|
||||
{
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
UpdateMinMaxInputTexts();
|
||||
UpdateValueInputText(slider.value);
|
||||
}
|
||||
|
||||
public void SetWholeNumbers(bool wholeNumbers)
|
||||
{
|
||||
slider.wholeNumbers = wholeNumbers;
|
||||
SyncAllInputFields();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
{
|
||||
customAction += _ => action();
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -59,6 +59,27 @@ namespace Ichni.Editor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滑块控制属性,适用于 float/int 类型字段,自动生成带范围限制的 Slider 替代 InputField。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class DynamicUISliderAttribute : DynamicUIAttribute
|
||||
{
|
||||
public float Min { get; set; }
|
||||
public float Max { get; set; }
|
||||
public bool WholeNumbers { get; set; }
|
||||
|
||||
public DynamicUISliderAttribute(string name = "", string group = "Default",
|
||||
float min = 0f, float max = 1f, bool wholeNumbers = false,
|
||||
int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
|
||||
: base(name, group, span, height, autoUpdate, readOnly)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
WholeNumbers = wholeNumbers;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作按钮卡片,挂载在没有任何参数的 Method() 方法上
|
||||
/// </summary>
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Ichni.Editor
|
||||
// 先排变量
|
||||
foreach (var field in fieldsInGroup)
|
||||
{
|
||||
int span = field.Attr.Span > 0 ? field.Attr.Span : SmartInferSpan(field.ValueType);
|
||||
int span = GetEffectiveSpan(field);
|
||||
span = Mathf.Clamp(span, 1, 3);
|
||||
|
||||
// 如果该行的槽被塞满了或者装不下了
|
||||
@@ -83,12 +83,12 @@ namespace Ichni.Editor
|
||||
currentLineBuffer.Clear();
|
||||
currentSpanCount = 0;
|
||||
}
|
||||
|
||||
|
||||
// 特殊兼容:由于 GridLayoutGroup 的强制统一 cellSize, 我们如果在同一行混搭不同 Span 宽度的,反而会让它变形。
|
||||
// 因此若当前这行的空间要求和前一个需求跨度不一致,也必须强行断点分层!
|
||||
if (currentLineBuffer.Count > 0)
|
||||
{
|
||||
var lastSpan = currentLineBuffer[0].Attr.Span > 0 ? currentLineBuffer[0].Attr.Span : SmartInferSpan(currentLineBuffer[0].ValueType);
|
||||
var lastSpan = GetEffectiveSpan(currentLineBuffer[0]);
|
||||
if (span != lastSpan)
|
||||
{
|
||||
FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
|
||||
@@ -124,8 +124,7 @@ namespace Ichni.Editor
|
||||
// 如果一行要放 3 份(Span 都是 1 拼成的),那格子数量也是 3(600/3=200一个)
|
||||
// 但是如果是独占的(Span 都是 3),格子数量反而应该是 1(600/1=600一个!)
|
||||
// 为了防止排版错开变形,我们通过 3 / span 来决定 Subcontainer 内部允许的等分元素数量。
|
||||
int subcontainerDivision = 3 / lineSpanCapacity;
|
||||
if (subcontainerDivision < 1) subcontainerDivision = 1;
|
||||
int subcontainerDivision = Mathf.Max(1, Mathf.RoundToInt(3f / lineSpanCapacity));
|
||||
|
||||
float rowHeight = GetHighestHeightRequirement(fields);
|
||||
var sub = container.GenerateSubcontainer(subcontainerDivision, rowHeight);
|
||||
@@ -169,6 +168,10 @@ namespace Ichni.Editor
|
||||
{
|
||||
inspector.GenerateDropdown(element, sub, title, fd.ValueType, pName);
|
||||
}
|
||||
else if (fd.Attr is DynamicUISliderAttribute sliderAttr)
|
||||
{
|
||||
inspector.GenerateSlider(element, sub, title, pName, sliderAttr.Min, sliderAttr.Max, sliderAttr.WholeNumbers);
|
||||
}
|
||||
else // 容错给普通的 InputField
|
||||
{
|
||||
inspector.GenerateInputField(element, sub, title, pName, fd.Attr.AutoUpdate);
|
||||
@@ -191,12 +194,17 @@ namespace Ichni.Editor
|
||||
int highest = 1;
|
||||
foreach (var f in fields)
|
||||
{
|
||||
int span = f.Attr.Span > 0 ? f.Attr.Span : SmartInferSpan(f.ValueType);
|
||||
int span = GetEffectiveSpan(f);
|
||||
if (span > highest) highest = span;
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
private static int GetEffectiveSpan(FieldDef fd)
|
||||
{
|
||||
return fd.Attr.Span > 0 ? fd.Attr.Span : SmartInferSpan(fd.ValueType);
|
||||
}
|
||||
|
||||
private static float SmartInferHeight(FieldDef fd)
|
||||
{
|
||||
// EmissionColorPicker 有 Toggle/RGB滑块/Intensity输入框,高度明显大于 BaseColorPicker
|
||||
|
||||
@@ -274,5 +274,31 @@ namespace Ichni.Editor
|
||||
subcontainer.dynamicUIElements.Add(hsvDrawer);
|
||||
return hsvDrawer;
|
||||
}
|
||||
|
||||
public DynamicUISlider GenerateSlider(DynamicUISubcontainer subcontainer,
|
||||
string title, float defaultValue = 0.5f, float min = 0f, float max = 1f, bool wholeNumbers = false)
|
||||
{
|
||||
DynamicUISlider slider = LeanPool
|
||||
.Spawn(EditorManager.instance.basePrefabs.slider, subcontainer.rect)
|
||||
.GetComponent<DynamicUISlider>();
|
||||
slider.Initialize(null, title, string.Empty, min, max, wholeNumbers);
|
||||
slider.slider.SetValueWithoutNotify(defaultValue);
|
||||
subcontainer.dynamicUIElements.Add(slider);
|
||||
return slider;
|
||||
}
|
||||
|
||||
public DynamicUISlider GenerateSlider(IBaseElement baseElement,
|
||||
DynamicUISubcontainer subcontainer, string title, string parameterName,
|
||||
float min = 0f, float max = 1f, bool wholeNumbers = false)
|
||||
{
|
||||
DynamicUISlider slider = LeanPool
|
||||
.Spawn(EditorManager.instance.basePrefabs.slider, subcontainer.rect)
|
||||
.GetComponent<DynamicUISlider>();
|
||||
slider.Initialize(baseElement, title, parameterName, min, max, wholeNumbers);
|
||||
var nav = new Navigation { mode = Navigation.Mode.None };
|
||||
slider.slider.navigation = nav;
|
||||
subcontainer.dynamicUIElements.Add(slider);
|
||||
return slider;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,11 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using DG.Tweening;
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using Sirenix.OdinInspector;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
@@ -24,9 +27,16 @@ namespace Ichni.Editor
|
||||
public Button clipLoadButton;
|
||||
public Button beatmapToolsButton;
|
||||
|
||||
[Title("Search")]
|
||||
public TMP_InputField searchInput;
|
||||
public RectTransform searchResultsRoot;
|
||||
public GameObject searchResultItemPrefab;
|
||||
|
||||
[Title("Windows")]
|
||||
public GeneralSecondaryWindow clipManagementWindow;
|
||||
|
||||
private readonly List<GameObject> searchResultItems = new List<GameObject>();
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start();
|
||||
@@ -40,6 +50,84 @@ namespace Ichni.Editor
|
||||
beatmapToolsButton.onClick.AddListener(GenerateBeatmapToolsWindow);
|
||||
//songInfoButton.onClick.AddListener(已经在songinfo好了);
|
||||
//projectInfoButton.onClick.AddListener(已经在projectinfo好了);
|
||||
|
||||
SetupSearch();
|
||||
}
|
||||
|
||||
private void SetupSearch()
|
||||
{
|
||||
searchInput.onValueChanged.AddListener(OnSearchValueChanged);
|
||||
searchInput.onSelect.AddListener(_ =>
|
||||
{
|
||||
searchResultsRoot.transform.DOScaleX(1, 0.25f);
|
||||
searchResultsRoot.gameObject.SetActive(true);
|
||||
});
|
||||
// searchInput.onDeselect.AddListener(_ =>
|
||||
// {
|
||||
// // 延迟隐藏,避免点击结果项时面板先消失了
|
||||
//
|
||||
// });
|
||||
searchResultsRoot.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void OnSearchValueChanged(string query)
|
||||
{
|
||||
ClearSearchResults();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query)) return;
|
||||
|
||||
string lowerQuery = query.ToLower();
|
||||
|
||||
var matches = EditorManager.instance.beatmapContainer.gameElementList
|
||||
.Where(e => e != null && e.elementName.ToLower().Contains(lowerQuery))
|
||||
.Take(20)
|
||||
.ToList();
|
||||
|
||||
if (matches.Count == 0) return;
|
||||
|
||||
searchResultsRoot.gameObject.SetActive(true);
|
||||
|
||||
foreach (var element in matches)
|
||||
{
|
||||
var item = LeanPool.Spawn(searchResultItemPrefab, searchResultsRoot);
|
||||
var itemText = item.GetComponentInChildren<TMP_Text>();
|
||||
if (itemText != null)
|
||||
itemText.text = element.elementName + " " + element.GetType().ToString();
|
||||
|
||||
var button = item.GetComponentInChildren<Button>();
|
||||
if (button != null)
|
||||
{
|
||||
GameElement captured = element;
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() =>
|
||||
{
|
||||
EditorManager.instance.uiManager.hierarchy.FindTab(captured);
|
||||
|
||||
searchResultsRoot.transform.DOScaleX(0, 0.25f).OnComplete(() =>
|
||||
{
|
||||
searchInput.text = string.Empty;
|
||||
ClearSearchResults();
|
||||
searchResultsRoot.gameObject.SetActive(false);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
searchResultItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSearchResults()
|
||||
{
|
||||
foreach (var item in searchResultItems)
|
||||
{
|
||||
if (item != null)
|
||||
LeanPool.Despawn(item);
|
||||
}
|
||||
searchResultItems.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +302,7 @@ namespace Ichni.Editor
|
||||
flick.Refresh();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var ResetNoteAudioButton = beatmapToolsWindow.GenerateButton(beatmapToolsSettings, "Reset Note Audio", () =>
|
||||
{
|
||||
List<NoteBase> allNotes = EditorManager.instance.beatmapContainer.gameElementList.FindAll(x => x is NoteBase).ConvertAll(x => x as NoteBase);
|
||||
@@ -227,11 +315,11 @@ namespace Ichni.Editor
|
||||
{
|
||||
hold.submoduleList.Remove(hold.noteAudioSubmodule);
|
||||
hold.noteAudioSubmodule = null;
|
||||
hold.noteAudioSubmodule = new NoteAudioSubmodule(hold,
|
||||
new List<string>(){"DefaultEndHold"},
|
||||
hold.noteAudioSubmodule = new NoteAudioSubmodule(hold,
|
||||
new List<string>() { "DefaultEndHold" },
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>(){"DefaultStartHold"});
|
||||
new List<string>() { "DefaultStartHold" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,8 +329,8 @@ namespace Ichni.Editor
|
||||
{
|
||||
flick.submoduleList.Remove(flick.noteAudioSubmodule);
|
||||
flick.noteAudioSubmodule = null;
|
||||
flick.noteAudioSubmodule = new NoteAudioSubmodule(flick,
|
||||
new List<string>(){"DefaultFlick"},
|
||||
flick.noteAudioSubmodule = new NoteAudioSubmodule(flick,
|
||||
new List<string>() { "DefaultFlick" },
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>(), new List<string>(),
|
||||
new List<string>());
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace Ichni.RhythmGame
|
||||
public GameObject baseColorPicker;
|
||||
public GameObject emissionColorPicker;
|
||||
public GameObject hsvDrawer;
|
||||
public GameObject slider;
|
||||
|
||||
[Title("DynamicUI相关-Composite")] public GameObject generalSecondaryWindow;
|
||||
public GameObject compositeParameterWindow;
|
||||
|
||||
@@ -42,10 +42,10 @@ namespace Ichni
|
||||
#region [编辑器首选项] Editor Preferences
|
||||
// 这些首选项字段放在 ProjectContainer 中,以便 SetUpInspector 将 this 作为 IBaseElement owner 传给 GenerateToggle
|
||||
public NoteBase.NoteJudgeType currentJudgeType;
|
||||
public bool useClickSelect;
|
||||
public bool useNotePrefab;
|
||||
public bool ExpandWhileClick;
|
||||
public bool useQuickMove;
|
||||
public bool useClickSelect = true;
|
||||
public bool useNotePrefab = true;
|
||||
public bool ExpandWhileClick = true;
|
||||
public bool useQuickMove = false;
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
|
||||
Reference in New Issue
Block a user