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;
+ }
+ }
}