diff --git a/SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs b/SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs index 6c45a72..8074406 100644 --- a/SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs +++ b/SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs @@ -40,6 +40,8 @@ propertyGrid_Skin = new PropertyGrid(); contextMenuStrip_Skin = new ContextMenuStrip(components); toolStripMenuItem_ReloadSkins = new ToolStripMenuItem(); + tabPage_Slot = new TabPage(); + propertyGrid_Slot = new PropertyGrid(); tabPage_Animation = new TabPage(); propertyGrid_Animation = new PropertyGrid(); contextMenuStrip_Animation = new ContextMenuStrip(components); @@ -53,6 +55,7 @@ tabPage_Transform.SuspendLayout(); tabPage_Skin.SuspendLayout(); contextMenuStrip_Skin.SuspendLayout(); + tabPage_Slot.SuspendLayout(); tabPage_Animation.SuspendLayout(); contextMenuStrip_Animation.SuspendLayout(); tabPage_Debug.SuspendLayout(); @@ -65,6 +68,7 @@ tabControl.Controls.Add(tabPage_Render); tabControl.Controls.Add(tabPage_Transform); tabControl.Controls.Add(tabPage_Skin); + tabControl.Controls.Add(tabPage_Slot); tabControl.Controls.Add(tabPage_Animation); tabControl.Controls.Add(tabPage_Debug); tabControl.Dock = DockStyle.Fill; @@ -181,6 +185,28 @@ toolStripMenuItem_ReloadSkins.Text = "重新加载所选皮肤"; toolStripMenuItem_ReloadSkins.Click += toolStripMenuItem_ReloadSkins_Click; // + // tabPage_Slot + // + tabPage_Slot.BackColor = SystemColors.Control; + tabPage_Slot.Controls.Add(propertyGrid_Slot); + tabPage_Slot.Location = new Point(4, 4); + tabPage_Slot.Margin = new Padding(0); + tabPage_Slot.Name = "tabPage_Slot"; + tabPage_Slot.Size = new Size(364, 370); + tabPage_Slot.TabIndex = 6; + tabPage_Slot.Text = "槽位"; + // + // propertyGrid_Slot + // + propertyGrid_Slot.Dock = DockStyle.Fill; + propertyGrid_Slot.HelpVisible = false; + propertyGrid_Slot.Location = new Point(0, 0); + propertyGrid_Slot.Name = "propertyGrid_Slot"; + propertyGrid_Slot.PropertySort = PropertySort.Alphabetical; + propertyGrid_Slot.Size = new Size(364, 370); + propertyGrid_Slot.TabIndex = 2; + propertyGrid_Slot.ToolbarVisible = false; + // // tabPage_Animation // tabPage_Animation.BackColor = SystemColors.Control; @@ -260,6 +286,7 @@ tabPage_Transform.ResumeLayout(false); tabPage_Skin.ResumeLayout(false); contextMenuStrip_Skin.ResumeLayout(false); + tabPage_Slot.ResumeLayout(false); tabPage_Animation.ResumeLayout(false); contextMenuStrip_Animation.ResumeLayout(false); tabPage_Debug.ResumeLayout(false); @@ -286,5 +313,7 @@ private ToolStripMenuItem toolStripMenuItem_RemoveAnimation; private TabPage tabPage_Debug; private PropertyGrid propertyGrid_Debug; + private TabPage tabPage_Slot; + private PropertyGrid propertyGrid_Slot; } } diff --git a/SpineViewer/Controls/SpineViewPropertyGrid.cs b/SpineViewer/Controls/SpineViewPropertyGrid.cs index 85ddec4..1ea47bf 100644 --- a/SpineViewer/Controls/SpineViewPropertyGrid.cs +++ b/SpineViewer/Controls/SpineViewPropertyGrid.cs @@ -34,6 +34,7 @@ namespace SpineViewer.Controls propertyGrid_Render.SelectedObject = null; propertyGrid_Transform.SelectedObject = null; propertyGrid_Skin.SelectedObject = null; + propertyGrid_Slot.SelectedObject = null; propertyGrid_Animation.SelectedObject = null; propertyGrid_Debug.SelectedObject = null; } @@ -44,6 +45,7 @@ namespace SpineViewer.Controls propertyGrid_Render.SelectedObjects = value.Select(e => e.Render).ToArray(); propertyGrid_Transform.SelectedObjects = value.Select(e => e.Transform).ToArray(); propertyGrid_Skin.SelectedObjects = value.Select(e => e.Skin).ToArray(); + propertyGrid_Slot.SelectedObjects = value.Select(e => e.Slot).ToArray(); propertyGrid_Animation.SelectedObjects = value.Select(e => e.Animation).ToArray(); propertyGrid_Debug.SelectedObjects = value.Select(e => e.Debug).ToArray(); } diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs index e700783..adaceed 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs @@ -106,7 +106,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -187,7 +187,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject set => skeleton.FlipY = value; } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs index 4277aeb..bcbdc37 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs @@ -105,7 +105,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -186,7 +186,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject set => skeleton.FlipY = value; } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs index 5efcb26..d7e6dbf 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs @@ -102,7 +102,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -157,7 +157,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs index 2153dbd..7bdd895 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs @@ -109,7 +109,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -164,7 +164,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs index b869411..740ca99 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs @@ -105,7 +105,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -160,7 +160,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs index 8ca2136..ff10359 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs @@ -105,7 +105,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -160,7 +160,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs b/SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs index fa84fad..06f3e7a 100644 --- a/SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs +++ b/SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs @@ -105,7 +105,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray()); SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray(); - AnimationNames = skeletonData.Animations.Select(v => v.Name).ToImmutableArray(); + AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)]; skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器 animationStateData = new AnimationStateData(skeletonData); @@ -160,7 +160,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject } } - protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment.Name ?? EMPTY_ATTACHMENT; + protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT; protected override void setSlotAttachment(string slot, string name) { diff --git a/SpineViewer/Spine/SpineView/SpineObjectProperty.cs b/SpineViewer/Spine/SpineView/SpineObjectProperty.cs index 9ffae04..767985a 100644 --- a/SpineViewer/Spine/SpineView/SpineObjectProperty.cs +++ b/SpineViewer/Spine/SpineView/SpineObjectProperty.cs @@ -27,6 +27,10 @@ namespace SpineViewer.Spine.SpineView [DisplayName("皮肤")] public SpineSkinProperty Skin { get; } = new(spine); + [TypeConverter(typeof(ExpandableObjectConverter))] + [DisplayName("皮肤")] + public SpineSlotProperty Slot { get; } = new(spine); + [TypeConverter(typeof(ExpandableObjectConverter))] [DisplayName("动画")] public SpineAnimationProperty Animation { get; } = new(spine); diff --git a/SpineViewer/Spine/SpineView/SpineSlotProperty.cs b/SpineViewer/Spine/SpineView/SpineSlotProperty.cs new file mode 100644 index 0000000..5e4f29a --- /dev/null +++ b/SpineViewer/Spine/SpineView/SpineSlotProperty.cs @@ -0,0 +1,177 @@ +using SpineViewer.Spine; +using SpineViewer.Utils; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Spine.SpineView +{ + /// + /// 用于在 PropertyGrid 上显示槽位附件加载情况包装类 + /// + public class SpineSlotProperty(SpineObject spine) : ICustomTypeDescriptor + { + [Browsable(false)] + public SpineObject Spine { get; } = spine; + + /// + /// 显示所有槽位集合 + /// + public override string ToString() => $"[{string.Join(", ", Spine.SlotAttachmentNames.Keys)}]"; + + public override bool Equals(object? obj) + { + if (obj is SpineAnimationProperty prop) return ToString() == prop.ToString(); + return base.Equals(obj); + } + + public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode()); + + #region ICustomTypeDescriptor 接口实现 + + // XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确 + + /// + /// 属性描述符缓存 + /// + private static readonly Dictionary pdCache = []; + + public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true); + public string? GetClassName() => TypeDescriptor.GetClassName(this, true); + public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true); + public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true); + public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true); + public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true); + public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true); + public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true); + public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true); + public object? GetPropertyOwner(PropertyDescriptor? pd) => this; + public PropertyDescriptorCollection GetProperties() => GetProperties(null); + public PropertyDescriptorCollection GetProperties(Attribute[]? attributes) + { + var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast().ToArray()); + foreach (var slotName in Spine.SlotAttachmentNames.Keys) + { + if (!pdCache.TryGetValue(slotName, out var pd)) + pdCache[slotName] = pd =new SlotPropertyDescriptor(slotName, [new DisplayNameAttribute($"{slotName}")]); + props.Add(pd); + } + return props; + } + + /// + /// 槽位属性描述符, 实现对属性的读取和赋值 + /// + internal class SlotPropertyDescriptor(string name, Attribute[]? attributes) : PropertyDescriptor($"Slot_{name}", attributes) + { + public string SlotName { get; } = name; + + public override Type ComponentType => typeof(SpineSlotProperty); + public override bool IsReadOnly => false; + public override Type PropertyType => typeof(SlotProperty); + public override bool CanResetValue(object component) => false; + public override void ResetValue(object component) { } + public override bool ShouldSerializeValue(object component) => false; + + /// + /// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性 + /// + public override object? GetValue(object? component) + { + if (component is SpineSlotProperty slots) + return slots.Spine.GetSlotAttachment(SlotName); + return null; + } + + /// + /// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理 + /// + public override void SetValue(object? component, object? value) + { + if (component is SpineSlotProperty slots) + { + if (value is string s) + slots.Spine.SetSlotAttachment(SlotName, s); + } + } + } + + #endregion + } + + /// + /// 对 .Slot_{name} 属性的包装类 + /// + [TypeConverter(typeof(SlotPropertyConverter))] + public class SlotProperty(SpineObject spine, string name) + { + private readonly SpineObject spine = spine; + + [Browsable(false)] + public string Name { get; } = name; + + /// + /// 实现了默认的转为字符串的方式 + /// + public override string ToString() => spine.GetSlotAttachment(Name); + + /// + /// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容 + /// + public override bool Equals(object? obj) + { + if (obj is SlotProperty) return ToString() == obj.ToString(); + return base.Equals(obj); + } + + /// + /// 哈希码需要和 Equals 行为类似 + /// + public override int GetHashCode() => HashCode.Combine(typeof(SlotProperty).FullName.GetHashCode(), ToString().GetHashCode()); + } + + /// + /// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力 + /// + public class SlotPropertyConverter : StringConverter + { + // NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转 + // ToString 实现了 ConvertTo + // SetValue 实现了从字符串设置属性 + + public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true; + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true; + + public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) + { + if (context?.PropertyDescriptor is SpineSlotProperty.SlotPropertyDescriptor pd) + { + if (context?.Instance is SpineSlotProperty slots) + { + if (slots.Spine.SlotAttachmentNames.TryGetValue(pd.SlotName, out var names)) + return new StandardValuesCollection(names); + } + else if (context?.Instance is object[] instances && instances.All(x => x is SpineSlotProperty)) + { + // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型 + var spinesSlots = instances.Cast().ToArray(); + if (spinesSlots.Length > 0) + { + IEnumerable common = []; + foreach (var t in spinesSlots) + { + if (t.Spine.SlotAttachmentNames.TryGetValue(pd.SlotName, out var names)) + common = common.Union(names); + } + return new StandardValuesCollection(common.ToArray()); + } + } + } + return base.GetStandardValues(context); + } + } +}