#nullable enable using System.Diagnostics.CodeAnalysis; using ImGuiNET; using T3.Core.Animation; using T3.Core.DataTypes.DataSet; using T3.Core.Operator; using T3.Core.Operator.Interfaces; using T3.Core.Operator.Slots; using T3.Core.Resource.Assets; using T3.Editor.Gui.OutputUi; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.IoServices; namespace T3.Editor.Gui.Windows.TimeLine; /// /// Inline DataClip edit pane shown below the dope sheet when /// is on AND a TimeClip with a /// DataClip output is selected. The actual channel / event rendering is delegated /// to in its embedded mode (X driven by the parent /// , no own toolbar / raster / auto-scroll), so this class /// stays a thin host: figure out which clip the user picked, resolve its DataSet, hand /// both to the shared canvas, and own the chrome around it (close button, empty state). /// internal sealed class InlineDataClipArea { public InlineDataClipArea(TimeLineCanvas timeLineCanvas) { _timeLineCanvas = timeLineCanvas; } /// True when at least one selected op-TimeClip exposes a Slot<DataClip>. public static bool HasSelectedDataClipInstance(TimeLineCanvas timeLineCanvas, Instance compositionOp) { foreach (var timeClip in timeLineCanvas.ClipArea.OpClips.EnumerateSelectedClips()) { if (!compositionOp.Children.TryGetChildInstance(timeClip.Id, out var instance) || instance == null) continue; foreach (var slot in instance.Outputs) { if (slot is Slot) return true; } } return false; } public void Draw(Instance compositionOp, float height) { if (!TryResolveSelectedDataClip(compositionOp, out var clipInstance, out var clipTimeClip)) return; // Reset Y-scroll when the pane first appears OR the user picks a different clip // — carrying a previous pan offset over is disorienting in both cases. var thisFrame = ImGui.GetFrameCount(); var justAppeared = _lastDrawnFrame != thisFrame - 1; _lastDrawnFrame = thisFrame; var selectedClipKey = clipInstance.SymbolChildId.GetHashCode(); var clipChanged = selectedClipKey != _lastClipKey; _lastClipKey = selectedClipKey; var dataClip = TryBuildEmbeddedClip(clipInstance, clipTimeClip); if (dataClip == null) { DrawEmptyState(height, "Selected clip has no DataSet to display."); return; } ImGui.PushStyleColor(ImGuiCol.ChildBg, UiColors.BackgroundFull.Fade(0.2f).Rgba); ImGui.BeginChild("##inlineDataClipArea", new Vector2(0, height), ImGuiChildFlags.None, ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoScrollWithMouse); { // Scroll-reset and close button live INSIDE the embedded view's Scrollable // child — anything we draw out here gets blocked by that child's hit-rect. _embeddedView.DrawEmbedded(dataClip, _timeLineCanvas, drawOverlay: () => { if (clipChanged || justAppeared) ImGui.SetScrollY(0); TimelineDetailsArea.DrawCloseButton(_timeLineCanvas); }); } ImGui.EndChild(); ImGui.PopStyleColor(); } /// First selected TimeClip with a DataClip output; multi-clip editing is future work. private bool TryResolveSelectedDataClip(Instance compositionOp, [NotNullWhen(true)] out Instance? clipInstance, [NotNullWhen(true)] out TimeClip? clipTimeClip) { clipInstance = null; clipTimeClip = null; foreach (var timeClip in _timeLineCanvas.ClipArea.OpClips.EnumerateSelectedClips()) { if (!compositionOp.Children.TryGetChildInstance(timeClip.Id, out var instance) || instance == null) continue; foreach (var slot in instance.Outputs) { if (slot is not Slot) continue; clipInstance = instance; clipTimeClip = timeClip; return true; } } return false; } /// /// Builds a transient from the live DataSet + a fresh /// . DataSet resolution mirrors /// 's slot → recording → cache fall-back. /// private static DataClip? TryBuildEmbeddedClip(Instance clipInstance, TimeClip clipTimeClip) { var dataSet = TryResolveDataSet(clipInstance); if (dataSet == null) return null; var bpm = Playback.Current?.Bpm ?? 120.0; var mapping = new TimeRangeMapping(clipTimeClip.TimeRange, clipTimeClip.SourceRange, bpm); return new DataClip { Set = dataSet, Mapping = mapping }; } private static DataSet? TryResolveDataSet(Instance instance) { foreach (var slot in instance.Outputs) { if (slot is Slot { Value: { Set: not null } cached }) return cached.Set; } if (RecordingSession.TryGetLiveDataSet(instance.SymbolChildId, out var liveSet) && liveSet != null) return liveSet; if (instance is not IDescriptiveFilename descriptive) return null; var path = descriptive.SourcePathSlot.TypedInputValue.Value; if (string.IsNullOrEmpty(path)) return null; if (!AssetRegistry.TryResolveAddress(path, instance, out var absolutePath, out _)) return null; return DataClipFiles.TryGetDataSetForFile(path, absolutePath, out var dataSet, out _) ? dataSet : null; } private static void DrawEmptyState(float height, string message) { ImGui.BeginChild("##inlineDataClipAreaEmpty", new Vector2(0, height), ImGuiChildFlags.None, ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoScrollbar); var size = ImGui.CalcTextSize(message); var pos = ImGui.GetWindowPos() + (ImGui.GetWindowSize() - size) * 0.5f; ImGui.GetWindowDrawList().AddText(pos, UiColors.TextMuted, message); ImGui.EndChild(); } private readonly TimeLineCanvas _timeLineCanvas; // Shared view instance — embedded mode rendering uses the parent timeline canvas // for X transforms, so we hold one per host. State (channel selection, time-range // selection, etc.) persists across draws as it would in a standalone DataSetViewCanvas. private readonly DataSetViewCanvas _embeddedView = new(); // Selected-clip identity from last frame; switching clips resets the vertical scroll // on the embedded view's child window. private int _lastClipKey; private int _lastDrawnFrame = -1; }