增加多轨道动画编辑
This commit is contained in:
139
SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
generated
Normal file
139
SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
namespace SpineViewer.Dialogs
|
||||||
|
{
|
||||||
|
partial class AnimationTracksEditorDialog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Required designer variable.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up any resources being used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required method for Designer support - do not modify
|
||||||
|
/// the contents of this method with the code editor.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
Normal file
43
SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
Normal file
@@ -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<GridItem>().Last();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
SpineViewer/Dialogs/AnimationTracksEditorDialog.resx
Normal file
120
SpineViewer/Dialogs/AnimationTracksEditorDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
||||||
124
SpineViewer/Spine/AnimationTracks.cs
Normal file
124
SpineViewer/Spine/AnimationTracks.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="spine"></param>
|
||||||
|
/// <param name="i"></param>
|
||||||
|
[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));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现了默认的转为字符串的方式
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString() => spine.GetAnimation(Index);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
|
||||||
|
/// </summary>
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is TrackWrapper tr) return ToString() == obj.ToString();
|
||||||
|
return base.Equals(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 哈希码需要和 Equals 行为类似
|
||||||
|
/// </summary>
|
||||||
|
public override int GetHashCode() => ToString().GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="spine">关联的 Spine 对象</param>
|
||||||
|
/// <param name="i">轨道索引</param>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||||
|
/// </summary>
|
||||||
|
public override object? GetValue(object? component) => new TrackWrapper(spine, idx);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||||
|
/// </summary>
|
||||||
|
public override void SetValue(object? component, object? value)
|
||||||
|
{
|
||||||
|
if (value is string s) spine.SetAnimation(idx, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="spine">关联的 Spine 对象</param>
|
||||||
|
public class AnimationTracksType(Spine spine) : ICustomTypeDescriptor
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, TrackWrapperPropertyDescriptor> 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<TrackWrapperPropertyDescriptor>();
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在属性面板悬停可以按轨道顺序显示动画名称
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||||
|
|
||||||
|
|||||||
@@ -235,13 +235,6 @@ namespace SpineViewer.Spine
|
|||||||
|
|
||||||
#region 属性 | [3] 动画
|
#region 属性 | [3] 动画
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 包含的所有皮肤名称
|
|
||||||
/// </summary>
|
|
||||||
[Browsable(false)]
|
|
||||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
|
||||||
protected List<string> skinNames = [];
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
|
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -254,13 +247,6 @@ namespace SpineViewer.Spine
|
|||||||
}
|
}
|
||||||
protected abstract string skin { get; set; }
|
protected abstract string skin { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 包含的所有动画名称
|
|
||||||
/// </summary>
|
|
||||||
[Browsable(false)]
|
|
||||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
|
||||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -276,34 +262,58 @@ namespace SpineViewer.Spine
|
|||||||
/// 默认轨道动画时长
|
/// 默认轨道动画时长
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Category("[3] 动画"), DisplayName("轨道 0 动画时长")]
|
[Category("[3] 动画"), DisplayName("轨道 0 动画时长")]
|
||||||
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
|
public float Track0AnimationDuration => GetAnimationDuration(Track0Animation);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 默认轨道动画时长
|
/// 默认轨道动画时长
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Editor(typeof(CollectionEditor), typeof(UITypeEditor))]
|
[Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
|
||||||
[Category("[3] 动画"), DisplayName("多轨道动画")]
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||||
public AnimationTrackDict AnimationTracks { get; private set; }
|
[Category("[3] 动画"), DisplayName("多轨道动画管理")]
|
||||||
|
public AnimationTracksType AnimationTracks { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包含的所有皮肤名称
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||||
|
protected List<string> skinNames = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包含的所有动画名称
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||||
|
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取所有非 null 的轨道索引
|
/// 获取所有非 null 的轨道索引
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract int[] trackIndices { get; }
|
public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); }
|
||||||
|
protected abstract int[] getTrackIndices();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定轨道的当前动画, 如果没有, 应当返回空动画名称
|
/// 获取指定轨道的当前动画, 如果没有, 应当返回空动画名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public string GetAnimation(int track) { lock (_lock) return getAnimation(track); }
|
||||||
protected abstract string getAnimation(int track);
|
protected abstract string getAnimation(int track);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置某个轨道动画
|
/// 设置某个轨道动画
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public void SetAnimation(int track, string name) { lock (_lock) setAnimation(track, name); }
|
||||||
protected abstract void setAnimation(int track, string name);
|
protected abstract void setAnimation(int track, string name);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除某个轨道, 与设置空动画不同, 是彻底删除轨道内的东西
|
/// 清除某个轨道, 与设置空动画不同, 是彻底删除轨道内的东西
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract void clearTrack(int i);
|
public void ClearTrack(int i) { lock (_lock) clearTrack(i); }
|
||||||
|
protected abstract void clearTrack(int i); // XXX: 清除轨道之后被加载的附件还是会保留, 不会自动卸下, 除非使用 SetSlotsToSetupPose
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取动画时长, 如果动画不存在则返回 0
|
||||||
|
/// </summary>
|
||||||
|
public abstract float GetAnimationDuration(string name);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -384,11 +394,6 @@ namespace SpineViewer.Spine
|
|||||||
[Browsable(false)]
|
[Browsable(false)]
|
||||||
public Image Preview { get; private set; }
|
public Image Preview { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取动画时长, 如果动画不存在则返回 0
|
|
||||||
/// </summary>
|
|
||||||
public abstract float GetAnimationDuration(string name);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新内部状态
|
/// 更新内部状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -425,128 +430,5 @@ namespace SpineViewer.Spine
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 多轨动画管理集合
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="spine"></param>
|
|
||||||
public class AnimationTrackDict(Spine spine) : IDictionary<int, string>
|
|
||||||
{
|
|
||||||
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<int> Keys
|
|
||||||
{
|
|
||||||
get { lock (sp._lock) return sp.trackIndices; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICollection<string> 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<int, string> 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<int, string> 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<int, string>[] array, int arrayIndex)
|
|
||||||
{
|
|
||||||
lock (sp._lock) foreach (var i in sp.trackIndices) array[arrayIndex++] = new KeyValuePair<int, string>(i, sp.getAnimation(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<int, string>> GetEnumerator()
|
|
||||||
{
|
|
||||||
List<KeyValuePair<int, string>> cache;
|
|
||||||
lock (sp._lock)
|
|
||||||
{
|
|
||||||
cache = sp.trackIndices.Select(i => new KeyValuePair<int, string>(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<int, string> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<string> 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 class AnimationConverter : StringConverter
|
||||||
{
|
{
|
||||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||||
@@ -47,25 +73,34 @@ namespace SpineViewer.Spine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SkinConverter : StringConverter
|
/// <summary>
|
||||||
|
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||||
|
/// </summary>
|
||||||
|
public class TrackWrapperConverter : ExpandableObjectConverter
|
||||||
{
|
{
|
||||||
|
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
|
||||||
|
// ToString 实现了 ConvertTo
|
||||||
|
// SetValue 实现了从字符串设置属性
|
||||||
|
|
||||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||||
|
|
||||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||||
|
|
||||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
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<AnimationTracksType>().ToArray();
|
||||||
|
if (animTracks.Length > 0)
|
||||||
{
|
{
|
||||||
IEnumerable<string> common = spines[0].SkinNames;
|
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;
|
||||||
foreach (var spine in spines.Skip(1))
|
foreach (var t in animTracks.Skip(1))
|
||||||
common = common.Union(spine.SkinNames);
|
common = common.Union(t.Spine.AnimationNames);
|
||||||
return new StandardValuesCollection(common.ToArray());
|
return new StandardValuesCollection(common.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using SpineViewer.Dialogs;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Drawing.Design;
|
using System.Drawing.Design;
|
||||||
@@ -36,4 +37,28 @@ namespace SpineViewer.Spine
|
|||||||
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多轨道动画编辑器
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user