mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Show About page and seed running framework on first run
Two first-run behaviours that the previous version had and the port had lost. When a launch has nothing to restore (no saved tree path, no command-line target), greet the user with the About page in the main tab instead of an empty "(no selection)" view; the greeting is fired after the tree is ready and only takes the main tab when it's still empty and active, so it can't steal activation from a tab the user opened meanwhile (e.g. Compare...). And seed the default list with the .NET framework ILSpy itself runs on - every managed assembly in the running runtime's shared-framework directory. Headless tests opt out of that seed (re-opening ~150 assemblies per test is ~10x slower) and fall back to the minimal trio, leaving their expectations unchanged. Assisted-by: Claude:claude-opus-4-8:Claude Code
This commit is contained in:
@@ -267,12 +267,7 @@ namespace ICSharpCode.ILSpyX
|
||||
AddToListFromGAC("Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
|
||||
break;
|
||||
case object _ when path != null:
|
||||
foreach (var file in Directory.GetFiles(path, "*.dll"))
|
||||
{
|
||||
var dllname = Path.GetFileName(file);
|
||||
if (DoIncludeFile(dllname))
|
||||
AddToListFromDirectory(file);
|
||||
}
|
||||
AddFrameworkAssembliesFromDirectory(list, path);
|
||||
break;
|
||||
}
|
||||
return list;
|
||||
@@ -284,27 +279,38 @@ namespace ICSharpCode.ILSpyX
|
||||
if (file != null)
|
||||
list.OpenAssembly(file);
|
||||
}
|
||||
}
|
||||
|
||||
void AddToListFromDirectory(string file)
|
||||
/// <summary>
|
||||
/// Opens every managed framework assembly in <paramref name="directory"/> into
|
||||
/// <paramref name="list"/>, applying the same filter the preconfigured runtime lists use
|
||||
/// (skips native helpers like *_cor3.dll and lower-cased runtime libraries such as
|
||||
/// coreclr.dll / clrjit.dll). Shared by the "preconfigured runtime list" path and the
|
||||
/// first-run default list, which seeds itself from the shared-framework directory ILSpy
|
||||
/// is running on.
|
||||
/// </summary>
|
||||
public void AddFrameworkAssembliesFromDirectory(AssemblyList list, string directory)
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(directory, "*.dll"))
|
||||
{
|
||||
if (File.Exists(file))
|
||||
if (IsIncludedFrameworkFile(Path.GetFileName(file)))
|
||||
list.OpenAssembly(file);
|
||||
}
|
||||
}
|
||||
|
||||
bool DoIncludeFile(string fileName)
|
||||
{
|
||||
if (fileName == "Microsoft.DiaSymReader.Native.amd64.dll")
|
||||
return false;
|
||||
if (fileName.EndsWith("_cor3.dll", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (char.IsUpper(fileName[0]))
|
||||
return true;
|
||||
if (fileName == "netstandard.dll")
|
||||
return true;
|
||||
if (fileName == "mscorlib.dll")
|
||||
return true;
|
||||
static bool IsIncludedFrameworkFile(string fileName)
|
||||
{
|
||||
if (fileName == "Microsoft.DiaSymReader.Native.amd64.dll")
|
||||
return false;
|
||||
}
|
||||
if (fileName.EndsWith("_cor3.dll", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (char.IsUpper(fileName[0]))
|
||||
return true;
|
||||
if (fileName == "netstandard.dll")
|
||||
return true;
|
||||
if (fileName == "mscorlib.dll")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Avalonia.Headless.NUnit;
|
||||
|
||||
using AwesomeAssertions;
|
||||
|
||||
using ICSharpCode.ILSpyX;
|
||||
|
||||
using ILSpy;
|
||||
using ILSpy.AppEnv;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ICSharpCode.ILSpy.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class DefaultAssemblyListTests
|
||||
{
|
||||
[AvaloniaTest]
|
||||
public void Framework_Directory_Seed_Loads_Managed_Assemblies_And_Skips_Native()
|
||||
{
|
||||
// The first-run default list seeds itself from the shared-framework directory the
|
||||
// running runtime lives in, via AssemblyListManager.AddFrameworkAssembliesFromDirectory.
|
||||
// This exercises that seeding directly (the app wiring just calls it for the running
|
||||
// runtime's directory): managed framework assemblies load; native runtime libraries are
|
||||
// filtered out.
|
||||
var manager = AppComposition.Current.GetExport<SettingsService>().AssemblyListManager;
|
||||
var list = manager.CreateList("framework-seed-probe-" + Guid.NewGuid().ToString("N"));
|
||||
var frameworkDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
|
||||
|
||||
manager.AddFrameworkAssembliesFromDirectory(list, frameworkDir);
|
||||
|
||||
var assemblies = list.GetAssemblies();
|
||||
var names = assemblies.Select(a => a.ShortName).ToHashSet();
|
||||
|
||||
names.Should().Contain("System.Private.CoreLib");
|
||||
names.Should().Contain("System.Linq");
|
||||
names.Should().Contain("System.Collections");
|
||||
|
||||
assemblies.Length.Should().BeGreaterThan(20,
|
||||
"the whole shared-framework directory is far larger than a hard-coded handful");
|
||||
assemblies.Should().OnlyContain(a => Path.GetDirectoryName(a.FileName) == frameworkDir,
|
||||
"every seeded assembly comes from the framework directory");
|
||||
|
||||
// Native runtime libraries must not be loaded as assemblies.
|
||||
names.Should().NotContain(n => n.Contains("coreclr"));
|
||||
names.Should().NotContain(n => n.Contains("clrjit"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Headless.NUnit;
|
||||
|
||||
using AwesomeAssertions;
|
||||
|
||||
using ICSharpCode.ILSpy.Properties;
|
||||
|
||||
using ILSpy.AppEnv;
|
||||
using ILSpy.Docking;
|
||||
using ILSpy.ViewModels;
|
||||
using ILSpy.Views;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ICSharpCode.ILSpy.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class StartupAboutWelcomeTests
|
||||
{
|
||||
[AvaloniaTest]
|
||||
public async Task Startup_With_No_Saved_Selection_Shows_The_About_Page()
|
||||
{
|
||||
// On launch with nothing to restore (no saved tree-view path, no command-line target),
|
||||
// ILSpy should greet the user with the About page in the main tab, rather than an empty
|
||||
// "(no selection)" view. Mirrors the previous version's startup behaviour.
|
||||
|
||||
var window = AppComposition.Current.GetExport<MainWindow>();
|
||||
window.Show();
|
||||
var vm = (MainWindowViewModel)window.DataContext!;
|
||||
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
|
||||
|
||||
var dock = AppComposition.Current.GetExport<DockWorkspace>();
|
||||
|
||||
// The About page is shown asynchronously after the tree signals it is ready.
|
||||
await Waiters.WaitForAsync(
|
||||
() => dock.ActiveDecompilerTab is { } tab && Equals(tab.Title, Resources.About),
|
||||
description: "the empty-startup main tab should host the About welcome page");
|
||||
|
||||
// No tree node ended up selected, and the active page is the About page.
|
||||
((object?)vm.AssemblyTreeModel.SelectedItem).Should().BeNull(
|
||||
"nothing should be selected when there is no saved selection to restore");
|
||||
var active = dock.ActiveDecompilerTab;
|
||||
active.Should().NotBeNull();
|
||||
((object?)active!.Title).Should().Be(Resources.About);
|
||||
active.Text.Should().NotBeNullOrWhiteSpace("the About page must have rendered its content");
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,10 @@ public class TestApp : ProductionApp
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
// Tests boot the app repeatedly; seeding the entire shared framework (~150 assemblies)
|
||||
// per test is ~10x slower. Opt into the minimal default-list seed instead.
|
||||
ProductionApp.SeedFullFrameworkDefaultList = false;
|
||||
|
||||
var sessionsRoot = Path.Combine(Path.GetTempPath(), "ILSpy.Tests");
|
||||
// Sweep any leftover session dirs from previous runs first — ProcessExit-time cleanup
|
||||
// is best-effort and the active session's settings file is sometimes still locked when
|
||||
|
||||
@@ -38,6 +38,12 @@ namespace ILSpy
|
||||
public static CommandLineArguments? CommandLineArguments { get; private set; }
|
||||
public static CompositionHost? Composition { get; private set; }
|
||||
|
||||
// On first run the default assembly list is seeded with the entire shared-framework
|
||||
// directory the running runtime lives in (~150 assemblies). Headless tests flip this
|
||||
// off so each test boots with a minimal default instead of re-seeding the whole
|
||||
// framework per test (which is ~10x slower); see TestApp.
|
||||
public static bool SeedFullFrameworkDefaultList { get; set; } = true;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
@@ -56,6 +56,9 @@ namespace ILSpy.AssemblyTree
|
||||
readonly SettingsService settingsService;
|
||||
readonly LanguageService languageService;
|
||||
AssemblyListManager? listManager;
|
||||
// True until the first assembly-list activation completes — gates the startup-only
|
||||
// About-page greeting so switching lists later never re-triggers it.
|
||||
bool initialActivation = true;
|
||||
AssemblyListTreeNode? assemblyListTreeNode;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -298,6 +301,11 @@ namespace ILSpy.AssemblyTree
|
||||
settingsService.SessionSettings.ActiveAssemblyList = value;
|
||||
ShowAssemblyList(value);
|
||||
|
||||
// First activation is the startup one; a later switch to a different list isn't.
|
||||
// Only startup gets the empty-selection About-page greeting.
|
||||
bool isInitial = initialActivation;
|
||||
initialActivation = false;
|
||||
|
||||
// Restore the previously-selected tree node off the UI critical path. The walk
|
||||
// crosses an AssemblyTreeNode whose EnsureLazyChildren synchronously blocks on
|
||||
// GetLoadResultAsync — by going async here and awaiting the load, we let the
|
||||
@@ -311,36 +319,37 @@ namespace ILSpy.AssemblyTree
|
||||
if (App.CommandLineArguments?.NavigateTo is { Length: > 0 })
|
||||
return;
|
||||
var savedPath = settingsService.SessionSettings.ActiveTreeViewPath;
|
||||
if (savedPath is { Length: > 0 })
|
||||
_ = RestoreSelectedPathAsync(savedPath);
|
||||
_ = RestoreSelectedPathAsync(savedPath, isInitial);
|
||||
}
|
||||
|
||||
async Task RestoreSelectedPathAsync(string[] path)
|
||||
async Task RestoreSelectedPathAsync(string[]? path, bool isInitial)
|
||||
{
|
||||
using var _ = AppEnv.AppLog.Phase($"RestoreSelectedPathAsync ({path.Length} segments)");
|
||||
using var _ = AppEnv.AppLog.Phase($"RestoreSelectedPathAsync ({path?.Length ?? 0} segments, initial={isInitial})");
|
||||
try
|
||||
{
|
||||
if (Root == null)
|
||||
return;
|
||||
// Snapshot — if the user selects something else before the restore completes,
|
||||
// don't yank their selection out from under them.
|
||||
var initialSelection = SelectedItem;
|
||||
SharpTreeNode? node = Root;
|
||||
foreach (var element in path)
|
||||
SharpTreeNode? node = null;
|
||||
if (path is { Length: > 0 } && Root != null)
|
||||
{
|
||||
if (node == null)
|
||||
break;
|
||||
// Awaiting GetLoadResultAsync keeps EnsureLazyChildren — which itself does
|
||||
// GetAwaiter().GetResult() on the same task — from blocking the UI thread.
|
||||
// Once the load completes the .GetAwaiter().GetResult() returns instantly.
|
||||
if (node is AssemblyTreeNode asm)
|
||||
node = Root;
|
||||
foreach (var element in path)
|
||||
{
|
||||
using (AppEnv.AppLog.Phase($"await GetLoadResultAsync ({asm.LoadedAssembly.ShortName})"))
|
||||
await asm.LoadedAssembly.GetLoadResultAsync().ConfigureAwait(true);
|
||||
if (node == null)
|
||||
break;
|
||||
// Awaiting GetLoadResultAsync keeps EnsureLazyChildren — which itself does
|
||||
// GetAwaiter().GetResult() on the same task — from blocking the UI thread.
|
||||
// Once the load completes the .GetAwaiter().GetResult() returns instantly.
|
||||
if (node is AssemblyTreeNode asm)
|
||||
{
|
||||
using (AppEnv.AppLog.Phase($"await GetLoadResultAsync ({asm.LoadedAssembly.ShortName})"))
|
||||
await asm.LoadedAssembly.GetLoadResultAsync().ConfigureAwait(true);
|
||||
}
|
||||
using (AppEnv.AppLog.Phase($"EnsureLazyChildren ({node.GetType().Name} \"{element}\")"))
|
||||
node.EnsureLazyChildren();
|
||||
node = node.Children.FirstOrDefault(c => c.ToString() == element);
|
||||
}
|
||||
using (AppEnv.AppLog.Phase($"EnsureLazyChildren ({node.GetType().Name} \"{element}\")"))
|
||||
node.EnsureLazyChildren();
|
||||
node = node.Children.FirstOrDefault(c => c.ToString() == element);
|
||||
}
|
||||
// Wait for the tree view to be Loaded before assigning SelectedItem. Without
|
||||
// this, the SelectedItem assignment runs ShowSelectedNode → CurrentNodes →
|
||||
@@ -353,8 +362,19 @@ namespace ILSpy.AssemblyTree
|
||||
await Task.WhenAny(TreeReady, Task.Delay(TimeSpan.FromSeconds(5)))
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
if (node != null && node != Root && ReferenceEquals(SelectedItem, initialSelection))
|
||||
// Bail if the user selected something else while we were resolving.
|
||||
if (!ReferenceEquals(SelectedItem, initialSelection))
|
||||
return;
|
||||
if (node != null && node != Root)
|
||||
{
|
||||
SelectedItem = node;
|
||||
}
|
||||
else if (isInitial && SelectedItem == null)
|
||||
{
|
||||
// Launched with nothing to restore (no saved path, or it no longer resolves):
|
||||
// greet the user with the About page in the main tab instead of a blank view.
|
||||
ShowAboutWelcomePage();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -362,6 +382,19 @@ namespace ILSpy.AssemblyTree
|
||||
}
|
||||
}
|
||||
|
||||
// Open the About page as the startup welcome screen. The command is resolved lazily
|
||||
// through the menu registry (it's an ExportFactory, not instantiated until needed),
|
||||
// and its instance IS the AboutCommand, so we can drive its welcome path directly.
|
||||
void ShowAboutWelcomePage()
|
||||
{
|
||||
var registry = TryGetExport<MainMenuCommandRegistry>();
|
||||
var export = registry?.Commands
|
||||
.FirstOrDefault(c => c.Metadata.Header == nameof(ICSharpCode.ILSpy.Properties.Resources._About))
|
||||
?.CreateExport();
|
||||
if (export?.Value is Commands.AboutCommand about)
|
||||
about.ShowWelcome();
|
||||
}
|
||||
|
||||
void ShowAssemblyList(string name)
|
||||
{
|
||||
if (listManager == null)
|
||||
@@ -388,7 +421,7 @@ namespace ILSpy.AssemblyTree
|
||||
if (list.GetAssemblies().Length == 0 && list.ListName == AssemblyListManager.DefaultListName)
|
||||
{
|
||||
using (AppEnv.AppLog.Phase("LoadInitialAssemblies"))
|
||||
LoadInitialAssemblies(list);
|
||||
LoadInitialAssemblies(list, listManager);
|
||||
}
|
||||
AppEnv.AppLog.Mark($"AssemblyList contains {list.GetAssemblies().Length} assemblies");
|
||||
using (AppEnv.AppLog.Phase("new AssemblyListTreeNode"))
|
||||
@@ -602,18 +635,38 @@ namespace ILSpy.AssemblyTree
|
||||
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
|
||||
}
|
||||
|
||||
static void LoadInitialAssemblies(AssemblyList assemblyList)
|
||||
static void LoadInitialAssemblies(AssemblyList assemblyList, AssemblyListManager? manager)
|
||||
{
|
||||
System.Reflection.Assembly[] initialAssemblies = {
|
||||
typeof(object).Assembly,
|
||||
typeof(Uri).Assembly,
|
||||
typeof(System.Linq.Enumerable).Assembly,
|
||||
};
|
||||
foreach (var asm in initialAssemblies)
|
||||
// Headless tests opt out of the full-framework seed (it would re-open ~150 assemblies
|
||||
// per test); they fall back to the minimal trio the previous version shipped so their
|
||||
// expectations and runtime stay unchanged.
|
||||
if (!App.SeedFullFrameworkDefaultList || manager == null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(asm.Location))
|
||||
assemblyList.OpenAssembly(asm.Location);
|
||||
System.Reflection.Assembly[] minimal = {
|
||||
typeof(object).Assembly,
|
||||
typeof(Uri).Assembly,
|
||||
typeof(System.Linq.Enumerable).Assembly,
|
||||
};
|
||||
foreach (var asm in minimal)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(asm.Location))
|
||||
assemblyList.OpenAssembly(asm.Location);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// First-run default: seed the .NET framework assemblies ILSpy itself is running on -
|
||||
// every managed assembly in the shared-framework directory that hosts the running
|
||||
// runtime (the folder containing System.Private.CoreLib). The manager applies the
|
||||
// same managed-PE filter the preconfigured runtime lists use, so native runtime
|
||||
// libraries (coreclr, clrjit, *_cor3.dll, ...) are skipped.
|
||||
var coreLibLocation = typeof(object).Assembly.Location;
|
||||
if (string.IsNullOrEmpty(coreLibLocation))
|
||||
return;
|
||||
var frameworkDirectory = System.IO.Path.GetDirectoryName(coreLibLocation);
|
||||
if (string.IsNullOrEmpty(frameworkDirectory))
|
||||
return;
|
||||
manager.AddFrameworkAssembliesFromDirectory(assemblyList, frameworkDirectory);
|
||||
}
|
||||
|
||||
public void SelectNode(SharpTreeNode? node)
|
||||
|
||||
@@ -57,6 +57,27 @@ namespace ILSpy.Commands
|
||||
}
|
||||
|
||||
public override void Execute(object? parameter)
|
||||
{
|
||||
var output = BuildAboutOutput();
|
||||
var content = CreateTabContent(Resources.About, output, ".txt", isStaticContent: true);
|
||||
var tab = dockWorkspace.OpenNewTab(content);
|
||||
dockWorkspace.RecordStaticPage(tab, new Uri("resource:aboutpage"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the About page into the reusable main tab as the startup welcome screen.
|
||||
/// The content is non-static so the first tree-node selection cleanly reuses the tab
|
||||
/// and replaces it. Invoked by <see cref="AssemblyTree.AssemblyTreeModel"/> when ILSpy
|
||||
/// launches with no node selected and nothing to restore.
|
||||
/// </summary>
|
||||
public void ShowWelcome()
|
||||
{
|
||||
var output = BuildAboutOutput();
|
||||
var content = CreateTabContent(Resources.About, output, ".txt", isStaticContent: false);
|
||||
dockWorkspace.ShowWelcomePage(content);
|
||||
}
|
||||
|
||||
AvaloniaEditTextOutput BuildAboutOutput()
|
||||
{
|
||||
var output = new AvaloniaEditTextOutput { Title = Resources.About };
|
||||
output.WriteLine(Resources.ILSpyVersion + DecompilerVersionInfo.FullVersionWithCommitHash);
|
||||
@@ -81,11 +102,10 @@ namespace ILSpy.Commands
|
||||
foreach (var (phrase, uri) in Links)
|
||||
output.AddVisualLineElementGenerator(new ResourceLinkGenerator(phrase, uri));
|
||||
|
||||
var (tab, _) = OpenInNewTab(Resources.About, output, ".txt");
|
||||
dockWorkspace.RecordStaticPage(tab, new Uri("resource:aboutpage"));
|
||||
return output;
|
||||
}
|
||||
|
||||
(ContentTabPage Tab, DecompilerTabPageModel Content) OpenInNewTab(string title, AvaloniaEditTextOutput output, string syntaxExtension)
|
||||
DecompilerTabPageModel CreateTabContent(string title, AvaloniaEditTextOutput output, string syntaxExtension, bool isStaticContent)
|
||||
{
|
||||
var content = new DecompilerTabPageModel {
|
||||
Title = title,
|
||||
@@ -96,13 +116,13 @@ namespace ILSpy.Commands
|
||||
DefinitionLookup = output.DefinitionLookup,
|
||||
UIElements = output.UIElements,
|
||||
Foldings = output.Foldings,
|
||||
IsStaticContent = true,
|
||||
IsStaticContent = isStaticContent,
|
||||
CustomElementGenerators = output.ElementGenerators.Count > 0
|
||||
? new List<VisualLineElementGenerator>(output.ElementGenerators)
|
||||
: null,
|
||||
};
|
||||
content.OpenUriRequested += OnOpenUri;
|
||||
return (dockWorkspace.OpenNewTab(content), content);
|
||||
return content;
|
||||
}
|
||||
|
||||
bool OnOpenUri(Uri uri)
|
||||
@@ -122,7 +142,8 @@ namespace ILSpy.Commands
|
||||
using var reader = new StreamReader(stream);
|
||||
var output = new AvaloniaEditTextOutput { Title = resourceName };
|
||||
output.Write(reader.ReadToEnd());
|
||||
var (tab, _) = OpenInNewTab(resourceName, output, ".txt");
|
||||
var content = CreateTabContent(resourceName, output, ".txt", isStaticContent: true);
|
||||
var tab = dockWorkspace.OpenNewTab(content);
|
||||
dockWorkspace.RecordStaticPage(tab, new Uri("resource:" + resourceName));
|
||||
}
|
||||
|
||||
|
||||
@@ -1091,6 +1091,32 @@ namespace ILSpy.Docking
|
||||
return tab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders <paramref name="content"/> in the reusable MainTab as a transient welcome
|
||||
/// page (the About page shown at startup when nothing is selected). Unlike Help >
|
||||
/// About this does not pin a static page: the content is non-static, so the first
|
||||
/// tree-node selection reuses the MainTab and replaces it, leaving the user with a
|
||||
/// single tab rather than a stray empty one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The greeting is fired asynchronously at startup (after the tree signals ready), so by
|
||||
/// the time it runs the user may already have opened a document tab that doesn't move the
|
||||
/// tree selection (e.g. Compare...). Only greet when the MainTab is still empty AND still
|
||||
/// the active document; otherwise stealing activation would yank the user off whatever
|
||||
/// they just opened.
|
||||
/// </remarks>
|
||||
public void ShowWelcomePage(TextView.DecompilerTabPageModel content)
|
||||
{
|
||||
if (factory.MainTab is not { } main)
|
||||
return;
|
||||
if (main.Content != null)
|
||||
return;
|
||||
if (factory.Documents is { } docs && !ReferenceEquals(docs.ActiveDockable, main))
|
||||
return;
|
||||
main.Content = content;
|
||||
ActivateMainTabIfNeeded(main);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VS-style "pin tab" gesture. The current <c>factory.MainTab</c> keeps its position,
|
||||
/// content, and active state but flips to pinned
|
||||
|
||||
Reference in New Issue
Block a user