#nullable enable using ImGuiNET; using T3.Core.DataTypes.Vector; using T3.Core.Operator; using T3.Core.SystemUi; using T3.Core.Utils; using T3.Editor.Gui.Dialogs; using T3.Editor.Gui.Help; using T3.Editor.Gui.Legacy.Interaction.Connections; using T3.Editor.Gui.Input; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.UiHelpers.Thumbnails; using T3.Editor.Gui.Windows.AssetLib; using T3.Editor.UiModel; using T3.Editor.UiModel.Helpers; using T3.Editor.UiModel.InputsAndTypes; using T3.Editor.UiModel.ProjectHandling; namespace T3.Editor.Gui.Windows.SymbolLib; /// /// The SymbolLibrary window displays a hierarchical tree of all defined symbols, organized by namespace. /// It provides search, filtering, and management features for symbols, including drag-and-drop, renaming namespaces, /// deleting symbols, and visual feedback for selection and usage dependencies. /// /// /// This class is the main UI for browsing, searching, and managing operator symbols in the editor. /// It supports advanced features such as dependency scanning, random prompt suggestions, and context menus for symbol actions. /// [HelpUiID("SymbolLibrary")] internal sealed class SymbolLibrary : Window { /// /// Initializes a new instance of the window. /// Sets up symbol filtering, random prompt generation, and filtering UI logic. /// Also populates the symbol tree with all available symbols. /// internal SymbolLibrary() { _filter.SearchString = ""; _randomPromptGenerator = new RandomPromptGenerator(_filter); _libraryFiltering = new LibraryFiltering(this); Config.Title = "Symbol Library"; _treeNode.PopulateCompleteTree(); } /// /// Opens the window, expands the namespace tree to , and scrolls it into /// view (without touching the search filter). Used by operator links in markdown. /// internal void Reveal(Guid symbolId) { _expandToSymbolTargetId = symbolId; _expandToSymbolTriggered = true; _scrollToSymbolId = symbolId; // The highlight normally mirrors the graph selection. Hold the revealed symbol // highlighted until that selection changes, so reveals from documentation links // (which don't select anything) stay visible. _revealHighlightActive = true; _graphSelectionAtReveal = TryGetSingleSelectedSymbolId(); Config.Visible = true; } /// /// Draws the main content of the Symbol Library window, including dialogs and the symbol tree or usage view. /// protected override void DrawContent() { // Update highlight/aim icon state for selected symbol UpdateSelectedSymbolHighlight(); // Show rename namespace dialog if needed if (_subtreeNodeToRename != null) _renameNamespaceDialog.Draw(_subtreeNodeToRename); // Show delete symbol dialog if needed if (_symbolToDelete != null) _deleteSymbolDialog.Draw(_symbolToDelete); ImGui.PushStyleVar(ImGuiStyleVar.IndentSpacing, 10); // Show usages view if a symbol's usage is being inspected if (_symbolUsageReferenceFilter != null) { DrawUsagesAReferencedSymbol(); } else { DrawView(); } ImGui.PopStyleVar(1); } // Indicates if a refresh of the symbol library is needed //private static bool _refreshTriggered; private static readonly TreeHandler _treeHandler = new(); /// /// Draws the main symbol library view, including search bar, filters, and the result tree. /// private void DrawView() { _treeHandler.Update(); var iconCount = 2; if (_wasScanned) iconCount++; // Draw search input field CustomComponents.DrawInputFieldWithPlaceholder( "Search symbols...", ref _filter.SearchString, -ImGui.GetFrameHeight() * iconCount + 16); // Collapse icon { ImGui.SameLine(); var collapseIconState = _treeHandler.NoFolderOpen ? CustomComponents.ButtonStates.Default : CustomComponents.ButtonStates.Emphasized; if (CustomComponents.IconButton(Icon.TreeCollapse, Vector2.Zero, collapseIconState)) { _treeHandler.CollapseAll(); } } ImGui.SameLine(); // Draw refresh button and handle refresh logic if (CustomComponents.IconButton(Icon.Refresh, Vector2.Zero, CustomComponents.ButtonStates.Default) || NeedsRebuild) { UpdateSymbolLibraryState(); } CustomComponents.TooltipForLastItem( "Scan usage dependencies for symbols", "This can be useful for cleaning up operator name spaces."); // Draw filter toggles if scan was performed if (_wasScanned) { _libraryFiltering.DrawSymbolFilters(); } var textColor = UiColors.Text.Fade(0.8f); ImGui.PushStyleColor(ImGuiCol.Text, textColor.Rgba); ImGui.BeginChild("scrolling", Vector2.Zero, ImGuiChildFlags.None, ImGuiWindowFlags.NoBackground); { // Show filtered or full tree depending on filter/search state if (_libraryFiltering.AnyFilterActive) { DrawNode(FilteredTree); } else if (string.IsNullOrEmpty(_filter.SearchString)) { DrawNode(_treeNode); } else if (_filter.SearchString.Contains('?')) { _randomPromptGenerator.DrawRandomPromptList(); } else { DrawFilteredList(); } } ImGui.PopStyleColor(); CustomComponents.HandleDragScrolling(this); ImGui.EndChild(); } /// /// Updates the symbol library state by repopulating the tree and updating analysis details. /// private void UpdateSymbolLibraryState() { _treeHandler.Reset(); _treeNode.PopulateCompleteTree(); ExampleSymbolLinking.UpdateExampleLinks(); SymbolAnalysis.UpdateDetails(); _wasScanned = true; _lastUpdateVersion = EditorSymbolPackage.SymbolStructureVersionCounter; } private bool NeedsRebuild => _lastUpdateVersion < EditorSymbolPackage.SymbolStructureVersionCounter; private int _lastUpdateVersion; /// /// Shows a list of usages for a referenced symbol if the "used by" indicator was clicked. /// private void DrawUsagesAReferencedSymbol() { if (_symbolUsageReferenceFilter == null) return; ImGui.Text("Usages of " + _symbolUsageReferenceFilter.Name + ":"); if (ImGui.Button("Clear")) { _symbolUsageReferenceFilter = null; } else { ImGui.Separator(); ImGui.BeginChild("scrolling"); { if (SymbolAnalysis.DetailsInitialized && SymbolAnalysis.InformationForSymbolIds.TryGetValue(_symbolUsageReferenceFilter.Id, out var info)) { // TODO: this should be cached... var allSymbols = EditorSymbolPackage.AllSymbols.ToDictionary(s => s.Id); foreach (var id in info.DependingSymbols) { if (allSymbols.TryGetValue(id, out var symbol)) { // Use instance method this.DrawSymbolItemInstance(symbol); } } } } CustomComponents.HandleDragScrolling(this); ImGui.EndChild(); } } /// /// Draws a flat list of matching symbols when search is active. /// private void DrawFilteredList() { _filter.UpdateIfNecessary(null); foreach (var symbolUi in _filter.MatchingSymbolUis) { this.DrawSymbolItemInstance(symbolUi.Symbol); } } // --- Expand-to-symbol logic --- // Indicates if the tree should expand to reveal a specific symbol private bool _expandToSymbolTriggered; // The target symbol ID to expand to private Guid? _expandToSymbolTargetId; // Set by Reveal() and the aim icon: the symbol item scrolls itself into view once, then clears this. private Guid? _scrollToSymbolId; /// /// Checks if a is in the path to a symbol, returning the path if found. /// private static bool IsInPathToSymbol(NamespaceTreeNode node, Guid symbolId, out List path) { path = new List(); return FindPathRecursive(node, symbolId, path); } /// /// Recursively searches for a symbol in the tree and builds the path to it. /// private static bool FindPathRecursive(NamespaceTreeNode node, Guid symbolId, List path) { if (node.Symbols.Any(s => s.Id == symbolId)) { path.Add(node); return true; } foreach (var child in node.Children) { if (FindPathRecursive(child, symbolId, path)) { path.Add(node); return true; } } return false; } private static string _draggedNamespace = string.Empty; /// /// Recursively draws namespace nodes and their symbols in the tree view. /// /// The subtree node to draw. private void DrawNode(NamespaceTreeNode subtree) { if (subtree.Name == NamespaceTreeNode.RootNodeId) { DrawNodeItems(subtree); } else { ImGui.PushID(subtree.Id); ImGui.SetNextItemWidth(10); if (subtree.Name == "Lib" && !_openedLibFolderOnce) { ImGui.SetNextItemOpen(true); _openedLibFolderOnce = true; } // --- Aim icon logic for tree nodes --- var selectedSymbolId = _lastSelectedSymbolId; var containsSelected = selectedSymbolId != null && ContainsSymbolRecursive(subtree, selectedSymbolId.Value); // Expand all nodes in the path to the target symbol if triggered if (_expandToSymbolTriggered && _expandToSymbolTargetId.HasValue) { if (IsInPathToSymbol(subtree, _expandToSymbolTargetId.Value, out var path) && path.Contains(subtree)) { ImGui.SetNextItemOpen(true, ImGuiCond.Always); } } _treeHandler.UpdateForNode(subtree.Id); var isProject = subtree.FolderType == NamespaceTreeNode.SymbolFolderTypes.Project; if (isProject) { ImGui.PushFont(Fonts.FontBold); } else { ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Text.Fade(0.7f).Rgba); } // Tight rows and subtle hover matching the folder styling of the asset library ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Color.Transparent.Rgba); ImGui.PushStyleColor(ImGuiCol.HeaderActive, Color.Transparent.Rgba); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); var isOpen = ImGui.TreeNode(subtree.Name); ImGui.PopStyleVar(2); ImGui.PopStyleColor(2); CustomComponents.DrawHoverHighlightOnLastItem(); if (isProject) { ImGui.PopFont(); } else { ImGui.PopStyleColor(); } if (DragAndDropHandling.HandleDragSourceForLastItem( DragAndDropHandling.DragTypes.SymbolNameSpace, subtree.Namespace)) { _draggedNamespace = subtree.Namespace; } // Draw aim icon if this node contains the selected symbol and is not open if (!isOpen && containsSelected) { var h = ImGui.GetFontSize(); CustomComponents.RightAlign(h); var clicked = ImGui.InvisibleButton("Reveal", new Vector2(h)); if (ImGui.IsItemHovered()) { CustomComponents.TooltipForLastItem("Reveal selected operator"); } // Animate aim icon var timeSinceChange = (float)(ImGui.GetTime() - _lastSelectionTime); var fadeProgress = Clamp01(timeSinceChange / 0.5f); var blinkFade = -MathF.Cos(timeSinceChange * 15f) * (1f - fadeProgress) * 0.7f + 0.75f; var color = UiColors.StatusActivated.Fade(blinkFade); Icons.DrawIconOnLastItem(Icon.Aim, color); // Optionally, scroll to item if just selected if (_expandToSymbolTriggered && selectedSymbolId.HasValue) { ImGui.SetScrollHereY(); } // Set expand trigger if clicked if (clicked && selectedSymbolId.HasValue) { _expandToSymbolTriggered = true; _expandToSymbolTargetId = selectedSymbolId; _scrollToSymbolId = selectedSymbolId; } } // Context menu for namespace node CustomComponents.ContextMenuForItem(() => { if (ImGui.MenuItem("Rename Namespace")) { _subtreeNodeToRename = subtree; _renameNamespaceDialog.ShowNextFrame(); } }); HandleDroppingNamespace(subtree); if (isOpen) { _treeHandler.NoFolderOpen = false; // Reset expand trigger after expanding and target is visible if (_expandToSymbolTriggered && _expandToSymbolTargetId.HasValue && ContainsSymbolRecursive(subtree, _expandToSymbolTargetId.Value)) { // If this is the last node in the path, reset if (subtree.Symbols.Any(s => s.Id == _expandToSymbolTargetId.Value)) { _expandToSymbolTriggered = false; _expandToSymbolTargetId = null; } } HandleDroppingSymbol(subtree); DrawNodeItems(subtree); ImGui.TreePop(); } // else // { // // Small helper button for quickly dropping dragged symbols into unopened namespaces. // if (DragAndDropHandling.IsDragging) // { // ImGui.SameLine(); // ImGui.PushID("DropButton"); // ImGui.Button(" <-", new Vector2(50, 15)); // HandleDropTarget(subtree); // ImGui.PopID(); // } // } ImGui.PopID(); } } /// /// Checks if a contains a symbol with the given ID, recursively. /// private static bool ContainsSymbolRecursive(NamespaceTreeNode node, Guid symbolId) { foreach (var s in node.Symbols) { if (s.Id == symbolId) return true; } foreach (var child in node.Children) { if (ContainsSymbolRecursive(child, symbolId)) return true; } return false; } /// /// Draws all child namespaces and symbols of a subtree node. /// private void DrawNodeItems(NamespaceTreeNode subtree) { // Using a for loop to prevent modification during iteration exception for (var index = 0; index < subtree.Children.Count; index++) { var subspace = subtree.Children[index]; DrawNode(subspace); } // Use a copy of the list to avoid modification issues when symbols are moved/deleted. for (var index = 0; index < subtree.Symbols.ToList().Count; index++) { var symbol = subtree.Symbols.ToList()[index]; this.DrawSymbolItemInstance(symbol); } } /// /// Handles drag-and-drop onto a namespace node to move symbols between namespaces. /// private static void HandleDroppingSymbol(NamespaceTreeNode subtree) { var resultSymbol = DragAndDropHandling.TryHandleDropOnItem(DragAndDropHandling.DragTypes.Symbol, out var data); if (resultSymbol != DragAndDropHandling.DragInteractionResult.Dropped) return; if (!Guid.TryParse(data, out var symbolId)) return; if (!MoveSymbolToNamespace(symbolId, subtree.GetAsString(), out var reason)) BlockingWindow.Instance.ShowMessageBox(reason, "Could not move symbol's namespace"); EditorSymbolPackage.NotifySymbolStructureChange(); } /// /// Handles drag-and-drop onto a namespace node to move symbols between namespaces. /// private static void HandleDroppingNamespace(NamespaceTreeNode subtree) { if (subtree.Namespace.StartsWith(_draggedNamespace)) return; var targetNamespace = subtree.Namespace; var resultSymbol = DragAndDropHandling.TryHandleDropOnItem(DragAndDropHandling.DragTypes.SymbolNameSpace, out var data); if (resultSymbol != DragAndDropHandling.DragInteractionResult.Dropped) return; if (!EditableSymbolProject.TryGetEditableProjectOfNamespace(_draggedNamespace, out var sourceProject)) { Log.Warning($"Can't find editable project for {_draggedNamespace}"); return; } if (!EditableSymbolProject.TryGetEditableProjectOfNamespace(targetNamespace, out var targetProject)) { Log.Warning($"Can't find editable project for {_draggedNamespace}"); return; } List draggedSymbols = []; foreach (var symbol in sourceProject.Symbols.Values) { if (!symbol.Namespace.StartsWith(_draggedNamespace, StringComparison.OrdinalIgnoreCase)) continue; draggedSymbols.Add(symbol); } var lastNamespacePartIndex = _draggedNamespace.LastIndexOf('.'); var lastNamespacePart = lastNamespacePartIndex > 0 ? _draggedNamespace[(lastNamespacePartIndex + 1)..] : _draggedNamespace; for (var index = 0; index < draggedSymbols.Count; index++) { var symbol = draggedSymbols[index]; var target = targetNamespace; if (symbol.Namespace.StartsWith(_draggedNamespace, StringComparison.OrdinalIgnoreCase) && symbol.Namespace.Length > _draggedNamespace.Length) { target += (symbol.Namespace[_draggedNamespace.Length..]); } else { target = targetNamespace + "." + lastNamespacePart; } var skipProjectReload = index < draggedSymbols.Count - 1; if (!MoveSymbolToNamespace(symbol.Id, target, out var reason, skipProjectReload)) { Log.Warning($"Failed to move {symbol} to {target}: {reason}"); } } EditorSymbolPackage.NotifySymbolStructureChange(); } /// /// Moves a symbol to a new namespace, respecting read-only package restrictions. /// private static bool MoveSymbolToNamespace(Guid symbolId, string nameSpace, out string reason, bool skipProjectReload = false) { if (!SymbolUiRegistry.TryGetSymbolUi(symbolId, out var symbolUi)) { reason = $"Could not find symbol with id '{symbolId}'"; return false; } if (symbolUi.Symbol.Namespace == nameSpace) { reason = string.Empty; return true; } if (symbolUi.Symbol.SymbolPackage.IsReadOnly) { reason = $"Could not move symbol [{symbolUi.Symbol.Name}] because its package is not modifiable"; return false; } return EditableSymbolProject.ChangeSymbolNamespace(symbolUi.Symbol, nameSpace, out reason, skipProjectReload); } /// /// Returns an empty list, as only one instance of SymbolLibrary is supported. /// internal override List GetInstances() { return []; } // --- State fields --- // Indicates if the library was scanned for dependencies private bool _wasScanned; /// /// The filtered tree of namespaces and symbols, updated by filters. /// internal readonly NamespaceTreeNode FilteredTree = new(NamespaceTreeNode.RootNodeId); // The namespace node currently being renamed private NamespaceTreeNode? _subtreeNodeToRename; // Tracks if the Lib folder was opened once private bool _openedLibFolderOnce; // The root node of the full symbol tree private readonly NamespaceTreeNode _treeNode = new(NamespaceTreeNode.RootNodeId); // Corner rounding (unscaled px) for symbol item buttons and their badges private const float ItemRounding = 3f; // The symbol filter for search and matching private readonly SymbolFilter _filter = new(); // Dialog for renaming namespaces private static readonly RenameNamespaceDialog _renameNamespaceDialog = new(); // The symbol currently being inspected for usages private static Symbol? _symbolUsageReferenceFilter; // Generator for random prompt suggestions private readonly RandomPromptGenerator _randomPromptGenerator; // Filtering UI and logic for the library private readonly LibraryFiltering _libraryFiltering; // Dialog for deleting symbols private static readonly DeleteSymbolDialog _deleteSymbolDialog = new(); // Controls visibility of the delete dialog private static bool _showDeleteDialog = true; // The symbol currently selected for deletion private static Symbol? _symbolToDelete; // --- Highlight and Aim Icon for selected operator in node graph --- // Store the last selected symbol id and time for highlight/aim icon animation private static Guid? _lastSelectedSymbolId; private static double _lastSelectionTime; // A highlight from Reveal() persists until the graph selection changes away from this snapshot. private static bool _revealHighlightActive; private static Guid? _graphSelectionAtReveal; /// /// Updates the highlight/aim icon state for the currently selected symbol in the node graph. /// private void UpdateSelectedSymbolHighlight() { if (ProjectView.Focused?.NodeSelection == null) return; var selectedSymbolId = TryGetSingleSelectedSymbolId(); // A Reveal() highlight is not backed by the graph selection - keep it // (set via _scrollToSymbolId in DrawSymbolItemInstance) until that selection changes. if (_revealHighlightActive) { if (selectedSymbolId == _graphSelectionAtReveal) return; _revealHighlightActive = false; } // Only highlight if exactly one operator is selected in the node graph if (selectedSymbolId.HasValue) { if (_lastSelectedSymbolId != selectedSymbolId) { _lastSelectedSymbolId = selectedSymbolId; _lastSelectionTime = ImGui.GetTime(); } } else { _lastSelectedSymbolId = null; } } private static Guid? TryGetSingleSelectedSymbolId() { var nodeSelection = ProjectView.Focused?.NodeSelection; if (nodeSelection == null) return null; var selectedChildUis = nodeSelection.GetSelectedChildUis().ToList(); if (selectedChildUis.Count != 1) return null; return selectedChildUis[0].SymbolChild?.Symbol.Id; } // Helper for clamping float/double values between 0 and 1 private static float Clamp01(float v) => v < 0 ? 0 : v > 1 ? 1 : v; private static float Clamp01(double v) => v < 0 ? 0 : v > 1 ? 1 : (float)v; /// /// Static wrapper for drawing a symbol item, used for external static calls. /// internal static void DrawSymbolItemStatic(Symbol symbol) { // Use WindowManager to get all windows and find the first SymbolLibrary instance var symbolLibraryInstance = T3.Editor.Gui.Windows.Layouts.WindowManager.GetAllWindows().OfType().FirstOrDefault(); symbolLibraryInstance?.DrawSymbolItemInstance(symbol); } /// /// Static method for legacy external calls. /// internal static void DrawSymbolItem(Symbol symbol) { DrawSymbolItemStatic(symbol); } /// /// Instance method for drawing a symbol item in the tree, including highlight, context menu, and dependency badges. /// /// The symbol to draw. internal void DrawSymbolItemInstance(Symbol symbol) { if (!symbol.TryGetSymbolUi(out var symbolUi)) return; ImGui.PushID(symbol.Id.GetHashCode()); { var color = symbol.OutputDefinitions.Count > 0 ? TypeUiRegistry.GetPropertiesForType(symbol.OutputDefinitions[0]?.ValueType).Color : UiColors.Gray; // A pending Reveal() scrolls this item into view and highlights it. The path nodes are // expanded by the _expandToSymbolTargetId logic above, so by here the item is laid out. if (_scrollToSymbolId == symbol.Id) { ImGui.SetScrollHereY(0.5f); _lastSelectedSymbolId = symbol.Id; _lastSelectionTime = ImGui.GetTime(); _scrollToSymbolId = null; } // Skip layout and badge work for rows outside the visible scroll region — the tree // can hold thousands of symbols. A dummy of the same height keeps scroll extents stable. var rowHeight = ImGui.GetFrameHeight(); if (!ImGui.IsRectVisible(new Vector2(1, rowHeight))) { ImGui.Dummy(new Vector2(1, rowHeight)); ImGui.PopID(); return; } // --- Highlight and Aim Icon for selected symbol --- var isSelected = false; var timeSinceSelection = 0f; if (_lastSelectedSymbolId.HasValue && symbol.Id == _lastSelectedSymbolId.Value) { isSelected = true; timeSinceSelection = (float)(ImGui.GetTime() - _lastSelectionTime); } // Tag “bookmark” button in front of symbol button. if (ParameterWindow.DrawSymbolTagsButton(symbolUi)) symbolUi.FlagAsModified(); ImGui.SameLine(); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, ItemRounding * T3Ui.UiScaleFactor); ImGui.PushStyleColor(ImGuiCol.Button, ColorVariations.OperatorBackground.Apply(color).Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ColorVariations.OperatorBackgroundHover.Apply(color).Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonActive, ColorVariations.OperatorBackgroundHover.Apply(color).Rgba); ImGui.PushStyleColor(ImGuiCol.Text, ColorVariations.OperatorLabel.Apply(color).Rgba); bool buttonPressed = ImGui.Button(symbol.Name.AddSpacesForImGuiOutput()); ImGui.PopStyleVar(); if (buttonPressed) HelpWindow.ShowTopic(HelpTopic.ForOperator(symbol.Id)); // Get button rect for icon and highlight var buttonMin = ImGui.GetItemRectMin(); var buttonMax = ImGui.GetItemRectMax(); // Draw highlight border if selected (drawn last, on top) if (isSelected) { var fadeProgress = Clamp01(timeSinceSelection / 0.3f); var blinkFade = MathUtils.Lerp(-MathF.Cos(timeSinceSelection * 15f) * 0.8f + 0.2f, 1, fadeProgress); var highlightColor = UiColors.StatusActivated.Fade(blinkFade); ImGui.GetWindowDrawList().AddRect(buttonMin, buttonMax, highlightColor, ItemRounding * T3Ui.UiScaleFactor); } // Show tooltip with description if hovered if (ImGui.IsItemHovered()) { // Let the Help window follow the symbol the mouse is over here, just like hovering it in the graph. HoveredHelpTarget.SetOperator(symbol.Id); var thumbnailRect = ThumbnailManager.GetThumbnail(symbol.Id, symbol.SymbolPackage, ThumbnailManager.Categories.PackageMeta); ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeAll); // The Help window already previews the hovered symbol — the tooltip would double it. if (!HelpWindow.HoverPreviewActive && (!string.IsNullOrEmpty(symbolUi.Description) || thumbnailRect.IsReady)) { CustomComponents.BeginTooltip(600); { if (!string.IsNullOrEmpty(symbolUi.Description)) { ImGui.BeginGroup(); { ImGui.PushTextWrapPos(ImGui.GetFontSize() * 25.0f); ImGui.TextUnformatted(symbolUi.Description); ImGui.PopTextWrapPos(); } ImGui.EndGroup(); } if (thumbnailRect.IsReady) { ImGui.SameLine(0, 10); ImGui.BeginGroup(); { thumbnailRect.AsImguiImage(); } ImGui.EndGroup(); } } CustomComponents.EndTooltip(); } } ImGui.PopStyleColor(4); HandleDragAndDropForSymbolItem(symbol); // Styled context menu with symbol name as header and proper popup padding. CustomComponents.ContextMenuForItem( drawMenuItems: () => { // Existing symbol-specific menu CustomComponents.DrawSymbolCodeContextMenuItem(symbol); ImGui.Separator(); // Delete symbol menu entry if (ImGui.MenuItem("Delete Symbol")) { _symbolToDelete = symbol; _deleteSymbolDialog.ShowNextFrame(); } }, title: symbol.Name, id: "##symbolTreeSymbolContextMenu"); // Experimental Thumbnails // var keepCursor = ImGui.GetCursorPos(); // CustomComponents.RightAlign(50); // if (!ThumbnailManager.GetThumbnail(symbol.Id, symbol.SymbolPackage).AsImguiImage(30)) // { // ImGui.Dummy(new Vector2(10)); // } // ImGui.SetCursorPos(keepCursor); // Draw dependency badges if analysis is available if (SymbolAnalysis.DetailsInitialized && SymbolAnalysis.InformationForSymbolIds.TryGetValue(symbol.Id, out var info)) { ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba); ListSymbolSetWithTooltip( 250, Icon.Dependencies, "{0}", string.Empty, "requires...", info.RequiredSymbolIds.ToList()); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.StatusAttention.Rgba); ListSymbolSetWithTooltip( 300, Icon.None, "{0}", string.Empty, "has invalid references...", info.InvalidRequiredIds); ImGui.PopStyleColor(); if (ListSymbolSetWithTooltip( 340, Icon.Referenced, "{0}", " NOT USED", "used by...", info.DependingSymbols.ToList())) { _symbolUsageReferenceFilter = symbol; } ImGui.PopStyleColor(); } // Draw example badges if available if (ExampleSymbolLinking.TryGetExamples(symbol.Id, out var examples)) { ImGui.PushFont(Fonts.FontSmall); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, ItemRounding * T3Ui.UiScaleFactor); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f * ImGui.GetStyle().Alpha); for (var index = 0; index < examples.Count; index++) { var exampleSymbolUi = examples[index]; ImGui.SameLine(); ImGui.Button("EXAMPLE"); HandleDragAndDropForSymbolItem(exampleSymbolUi.Symbol); } ImGui.PopStyleVar(2); ImGui.PopFont(); } } ImGui.PopID(); // Modal delete confirmation dialog for the symbol selected via context menu. if (_symbolToDelete != null && ImGui.BeginPopupModal("DeleteSymbol", ref _showDeleteDialog)) { _deleteSymbolDialog.Draw(_symbolToDelete); if (!_showDeleteDialog) // Dialog closed { _symbolToDelete = null; } ImGui.EndPopup(); } } /// /// Draws small badges for symbol dependencies, invalid references, or usages, with tooltips. /// private static bool ListSymbolSetWithTooltip(float x, Icon icon, string setTitleFormat, string emptySetTitle, string toolTopTitle, List symbolSet) { var activated = false; ImGui.PushID(icon.ToString()); ImGui.SameLine(x, 10); if (symbolSet.Count > 0) { icon.DrawAtCursor(); CustomComponents.TooltipForLastItem(DrawTooltip); ImGui.SameLine(0, 0); } if (symbolSet.Count == 0) { ImGui.TextUnformatted(emptySetTitle); } else { ImGui.TextUnformatted(string.Format(setTitleFormat, symbolSet.Count)); CustomComponents.TooltipForLastItem(DrawTooltip); if (ImGui.IsItemClicked()) { activated = true; } } ImGui.PopID(); return activated; // Tooltip callback to show detailed symbol list void DrawTooltip() { var allSymbolUis = EditorSymbolPackage.AllSymbolUis; var matches = allSymbolUis .Where(s => symbolSet.Contains(s.Symbol.Id)) .OrderBy(s => s.Symbol.Namespace) .ThenBy(s => s.Symbol.Name); ImGui.BeginTooltip(); ImGui.TextUnformatted(toolTopTitle); FormInputs.AddVerticalSpace(); ListSymbols(matches); ImGui.EndTooltip(); } } /// /// Helper to render grouped symbol lists inside dependency tooltips. /// private static void ListSymbols(IOrderedEnumerable symbolUis) { var lastGroupName = string.Empty; ColumnLayout.StartLayout(25); foreach (var required in symbolUis) { var projectName = required.Symbol.SymbolPackage.RootNamespace; if (projectName != lastGroupName) { lastGroupName = projectName; FormInputs.AddVerticalSpace(5); ImGui.PushFont(Fonts.FontSmall); ImGui.TextUnformatted(projectName); ImGui.PopFont(); } var hasIssues = required.Tags.HasFlag(SymbolUi.SymbolTags.Obsolete) | required.Tags.HasFlag(SymbolUi.SymbolTags.NeedsFix); var color = hasIssues ? UiColors.StatusAttention : UiColors.Text; ImGui.PushStyleColor(ImGuiCol.Text, color.Rgba); ColumnLayout.StartGroupAndWrapIfRequired(1); ImGui.TextUnformatted(required.Symbol.Name); ColumnLayout.ExtendWidth(ImGui.GetItemRectSize().X); ImGui.PopStyleColor(); } } /// /// Handles drag-and-drop source for symbol items and click-to-insert behavior. /// internal static void HandleDragAndDropForSymbolItem(Symbol symbol) { if (IsSymbolCurrentCompositionOrAParent(symbol)) return; DragAndDropHandling.HandleDragSourceForLastItem( DragAndDropHandling.DragTypes.Symbol, symbol.Id.ToString()); if (!ImGui.IsItemDeactivated()) return; var wasClick = ImGui.GetMouseDragDelta().Length() < 4; if (wasClick) { var components = ProjectView.Focused; if (components == null) { Log.Error($"No focused graph window found"); } else if (components.NodeSelection.GetSelectedChildUis().Count() == 1) { ConnectionMaker.InsertSymbolInstance(components, symbol); } } } /// /// Prevents dragging the current composition or any of its parents into itself. /// internal static bool IsSymbolCurrentCompositionOrAParent(Symbol symbol) { var components = ProjectView.Focused; if (components?.CompositionInstance == null) return false; var comp = components.CompositionInstance; if (comp.Symbol.Id == symbol.Id) { return true; } var instance = comp; while (instance != null) { if (instance.Symbol.Id == symbol.Id) return true; instance = instance.Parent; } return false; } }