mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
6.1 KiB
181 lines
6.1 KiB
// 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.Collections.Generic; |
|
using System.ComponentModel; |
|
using System.Composition; |
|
|
|
using Dock.Model.Controls; |
|
using Dock.Model.Core; |
|
using Dock.Model.Core.Events; |
|
|
|
using ILSpy.Analyzers; |
|
using ILSpy.AssemblyTree; |
|
using ILSpy.Languages; |
|
using ILSpy.Search; |
|
using ILSpy.TextView; |
|
using ILSpy.TreeNodes; |
|
using ILSpy.ViewModels; |
|
|
|
namespace ILSpy.Docking |
|
{ |
|
[Export] |
|
[Shared] |
|
public class DockWorkspace |
|
{ |
|
readonly ILSpyDockFactory factory; |
|
readonly AssemblyTreeModel assemblyTreeModel; |
|
readonly LanguageService languageService; |
|
|
|
public IFactory Factory => factory; |
|
|
|
public IRootDock Layout { get; } |
|
|
|
public IReadOnlyList<ToolPaneMenuItem> ToolPaneMenuItems { get; } |
|
|
|
[ImportingConstructor] |
|
public DockWorkspace( |
|
AssemblyTreeModel assemblyTreeModel, |
|
SearchPaneModel searchPaneModel, |
|
AnalyzerTreeViewModel analyzerTreeViewModel, |
|
LanguageService languageService) |
|
{ |
|
this.assemblyTreeModel = assemblyTreeModel; |
|
this.languageService = languageService; |
|
factory = new ILSpyDockFactory(assemblyTreeModel, searchPaneModel, analyzerTreeViewModel); |
|
Layout = factory.CreateLayout(); |
|
if (factory.InitialDecompilerTab is { } initialTab) |
|
initialTab.Language = languageService.CurrentLanguage; |
|
|
|
assemblyTreeModel.PropertyChanged += OnAssemblyTreePropertyChanged; |
|
languageService.PropertyChanged += OnLanguagePropertyChanged; |
|
// Layout/factory initialization (locators, parent/factory wiring) is done by |
|
// the DockControl in MainWindow.axaml via InitializeFactory/InitializeLayout. |
|
|
|
ToolPaneMenuItems = new List<ToolPaneMenuItem> { |
|
new(assemblyTreeModel, factory), |
|
new(searchPaneModel, factory), |
|
new(analyzerTreeViewModel, factory), |
|
}; |
|
|
|
factory.DockableAdded += OnDocumentMembershipChanged; |
|
factory.DockableRemoved += OnDocumentMembershipChanged; |
|
factory.DockableClosed += OnDocumentMembershipChanged; |
|
factory.DockableClosing += OnDockableClosing; |
|
|
|
// TODO: layout persistence (load on startup, save on exit). DockSerializer.SystemTextJson |
|
// trips System.Text.Json's MaxDepth=64 limit on our layout because Dock's JsonConverterList<T> |
|
// calls JsonSerializer.Serialize per list element, which doesn't preserve the writer's |
|
// reference-tracking state between calls — so Owner/Factory back-refs aren't deduplicated as |
|
// $ref. Options when revisiting: try Dock.Serializer.Newtonsoft (better cycle handling), |
|
// or build a custom JsonSerializerOptions with MaxDepth raised + replicate Dock's internal |
|
// polymorphic resolver. |
|
} |
|
|
|
void OnDocumentMembershipChanged(object? sender, EventArgs e) => UpdateLastDocumentCanClose(); |
|
|
|
// Convert a tool pane's "close" (X button) into "hide" so the Window menu can restore it. |
|
// Documents are still closed for real — they get garbage-collected. |
|
void OnDockableClosing(object? sender, DockableClosingEventArgs e) |
|
{ |
|
if (e.Dockable is ToolPaneModel toolPane) |
|
{ |
|
e.Cancel = true; |
|
factory.HideDockable(toolPane); |
|
} |
|
} |
|
|
|
// When only one document is open, make it un-closeable so the user can't end up with an |
|
// empty document area. |
|
void UpdateLastDocumentCanClose() |
|
{ |
|
var docs = factory.Documents?.VisibleDockables; |
|
if (docs == null) |
|
return; |
|
bool canClose = docs.Count > 1; |
|
foreach (var d in docs) |
|
{ |
|
d.CanClose = canClose; |
|
} |
|
} |
|
|
|
void OnAssemblyTreePropertyChanged(object? sender, PropertyChangedEventArgs e) |
|
{ |
|
if (e.PropertyName == nameof(AssemblyTreeModel.SelectedItem)) |
|
ShowSelectedNode(); |
|
} |
|
|
|
void OnLanguagePropertyChanged(object? sender, PropertyChangedEventArgs e) |
|
{ |
|
if (e.PropertyName == nameof(LanguageService.CurrentLanguage)) |
|
{ |
|
if (GetActiveDecompilerTab() is { } tab) |
|
{ |
|
tab.Language = languageService.CurrentLanguage; |
|
// Re-decompile by re-assigning the same node so the tab refreshes for the new language. |
|
var node = tab.CurrentNode; |
|
tab.CurrentNode = null; |
|
tab.CurrentNode = node; |
|
} |
|
} |
|
} |
|
|
|
void ShowSelectedNode() |
|
{ |
|
if (assemblyTreeModel.SelectedItem is not ILSpyTreeNode node) |
|
return; |
|
var tab = GetActiveDecompilerTab(); |
|
if (tab == null) |
|
{ |
|
tab = factory.InitialDecompilerTab; |
|
if (tab == null || factory.Documents == null) |
|
return; |
|
factory.AddDockable(factory.Documents, tab); |
|
factory.SetActiveDockable(tab); |
|
factory.SetFocusedDockable(factory.Documents, tab); |
|
} |
|
tab.Language = languageService.CurrentLanguage; |
|
tab.CurrentNode = node; |
|
} |
|
|
|
DecompilerTabPageModel? GetActiveDecompilerTab() |
|
{ |
|
if (factory.Documents?.ActiveDockable is DecompilerTabPageModel active) |
|
return active; |
|
if (factory.Documents?.VisibleDockables != null) |
|
{ |
|
foreach (var d in factory.Documents.VisibleDockables) |
|
if (d is DecompilerTabPageModel m) |
|
return m; |
|
} |
|
return null; |
|
} |
|
|
|
public TabPageModel OpenNewTab(TabPageModel tab) |
|
{ |
|
if (factory.Documents != null) |
|
{ |
|
factory.AddDockable(factory.Documents, tab); |
|
factory.SetActiveDockable(tab); |
|
factory.SetFocusedDockable(factory.Documents, tab); |
|
} |
|
return tab; |
|
} |
|
} |
|
}
|
|
|