#nullable enable using System.Diagnostics; using System.Threading.Tasks; using ImGuiNET; using T3.Core.DataTypes.Vector; using T3.Core.Settings; using T3.Core.SystemUi; using T3.Editor.Gui.Input; using T3.Editor.Gui.Styling; using T3.Editor.Gui.Styling.Markdown; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows.Layouts; using T3.Editor.Gui.Windows.TestRunner; namespace T3.Editor.Gui.Dialog; /// /// Floating, non-modal welcome window for alpha builds, shown the first time a user runs a version /// they haven't run in this folder before. A Settings-style sidebar splits it into Welcome (intro + /// release notes), Import Settings, Import Projects, and Test new Features. Reopenable via /// Help → Welcome. Stable builds show the lighter instead. /// [HelpUiID("WelcomeAlphaWindow")] internal sealed class WelcomeAlphaWindow : WelcomeWindowBase { internal WelcomeAlphaWindow() { Config.Title = "Welcome to the TiXL Alpha"; } protected override void DrawContent() { if (!_isInitialized) { Initialize(); _isInitialized = true; } // Handle background project-copy completion on the UI thread, regardless of the active tab. if (_projectImportTask is { IsCompleted: true }) { _projectImportTask = null; _projectsImported = true; if (_previous != null) _projects = PreviousVersionImport.EnumerateProjects(_previous); } NavigationSidebar.BeginLayout(); DrawHeaderBand($"Thanks for testing v{FileLocations.TixlVersion} Alpha"); DrawSidebar(); DrawContentPanel(); } protected override void Close() { base.Close(); _isInitialized = false; } private void Initialize() { _activeTab = Tab.Welcome; _settingsImported = false; _projectsImported = false; _importSettings = _importThemes = _importKeymaps = _importLayouts = true; _selectedTestSetId = null; _hasPrevious = PreviousVersionImport.TryFindBest(out _previous) && _previous != null; if (_previous != null) { _settingsAvailable = PreviousVersionImport.HasSettingsFile(_previous); _themesAvailable = PreviousVersionImport.HasSubfolder(_previous, FileLocations.ThemeSubFolder); _keymapsAvailable = PreviousVersionImport.HasSubfolder(_previous, FileLocations.KeyBindingSubFolder); _layoutsAvailable = PreviousVersionImport.HasSubfolder(_previous, PreviousVersionImport.LayoutsSubfolder); _projects = PreviousVersionImport.EnumerateProjects(_previous); foreach (var p in _projects) _projectSelection[p.Name] = !p.AlreadyImported; } else { _projects = []; } _testSets = TestSetParser.LoadAll(TestSetParser.ResolveTestsDirectory()); LoadReleaseNotes(); } private void DrawSidebar() { NavigationSidebar.BeginColumn("sidebar", 150); if (NavigationSidebar.Item("Welcome", _activeTab == Tab.Welcome)) _activeTab = Tab.Welcome; if (NavigationSidebar.Item("Import Settings", _activeTab == Tab.ImportSettings, showCheckmark: _settingsImported)) _activeTab = Tab.ImportSettings; if (NavigationSidebar.Item("Import Projects", _activeTab == Tab.ImportProjects, showCheckmark: _projectsImported)) _activeTab = Tab.ImportProjects; if (NavigationSidebar.Item("Test new Features", _activeTab == Tab.TestFeatures)) _activeTab = Tab.TestFeatures; NavigationSidebar.EndColumn(); } private void DrawContentPanel() { var title = _activeTab switch { Tab.Welcome => "Welcome", Tab.ImportSettings => "Import settings", Tab.ImportProjects => "Import Projects", Tab.TestFeatures => $"Test latest features in v{FileLocations.TixlVersion}", _ => string.Empty, }; NavigationSidebar.BeginContentPanel(title, this); switch (_activeTab) { case Tab.Welcome: DrawWelcomeTab(); break; case Tab.ImportSettings: DrawImportSettingsTab(); break; case Tab.ImportProjects: DrawImportProjectsTab(); break; case Tab.TestFeatures: DrawTestFeaturesTab(); break; } NavigationSidebar.EndContentPanel(); } private void DrawWelcomeTab() { // Rendered as markdown so the [text](url) links sit inline in the prose. _welcomeMarkdown.Draw(AlphaWelcomeMarkdown, onUrl: url => CoreUi.Instance.OpenWithDefaultApplication(url)); FormInputs.AddVerticalSpace(8); ImGui.Separator(); DrawReleaseNotes(); } private void DrawImportSettingsTab() { if (!RequirePrevious()) return; ImGui.TextColored(UiColors.TextMuted, $"Copied from \"{_previous!.FolderName}\". The original is left untouched."); FormInputs.AddVerticalSpace(6); DrawCategoryCheckbox("Editor Settings", ref _importSettings, _settingsAvailable); DrawCategoryCheckbox("Themes", ref _importThemes, _themesAvailable); DrawCategoryCheckbox("Keyboard Maps", ref _importKeymaps, _keymapsAvailable); DrawCategoryCheckbox("Layouts", ref _importLayouts, _layoutsAvailable); FormInputs.AddVerticalSpace(8); var anySelected = (_importSettings && _settingsAvailable) || (_importThemes && _themesAvailable) || (_importKeymaps && _keymapsAvailable) || (_importLayouts && _layoutsAvailable); ImGui.BeginDisabled(!anySelected); if (CustomComponents.DrawCtaButton("Import", Icon.None, anySelected ? UiColors.ForegroundFull : UiColors.TextMuted, anySelected ? UiColors.StatusActivated : UiColors.BackgroundButton, Color.Transparent)) { RunSettingsImport(_previous!); } ImGui.EndDisabled(); if (_settingsImported) { ImGui.SameLine(); ImGui.TextColored(UiColors.StatusActivated, "Imported."); FormInputs.AddVerticalSpace(6); DrawRestartEditorButton("Restart TiXL to apply the imported settings, themes, keymaps and layouts."); } } private void DrawImportProjectsTab() { if (!RequirePrevious()) return; ImGui.TextColored(UiColors.TextMuted, $"Copied to {FileLocations.DefaultProjectFolder}."); ImGui.TextColored(UiColors.TextMuted, "Projects already present there are skipped."); FormInputs.AddVerticalSpace(6); if (_projects.Count == 0) { ImGui.TextColored(UiColors.TextMuted, "No projects found in the previous version."); return; } var scale = T3Ui.UiScaleFactor; foreach (var project in _projects) { ImGui.PushID(project.Name); var selected = _projectSelection.TryGetValue(project.Name, out var v) && v; ImGui.BeginDisabled(project.AlreadyImported); if (ImGui.Checkbox("##sel", ref selected)) _projectSelection[project.Name] = selected; ImGui.EndDisabled(); ImGui.SameLine(); var label = project.AlreadyImported ? $"{project.Name} (already imported)" : project.Name; ImGui.TextColored(project.AlreadyImported ? UiColors.TextMuted : UiColors.Text, label); ImGui.SameLine(); CustomComponents.RightAlign(ImGui.GetFrameHeight()); if (CustomComponents.TransparentIconButton(Icon.FolderOpen, new Vector2(ImGui.GetFrameHeight(), ImGui.GetFrameHeight()))) CoreUi.Instance.OpenWithDefaultApplication(project.SourcePath); ImGui.PopID(); } FormInputs.AddVerticalSpace(8); var anySelected = false; foreach (var project in _projects) anySelected |= !project.AlreadyImported && _projectSelection.TryGetValue(project.Name, out var v) && v; ImGui.BeginDisabled(!anySelected || _projectImportTask is { IsCompleted: false }); if (CustomComponents.DrawCtaButton("Import", Icon.None, anySelected ? UiColors.ForegroundFull : UiColors.TextMuted, anySelected ? UiColors.StatusActivated : UiColors.BackgroundButton, Color.Transparent)) { RunProjectImport(); } ImGui.EndDisabled(); if (_projectImportTask is { IsCompleted: false }) { ImGui.SameLine(); ImGui.TextColored(UiColors.StatusAnimated, "Importing…"); } else if (_projectsImported) { ImGui.SameLine(); ImGui.TextColored(UiColors.StatusActivated, "Imported."); FormInputs.AddVerticalSpace(6); DrawRestartEditorButton("Restart TiXL to load the imported projects."); } } private static void DrawRestartEditorButton(string tooltip) { if (CustomComponents.DrawCtaButton("Restart Editor", Icon.None, UiColors.ForegroundFull, UiColors.StatusActivated, Color.Transparent)) RestartEditor(); CustomComponents.TooltipForLastItem(tooltip); } /// Launches a fresh editor process and exits this one. Settings are saved by the process-exit handler. private static void RestartEditor() { var exePath = Environment.ProcessPath; if (string.IsNullOrEmpty(exePath)) { Log.Warning("Could not restart: the executable path is unknown."); return; } try { Process.Start(exePath); } catch (Exception e) { Log.Warning($"Could not restart the editor: {e.Message}"); return; } Environment.Exit(0); } private void DrawTestFeaturesTab() { ImGui.TextColored(UiColors.TextMuted, "Walk through recently added features with guided steps."); FormInputs.AddVerticalSpace(6); var scale = T3Ui.UiScaleFactor; // Reserve the footer height so the list scrolls independently and the CTA stays pinned at // the bottom (mirrors ManualTestRunnerWindow.DrawPickState). var ctaSize = CustomComponents.GetCtaButtonSize("Start Test", Icon.None); var footerH = ctaSize.Y + 12 * scale; ImGui.BeginChild("setlist", new Vector2(0, -footerH), ImGuiChildFlags.None, ImGuiWindowFlags.NoBackground); { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 4 * scale)); if (_testSets.Count == 0) { ImGui.TextColored(UiColors.TextMuted, "No feature tests found."); } else { foreach (var set in _testSets) { if (TestSetRow.Draw(set, _selectedTestSetId == set.Id, height: 50).Clicked) _selectedTestSetId = set.Id; } } ImGui.PopStyleVar(); } ImGui.EndChild(); var hasSelection = _selectedTestSetId != null; CustomComponents.RightAlign(ctaSize.X, sameLine: false); ImGui.BeginDisabled(!hasSelection); if (CustomComponents.DrawCtaButton("Start Test", Icon.None, hasSelection ? UiColors.ForegroundFull : UiColors.TextMuted, hasSelection ? UiColors.StatusActivated : UiColors.BackgroundButton, Color.Transparent)) { WindowManager.GuidedFeatureTestsWindow.StartSet(_selectedTestSetId!); Config.Visible = false; // base calls Close() → stamps the version marker } ImGui.EndDisabled(); } private bool RequirePrevious() { if (_hasPrevious) return true; ImGui.TextColored(UiColors.TextMuted, "No previous TiXL version was found to import from."); return false; } private void RunSettingsImport(PreviousVersionImport.PreviousVersion previous) { if (_importSettings && _settingsAvailable) PreviousVersionImport.ImportSettingsAllowlist(previous); if (_importThemes && _themesAvailable) PreviousVersionImport.ImportSubfolder(previous, FileLocations.ThemeSubFolder); if (_importKeymaps && _keymapsAvailable) PreviousVersionImport.ImportSubfolder(previous, FileLocations.KeyBindingSubFolder); if (_importLayouts && _layoutsAvailable) PreviousVersionImport.ImportSubfolder(previous, PreviousVersionImport.LayoutsSubfolder); _settingsImported = true; } private void RunProjectImport() { var toImport = new List(); foreach (var project in _projects) { if (!project.AlreadyImported && _projectSelection.TryGetValue(project.Name, out var v) && v) toImport.Add(project); } if (toImport.Count == 0) return; // Copy on a background thread so a large tree doesn't freeze the editor. Completion is handled // on the UI thread (see DrawImportProjectsTab) to avoid touching _projects off-thread. _projectImportTask = Task.Run(() => { foreach (var project in toImport) { try { PreviousVersionImport.ImportProject(project); } catch (Exception e) { Log.Warning($"Importing project '{project.Name}' failed: {e.Message}"); } } }); } private static void DrawCategoryCheckbox(string label, ref bool value, bool available) { ImGui.BeginDisabled(!available); var shown = value && available; if (ImGui.Checkbox(label, ref shown)) value = shown; ImGui.EndDisabled(); if (!available) CustomComponents.TooltipForLastItem($"No {label.ToLowerInvariant()} found in the previous version."); } private enum Tab { Welcome, ImportSettings, ImportProjects, TestFeatures, } private const string AlphaWelcomeMarkdown = """ To avoid affecting already installed versions and your projects, alpha versions always [create their own folders](https://github.com/tixl3d/tixl/wiki) for Settings and Projects. Use the import options on the left to copy data over. This version is under [active development](https://github.com/tixl3d/tixl/wiki/dev.UsingDev) — please don't use it for production work. Please report all issues on our [Discord server](https://discord.gg/YmSyQdeH3S), or browse the [project planning board](https://github.com/orgs/tixl3d/projects/3/views/8). """; private readonly MarkdownView _welcomeMarkdown = new(new MarkdownView.Options()); private bool _isInitialized; private Tab _activeTab = Tab.Welcome; private bool _hasPrevious; private PreviousVersionImport.PreviousVersion? _previous; private bool _settingsAvailable; private bool _themesAvailable; private bool _keymapsAvailable; private bool _layoutsAvailable; private bool _importSettings = true; private bool _importThemes = true; private bool _importKeymaps = true; private bool _importLayouts = true; private bool _settingsImported; private IReadOnlyList _projects = []; private readonly Dictionary _projectSelection = new(); private bool _projectsImported; private Task? _projectImportTask; private IReadOnlyList _testSets = []; private string? _selectedTestSetId; }