#nullable enable using ImGuiNET; using T3.Core.Operator; using T3.Editor.App; using T3.Editor.Gui.Input; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.UiModel; using T3.Editor.UiModel.InputsAndTypes; using T3.Editor.UiModel.ProjectHandling; using T3.SystemUi; namespace T3.Editor.Gui.Dialog; [HelpUiID("SearchWindow")] internal sealed class SearchDialog : ModalDialog { public SearchDialog() { DialogSize = new Vector2(500, 300); Padding = 4; } public void Draw() { if (BeginDialog("Search")) { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); if (!_isOpen) { _justOpened = true; _isOpen = true; } else { _justOpened = false; } if (_justOpened) { ImGui.SetKeyboardFocusHere(); } var needsUpdate = _justOpened; FormInputs.SetIndentToLeft(); FormInputs.SetWidth(0.7f); needsUpdate |= FormInputs.AddStringInput("##searchInput", ref _searchString, "Search", null, null); ImGui.SameLine(0,10); FormInputs.SetWidth(1f); needsUpdate |= FormInputs.AddEnumDropdown(ref _searchMode, ""); FormInputs.ResetWidth(); FormInputs.AddVerticalSpace(); if (needsUpdate) { UpdateResults(); } var clickedOutside = ImGui.IsMouseClicked(ImGuiMouseButton.Left) && !ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); if (ImGui.IsKeyReleased(ImGuiKey.Enter) || ImGui.IsKeyReleased(ImGuiKey.Escape) || clickedOutside) { ImGui.CloseCurrentPopup(); } DrawResultsList(); ImGui.PopStyleVar(); EndDialogContent(); } else { _isOpen = false; } EndDialog(); } private bool _isOpen; private void DrawResultsList() { var size = ImGui.GetContentRegionAvail(); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(5, 5)); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(10, 10)); var matchingItems = _matchingItems; if (ImGui.BeginChild("##results", size, ImGuiChildFlags.FrameStyle)) { if (ImGui.IsKeyReleased(Key.CursorDown.ToImGuiKey())) { UiListHelpers.AdvanceSelectedItem(matchingItems!, ref _selectedInstance!, 1); _selectedItemChanged = true; var index = matchingItems.IndexOf(_selectedInstance); if (index == 0) ImGui.SetScrollY(0); } else if (ImGui.IsKeyReleased(Key.CursorUp.ToImGuiKey())) { UiListHelpers.AdvanceSelectedItem(matchingItems!, ref _selectedInstance!, -1); _selectedItemChanged = true; var index = matchingItems.IndexOf(_selectedInstance); if (index * ImGui.GetTextLineHeight() > ImGui.GetScrollY() + ImGui.GetContentRegionAvail().Y) { Log.Debug("Would scroll down"); ImGui.SetScrollY(ImGui.GetScrollY() + ImGui.GetContentRegionAvail().Y); } } unsafe { var clipperData = new ImGuiListClipper(); var listClipperPtr = new ImGuiListClipperPtr(&clipperData); listClipperPtr.Begin(matchingItems.Count, ImGui.GetTextLineHeight()); while (listClipperPtr.Step()) { for (var i = listClipperPtr.DisplayStart; i < listClipperPtr.DisplayEnd; ++i) { if (i < 0 || i >= matchingItems.Count) continue; DrawItem(matchingItems[i]); } } listClipperPtr.End(); } } ImGui.EndChild(); ImGui.PopStyleVar(2); } private void DrawItem(SearchResult item) { var components = item.GraphCanvas; if (components == null) return; var symbolHash = item.Id.GetHashCode(); ImGui.PushID(symbolHash); { var instance = item.Instance; if (item.Kind == SearchResult.Kinds.Instance) { var symbolNamespace = instance.Symbol.Namespace; var isRelevantNamespace = symbolNamespace.StartsWith("Lib.") || symbolNamespace.StartsWith("examples.lib."); var color = instance.Symbol.OutputDefinitions.Count > 0 ? TypeUiRegistry.GetPropertiesForType(instance.Symbol.OutputDefinitions[0]?.ValueType).Color : UiColors.Gray; if (!isRelevantNamespace) { color = color.Fade(0.4f); } ImGui.PushStyleColor(ImGuiCol.Header, ColorVariations.OperatorBackground.Apply(color).Rgba); var hoverColor = ColorVariations.OperatorBackgroundHover.Apply(color).Rgba; hoverColor.W = 0.1f; ImGui.PushStyleColor(ImGuiCol.HeaderHovered, hoverColor); ImGui.PushStyleColor(ImGuiCol.Text, ColorVariations.OperatorLabel.Apply(color).Rgba); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, 4)); var isSelected = item == _selectedInstance; ImGui.PushID(item.Id.GetHashCode()); var hasBeenClicked = ImGui.Selectable("##Selectable", isSelected); ImGui.PopID(); _selectedItemChanged |= hasBeenClicked; var path = instance.InstancePath; var readablePath = string.Join(" / ", components.OpenedProject.Structure.GetReadableInstancePath(path, false)); if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { components.GraphView.OpenAndFocusInstance(path); _selectedInstance = item; _selectedItemChanged = false; } else if (_selectedItemChanged && _selectedInstance == item) { UiListHelpers.ScrollToMakeItemVisible(); components.GraphView.OpenAndFocusInstance(path); _selectedItemChanged = false; } ImGui.SameLine(); ImGui.TextUnformatted(instance.Symbol.Name); if (instance.SymbolChild.HasCustomName) { ImGui.SameLine(0, 10); CustomComponents.StylizedText($"\"{instance.SymbolChild.Name}\"", Fonts.FontNormal, UiColors.Gray); } ImGui.SameLine(0, 10); CustomComponents.StylizedText(readablePath, Fonts.FontNormal, UiColors.Gray.Fade(0.5f)); ImGui.PopStyleVar(); ImGui.PopStyleColor(3); } else if (item.Section != null) { var isSelected = item == _selectedInstance; var hasBeenClicked = ImGui.Selectable($"##Selectable{symbolHash.ToString()}", isSelected); _selectedItemChanged |= hasBeenClicked; var activate = false; if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { _selectedInstance = item; activate = true; } else if (_selectedItemChanged && _selectedInstance == item) { UiListHelpers.ScrollToMakeItemVisible(); _selectedItemChanged = false; activate = true; } if (activate) { components.GraphView.OpenAndFocusSection(item.Instance.InstancePath, item.Section.Id); _selectedInstance = item; } ImGui.SameLine(0,2); ImGui.TextUnformatted(item.Name); ImGui.SameLine(0,10); CustomComponents.StylizedText("(Section)", Fonts.FontNormal, UiColors.TextMuted); } else { var isSelected = item == _selectedInstance; var hasBeenClicked = ImGui.Selectable($"##Selectable{symbolHash.ToString()}", isSelected); _selectedItemChanged |= hasBeenClicked; var activate = false; if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { _selectedInstance = item; activate = true; } else if (_selectedItemChanged && _selectedInstance == item) { UiListHelpers.ScrollToMakeItemVisible(); _selectedItemChanged = false; activate = true; } if (activate) { components.GraphView.OpenAndFocusInputOrOutput(item.Instance.InstancePath, item.Id); _selectedInstance = item; } ImGui.SameLine(0,2); ImGui.TextUnformatted(item.Name); ImGui.SameLine(0,10); CustomComponents.StylizedText(item.Kind == SearchResult.Kinds.InputNode ? "(Input)" : "(Output)", Fonts.FontNormal, UiColors.TextMuted); } } ImGui.PopID(); } private void UpdateResults() { _matchingItems.Clear(); var components = ProjectView.Focused; if (components == null) return; // Show navigation / selection history if not searching if (string.IsNullOrEmpty(_searchString)) { var previousInstances = components.NavigationHistory .GetPreviouslySelectedInstances() .Select(instance => new SearchResult(instance)); _matchingItems.AddRange(previousInstances); if (_matchingItems.Count > 0) _selectedInstance = _matchingItems[0]; return; } // var compositionOp = components.CompositionInstance; var composition = _searchMode switch { SearchModes.Global => components.RootInstance, SearchModes.Local => compositionOp, SearchModes.LocalAndInChildren => compositionOp, _ => throw new ArgumentOutOfRangeException() }; if (composition == null) return; if (!string.IsNullOrEmpty(_searchString)) { FindAllChildren(composition); } _matchingItems.Sort((foundA, foundB) => { // 1. Sections and input/output nodes before instances var aIsInstance = foundA.Kind == SearchResult.Kinds.Instance; var bIsInstance = foundB.Kind == SearchResult.Kinds.Instance; if (aIsInstance != bIsInstance) { return aIsInstance.CompareTo(bIsInstance); } // 2. Then sort by Name alphabetically return string.Compare(foundA.Name, foundB.Name, StringComparison.Ordinal); }); } private void FindAllChildren(Instance composition) { var symbolUi = composition.GetSymbolUi(); foreach (var section in symbolUi.Sections.Values) { if (section.Label.Contains(_searchString, StringComparison.InvariantCultureIgnoreCase) || section.Title.Contains(_searchString, StringComparison.InvariantCultureIgnoreCase)) _matchingItems.Add(new SearchResult(composition, section)); } foreach (var inputUi in symbolUi.InputUis.Values) { if (inputUi.InputDefinition.Name.Contains(_searchString, StringComparison.InvariantCultureIgnoreCase)) _matchingItems.Add(new SearchResult(composition, inputUi.InputDefinition.Id, inputUi.InputDefinition.Name, isInput: true)); } foreach (var outputUi in symbolUi.OutputUis.Values) { if (outputUi.OutputDefinition.Name.Contains(_searchString, StringComparison.InvariantCultureIgnoreCase)) _matchingItems.Add(new SearchResult(composition, outputUi.OutputDefinition.Id, outputUi.OutputDefinition.Name, isInput: false)); } foreach (var child in composition.Children.Values) { var symbolChild = child.SymbolChild; if (child.Symbol.Name.Contains(_searchString, StringComparison.InvariantCultureIgnoreCase) || (symbolChild.HasCustomName && symbolChild.Name.Contains(_searchString, StringComparison.InvariantCultureIgnoreCase))) { _matchingItems.Add(new SearchResult(child)); } if (_searchMode == SearchModes.Local) continue; FindAllChildren(child); } } private readonly List _matchingItems = new(); private bool _justOpened; private static string _searchString = string.Empty; private SearchResult? _selectedInstance; private bool _selectedItemChanged; private SearchModes _searchMode = SearchModes.Local; private sealed class SearchResult { public enum Kinds { Instance, Section, InputNode, OutputNode, } public SearchResult(Instance instance) { Kind = Kinds.Instance; Instance = instance; GraphCanvas = ProjectView.Focused; Id = instance.SymbolChildId; } public SearchResult(Instance compositionInstance, Section section) { Kind = Kinds.Section; Instance = compositionInstance; Section = section; GraphCanvas = ProjectView.Focused; Id = section.Id; } public SearchResult(Instance compositionInstance, Guid ioId, string ioName, bool isInput) { Kind = isInput ? Kinds.InputNode : Kinds.OutputNode; Instance = compositionInstance; GraphCanvas = ProjectView.Focused; Id = ioId; _ioName = ioName; } public readonly Kinds Kind; public readonly Guid Id; public readonly Instance Instance; public readonly Section? Section; public readonly ProjectView? GraphCanvas; public string Name => Kind switch { Kinds.Section => Section?.Label ?? string.Empty, Kinds.InputNode or Kinds.OutputNode => _ioName, _ => Instance.Symbol.Name, }; private readonly string _ioName = string.Empty; } private enum SearchModes { Local, LocalAndInChildren, Global, } }