diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs b/SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs new file mode 100644 index 0000000..c5ea66e --- /dev/null +++ b/SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs @@ -0,0 +1,139 @@ +namespace SpineViewer.Dialogs +{ + partial class AnimationTracksEditorDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + panel = new Panel(); + tableLayoutPanel1 = new TableLayoutPanel(); + flowLayoutPanel1 = new FlowLayoutPanel(); + button_Add = new Button(); + button_Delete = new Button(); + propertyGrid_AnimationTracks = new PropertyGrid(); + panel.SuspendLayout(); + tableLayoutPanel1.SuspendLayout(); + flowLayoutPanel1.SuspendLayout(); + SuspendLayout(); + // + // panel + // + panel.Controls.Add(tableLayoutPanel1); + panel.Dock = DockStyle.Fill; + panel.Location = new Point(0, 0); + panel.Name = "panel"; + panel.Padding = new Padding(50, 15, 50, 10); + panel.Size = new Size(666, 483); + panel.TabIndex = 0; + // + // tableLayoutPanel1 + // + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1); + tableLayoutPanel1.Controls.Add(propertyGrid_AnimationTracks, 0, 0); + tableLayoutPanel1.Dock = DockStyle.Fill; + tableLayoutPanel1.Location = new Point(50, 15); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 2; + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle()); + tableLayoutPanel1.Size = new Size(566, 458); + tableLayoutPanel1.TabIndex = 0; + // + // flowLayoutPanel1 + // + flowLayoutPanel1.AutoSize = true; + flowLayoutPanel1.Controls.Add(button_Add); + flowLayoutPanel1.Controls.Add(button_Delete); + flowLayoutPanel1.Dock = DockStyle.Fill; + flowLayoutPanel1.Location = new Point(3, 415); + flowLayoutPanel1.Name = "flowLayoutPanel1"; + flowLayoutPanel1.Size = new Size(560, 40); + flowLayoutPanel1.TabIndex = 2; + // + // button_Add + // + button_Add.Location = new Point(3, 3); + button_Add.Name = "button_Add"; + button_Add.Size = new Size(112, 34); + button_Add.TabIndex = 0; + button_Add.Text = "添加"; + button_Add.UseVisualStyleBackColor = true; + button_Add.Click += button_Add_Click; + // + // button_Delete + // + button_Delete.Location = new Point(121, 3); + button_Delete.Name = "button_Delete"; + button_Delete.Size = new Size(112, 34); + button_Delete.TabIndex = 1; + button_Delete.Text = "删除"; + button_Delete.UseVisualStyleBackColor = true; + button_Delete.Click += button_Delete_Click; + // + // propertyGrid_AnimationTracks + // + propertyGrid_AnimationTracks.Dock = DockStyle.Fill; + propertyGrid_AnimationTracks.HelpVisible = false; + propertyGrid_AnimationTracks.Location = new Point(3, 3); + propertyGrid_AnimationTracks.Name = "propertyGrid_AnimationTracks"; + propertyGrid_AnimationTracks.PropertySort = PropertySort.NoSort; + propertyGrid_AnimationTracks.Size = new Size(560, 406); + propertyGrid_AnimationTracks.TabIndex = 1; + propertyGrid_AnimationTracks.ToolbarVisible = false; + // + // AnimationTracksEditorDialog + // + AutoScaleDimensions = new SizeF(11F, 24F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(666, 483); + Controls.Add(panel); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + Name = "AnimationTracksEditorDialog"; + ShowIcon = false; + ShowInTaskbar = false; + StartPosition = FormStartPosition.CenterScreen; + Text = "多轨道动画实时编辑器"; + panel.ResumeLayout(false); + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + flowLayoutPanel1.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private Panel panel; + private TableLayoutPanel tableLayoutPanel1; + private FlowLayoutPanel flowLayoutPanel1; + private Button button_Add; + private Button button_Delete; + private PropertyGrid propertyGrid_AnimationTracks; + } +} \ No newline at end of file diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs b/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs new file mode 100644 index 0000000..647d1f3 --- /dev/null +++ b/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs @@ -0,0 +1,43 @@ +using SpineViewer.Spine; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace SpineViewer.Dialogs +{ + public partial class AnimationTracksEditorDialog : Form + { + private readonly Spine.Spine spine; + public AnimationTracksEditorDialog(Spine.Spine spine) + { + InitializeComponent(); + this.spine = spine; + propertyGrid_AnimationTracks.SelectedObject = spine.AnimationTracks; + } + + private void button_Add_Click(object sender, EventArgs e) + { + spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]); + propertyGrid_AnimationTracks.Refresh(); + } + + private void button_Delete_Click(object sender, EventArgs e) + { + if (propertyGrid_AnimationTracks.SelectedGridItem?.Value is TrackWrapper tr) + { + if (tr.Index == 0) + MessageBox.Info("必须保留轨道 0"); + else + spine.ClearTrack(tr.Index); + } + propertyGrid_AnimationTracks.Refresh(); + propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem.Parent.GridItems.Cast().Last(); + } + } +} diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.resx b/SpineViewer/Dialogs/AnimationTracksEditorDialog.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/SpineViewer/Dialogs/AnimationTracksEditorDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/SpineViewer/Spine/AnimationTracks.cs b/SpineViewer/Spine/AnimationTracks.cs new file mode 100644 index 0000000..448500a --- /dev/null +++ b/SpineViewer/Spine/AnimationTracks.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.Spine +{ + /// + /// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑 + /// + /// + /// + [TypeConverter(typeof(TrackWrapperConverter))] + public class TrackWrapper(Spine spine, int i) + { + private readonly Spine spine = spine; + + [Browsable(false)] + public int Index { get; } = i; + + [DisplayName("时长")] + public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index)); + + /// + /// 实现了默认的转为字符串的方式 + /// + public override string ToString() => spine.GetAnimation(Index); + + /// + /// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容 + /// + public override bool Equals(object? obj) + { + if (obj is TrackWrapper tr) return ToString() == obj.ToString(); + return base.Equals(obj); + } + + /// + /// 哈希码需要和 Equals 行为类似 + /// + public override int GetHashCode() => ToString().GetHashCode(); + } + + /// + /// 轨道属性描述符, 实现对属性的读取和赋值 + /// + /// 关联的 Spine 对象 + /// 轨道索引 + public class TrackWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")]) + { + private readonly Spine spine = spine; + private readonly int idx = i; + + public override Type ComponentType => typeof(AnimationTracksType); + public override bool IsReadOnly => false; + public override Type PropertyType => typeof(TrackWrapper); + 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) => new TrackWrapper(spine, idx); + + /// + /// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理 + /// + public override void SetValue(object? component, object? value) + { + if (value is string s) spine.SetAnimation(idx, s); + } + } + + /// + /// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性 + /// + /// 关联的 Spine 对象 + public class AnimationTracksType(Spine spine) : ICustomTypeDescriptor + { + private readonly Dictionary pdCache = []; + public Spine Spine { get; } = spine; + + // XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确 + + 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 List(); + foreach (var i in Spine.GetTrackIndices()) + { + if (!pdCache.ContainsKey(i)) + pdCache[i] = new TrackWrapperPropertyDescriptor(Spine, i); + props.Add(pdCache[i]); + } + return new PropertyDescriptorCollection(props.ToArray()); + } + + /// + /// 在属性面板悬停可以按轨道顺序显示动画名称 + /// + public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]"; + + public override bool Equals(object? obj) + { + if (obj is AnimationTracksType tracks) return ToString() == tracks.ToString(); + return base.Equals(obj); + } + + public override int GetHashCode() => ToString().GetHashCode(); + } +} diff --git a/SpineViewer/Spine/Implementations/Spine/Spine21.cs b/SpineViewer/Spine/Implementations/Spine/Spine21.cs index df386f1..34ca268 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine21.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine21.cs @@ -171,7 +171,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine36.cs b/SpineViewer/Spine/Implementations/Spine/Spine36.cs index 814b04b..b67213c 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine36.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine36.cs @@ -170,7 +170,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine37.cs b/SpineViewer/Spine/Implementations/Spine/Spine37.cs index 0342880..89a0a00 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine37.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine37.cs @@ -140,7 +140,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine38.cs b/SpineViewer/Spine/Implementations/Spine/Spine38.cs index 6adc06b..76953ff 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine38.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine38.cs @@ -146,7 +146,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine40.cs b/SpineViewer/Spine/Implementations/Spine/Spine40.cs index c6d7ca8..528e58a 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine40.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine40.cs @@ -142,7 +142,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine41.cs b/SpineViewer/Spine/Implementations/Spine/Spine41.cs index 0d21c75..f995319 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine41.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine41.cs @@ -142,7 +142,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Implementations/Spine/Spine42.cs b/SpineViewer/Spine/Implementations/Spine/Spine42.cs index 046baa3..d6fc73e 100644 --- a/SpineViewer/Spine/Implementations/Spine/Spine42.cs +++ b/SpineViewer/Spine/Implementations/Spine/Spine42.cs @@ -142,7 +142,7 @@ namespace SpineViewer.Spine.Implementations.Spine } } - protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); + protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray(); protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION; diff --git a/SpineViewer/Spine/Spine.cs b/SpineViewer/Spine/Spine.cs index 962b5b3..6e00cc6 100644 --- a/SpineViewer/Spine/Spine.cs +++ b/SpineViewer/Spine/Spine.cs @@ -235,13 +235,6 @@ namespace SpineViewer.Spine #region 属性 | [3] 动画 - /// - /// 包含的所有皮肤名称 - /// - [Browsable(false)] - public ReadOnlyCollection SkinNames { get; private set; } - protected List skinNames = []; - /// /// 使用的皮肤名称, 如果设置的皮肤不存在则忽略 /// @@ -254,13 +247,6 @@ namespace SpineViewer.Spine } protected abstract string skin { get; set; } - /// - /// 包含的所有动画名称 - /// - [Browsable(false)] - public ReadOnlyCollection AnimationNames { get; private set; } - protected List animationNames = [EMPTY_ANIMATION]; - /// /// 默认轨道动画名称, 如果设置的动画不存在则忽略 /// @@ -276,34 +262,58 @@ namespace SpineViewer.Spine /// 默认轨道动画时长 /// [Category("[3] 动画"), DisplayName("轨道 0 动画时长")] - public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示 + public float Track0AnimationDuration => GetAnimationDuration(Track0Animation); /// /// 默认轨道动画时长 /// - [Editor(typeof(CollectionEditor), typeof(UITypeEditor))] - [Category("[3] 动画"), DisplayName("多轨道动画")] - public AnimationTrackDict AnimationTracks { get; private set; } + [Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))] + [TypeConverter(typeof(ExpandableObjectConverter))] + [Category("[3] 动画"), DisplayName("多轨道动画管理")] + public AnimationTracksType AnimationTracks { get; private set; } + + /// + /// 包含的所有皮肤名称 + /// + [Browsable(false)] + public ReadOnlyCollection SkinNames { get; private set; } + protected List skinNames = []; + + /// + /// 包含的所有动画名称 + /// + [Browsable(false)] + public ReadOnlyCollection AnimationNames { get; private set; } + protected List animationNames = [EMPTY_ANIMATION]; /// /// 获取所有非 null 的轨道索引 /// - protected abstract int[] trackIndices { get; } + public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); } + protected abstract int[] getTrackIndices(); /// /// 获取指定轨道的当前动画, 如果没有, 应当返回空动画名称 /// + public string GetAnimation(int track) { lock (_lock) return getAnimation(track); } protected abstract string getAnimation(int track); /// /// 设置某个轨道动画 /// + public void SetAnimation(int track, string name) { lock (_lock) setAnimation(track, name); } protected abstract void setAnimation(int track, string name); /// /// 清除某个轨道, 与设置空动画不同, 是彻底删除轨道内的东西 /// - protected abstract void clearTrack(int i); + public void ClearTrack(int i) { lock (_lock) clearTrack(i); } + protected abstract void clearTrack(int i); // XXX: 清除轨道之后被加载的附件还是会保留, 不会自动卸下, 除非使用 SetSlotsToSetupPose + + /// + /// 获取动画时长, 如果动画不存在则返回 0 + /// + public abstract float GetAnimationDuration(string name); #endregion @@ -384,11 +394,6 @@ namespace SpineViewer.Spine [Browsable(false)] public Image Preview { get; private set; } - /// - /// 获取动画时长, 如果动画不存在则返回 0 - /// - public abstract float GetAnimationDuration(string name); - /// /// 更新内部状态 /// @@ -425,128 +430,5 @@ namespace SpineViewer.Spine #endregion - /// - /// 多轨动画管理集合 - /// - /// - public class AnimationTrackDict(Spine spine) : IDictionary - { - private readonly Spine sp = spine; - - public string this[int key] - { - get - { - lock (sp._lock) - { - if (!sp.trackIndices.Contains(key)) - throw new KeyNotFoundException($"Track {key} not found."); - return sp.getAnimation(key); - } - } - set - { - lock (sp._lock) sp.setAnimation(key, value); - } - } - - public ICollection Keys - { - get { lock (sp._lock) return sp.trackIndices; } - } - - public ICollection Values - { - get { lock (sp._lock) return sp.trackIndices.Select(sp.getAnimation).ToArray(); } - } - - public int Count - { - get { lock (sp._lock) return sp.trackIndices.Length; } - } - - public bool IsReadOnly => false; - - public void Add(int key, string value) - { - lock (sp._lock) sp.setAnimation(key, value); - } - - public void Add(KeyValuePair item) - { - lock (sp._lock) sp.setAnimation(item.Key, item.Value); - } - - public void Clear() - { - lock (sp._lock) foreach (var i in sp.trackIndices) sp.setAnimation(i, EMPTY_ANIMATION); - } - - public bool Contains(KeyValuePair item) - { - lock (sp._lock) return sp.trackIndices.Contains(item.Key) && sp.getAnimation(item.Key) == item.Value; - } - - public bool ContainsKey(int key) - { - lock (sp._lock) return sp.trackIndices.Contains(key); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - lock (sp._lock) foreach (var i in sp.trackIndices) array[arrayIndex++] = new KeyValuePair(i, sp.getAnimation(i)); - } - - public IEnumerator> GetEnumerator() - { - List> cache; - lock (sp._lock) - { - cache = sp.trackIndices.Select(i => new KeyValuePair(i, sp.getAnimation(i))).ToList(); - } - foreach (var item in cache) - { - yield return item; - } - } - - public bool Remove(int key) - { - lock (sp._lock) - { - sp.setAnimation(key, EMPTY_ANIMATION); - sp.clearTrack(key); - } - return true; - } - - public bool Remove(KeyValuePair item) - { - lock (sp._lock) - { - sp.setAnimation(item.Key, EMPTY_ANIMATION); - sp.clearTrack(item.Key); - } - return true; - } - - public bool TryGetValue(int key, [MaybeNullWhen(false)] out string value) - { - value = null; - lock (sp._lock) - { - if (!sp.trackIndices.Contains(key)) return false; - value = sp.getAnimation(key); - return true; - } - } - - public override string ToString() - { - lock (sp._lock) return string.Join(", ", sp.trackIndices.Select(sp.getAnimation)); - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } } -} +} \ No newline at end of file diff --git a/SpineViewer/Spine/TypeConverter.cs b/SpineViewer/Spine/TypeConverter.cs index e292667..f13ddd2 100644 --- a/SpineViewer/Spine/TypeConverter.cs +++ b/SpineViewer/Spine/TypeConverter.cs @@ -21,6 +21,32 @@ namespace SpineViewer.Spine } } + public class SkinConverter : StringConverter + { + public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true; + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true; + + public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) + { + if (context?.Instance is Spine obj) + { + return new StandardValuesCollection(obj.SkinNames); + } + else if (context?.Instance is Spine[] spines) + { + if (spines.Length > 0) + { + IEnumerable common = spines[0].SkinNames; + foreach (var spine in spines.Skip(1)) + common = common.Union(spine.SkinNames); + return new StandardValuesCollection(common.ToArray()); + } + } + return base.GetStandardValues(context); + } + } + public class AnimationConverter : StringConverter { public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true; @@ -47,25 +73,34 @@ namespace SpineViewer.Spine } } - public class SkinConverter : StringConverter + /// + /// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力 + /// + public class TrackWrapperConverter : ExpandableObjectConverter { + // 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?.Instance is Spine obj) + if (context.Instance is AnimationTracksType animTrack) { - return new StandardValuesCollection(obj.SkinNames); + return new StandardValuesCollection(animTrack.Spine.AnimationNames); } - else if (context?.Instance is Spine[] spines) + else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracksType)) { - if (spines.Length > 0) + // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 AnimationTracksType[] 类型 + var animTracks = instances.Cast().ToArray(); + if (animTracks.Length > 0) { - IEnumerable common = spines[0].SkinNames; - foreach (var spine in spines.Skip(1)) - common = common.Union(spine.SkinNames); + IEnumerable common = animTracks[0].Spine.AnimationNames; + foreach (var t in animTracks.Skip(1)) + common = common.Union(t.Spine.AnimationNames); return new StandardValuesCollection(common.ToArray()); } } diff --git a/SpineViewer/Spine/UITypeEditor.cs b/SpineViewer/Spine/UITypeEditor.cs index 6e75505..218cd18 100644 --- a/SpineViewer/Spine/UITypeEditor.cs +++ b/SpineViewer/Spine/UITypeEditor.cs @@ -1,4 +1,5 @@ -using System; +using SpineViewer.Dialogs; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing.Design; @@ -36,4 +37,28 @@ namespace SpineViewer.Spine openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*"; } } + + /// + /// 多轨道动画编辑器 + /// + public class AnimationTracksEditor : UITypeEditor + { + public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal; + + public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value) + { + if (provider == null || context == null || context.Instance is not Spine) + return value; + + IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; + if (editorService == null) + return value; + + using (var dialog = new AnimationTracksEditorDialog((Spine)context.Instance)) + editorService.ShowDialog(dialog); + + TypeDescriptor.Refresh(context.Instance); + return value; + } + } }