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 { /// /// 日志器 /// private readonly Logger logger = LogManager.GetCurrentClassLogger(); /// /// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身 /// public readonly ReadOnlyCollection Spines; /// /// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines /// private readonly List spines = []; /// /// 用于属性页显示模型参数的包装类 /// private readonly Dictionary spinePropertyWrappers = []; public SpineListView() { InitializeComponent(); Spines = spines.AsReadOnly(); } /// /// 显示骨骼信息的属性面板 /// [Category("自定义"), Description("用于显示模型属性的组合属性页")] public SpinePropertyGrid? SpinePropertyGrid { get; set; } /// /// 选中的索引 /// public ListView.SelectedIndexCollection SelectedIndices => listView.SelectedIndices; /// /// 弹出添加对话框在末尾添加 /// public void Add() => Insert(); /// /// 弹出添加对话框在指定位置之前插入一项, 如果索引无效则在末尾添加 /// private void Insert(int index = -1) { var dialog = new Dialogs.OpenSpineDialog(); if (dialog.ShowDialog() != DialogResult.OK) return; Insert(dialog.Result, index); } /// /// 从结果在指定位置之前插入一项, 如果索引无效则在末尾添加 /// 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(); } /// /// 弹出批量添加对话框 /// public void BatchAdd() { var openDialog = new Dialogs.BatchOpenSpineDialog(); if (openDialog.ShowDialog() != DialogResult.OK) return; BatchAdd(openDialog.Result); } /// /// 从结果批量添加 /// private void BatchAdd(Dialogs.BatchOpenSpineDialogResult result) { var progressDialog = new Dialogs.ProgressDialog(); progressDialog.DoWork += BatchAdd_Work; progressDialog.RunWorkerAsync(result); progressDialog.ShowDialog(); } /// /// 批量添加后台任务 /// 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(); } /// /// 从拖放/复制的路径列表添加 /// private void AddFromFileDrop(IEnumerable paths) { List 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().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().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 { } }