#nullable enable using T3.Core.DataTypes.DataSet; namespace T3.Editor.UiModel.Commands.Animation; /// /// Undoable removal of either whole s or events inside a time /// range on selected channels. Backs the "Remove" button in the DataSet output view; the /// two modes share a command so the user gets a single undo entry regardless of which /// branch fired. Captures the data needed for restore at construction time; /// applies the removal and re-inserts in reverse order so indices line /// up with the original list state. /// /// /// Event lists may be appended to from a background recorder thread, so every mutation /// takes the per-channel Events lock. The command captures references to live /// objects; if the itself is replaced /// (e.g. file reload) before undo runs, the restore quietly no-ops on the missing channels /// rather than throwing. /// internal sealed class RemoveDataSetItemsCommand : ICommand { public string Name => _isChannelRemoval ? "Remove DataSet channels" : "Remove DataSet events"; public bool IsUndoable => true; /// /// Channel-removal mode: drops the listed channels entirely (events and metadata). /// Caller supplies the channels to remove — usually the user's selection. Undo /// re-inserts them at their original indices. /// public static RemoveDataSetItemsCommand ForChannels(DataSet dataSet, IReadOnlyList channels) { var entries = new List(channels.Count); foreach (var channel in channels) { var index = dataSet.Channels.IndexOf(channel); if (index < 0) continue; entries.Add(new ChannelEntry(index, channel)); } // Stable removal order: descending index so each remove doesn't shift the next. entries.Sort((a, b) => b.OriginalIndex.CompareTo(a.OriginalIndex)); return new RemoveDataSetItemsCommand(dataSet, channelEntries: entries); } /// /// Event-range mode: deletes every event whose falls in /// [min(rangeA, rangeB), max(rangeA, rangeB)] on each supplied channel. If /// is empty the full list is /// the implicit selection — matches the "no-channel-selection means all channels" /// behaviour the Remove button already used. /// public static RemoveDataSetItemsCommand ForEventRange(DataSet dataSet, IReadOnlyList channels, double rangeA, double rangeB) { var rangeMin = Math.Min(rangeA, rangeB); var rangeMax = Math.Max(rangeA, rangeB); var effective = channels.Count > 0 ? channels : (IReadOnlyList)dataSet.Channels; var entries = new List(effective.Count); foreach (var channel in effective) { // Snapshot the indices + values that match the range so Undo can re-insert. // Walking once here is cheaper than re-scanning Events in Do() — and lets // Undo restore even if the channel is later cleared by another command. List? matches = null; lock (channel.Events) { for (var i = 0; i < channel.Events.Count; i++) { var ev = channel.Events[i]; if (ev == null) continue; if (ev.Time < rangeMin || ev.Time > rangeMax) continue; matches ??= new List(); matches.Add(new EventRestore(i, ev)); } } if (matches != null) entries.Add(new EventRangeEntry(channel, matches)); } return new RemoveDataSetItemsCommand(dataSet, eventRangeEntries: entries); } private RemoveDataSetItemsCommand(DataSet dataSet, List? channelEntries = null, List? eventRangeEntries = null) { _dataSet = dataSet; _channelEntries = channelEntries; _eventRangeEntries = eventRangeEntries; _isChannelRemoval = channelEntries != null; } public void Do() { if (_channelEntries != null) { // Entries are already in descending-index order so each remove leaves the // surviving indices intact for the next one. foreach (var entry in _channelEntries) { if (entry.OriginalIndex >= 0 && entry.OriginalIndex < _dataSet.Channels.Count && ReferenceEquals(_dataSet.Channels[entry.OriginalIndex], entry.Channel)) { _dataSet.Channels.RemoveAt(entry.OriginalIndex); } else { // Channel was moved/replaced since capture — fall back to a reference // remove so we still drop the right object if it's still present. _dataSet.Channels.Remove(entry.Channel); } } PersistChange(); return; } if (_eventRangeEntries != null) { foreach (var entry in _eventRangeEntries) { lock (entry.Channel.Events) { // Iterate the captured restores in descending index order so each // removal at i doesn't shift the index of the next. for (var i = entry.Events.Count - 1; i >= 0; i--) { var restore = entry.Events[i]; if (restore.OriginalIndex >= 0 && restore.OriginalIndex < entry.Channel.Events.Count && ReferenceEquals(entry.Channel.Events[restore.OriginalIndex], restore.Event)) { entry.Channel.Events.RemoveAt(restore.OriginalIndex); } else { // Index has drifted (e.g. concurrent recording). Reference // remove keeps the contract — the event we promised to remove // is gone, even if it costs an O(n) scan. entry.Channel.Events.Remove(restore.Event); } } } } PersistChange(); } } public void Undo() { if (_channelEntries != null) { // Insert in ascending order so each insertion lands at the captured index // without later inserts displacing earlier ones. for (var i = _channelEntries.Count - 1; i >= 0; i--) { var entry = _channelEntries[i]; var insertAt = Math.Min(entry.OriginalIndex, _dataSet.Channels.Count); _dataSet.Channels.Insert(insertAt, entry.Channel); } PersistChange(); return; } if (_eventRangeEntries != null) { foreach (var entry in _eventRangeEntries) { lock (entry.Channel.Events) { // Restore in ascending captured-index order so earlier inserts // don't push later ones out of place. for (var i = 0; i < entry.Events.Count; i++) { var restore = entry.Events[i]; var insertAt = Math.Min(restore.OriginalIndex, entry.Channel.Events.Count); entry.Channel.Events.Insert(insertAt, restore.Event); } } } PersistChange(); } } /// /// Mirrors the in-memory mutation to the source .data file. If the dataset /// isn't cache-owned (e.g. an in-flight recording before EndRecording has /// finalised the file) the edit stays in memory only — logged so the user can spot /// that the change won't survive a reload, but not surfaced as an error since the /// in-memory effect still played out. /// private void PersistChange() { if (!DataSetCache.TryWriteBack(_dataSet, out var reason)) { Log.Warning($"RemoveDataSetItemsCommand: in-memory edit applied but not persisted ({reason})"); } } private readonly DataSet _dataSet; private readonly List? _channelEntries; private readonly List? _eventRangeEntries; private readonly bool _isChannelRemoval; private readonly record struct ChannelEntry(int OriginalIndex, DataChannel Channel); private readonly record struct EventRestore(int OriginalIndex, DataEvent? Event); private sealed record EventRangeEntry(DataChannel Channel, List Events); }