Files
2026-07-13 13:13:17 +08:00

621 lines
23 KiB
C#

#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<HelpLink> _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<HelpLink> _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<HelpLink> _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<HelpLink> _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;
}