#nullable enable using System.Threading.Tasks; using ImGuiNET; using T3.Core.Compilation; using T3.Core.Stats; using T3.Core.SystemUi; using T3.Core.Utils; using T3.Editor.Gui.Graph.Dialogs; using T3.Editor.Gui.Window; using T3.Editor.Gui.Input; using T3.Editor.Gui.Interaction; using T3.Editor.Gui.Interaction.Keyboard; using T3.Editor.Gui.Interaction.StartupCheck; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.UiHelpers.Thumbnails; using T3.Editor.Gui.UiHelpers.Wiki; using T3.Editor.Gui.Windows; using T3.Editor.Gui.Windows.Analyze; using T3.Editor.Gui.Windows.Layouts; using T3.Editor.Gui.Windows.RenderExport; using T3.Editor.Skills.Ui; using T3.Editor.UiModel; using T3.Editor.UiModel.Commands; using T3.Editor.UiModel.Exporting; using T3.Editor.UiModel.Helpers; using T3.Editor.UiModel.Modification; using T3.Editor.UiModel.ProjectHandling; using ShaderCompiler = T3.Core.Resource.ShaderCompiling.ShaderCompiler; namespace T3.Editor.Gui; internal static class AppMenuBar { internal static void DrawAppMenuBar() { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(6, 6) * T3Ui.UiScaleFactor); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, T3Style.WindowPaddingForMenus * T3Ui.UiScaleFactor); ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0); if (ImGui.BeginMainMenuBar()) { // Enable app menu after click if only visible during hover if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { UserSettings.Config.ShowMainMenu = true; } ImGui.SetCursorPos(new Vector2(0, -1)); // Shift to make menu items selected when hitting top of screen ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1); ImGui.PushStyleVar(ImGuiStyleVar.PopupRounding, 3); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(8, 8)); ImGui.PushStyleColor(ImGuiCol.Separator, UiColors.BackgroundFull.Fade(0.5f).Rgba); DrawMainMenu(); ImGui.PopStyleColor(); ImGui.PopStyleVar(3); DrawVersionIndicator(); DrawErrorsIndicator(); T3Metrics.DrawRenderPerformanceGraph(); ImGui.SameLine(); if (!UserSettings.Config.EnableKeyboardShortCuts) { ImGui.TextColored(UiColors.StatusAttention, "Keyboard Disabled!"); if (ImGui.IsItemClicked()) { UserSettings.Config.EnableKeyboardShortCuts = true; } ImGui.SameLine(); } Program.StatusErrorLine.Draw(); ImGui.EndMainMenuBar(); } ImGui.PopStyleVar(3); } private static void DrawErrorsIndicator() { ImGui.SameLine(0, AppBarSpacingX); var isHovered = false; { var hasErrors = OperatorDiagnostics.StatusUpdates.Count > 0; if (!hasErrors) { ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba); Icon.Checkmark.DrawAtCursor(); ImGui.PopStyleColor(); } else { var timeSinceChange = (float)(ImGui.GetTime() - OperatorDiagnostics.LastChangeTime).Clamp(0, 1); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, MathUtils.Lerp(1, 0.4f, timeSinceChange)); Icon.Warning.DrawAtCursor(); isHovered = ImGui.IsItemHovered(); ImGui.SameLine(0, 0); ImGui.TextUnformatted($"{OperatorDiagnostics.StatusUpdates.Count}"); ImGui.PopStyleVar(); } var isGroupHovered = isHovered || ImGui.IsItemHovered(); if (!isGroupHovered) return; CustomComponents.BeginTooltip(1200); { if (OperatorDiagnostics.StatusUpdates.Count == 0) { ImGui.TextUnformatted("No problems"); } else { if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { OperatorDiagnostics.StatusUpdates.Clear(); } CustomComponents.HelpText("Recent Operators problems. Click to clear..."); foreach (var x in OperatorDiagnostics.StatusUpdates.Values.OrderByDescending(x => x.Time)) { if (Structure.TryGetInstanceFromPath(x.IdPath, out _, out var readableInstancePath)) { var timeSince = ImGui.GetTime() - x.Time; var fadeLine = ((float)timeSince).RemapAndClamp(10, 100, 1, 0.4f); ImGui.PushFont(Fonts.FontSmall); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, fadeLine); var notFirst = false; foreach (var p in readableInstancePath) { if (notFirst) { ImGui.SameLine(0, 4); ImGui.TextColored(UiColors.TextMuted, "/"); ImGui.SameLine(0, 4); } notFirst = true; ImGui.TextUnformatted(p); } ImGui.SameLine(0, 10); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba); ImGui.TextUnformatted(StringUtils.GetReadableRelativeTime(timeSince)); ImGui.PopStyleColor(); ImGui.PopFont(); var color = x.Statuses switch { OperatorDiagnostics.Statuses.HasWarning => UiColors.StatusAttention, OperatorDiagnostics.Statuses.HasError => UiColors.StatusError, _ => UiColors.TextMuted }; ImGui.PushStyleColor(ImGuiCol.Text, color.Rgba); ImGui.TextUnformatted(x.Message); ImGui.PopStyleColor(); ImGui.PopStyleVar(); FormInputs.AddVerticalSpace(1); } } } } CustomComponents.EndTooltip(); } } private static void DrawVersionIndicator() { if (!UserSettings.Config.FullScreen) return; ImGui.SameLine(0, AppBarSpacingX); //ImGui.Dummy(new Vector2(10, 10)); //ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 2); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.ForegroundFull.Fade(0.2f).Rgba); ImGui.TextUnformatted(Program.FormattedEditorVersion); ImGui.PopStyleColor(); } // Thin wrappers over the styled menu components so the AppMenu dropdowns match the graph context menu. // The label seeds a stable id (DrawMenuItem also keys its hit-test off the label), and the active // toggle is emphasized while everything else is muted — same model as the context menu. private static bool MenuItem(string label, string? shortcut = null, bool isChecked = false, bool isEnabled = true) => CustomComponents.DrawMenuItem(label.GetHashCode(), Icon.None, label, shortcut, isChecked, isEnabled, reserveIconColumn: false, state: isChecked ? CustomComponents.ButtonStates.Emphasized : CustomComponents.ButtonStates.Default); private static bool MenuItemToggle(string label, ref bool value, string? shortcut = null) { var clicked = MenuItem(label, shortcut, isChecked: value); if (clicked) value = !value; return clicked; } // Nested submenu header (NOT for the horizontal top-level bar entries, which stay native ImGui.BeginMenu). private static bool BeginSubMenu(string label, bool isEnabled = true) => CustomComponents.DrawSubMenu(label.GetHashCode(), label, isEnabled); private static void DrawMainMenu() { if (ImGui.BeginMenu("TiXL")) { var currentProject = ProjectView.Focused?.OpenedProject.Package; UserSettings.Config.ShowMainMenu = true; var showNewTemplateOption = !T3Ui.IsCurrentlySaving && currentProject != null; if (MenuItem("New Project...")) { T3Ui.NewProjectDialog.ShowNextFrame(); } if (MenuItem("New Operator...", UserActions.New.ListShortcuts(), isEnabled: showNewTemplateOption)) { T3Ui.CreateFromTemplateDialog.ShowNextFrame(); } if (BeginSubMenu("Recent Projects...", !T3Ui.IsCurrentlySaving && EditableSymbolProject.AllProjects.Any(x => x.HasHome))) { foreach (var package in EditableSymbolProject.AllProjects) { if (!package.HasHome) continue; var name = package.DisplayName; if (MenuItem(name)) { if (GraphWindow.GraphWindowInstances.Count > 0) { BlockingWindow.Instance.ShowMessageBox("Would you like to open this project in a new window?", "Opening " + name, "Yes", "No"); } Log.Error("Not implemented yet"); } } ImGui.EndMenu(); } if (currentProject is { IsReadOnly: false } && currentProject is EditableSymbolProject project) { //ImGui.Separator(); if (BeginSubMenu("Open Project in...")) { if (MenuItem("File Explorer")) { CoreUi.Instance.OpenWithDefaultApplication(project.Folder); } if (MenuItem("Resource Folder")) { CoreUi.Instance.OpenWithDefaultApplication(project.AssetsFolder); } if (MenuItem("Development IDE")) { CoreUi.Instance.OpenWithDefaultApplication(project.CsProjectFile.FullPath); } ImGui.EndMenu(); } } CustomComponents.SeparatorLine(); // Disabled, at least for now, as this is an incomplete (or not even started) operation on the Main branch atm if (MenuItem("Import Operators", isEnabled: !T3Ui.IsCurrentlySaving)) { BlockingWindow.Instance.ShowMessageBox("This feature is not yet available in the main branch. Stay tuned for updates!", "Not yet implemented"); //_importDialog.ShowNextFrame(); } CustomComponents.SeparatorLine(); if (MenuItem("Save Changes", UserActions.Save.ListShortcuts(), isEnabled: !T3Ui.IsCurrentlySaving)) { T3Ui.SaveInBackground(saveAll: false); } if (MenuItem("Save All", isEnabled: !T3Ui.IsCurrentlySaving)) { T3Ui.SaveInBackground(saveAll: true); } if (MenuItem("Set Project Thumbnail", isEnabled: RenderProcess.MainOutputTexture != null)) { if (currentProject != null && RenderProcess.MainOutputTexture != null) { ThumbnailManager.SaveThumbnail(currentProject.Id, currentProject, RenderProcess.MainOutputTexture, ThumbnailManager.Categories.PackageMeta); } } CustomComponents.SeparatorLine(); if (BeginSubMenu("Development Tools")) { if (MenuItem("Skill Map Editor")) SkillMapEditor.ShowNextFrame(); if (MenuItem("Tour Point Editor")) EditTourPointsPopup.ShowNextFrame(); CustomComponents.SeparatorLine(); if (MenuItem("Fix asset paths")) ConformAssetPaths.ConformAllPaths(); if (MenuItem("Check symbol dependencies")) { SymbolAnalysis.LogInvalidSymbolDependencies(); SymbolAnalysis.LogInvalidAssetReference(); } if (BeginSubMenu("Clear shader cache")) { if (MenuItem("Editor only")) ShaderCompiler.DeleteShaderCache(all: false); if (MenuItem("All editor and player versions")) ShaderCompiler.DeleteShaderCache(all: true); ImGui.EndMenu(); } if (BeginSubMenu("Documentation")) { if (MenuItem("Export as WIKI")) ExportWikiDocumentation.ExportWiki(); if (MenuItem("Export to JSON")) ExportDocumentationStrings.ExportDocumentationAsJson(); if (MenuItem("Import from JSON")) ExportDocumentationStrings.ImportDocumentationAsJson(); ImGui.EndMenu(); } if (BeginSubMenu("Debug")) { if (MenuItem("ImGUI Demo", isChecked: WindowManager.DemoWindowVisible)) WindowManager.DemoWindowVisible = !WindowManager.DemoWindowVisible; if (MenuItem("ImGUI Metrics", isChecked: WindowManager.MetricsWindowVisible)) WindowManager.MetricsWindowVisible = !WindowManager.MetricsWindowVisible; ImGui.EndMenu(); } WindowManager.UtilitiesWindow.DrawMenuItemToggle(); WindowManager.GuidedFeatureTestsWindow.DrawMenuItemToggle(); ImGui.EndMenu(); } CustomComponents.SeparatorLine(); WindowManager.SettingsWindow.DrawMenuItemToggle(); { var hasSettings = ProjectView.Focused?.CompositionInstance?.Symbol.CompositionSettings is { Enabled: true }; var window = WindowManager.ProjectSettingsWindow; if (MenuItem("Composition Settings", isChecked: hasSettings)) { window.Config.Visible = !window.Config.Visible; } } CustomComponents.SeparatorLine(); if (MenuItem("Exit", isEnabled: !T3Ui.IsCurrentlySaving)) { T3Ui.ExitDialog.ShowNextFrame(); } if (ImGui.IsItemHovered() && T3Ui.IsCurrentlySaving) { ImGui.SetTooltip("Can't exit while saving is in progress"); } ImGui.EndMenu(); } if (ImGui.BeginMenu("Edit")) { UserSettings.Config.ShowMainMenu = true; if (MenuItem("Undo", "CTRL+Z", isEnabled: UndoRedoStack.CanUndo)) { UndoRedoStack.Undo(); } if (MenuItem("Redo", "CTRL+SHIFT+Z", isEnabled: UndoRedoStack.CanRedo)) { UndoRedoStack.Redo(); } CustomComponents.SeparatorLine(); var sectionView = ProjectView.Focused; var canAddSection = sectionView?.CompositionInstance != null && !T3Ui.IsCurrentlySaving; if (MenuItem("Add Section", UserActions.AddSection.ListShortcuts(), isEnabled: canAddSection)) { // Placed around the selection, or at the center of the graph view var canvas = sectionView!.GraphView.Canvas; var centerScreen = canvas.WindowPos + canvas.WindowSize / 2; NodeActions.AddSection(sectionView.NodeSelection, canvas, sectionView.CompositionInstance!, centerScreen); sectionView.GraphView.FlagStructureAsChanged(); } CustomComponents.SeparatorLine(); if (BeginSubMenu("Bookmarks")) { GraphBookmarkNavigation.DrawBookmarksMenu(); ImGui.EndMenu(); } CustomComponents.SeparatorLine(); var exportView = ProjectView.Focused; var exportChildUis = exportView?.NodeSelection.GetSelectedChildUis().ToList(); var canExport = exportView?.CompositionInstance != null && exportChildUis is { Count: 1 }; if (MenuItem("Export as Executable", isEnabled: canExport)) { var exportChild = exportChildUis![0]; var exportName = exportChild.SymbolChild.ReadableName; switch (PlayerExporter.TryExportInstance(exportView!.CompositionInstance!, exportChild, out var reason, out var exportDir)) { case false: Log.Error(reason); BlockingWindow.Instance.ShowMessageBox(reason, $"Failed to export {exportName}"); break; default: Log.Info(reason); BlockingWindow.Instance.ShowMessageBox(reason, $"Exported {exportName} successfully!"); CoreUi.Instance.OpenWithDefaultApplication(exportDir); break; } } ImGui.EndMenu(); } if (ImGui.BeginMenu("View")) { UserSettings.Config.ShowMainMenu = true; CustomComponents.DrawMenuGroupLabel("UI Elements..."); MenuItemToggle("Main Menu", ref UserSettings.Config.ShowMainMenu); MenuItemToggle("Graph Title", ref UserSettings.Config.ShowTitleAndDescription); MenuItemToggle("Graph Minimap", ref UserSettings.Config.ShowMiniMap); MenuItemToggle("Graph Toolbar", ref UserSettings.Config.ShowToolbar); MenuItemToggle("Timeline", ref UserSettings.Config.ShowTimeline); if (MenuItem("Toggle All", UserActions.ToggleAllUiElements.ListShortcuts(), isEnabled: !T3Ui.IsCurrentlySaving)) { UiConfig.ToggleAllUiElements(); } CustomComponents.SeparatorLine(); MenuItemToggle("Interactions Overlay", ref UserSettings.Config.ShowInteractionOverlay); CustomComponents.SeparatorLine(); MenuItemToggle("Fullscreen UI", ref UserSettings.Config.FullScreen, UserActions.ToggleFullscreen.ListShortcuts()); CustomComponents.SeparatorLine(); if (MenuItem("Focus Mode", UserActions.ToggleFocusMode.ListShortcuts(), isChecked: LayoutHandling.FocusMode)) { UiConfig.ToggleFocusMode(); } ImGui.EndMenu(); } if (ImGui.BeginMenu("Windows")) { WindowManager.DrawWindowMenuContent(); ImGui.EndMenu(); } if (ImGui.BeginMenu("Help")) { if (MenuItem(RuntimeAssemblies.IsAlpha ? "Welcome to Alpha" : "Welcome")) { WindowManager.WelcomeWindow.Open(); } WindowManager.HelpWindow.DrawMenuItemToggle(); CustomComponents.SeparatorLine(); if (BeginSubMenu("Documentation")) { foreach (var link in _helpLinks) { link.DrawMenuItem(); } ImGui.EndMenu(); } if (BeginSubMenu("YouTube")) { foreach (var link in _youTubeLinks) { link.DrawMenuItem(); } ImGui.EndMenu(); } CustomComponents.SeparatorLine(); foreach (var link in _otherLinks) { link.DrawMenuItem(); } CustomComponents.SeparatorLine(); if (BeginSubMenu("Send Feedback")) { foreach (var link in _feedbackLinks) { link.DrawMenuItem(); } ImGui.EndMenu(); } CustomComponents.SeparatorLine(); _licenseLink.DrawMenuItem(); if (MenuItem("About TiXL")) { T3Ui.AboutDialog.ShowNextFrame(); } ImGui.EndMenu(); } } private sealed class HelpLink(string title, string url, string toolTip = "") { private string Title { get; } = title; private string Url { get; } = url; private string ToolTip { get; } = toolTip; public void DrawMenuItem() { if (MenuItem(Title)) { CoreUi.Instance.OpenWithDefaultApplication(Url); } CustomComponents.TooltipForLastItem("Open link in browser", ToolTip); } } private const string GitHubBaseUrl = "https://github.com/tixl3d/tixl/"; private const string WikiRootUrl = GitHubBaseUrl + "wiki/"; private static readonly List _helpLinks = [ new("Introduction", WikiRootUrl + "help.Introduction"), new("Operator Library", WikiRootUrl + "lib"), new("FAQ", WikiRootUrl + "help.FAQ"), new("Video Tutorials", WikiRootUrl + "help.VideoTutorials"), new("Using Backups", WikiRootUrl + "help.Backups"), ]; private static readonly List _youTubeLinks = [ new("Getting Started (15min)", "https://www.youtube.com/watch?v=_zvzX0fZ8sc"), new("Tutorials (Playlist)", "https://www.youtube.com/playlist?list=PLj-rnPROvbn3LigXGRSDvmLtgTwmNHcQs"), new("Tip of the Day (Playlist)", "https://www.youtube.com/watch?v=Jpvyg-LR3f0&list=PLj-rnPROvbn2cfnUwuyb5gRj-juOYUC7T"), new("Made with TiXL", "https://www.youtube.com/playlist?list=PLj-rnPROvbn3LNU34daaRk5EiaXlwo2E0"), ]; private static readonly List _otherLinks = [ new("TiXL Web-Site", "https://tixl.app"), new("Latest Releases", "https://github.com/tixl3d/tixl/releases"), new("Discord Community", "https://discord.com/invite/YmSyQdeH3S", "Join a friendly and welcoming community of enthusiasts. Ask questions, Learn from each other, share or just hang out."), new("Meet Up (every 2nd week)", "https://discord.com/invite/WX94pzKj?event=1359348185914544312", "We meet every 2nd week to share our screens answer questions and hang out."), ]; private static readonly List _feedbackLinks = [ new("Report Issue", GitHubBaseUrl + "/issues/new?template=bug_report.md", "Please search for other related issues before posting..."), new("Request Feature", GitHubBaseUrl + "/issues/new?template=feature-request.md", "Please search for other related issues before posting..."), ]; private static readonly HelpLink _licenseLink = new("MIT License", GitHubBaseUrl + "?tab=MIT-1-ov-file#readme"); public static readonly float AppBarSpacingX = 20; }