#nullable enable using System.Diagnostics; using T3.Core.Operator; using T3.Core.Resource; using ImGuiNET; using T3.Editor.Gui; using T3.Editor.Gui.Dialogs; using T3.Editor.Gui.Window; using T3.Editor.Gui.Interaction; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows.Layouts; using T3.Editor.Gui.Windows.Output; using T3.Editor.Gui.Windows.TimeLine; using T3.Editor.UiModel.Selection; namespace T3.Editor.UiModel.ProjectHandling; internal sealed partial class ProjectView { public readonly NavigationHistory NavigationHistory; public readonly NodeSelection NodeSelection; public readonly GraphImageBackground GraphImageBackground; public readonly NodeNavigation NodeNavigation; public Structure Structure => OpenedProject.Structure; public IGraphView GraphView { get; set; } = null!; // TODO: remove set accessibility public OpenedProject OpenedProject { get; } private readonly List _compositionPath = []; private InstanceView? _instView; public InstanceView? InstView => _instView; public Instance? CompositionInstance => InstView?.Instance; public Instance RootInstance => GetRoot(); private bool _subscribedToRootDisposing; private Instance GetRoot() { var package = OpenedProject.Package; if (!package.Symbols.TryGetValue(package.HomeSymbolId, out var rootSymbol)) { throw new Exception("Root symbol not found in project."); } if (!rootSymbol.TryGetParentlessInstance(out var rootInstance)) { throw new Exception("Root instance could not be created?"); } if(!_subscribedToRootDisposing) { _subscribedToRootDisposing = true; rootInstance.Disposing += OnRootDisposed; } return rootInstance; } private void OnRootDisposed(IResourceConsumer rootAsResourceConsumer) { _subscribedToRootDisposing = false; rootAsResourceConsumer.Disposing -= OnRootDisposed; NodeSelection.Clear(); FlagChanges(ProjectView.ChangeTypes.Children | ProjectView.ChangeTypes.Composition); } private readonly Stack _compositionsAwaitingDisposal = []; private readonly Stack _compositionReloadStack = []; private bool _waitingOnReload; private void SetCompositionOp(Instance? newCompositionOp) { if (newCompositionOp == null) { if (_instView != null) { DisposeComposition(_instView, []); } _instView = null; return; } if (_instView != null) { if (_instView.Is(newCompositionOp)) { return; } var path = new List(); Structure.PopulateInstancePath(newCompositionOp, path); DisposeComposition(_instView, path); } _instView = InstanceView.GetForInstance(newCompositionOp); if (_instView != null) { OnCompositionChanged?.Invoke(this, _instView.SymbolChildId); } return; void DisposeComposition(InstanceView oldComposition, List path) { _compositionsAwaitingDisposal.Push(oldComposition); while (_compositionsAwaitingDisposal.TryPop(out var compositionToDispose)) { if (path.Contains(compositionToDispose.SymbolChildId)) { // not ready yet to dispose as it is still part of the current composition stack _compositionsAwaitingDisposal.Push(compositionToDispose); break; } compositionToDispose.Dispose(); if (compositionToDispose is { CheckoutCount: 0, IsReadOnly: true, HasBeenModified: true }) { _compositionReloadStack.Push(compositionToDispose); } } } } public void CheckDisposal() { if (_compositionReloadStack.TryPeek(out var nextToReload)) { ShowSymbolReloadDialog(nextToReload); } return; void ShowSymbolReloadDialog(InstanceView composition) { var symbolChildId = composition.SymbolChildId; var symbol = composition.Symbol; var symbolChildUi = composition.Instance.Parent?.GetSymbolUi().ChildUis[symbolChildId]; if (symbolChildUi != null && composition.Instance.Parent != null) { var parentSymbolId = composition.Instance.Parent.Symbol.Id; _duplicateSymbolDialog.ShowNextFrame(); // actually shows this frame _duplicateSymbolDialog.Draw(symbolGuid: parentSymbolId, selectedChildUis2: [symbolChildUi], nameSpace: ref _dupeReadonlyNamespace, newTypeName: ref _dupeReadonlyName, description: ref _dupeReadonlyDescription, isReload: true); if (!_waitingOnReload) { _duplicateSymbolDialog.Closed += ReloadSymbol; _waitingOnReload = true; } } return; void ReloadSymbol() { _duplicateSymbolDialog.Closed -= ReloadSymbol; _compositionReloadStack.Pop(); _waitingOnReload = false; var symbolPackage = (EditorSymbolPackage)symbol.SymbolPackage; symbolPackage.Reload(symbol.GetSymbolUi()); } } } public readonly TimeLineCanvas TimeLineCanvas; public delegate void CompositionChangedHandler(ProjectView projectView, Guid symbolChildId); public event CompositionChangedHandler? OnCompositionChanged; public void FlagChanges(ChangeTypes changeTypes) { OnCompositionContentChanged?.Invoke(this, changeTypes); } public event Action? OnCompositionContentChanged; [Flags] public enum ChangeTypes { None =0, Connections = 1<< 1, Children = 1<<2, Layout = 1<<3, Composition = 1<<4, GraphStyle = 1<<5, } #region initialization --------------------------------------------------------- public ProjectView(OpenedProject openedProject, NavigationHistory navigationHistory, NodeSelection nodeSelection, GraphImageBackground graphImageBackground) { OpenedProject = openedProject; NavigationHistory = navigationHistory; NodeSelection = nodeSelection; GraphImageBackground = graphImageBackground; var getCompositionOp = () => CompositionInstance; NodeNavigation = new NodeNavigation(openedProject.Structure, NavigationHistory, getCompositionOp); TimeLineCanvas = new TimeLineCanvas(NodeSelection, getCompositionOp, TrySetCompositionOpToChild); SetCompositionOp(RootInstance); } public static void CreateIndependentComponents(OpenedProject openedProject, out NavigationHistory navigationHistory, out NodeSelection nodeSelection, out GraphImageBackground graphImageBackground) { var structure = openedProject.Structure; navigationHistory = new NavigationHistory(structure); nodeSelection = new NodeSelection(navigationHistory, structure); graphImageBackground = new GraphImageBackground(nodeSelection, structure); } private void SaveUsersViewForCurrentComposition() { Debug.Assert(CompositionInstance != null); if (CompositionInstance == null || GraphView is not ScalableCanvas canvas) return; var lastSymbolChildId = CompositionInstance.SymbolChildId; var visibleCanvasArea = canvas.GetVisibleCanvasArea(); UserSettings.Config.ViewedCanvasAreaForSymbolChildId[lastSymbolChildId] = visibleCanvasArea; UserSettings.Save(); } public bool TrySetCompositionOp(IReadOnlyList newIdPath, ScalableCanvas.Transition transition = ScalableCanvas.Transition.JumpIn, Guid? alsoSelectChildId = null, bool tryRestoreViewArea = true) { var structure = OpenedProject.Structure; var newCompositionInstance = structure.GetInstanceFromIdPath(newIdPath); var compositionChanged = false; if (newCompositionInstance == null) { var pathString = string.Join('/', structure.GetReadableInstancePath(newIdPath)); Log.Error("Failed to find instance with path " + pathString); return false; } // Save previous view for user if (CompositionInstance != null && CompositionInstance != newCompositionInstance) { compositionChanged = true; SaveUsersViewForCurrentComposition(); } // Set new Composition if required var targetCompositionAlreadyActive = InstView != null && InstView.SymbolChildId == newCompositionInstance.SymbolChildId; if (targetCompositionAlreadyActive) { if (newIdPath[0] != RootInstance.SymbolChildId) { throw new Exception("Root instance is not the first element in the path"); } } else { SetCompositionOp(newCompositionInstance); } // Although the composition might already be active, the _compositionPath might not have been initialized yet. // TODO: This probably is an indication, that this should be refactored into CompositionOp and avoid holding this twice _compositionPath.Clear(); _compositionPath.AddRange(newIdPath); Debug.Assert(InstView != null); // Additionally select a child NodeSelection.Clear(); TimeLineCanvas.ClearSelection(); // This happens when jumping out of an open. if (ScalableCanvas != null) { if (alsoSelectChildId != null && InstView.Instance.Children.TryGetChildInstance(alsoSelectChildId.Value, out var instance)) { //var instance = InstView.Instance.Children[alsoSelectChildId.Value]; NodeSelection.SetSelection(instance.GetChildUi()!, instance); var bounds = NodeSelection.GetSelectionBounds(NodeSelection, instance, 400); ScalableCanvas.RequestTargetViewAreaWithTransition(bounds, compositionChanged ? transition : ScalableCanvas.Transition.Smooth); } else { Debug.Assert(CompositionInstance != null); var compositionOpSymbolChildId = CompositionInstance.SymbolChildId; IEnumerable childUisValues = InstView.SymbolUi.ChildUis.Values; if (tryRestoreViewArea) { var savedOrValidView = CanvasHelpers.GetSavedOrValidViewForComposition(compositionOpSymbolChildId, childUisValues); ScalableCanvas.RequestTargetViewAreaWithTransition(savedOrValidView, transition); } else { var areaOnCanvas = NodeSelection.GetSelectionBounds(NodeSelection, CompositionInstance); ScalableCanvas.RequestTargetViewAreaWithTransition(areaOnCanvas, transition); FocusViewToSelection(); } } } return true; } private ScalableCanvas? ScalableCanvas => GraphView as ScalableCanvas; public bool TrySetCompositionOpToChild(Guid symbolChildId) { List path = [.._compositionPath, symbolChildId]; return TrySetCompositionOp(path, ScalableCanvas.Transition.JumpIn); } public bool TrySetCompositionOpToParent() { if (_compositionPath.Count == 1) return false; var previousComposition = InstView; // new list as _compositionPath is mutable var path = _compositionPath.GetRange(0, _compositionPath.Count - 1); // pass the child UI only in case the previous composition was a cloned instance return TrySetCompositionOp(path, ScalableCanvas.Transition.JumpOut, previousComposition!.SymbolChildId); } #endregion public void SetBackgroundOutput(Instance instance) { GraphImageBackground.OutputInstance = instance; } private readonly DuplicateSymbolDialog _duplicateSymbolDialog = new(); private string _dupeReadonlyNamespace = ""; private string _dupeReadonlyName = ""; private string _dupeReadonlyDescription = ""; public void Close() { if(CompositionInstance != null) SaveUsersViewForCurrentComposition(); GraphView.Close(); if (Focused != this) return; foreach (var graphWindow in GraphWindow.GraphWindowInstances) { if (graphWindow.ProjectView == this) graphWindow.CloseView(); // if (graphWindow.Config.Visible && graphWindow.ProjectView != null) // { // Focused = graphWindow.ProjectView; // } } // Save output window state before clearing focus — pinning state // is now persisted in OutputWindowState and restored on project reopen foreach (var window in OutputWindow.OutputWindowInstances) { if (window is OutputWindow outputWindow) { outputWindow.SaveStateToProject(); } } // Save window layout to the project's root .t3ui SaveWindowLayoutToProject(); Focused = null; } public void SetAsFocused() { var projectChanged = Focused != this; Focused = this; if (projectChanged) RestoreWindowLayoutFromProject(); } /// /// Captures the current window layout and visibility into the focused project's root symbol, /// so a subsequent save persists the up-to-date state instead of the snapshot from the last /// project close. Uses ImGui and must be called on the UI thread. /// internal static void SnapshotWindowLayoutForFocusedProject() { Focused?.SaveWindowLayoutToProject(); } private void SaveWindowLayoutToProject() { if (!UserSettings.Config.SaveWindowLayoutsWithProjects) return; var symbolUi = RootInstance?.Symbol.GetSymbolUi(); if (symbolUi == null) return; var imGuiSettings = ImGui.SaveIniSettingsToMemory(); var imGuiVersion = ImGui.GetVersion(); var windowVisibility = new Dictionary(); foreach (var window in WindowManager.GetAllWindows()) { if (!string.IsNullOrEmpty(window.Config.Title)) windowVisibility[window.Config.Title] = window.Config.Visible; } var unchanged = symbolUi.WindowLayout == imGuiSettings && symbolUi.WindowLayoutImGuiVersion == imGuiVersion && IsVisibilityUnchanged(symbolUi.WindowVisibility, windowVisibility); if (unchanged) return; symbolUi.WindowLayout = imGuiSettings; symbolUi.WindowLayoutImGuiVersion = imGuiVersion; symbolUi.WindowVisibility = windowVisibility; symbolUi.FlagAsModified(); } private static bool IsVisibilityUnchanged(Dictionary? saved, Dictionary current) { if (saved == null || saved.Count != current.Count) return false; foreach (var (title, visible) in current) { if (!saved.TryGetValue(title, out var savedVisible) || savedVisible != visible) { return false; } } return true; } private void RestoreWindowLayoutFromProject() { if (!UserSettings.Config.SaveWindowLayoutsWithProjects) return; var symbolUi = RootInstance?.Symbol.GetSymbolUi(); if (symbolUi == null) return; // Restore window visibility if (symbolUi.WindowVisibility is { Count: > 0 }) { foreach (var window in WindowManager.GetAllWindows()) { if (!string.IsNullOrEmpty(window.Config.Title) && symbolUi.WindowVisibility.TryGetValue(window.Config.Title, out var visible)) { window.Config.Visible = visible; } } } // Restore ImGui docking layout if (string.IsNullOrEmpty(symbolUi.WindowLayout)) return; if (!string.IsNullOrEmpty(symbolUi.WindowLayoutImGuiVersion)) { if (Version.TryParse(symbolUi.WindowLayoutImGuiVersion, out var savedVersion) && Version.TryParse(ImGui.GetVersion(), out var currentVersion) && savedVersion.Major == currentVersion.Major && savedVersion.Minor == currentVersion.Minor) { Program.NewImGuiLayoutDefinition = symbolUi.WindowLayout; } } } public void FocusViewToSelection() { if (CompositionInstance == null) return; var areaOnCanvas = NodeSelection.GetSelectionBounds(NodeSelection, CompositionInstance); areaOnCanvas.Expand(200); GraphView.Canvas.FitAreaOnCanvas(areaOnCanvas); } public static ProjectView? Focused { get; private set; } /// /// True while a double-click's first click may predate this view being opened — e.g. the single /// click that opened the project from the hub. Callers should ignore double-clicks during this window. /// public bool OpenedWithinDoubleClickTime => ImGui.GetTime() - _createdAtTime < ImGui.GetIO().MouseDoubleClickTime; private readonly double _createdAtTime = ImGui.GetTime(); }