From b98f1adf534b88de143f31c7a274db4b850e9c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=83=AD=E5=BF=83=E5=B8=82=E6=B0=91=E7=9F=B3=E5=85=88?= =?UTF-8?q?=E7=94=9F?= <1249467256@qq.com> Date: Sun, 22 Mar 2026 00:41:15 +0800 Subject: [PATCH] Feat: Support batch operations (delete/move) for multi-selected tags in Super View Introduces comprehensive batch operations for the Super View meta editor. Users can now multi-select multiple tags in the UI to perform bulk deletions, as well as move multiple selected tags up or down simultaneously. The UI dynamically preserves the multi-selection state after moving, ensuring a seamless user experience. **Technical Rationale (Why this approach?):** * **Safe Enumeration (`.ToList()` Materialization):** By evaluating the selected items into a detached list via `.ToList()` before performing any structural modifications, we strictly avoid `InvalidOperationException` (Collection was modified; enumeration operation may not execute). This ensures absolute safety when manipulating the underlying `controller.ParsedFile.Attributes`. * **Directional Sorting for Cohesive Movement:** When moving multiple items, processing order is critical. For `ExecuteUp`, we sort the selected items in ascending index order. This guarantees the highest item moves first, preventing lower items from jumping over or overwriting higher ones. Conversely, `ExecuteDown` applies a descending sort to process the lowest items first. * **Adjacent Block Checking:** We implemented an adjacent evaluation (`!itemsToMove.Contains(itemAbove)`) to ensure that contiguously selected blocks of tags move together as a solid unit without collapsing into each other or shifting relative internal positions. * **Deferred View Updates:** Instead of refreshing the visual tree on every individual item swap or deletion, `controller.UpdateView()` is called exactly once after the entire batch operation concludes. This dramatically reduces layout passes, eliminating UI freezes during large multi-select operations. --- .../MetaEditor/Commands/DeleteEntryCommand.cs | 22 +++- .../MetaEditor/Commands/MoveEntryCommand.cs | 109 +++++++++++++----- 2 files changed, 100 insertions(+), 31 deletions(-) diff --git a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs index 269acbaf4..b5274cdfd 100644 --- a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs +++ b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/DeleteEntryCommand.cs @@ -7,14 +7,26 @@ internal class DeleteEntryCommand : IUiCommand { public void Execute(MetaDataEditorViewModel controller) { - var itemToRemove = controller.SelectedAttribute; - if (itemToRemove == null || controller.ParsedFile == null) - return; + if (controller.ParsedFile == null) return; + + // Get all selected items from the UI tags + var itemsToRemove = controller.Tags + .Where(x => x.IsSelected) + .Select(x => x._input) + .ToList(); + + if (!itemsToRemove.Any()) return; + + // Batch remove all selected tags + foreach (var item in itemsToRemove) + { + controller.ParsedFile.Attributes.Remove(item); + } - controller.ParsedFile.Attributes.Remove(itemToRemove); controller.UpdateView(); + + // Select the first item by default after deletion controller.SelectedTag = controller.Tags.FirstOrDefault(); } - } } diff --git a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs index d111d8223..1f8ffc1a1 100644 --- a/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs +++ b/Editors/MetaDataEditor/AnimationMeta/MetaEditor/Commands/MoveEntryCommand.cs @@ -7,39 +7,96 @@ internal class MoveEntryCommand : IUiCommand { public void ExecuteUp(MetaDataEditorViewModel controller) { - var itemToMove = controller.SelectedAttribute; - if (itemToMove == null || controller.ParsedFile == null) - return; - - var currentIndex = controller.ParsedFile.Attributes.IndexOf(itemToMove); - if (currentIndex == 0) - return; - - controller.ParsedFile.Attributes.Remove(itemToMove); - controller.ParsedFile.Attributes.Insert(currentIndex - 1, itemToMove); - controller.UpdateView(); - controller.SelectedTag = controller.Tags - .Where(x => x._input == itemToMove) - .FirstOrDefault(); + if (controller.ParsedFile == null) return; + + var itemsToMove = controller.Tags + .Where(x => x.IsSelected) + .Select(x => x._input) + .ToList(); + + if (!itemsToMove.Any()) return; + + var attributes = controller.ParsedFile.Attributes; + + // Sort ascending to move top items first, preventing them from jumping over each other + var sortedItems = itemsToMove.OrderBy(x => attributes.IndexOf(x)).ToList(); + bool moved = false; + + foreach (var item in sortedItems) + { + var currentIndex = attributes.IndexOf(item); + if (currentIndex > 0) + { + // If the item above is also in the selection, keep them as a block + var itemAbove = attributes[currentIndex - 1]; + if (!itemsToMove.Contains(itemAbove)) + { + attributes.RemoveAt(currentIndex); + attributes.Insert(currentIndex - 1, item); + moved = true; + } + } + } + + if (moved) + { + controller.UpdateView(); + + // Restore selection state for the moved items + foreach (var tag in controller.Tags.Where(t => itemsToMove.Contains(t._input))) + { + tag.IsSelected = true; + } + + controller.SelectedTag = controller.Tags.FirstOrDefault(x => x.IsSelected); + } } public void ExecuteDown(MetaDataEditorViewModel controller) { - var itemToMove = controller.SelectedAttribute; - if (itemToMove == null || controller.ParsedFile == null) - return; + if (controller.ParsedFile == null) return; + + var itemsToMove = controller.Tags + .Where(x => x.IsSelected) + .Select(x => x._input) + .ToList(); + + if (!itemsToMove.Any()) return; + + var attributes = controller.ParsedFile.Attributes; + + // Sort descending to move bottom items first + var sortedItems = itemsToMove.OrderByDescending(x => attributes.IndexOf(x)).ToList(); + bool moved = false; + + foreach (var item in sortedItems) + { + var currentIndex = attributes.IndexOf(item); + if (currentIndex < attributes.Count - 1) + { + // If the item below is also in the selection, keep them as a block + var itemBelow = attributes[currentIndex + 1]; + if (!itemsToMove.Contains(itemBelow)) + { + attributes.RemoveAt(currentIndex); + attributes.Insert(currentIndex + 1, item); + moved = true; + } + } + } - var currentIndex = controller.ParsedFile.Attributes.IndexOf(itemToMove); - if (currentIndex == controller.ParsedFile.Attributes.Count -1) - return; + if (moved) + { + controller.UpdateView(); - controller.ParsedFile.Attributes.Remove(itemToMove); - controller.ParsedFile.Attributes.Insert(currentIndex + 1, itemToMove); - controller.UpdateView(); - controller.SelectedTag = controller.Tags - .Where(x => x._input == itemToMove) - .FirstOrDefault(); + // Restore selection state for the moved items + foreach (var tag in controller.Tags.Where(t => itemsToMove.Contains(t._input))) + { + tag.IsSelected = true; + } + controller.SelectedTag = controller.Tags.FirstOrDefault(x => x.IsSelected); + } } } }