591 lines
22 KiB
C#
591 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
using System.Data;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using System.Collections.ObjectModel;
|
|
using SpineViewer.Spine;
|
|
using System.Reflection;
|
|
using System.Diagnostics;
|
|
using System.Collections.Specialized;
|
|
using NLog;
|
|
using SpineViewer.Extensions;
|
|
using SpineViewer.PropertyGridWrappers.Spine;
|
|
using SpineViewer.Utils;
|
|
|
|
namespace SpineViewer.Controls
|
|
{
|
|
public partial class SpineListView : UserControl
|
|
{
|
|
/// <summary>
|
|
/// 日志器
|
|
/// </summary>
|
|
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
|
|
|
/// <summary>
|
|
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
|
|
/// </summary>
|
|
public readonly ReadOnlyCollection<Spine.SpineObject> Spines;
|
|
|
|
/// <summary>
|
|
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
|
|
/// </summary>
|
|
private readonly List<Spine.SpineObject> spines = [];
|
|
|
|
/// <summary>
|
|
/// 用于属性页显示模型参数的包装类
|
|
/// </summary>
|
|
private readonly Dictionary<string, SpineWrapper> spinePropertyWrappers = [];
|
|
|
|
public SpineListView()
|
|
{
|
|
InitializeComponent();
|
|
Spines = spines.AsReadOnly();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 显示骨骼信息的属性面板
|
|
/// </summary>
|
|
[Category("自定义"), Description("用于显示模型属性的组合属性页")]
|
|
public SpinePropertyGrid? SpinePropertyGrid { get; set; }
|
|
|
|
/// <summary>
|
|
/// 选中的索引
|
|
/// </summary>
|
|
public ListView.SelectedIndexCollection SelectedIndices => listView.SelectedIndices;
|
|
|
|
/// <summary>
|
|
/// 弹出添加对话框在末尾添加
|
|
/// </summary>
|
|
public void Add() => Insert();
|
|
|
|
/// <summary>
|
|
/// 弹出添加对话框在指定位置之前插入一项, 如果索引无效则在末尾添加
|
|
/// </summary>
|
|
private void Insert(int index = -1)
|
|
{
|
|
var dialog = new Dialogs.OpenSpineDialog();
|
|
if (dialog.ShowDialog() != DialogResult.OK) return;
|
|
Insert(dialog.Result, index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从结果在指定位置之前插入一项, 如果索引无效则在末尾添加
|
|
/// </summary>
|
|
private void Insert(Dialogs.OpenSpineDialogResult result, int index = -1)
|
|
{
|
|
try
|
|
{
|
|
var spine = Spine.SpineObject.New(result.Version, result.SkelPath, result.AtlasPath);
|
|
|
|
// 如果索引无效则在末尾添加
|
|
if (index < 0 || index > listView.Items.Count)
|
|
index = listView.Items.Count;
|
|
|
|
// 锁定外部的读操作
|
|
lock (Spines) { spines.Insert(index, spine); }
|
|
spinePropertyWrappers[spine.ID] = new(spine);
|
|
listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
|
|
listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
|
|
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
|
|
|
|
// 选中新增项
|
|
listView.SelectedIndices.Clear();
|
|
listView.SelectedIndices.Add(index);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error(ex.ToString());
|
|
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
|
|
MessagePopup.Error(ex.ToString(), "骨骼加载失败");
|
|
}
|
|
|
|
logger.LogCurrentProcessMemoryUsage();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 弹出批量添加对话框
|
|
/// </summary>
|
|
public void BatchAdd()
|
|
{
|
|
var openDialog = new Dialogs.BatchOpenSpineDialog();
|
|
if (openDialog.ShowDialog() != DialogResult.OK) return;
|
|
BatchAdd(openDialog.Result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从结果批量添加
|
|
/// </summary>
|
|
private void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
|
|
{
|
|
var progressDialog = new Dialogs.ProgressDialog();
|
|
progressDialog.DoWork += BatchAdd_Work;
|
|
progressDialog.RunWorkerAsync(result);
|
|
progressDialog.ShowDialog();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 批量添加后台任务
|
|
/// </summary>
|
|
private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
|
|
{
|
|
var worker = sender as BackgroundWorker;
|
|
var arguments = e.Argument as Dialogs.BatchOpenSpineDialogResult;
|
|
var skelPaths = arguments.SkelPaths;
|
|
var version = arguments.Version;
|
|
|
|
int totalCount = skelPaths.Length;
|
|
int success = 0;
|
|
int error = 0;
|
|
|
|
worker.ReportProgress(0, $"已处理 0/{totalCount}");
|
|
for (int i = 0; i < totalCount; i++)
|
|
{
|
|
if (worker.CancellationPending)
|
|
{
|
|
e.Cancel = true;
|
|
break;
|
|
}
|
|
|
|
var skelPath = skelPaths[i];
|
|
|
|
try
|
|
{
|
|
var spine = Spine.SpineObject.New(version, skelPath);
|
|
var preview = spine.Preview;
|
|
lock (Spines) { spines.Add(spine); }
|
|
spinePropertyWrappers[spine.ID] = new(spine);
|
|
listView.Invoke(() =>
|
|
{
|
|
listView.SmallImageList.Images.Add(spine.ID, preview);
|
|
listView.LargeImageList.Images.Add(spine.ID, preview);
|
|
listView.Items.Add(new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
|
|
});
|
|
success++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error(ex.ToString());
|
|
logger.Error("Failed to load {}", skelPath);
|
|
error++;
|
|
}
|
|
|
|
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
|
}
|
|
|
|
// 选中最后一项
|
|
listView.Invoke(() =>
|
|
{
|
|
if (listView.Items.Count > 0)
|
|
{
|
|
listView.SelectedIndices.Clear();
|
|
listView.SelectedIndices.Add(listView.Items.Count - 1);
|
|
}
|
|
});
|
|
|
|
if (error > 0)
|
|
logger.Warn("Batch load {} successfully, {} failed", success, error);
|
|
else
|
|
logger.Info("{} skel loaded successfully", success);
|
|
|
|
logger.LogCurrentProcessMemoryUsage();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从拖放/复制的路径列表添加
|
|
/// </summary>
|
|
private void AddFromFileDrop(IEnumerable<string> paths)
|
|
{
|
|
List<string> validPaths = [];
|
|
foreach (var path in paths)
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
|
validPaths.Add(path);
|
|
}
|
|
else if (Directory.Exists(path))
|
|
{
|
|
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
|
{
|
|
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
|
validPaths.Add(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (validPaths.Count > 1)
|
|
{
|
|
if (validPaths.Count > 100)
|
|
{
|
|
if (MessagePopup.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
|
|
return;
|
|
}
|
|
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
|
|
}
|
|
else if (validPaths.Count > 0)
|
|
{
|
|
Insert(new Dialogs.OpenSpineDialogResult(SpineVersion.Auto, validPaths[0]));
|
|
}
|
|
}
|
|
|
|
private void listView_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
timer_SelectedIndexChangedDebounce.Stop();
|
|
timer_SelectedIndexChangedDebounce.Start();
|
|
}
|
|
|
|
private void timer_SelectedIndexChangedDebounce_Tick(object sender, EventArgs e)
|
|
{
|
|
timer_SelectedIndexChangedDebounce.Stop();
|
|
_listView_SelectedIndexChanged(listView, EventArgs.Empty);
|
|
}
|
|
|
|
private void _listView_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
lock (Spines)
|
|
{
|
|
if (SpinePropertyGrid is not null)
|
|
{
|
|
if (listView.SelectedIndices.Count <= 0)
|
|
SpinePropertyGrid.SelectedSpines = null;
|
|
else if (listView.SelectedIndices.Count <= 1)
|
|
SpinePropertyGrid.SelectedSpines = [spinePropertyWrappers[spines[listView.SelectedIndices[0]].ID]];
|
|
else
|
|
SpinePropertyGrid.SelectedSpines = listView.SelectedIndices.Cast<int>().Select(index => spinePropertyWrappers[spines[index].ID]).ToArray();
|
|
}
|
|
|
|
// 标记选中的 Spine
|
|
for (int i = 0; i < spines.Count; i++)
|
|
spines[i].IsSelected = listView.SelectedIndices.Contains(i);
|
|
}
|
|
|
|
// XXX: 图标显示的时候没法自动刷新顺序, 只能切换视图刷新, 不知道什么原理
|
|
if (listView.View == View.LargeIcon)
|
|
{
|
|
listView.BeginUpdate();
|
|
listView.View = View.List;
|
|
listView.View = View.LargeIcon;
|
|
listView.EndUpdate();
|
|
}
|
|
|
|
if (listView.SelectedItems.Count > 0)
|
|
listView.SelectedItems[0].EnsureVisible();
|
|
|
|
toolStripStatusLabel_CountInfo.Text = $"已选择 {listView.SelectedItems.Count} 项,共 {listView.Items.Count} 项";
|
|
}
|
|
|
|
private void listView_ItemDrag(object sender, ItemDragEventArgs e)
|
|
{
|
|
DoDragDrop(e.Item, DragDropEffects.Move);
|
|
}
|
|
|
|
private void listView_DragEnter(object sender, DragEventArgs e)
|
|
{
|
|
if (e.Data.GetDataPresent(DataFormats.Serializable))
|
|
e.Effect = DragDropEffects.Move;
|
|
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
|
e.Effect = DragDropEffects.Copy;
|
|
else
|
|
e.Effect = DragDropEffects.None;
|
|
}
|
|
|
|
private void listView_DragOver(object sender, DragEventArgs e)
|
|
{
|
|
if (e.Data.GetDataPresent(DataFormats.Serializable))
|
|
{
|
|
// 获取鼠标位置并确定目标索引
|
|
var point = listView.PointToClient(new(e.X, e.Y));
|
|
var targetItem = listView.GetItemAt(point.X, point.Y);
|
|
|
|
// 高亮目标项
|
|
if (targetItem != null)
|
|
{
|
|
foreach (ListViewItem item in listView.Items)
|
|
{
|
|
item.BackColor = listView.BackColor;
|
|
}
|
|
targetItem.BackColor = Color.LightGray;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void listView_DragDrop(object sender, DragEventArgs e)
|
|
{
|
|
if (e.Data.GetDataPresent(DataFormats.Serializable))
|
|
{
|
|
// 获取拖放源项和目标项
|
|
var draggedItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem));
|
|
int draggedIndex = draggedItem.Index;
|
|
var point = listView.PointToClient(new Point(e.X, e.Y));
|
|
var targetItem = listView.GetItemAt(point.X, point.Y);
|
|
int targetIndex = targetItem is null ? listView.Items.Count : targetItem.Index;
|
|
|
|
if (targetIndex <= draggedIndex)
|
|
{
|
|
lock (Spines)
|
|
{
|
|
var draggedSpine = spines[draggedIndex];
|
|
spines.RemoveAt(draggedIndex);
|
|
spines.Insert(targetIndex, draggedSpine);
|
|
}
|
|
listView.Items.RemoveAt(draggedIndex);
|
|
listView.Items.Insert(targetIndex, draggedItem);
|
|
}
|
|
else
|
|
{
|
|
lock (Spines)
|
|
{
|
|
var draggedSpine = spines[draggedIndex];
|
|
spines.RemoveAt(draggedIndex);
|
|
spines.Insert(targetIndex - 1, draggedSpine);
|
|
}
|
|
listView.Items.RemoveAt(draggedIndex);
|
|
listView.Items.Insert(targetIndex - 1, draggedItem);
|
|
}
|
|
|
|
// 重置背景颜色
|
|
foreach (ListViewItem item in listView.Items)
|
|
{
|
|
item.BackColor = listView.BackColor;
|
|
}
|
|
}
|
|
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
|
{
|
|
AddFromFileDrop((string[])e.Data.GetData(DataFormats.FileDrop));
|
|
}
|
|
}
|
|
|
|
private void contextMenuStrip_Opening(object sender, CancelEventArgs e)
|
|
{
|
|
var selectedIndices = listView.SelectedIndices;
|
|
var selectedCount = selectedIndices.Count;
|
|
var itemsCount = listView.Items.Count;
|
|
toolStripMenuItem_Insert.Enabled = selectedCount == 1;
|
|
toolStripMenuItem_Remove.Enabled = selectedCount >= 1;
|
|
toolStripMenuItem_MoveTop.Enabled = selectedCount == 1 && selectedIndices[0] != 0;
|
|
toolStripMenuItem_MoveUp.Enabled = selectedCount == 1 && selectedIndices[0] != 0;
|
|
toolStripMenuItem_MoveDown.Enabled = selectedCount == 1 && selectedIndices[0] != itemsCount - 1;
|
|
toolStripMenuItem_MoveBottom.Enabled = selectedCount == 1 && selectedIndices[0] != itemsCount - 1;
|
|
toolStripMenuItem_RemoveAll.Enabled = itemsCount > 0;
|
|
toolStripMenuItem_CopyPreview.Enabled = selectedCount > 0;
|
|
|
|
// 视图选项
|
|
toolStripMenuItem_LargeIconView.Checked = listView.View == View.LargeIcon;
|
|
toolStripMenuItem_ListView.Checked = listView.View == View.List;
|
|
toolStripMenuItem_DetailsView.Checked = listView.View == View.Details;
|
|
}
|
|
|
|
private void contextMenuStrip_Closed(object sender, ToolStripDropDownClosedEventArgs e)
|
|
{
|
|
// 不显示菜单的时候需要把菜单的各项功能启用, 这样才能正常捕获快捷键
|
|
foreach (var item in contextMenuStrip.Items)
|
|
if (item is ToolStripMenuItem tsmi)
|
|
tsmi.Enabled = true;
|
|
}
|
|
|
|
private void toolStripMenuItem_Add_Click(object sender, EventArgs e)
|
|
{
|
|
Insert();
|
|
}
|
|
|
|
private void toolStripMenuItem_BatchAdd_Click(object sender, EventArgs e)
|
|
{
|
|
BatchAdd();
|
|
}
|
|
|
|
private void toolStripMenuItem_Insert_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.SelectedIndices.Count == 1)
|
|
Insert(listView.SelectedIndices[0]);
|
|
}
|
|
|
|
private void toolStripMenuItem_Remove_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.SelectedIndices.Count <= 0)
|
|
return;
|
|
|
|
if (listView.SelectedIndices.Count > 1)
|
|
{
|
|
if (MessagePopup.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
|
|
return;
|
|
}
|
|
|
|
lock (Spines)
|
|
{
|
|
listView.BeginUpdate();
|
|
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
|
|
{
|
|
listView.Items.RemoveAt(i);
|
|
var spine = spines[i];
|
|
spines.RemoveAt(i);
|
|
spinePropertyWrappers.Remove(spine.ID);
|
|
listView.SmallImageList.Images.RemoveByKey(spine.ID);
|
|
listView.LargeImageList.Images.RemoveByKey(spine.ID);
|
|
spine.Dispose();
|
|
}
|
|
listView.EndUpdate();
|
|
}
|
|
}
|
|
|
|
private void toolStripMenuItem_MoveTop_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.SelectedIndices.Count != 1)
|
|
return;
|
|
|
|
var index = listView.SelectedIndices[0];
|
|
if (index > 0)
|
|
{
|
|
lock (Spines)
|
|
{
|
|
var spine = spines[index];
|
|
spines.RemoveAt(index);
|
|
spines.Insert(0, spine);
|
|
}
|
|
var item = listView.Items[index];
|
|
listView.Items.RemoveAt(index);
|
|
listView.Items.Insert(0, item);
|
|
}
|
|
}
|
|
|
|
private void toolStripMenuItem_MoveUp_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.SelectedIndices.Count != 1)
|
|
return;
|
|
|
|
var index = listView.SelectedIndices[0];
|
|
if (index > 0)
|
|
{
|
|
lock (Spines) { (spines[index - 1], spines[index]) = (spines[index], spines[index - 1]); }
|
|
var item = listView.Items[index];
|
|
listView.BeginUpdate();
|
|
listView.Items.RemoveAt(index);
|
|
listView.Items.Insert(index - 1, item);
|
|
listView.EndUpdate();
|
|
}
|
|
}
|
|
|
|
private void toolStripMenuItem_MoveDown_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.SelectedIndices.Count != 1)
|
|
return;
|
|
|
|
var index = listView.SelectedIndices[0];
|
|
if (index < listView.Items.Count - 1)
|
|
{
|
|
lock (Spines) { (spines[index], spines[index + 1]) = (spines[index + 1], spines[index]); }
|
|
var item = listView.Items[index];
|
|
listView.BeginUpdate();
|
|
listView.Items.RemoveAt(index);
|
|
listView.Items.Insert(index + 1, item);
|
|
listView.EndUpdate();
|
|
}
|
|
}
|
|
|
|
private void toolStripMenuItem_MoveBottom_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.SelectedIndices.Count != 1)
|
|
return;
|
|
|
|
var index = listView.SelectedIndices[0];
|
|
if (index < listView.Items.Count - 1)
|
|
{
|
|
lock (Spines)
|
|
{
|
|
var spine = spines[index];
|
|
spines.RemoveAt(index);
|
|
spines.Add(spine);
|
|
}
|
|
var item = listView.Items[index];
|
|
listView.Items.RemoveAt(index);
|
|
listView.Items.Add(item);
|
|
}
|
|
}
|
|
|
|
private void toolStripMenuItem_RemoveAll_Click(object sender, EventArgs e)
|
|
{
|
|
if (listView.Items.Count <= 0)
|
|
return;
|
|
|
|
if (MessagePopup.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
|
|
return;
|
|
|
|
listView.Items.Clear();
|
|
lock (Spines)
|
|
{
|
|
foreach (var spine in spines) spine.Dispose();
|
|
spines.Clear();
|
|
spinePropertyWrappers.Clear();
|
|
listView.SmallImageList.Images.Clear();
|
|
listView.LargeImageList.Images.Clear();
|
|
}
|
|
if (SpinePropertyGrid is not null)
|
|
SpinePropertyGrid.SelectedSpines = null;
|
|
}
|
|
|
|
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
|
|
{
|
|
var fileDropList = new StringCollection();
|
|
var tempDir = Path.Combine(Path.GetTempPath(), Process.GetCurrentProcess().ProcessName);
|
|
Directory.CreateDirectory(tempDir);
|
|
|
|
lock (Spines)
|
|
{
|
|
foreach (int i in listView.SelectedIndices)
|
|
{
|
|
var a = Process.GetCurrentProcess();
|
|
var spine = spines[i];
|
|
var path = Path.Combine(tempDir, $"{spine.ID}.png");
|
|
spine.Preview.Save(path);
|
|
fileDropList.Add(path);
|
|
}
|
|
}
|
|
if (fileDropList.Count > 0)
|
|
Clipboard.SetFileDropList(fileDropList);
|
|
}
|
|
|
|
private void toolStripMenuItem_AddFromClipboard_Click(object sender, EventArgs e)
|
|
{
|
|
if (Clipboard.ContainsFileDropList())
|
|
{
|
|
var fileDropList = Clipboard.GetFileDropList();
|
|
var paths = new string[fileDropList.Count];
|
|
fileDropList.CopyTo(paths, 0);
|
|
AddFromFileDrop(paths);
|
|
}
|
|
}
|
|
|
|
private void toolStripMenuItem_SelectAll_Click(object sender, EventArgs e)
|
|
{
|
|
listView.BeginUpdate();
|
|
foreach (ListViewItem item in listView.Items)
|
|
item.Selected = true;
|
|
listView.EndUpdate();
|
|
}
|
|
|
|
private void toolStripMenuItem_LargeIconView_Click(object sender, EventArgs e)
|
|
{
|
|
listView.View = View.LargeIcon;
|
|
}
|
|
|
|
private void toolStripMenuItem_ListView_Click(object sender, EventArgs e)
|
|
{
|
|
listView.View = View.List;
|
|
}
|
|
|
|
private void toolStripMenuItem_DetailsView_Click(object sender, EventArgs e)
|
|
{
|
|
listView.View = View.Details;
|
|
}
|
|
}
|
|
|
|
public class DefaultSpineConfig
|
|
{
|
|
|
|
}
|
|
}
|