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

439 lines
16 KiB
C#

#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;
/// <summary>
/// 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 <see cref="WelcomeWindow"/> instead.
/// </summary>
[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);
}
/// <summary>Launches a fresh editor process and exits this one. Settings are saved by the process-exit handler.</summary>
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<PreviousVersionImport.ProjectEntry>();
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<PreviousVersionImport.ProjectEntry> _projects = [];
private readonly Dictionary<string, bool> _projectSelection = new();
private bool _projectsImported;
private Task? _projectImportTask;
private IReadOnlyList<TestSet> _testSets = [];
private string? _selectedTestSetId;
}