Browse Source

Merge pull request #10510 from workgroupengineering/fixes/DevTools/WithoutApplicationLifetime

fix(DevTools): Debug Application without ApplicationLifetime
pull/11304/head
Max Katz 3 years ago
committed by GitHub
parent
commit
2130689cb5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.Desktop.slnf
  2. 6
      Avalonia.sln
  3. 7
      samples/AppWithoutLifetime/App.axaml
  4. 12
      samples/AppWithoutLifetime/App.axaml.cs
  5. 21
      samples/AppWithoutLifetime/AppWithoutLifetime.csproj
  6. 13
      samples/AppWithoutLifetime/MainWindow.axaml
  7. 30
      samples/AppWithoutLifetime/MainWindow.axaml.cs
  8. 28
      samples/AppWithoutLifetime/Program.cs
  9. 9
      samples/AppWithoutLifetime/Sub.axaml
  10. 24
      samples/AppWithoutLifetime/Sub.axaml.cs
  11. 18
      samples/AppWithoutLifetime/app.manifest
  12. 4
      samples/IntegrationTestApp/MainWindow.axaml.cs
  13. 13
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  14. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  15. 1
      src/Avalonia.Controls/DesktopApplicationExtensions.cs
  16. 5
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  17. 41
      src/Avalonia.Diagnostics/Diagnostics/Controls/TopLevelGroup.cs
  18. 143
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  19. 56
      src/Avalonia.Diagnostics/Diagnostics/IDevToolsTopLevelGroup.cs
  20. 87
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

1
Avalonia.Desktop.slnf

@ -3,6 +3,7 @@
"path": "Avalonia.sln", "path": "Avalonia.sln",
"projects": [ "projects": [
"packages\\Avalonia\\Avalonia.csproj", "packages\\Avalonia\\Avalonia.csproj",
"samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj",
"samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj",
"samples\\ControlCatalog\\ControlCatalog.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj",
"samples\\GpuInterop\\GpuInterop.csproj", "samples\\GpuInterop\\GpuInterop.csproj",

6
Avalonia.sln

@ -250,6 +250,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppWithoutLifetime", "samples\AppWithoutLifetime\AppWithoutLifetime.csproj", "{F8928267-688E-4A51-989C-612A72446D33}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}"
@ -613,6 +614,10 @@ Global
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.Build.0 = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -727,6 +732,7 @@ Global
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098} {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098} {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098} {4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}

7
samples/AppWithoutLifetime/App.axaml

@ -0,0 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AppWithoutLifetime.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

12
samples/AppWithoutLifetime/App.axaml.cs

@ -0,0 +1,12 @@
using Avalonia;
using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
}

21
samples/AppWithoutLifetime/AppWithoutLifetime.csproj

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

13
samples/AppWithoutLifetime/MainWindow.axaml

@ -0,0 +1,13 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AppWithoutLifetime.MainWindow"
Title="AppWithoutLifetime">
<StackPanel>
<TextBlock Text="Welcome to Avalonia!"/>
<CheckBox Content="Welcome to Avalonia!"/>
<Button Click="Open" Content="Open"/>
</StackPanel>
</Window>

30
samples/AppWithoutLifetime/MainWindow.axaml.cs

@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnLoaded()
{
this.AttachDevTools();
base.OnLoaded();
}
public void Open(object sender, RoutedEventArgs e)
{
new Sub().Show(this);
}
}

28
samples/AppWithoutLifetime/Program.cs

@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls;
using System;
namespace AppWithoutLifetime;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp().Start(AppMain, args);
}
private static void AppMain(Application app, string[] args)
{
app.Run(new MainWindow());
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}

9
samples/AppWithoutLifetime/Sub.axaml

@ -0,0 +1,9 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AppWithoutLifetime.Sub"
Title="Window1">
Welcome to Avalonia Sub!
</Window>

24
samples/AppWithoutLifetime/Sub.axaml.cs

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime;
public partial class Sub : Window
{
public Sub()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnLoaded()
{
this.AttachDevTools();
base.OnLoaded();
}
}

18
samples/AppWithoutLifetime/app.manifest

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AppWithoutLifetime"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

4
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -202,7 +202,7 @@ namespace IntegrationTestApp
{ {
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows) foreach (var window in lifetime.Windows.ToArray())
{ {
window.Activate(); window.Activate();
} }
@ -212,7 +212,7 @@ namespace IntegrationTestApp
{ {
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows) foreach (var window in lifetime.Windows.ToArray())
{ {
window.Show(); window.Show();
if (window.WindowState == WindowState.Minimized) if (window.WindowState == WindowState.Minimized)

13
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -16,7 +17,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
private int _exitCode; private int _exitCode;
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private bool _isShuttingDown; private bool _isShuttingDown;
private readonly HashSet<Window> _windows = new(); private readonly AvaloniaList<Window> _windows = new();
private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime; private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime;
@ -35,7 +36,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
private static void OnWindowOpened(object? sender, RoutedEventArgs e) private static void OnWindowOpened(object? sender, RoutedEventArgs e)
{ {
s_activeLifetime?._windows.Add((Window)sender!); var window = (Window)sender!;
if (s_activeLifetime is not null && !s_activeLifetime._windows.Contains(window))
{
s_activeLifetime._windows.Add(window);
}
} }
public ClassicDesktopStyleApplicationLifetime() public ClassicDesktopStyleApplicationLifetime()
@ -67,7 +72,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
public Window? MainWindow { get; set; } public Window? MainWindow { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<Window> Windows => _windows.ToArray(); public IReadOnlyList<Window> Windows => _windows;
private void HandleWindowClosed(Window? window) private void HandleWindowClosed(Window? window)
{ {
@ -161,7 +166,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners. // owners.
foreach (var w in Windows) foreach (var w in Windows.ToArray())
{ {
if (w.Owner is null) if (w.Owner is null)
{ {

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -15,6 +15,7 @@
<InternalsVisibleTo Include="Avalonia.Controls.ItemsRepeater, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.ItemsRepeater, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />

1
src/Avalonia.Controls/DesktopApplicationExtensions.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Threading; using System.Threading;
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading; using Avalonia.Threading;

5
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@ -5,7 +5,7 @@ using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Diagnostics.Controls namespace Avalonia.Diagnostics.Controls
{ {
class Application : AvaloniaObject internal class Application : TopLevelGroup
, Input.ICloseable, IDisposable , Input.ICloseable, IDisposable
{ {
@ -16,7 +16,8 @@ namespace Avalonia.Diagnostics.Controls
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty = public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>(); ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>();
public Application(Avalonia.Application application) public Application(ClassicDesktopStyleApplicationLifetimeTopLevelGroup group, Avalonia.Application application)
: base(group)
{ {
_application = application; _application = application;

41
src/Avalonia.Diagnostics/Diagnostics/Controls/TopLevelGroup.cs

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls;
namespace Avalonia.Diagnostics.Controls;
internal class TopLevelGroup : AvaloniaObject
{
public TopLevelGroup(IDevToolsTopLevelGroup group)
{
Group = group;
if (Group.Items is INotifyCollectionChanged incc)
{
incc.CollectionChanged += (_, args) =>
{
if (args.OldItems is not null)
{
foreach (TopLevel oldItem in args.OldItems)
{
Removed?.Invoke(this, oldItem);
}
}
if (args.NewItems is not null)
{
foreach (TopLevel newItem in args.NewItems)
{
Added?.Invoke(this, newItem);
}
}
};
}
}
public IDevToolsTopLevelGroup Group { get; }
public IReadOnlyList<TopLevel> Items => Group.Items;
public event EventHandler<TopLevel>? Added;
public event EventHandler<TopLevel>? Removed;
}

143
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Views; using Avalonia.Diagnostics.Views;
using Avalonia.Input; using Avalonia.Input;
@ -9,12 +11,9 @@ using Avalonia.Reactive;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
public static class DevTools internal static class DevTools
{ {
private static readonly Dictionary<AvaloniaObject, MainWindow> s_open = private static readonly Dictionary<IDevToolsTopLevelGroup, MainWindow> s_open = new();
new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
public static IDisposable Attach(TopLevel root, KeyGesture gesture) public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{ {
@ -26,11 +25,6 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options) public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{ {
if (s_attachedToApplication == true)
{
throw new ArgumentException("DevTools already attached to application", nameof(root));
}
void PreviewKeyDown(object? sender, KeyEventArgs e) void PreviewKeyDown(object? sender, KeyEventArgs e)
{ {
if (options.Gesture.Matches(e)) if (options.Gesture.Matches(e))
@ -39,109 +33,116 @@ namespace Avalonia.Diagnostics
} }
} }
return root.AddDisposableHandler( return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler(
InputElement.KeyDownEvent, InputElement.KeyDownEvent,
PreviewKeyDown, PreviewKeyDown,
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel);
} }
public static IDisposable Open(TopLevel root) => public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current,new DevToolsOptions(),root as Window); Open(new SingleViewTopLevelGroup(root), options, root as Window, null);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current, options, root as Window);
private static void DevToolsClosed(object? sender, EventArgs e) internal static IDisposable Open(IDevToolsTopLevelGroup group, DevToolsOptions options) =>
{ Open(group, options, null, null);
var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
if (window.Root is Controls.Application host)
{
s_open.Remove(host.Instance);
}
else
{
s_open.Remove(window.Root!);
}
}
internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null) internal static IDisposable Attach(Application application, DevToolsOptions options)
{ {
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
var openedDisposable = new SerialDisposableValue(); var openedDisposable = new SerialDisposableValue();
var result = new CompositeDisposable(2); var result = new CompositeDisposable(2);
result.Add(openedDisposable); result.Add(openedDisposable);
// Skip if call on Design Mode // Skip if call on Design Mode
if (!Avalonia.Controls.Design.IsDesignMode if (!Design.IsDesignMode)
&& !s_attachedToApplication)
{ {
var lifeTime = application.ApplicationLifetime var lifeTime = application.ApplicationLifetime
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime; as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime;
if (lifeTime is null) if (lifeTime is null)
{ {
throw new ArgumentNullException(nameof(Application.ApplicationLifetime)); throw new ArgumentNullException(nameof(application), "DevTools can only attach to applications that support IClassicDesktopStyleApplicationLifetime.");
} }
if (application.InputManager is { }) if (application.InputManager is not null)
{ {
s_attachedToApplication = true;
result.Add(application.InputManager.PreProcess.Subscribe(e => result.Add(application.InputManager.PreProcess.Subscribe(e =>
{ {
var owner = lifeTime.MainWindow;
if (e is RawKeyEventArgs keyEventArgs if (e is RawKeyEventArgs keyEventArgs
&& keyEventArgs.Type == RawKeyEventType.KeyUp && keyEventArgs.Type == RawKeyEventType.KeyUp
&& options.Gesture.Matches(keyEventArgs)) && options.Gesture.Matches(keyEventArgs))
{ {
openedDisposable.Disposable = Open(application, options, owner); openedDisposable.Disposable =
Open(new ClassicDesktopStyleApplicationLifetimeTopLevelGroup(lifeTime), options,
owner, application);
e.Handled = true;
} }
})); }));
} }
} }
return result; return result;
} }
private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default) private static IDisposable Open(IDevToolsTopLevelGroup topLevelGroup, DevToolsOptions options,
Window? owner, Application? app)
{ {
var focussedControl = KeyboardDevice.Instance?.FocusedElement as Control; var focussedControl = KeyboardDevice.Instance?.FocusedElement as Control;
if (application is null) AvaloniaObject root = topLevelGroup switch
{ {
throw new ArgumentNullException(nameof(application)); ClassicDesktopStyleApplicationLifetimeTopLevelGroup gr => new Controls.Application(gr, app ?? Application.Current!),
} SingleViewTopLevelGroup gr => gr.Items.First(),
if (s_open.TryGetValue(application, out var window)) _ => new Controls.TopLevelGroup(topLevelGroup)
};
// If single static toplevel is already visible in another devtools window, focus it.
if (s_open.TryGetValue(topLevelGroup, out var mainWindow))
{ {
window.Activate(); mainWindow.Activate();
window.SelectedControl(focussedControl); mainWindow.SelectedControl(focussedControl);
return Disposable.Empty;
} }
else if (topLevelGroup.Items.Count == 1 && topLevelGroup.Items is not INotifyCollectionChanged)
{ {
window = new MainWindow var singleTopLevel = topLevelGroup.Items.First();
{
Root = new Controls.Application(application), foreach (var group in s_open)
Width = options.Size.Width,
Height = options.Size.Height,
};
window.SetOptions(options);
window.SelectedControl(focussedControl);
window.Closed += DevToolsClosed;
s_open.Add(application, window);
if (options.ShowAsChildWindow && owner is { })
{ {
window.Show(owner); if (group.Key.Items.Contains(singleTopLevel))
} {
else group.Value.Activate();
{ group.Value.SelectedControl(focussedControl);
window.Show(); return Disposable.Empty;
}
} }
} }
return Disposable.Create(() => window?.Close());
var window = new MainWindow
{
Root = root,
Width = options.Size.Width,
Height = options.Size.Height,
Tag = topLevelGroup
};
window.SetOptions(options);
window.SelectedControl(focussedControl);
window.Closed += DevToolsClosed;
s_open.Add(topLevelGroup, window);
if (options.ShowAsChildWindow && owner is not null)
{
window.Show(owner);
}
else
{
window.Show();
}
return Disposable.Create(() => window.Close());
}
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
s_open.Remove((IDevToolsTopLevelGroup)window.Tag!);
} }
} }
} }

56
src/Avalonia.Diagnostics/Diagnostics/IDevToolsTopLevelGroup.cs

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Diagnostics;
internal interface IDevToolsTopLevelGroup
{
IReadOnlyList<TopLevel> Items { get; }
}
internal class ClassicDesktopStyleApplicationLifetimeTopLevelGroup : IDevToolsTopLevelGroup
{
private readonly IClassicDesktopStyleApplicationLifetime _lifetime;
public ClassicDesktopStyleApplicationLifetimeTopLevelGroup(IClassicDesktopStyleApplicationLifetime lifetime)
{
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
}
public IReadOnlyList<TopLevel> Items => _lifetime.Windows;
public override int GetHashCode()
{
return _lifetime.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is ClassicDesktopStyleApplicationLifetimeTopLevelGroup g && g._lifetime == _lifetime;
}
}
internal class SingleViewTopLevelGroup : IDevToolsTopLevelGroup
{
private readonly TopLevel _topLevel;
public SingleViewTopLevelGroup(TopLevel topLevel)
{
_topLevel = topLevel;
Items = new[] { topLevel ?? throw new ArgumentNullException(nameof(topLevel)) };
}
public IReadOnlyList<TopLevel> Items { get; }
public override int GetHashCode()
{
return _topLevel.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is SingleViewTopLevelGroup g && g._topLevel == _topLevel;
}
}

87
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels
Children = avaloniaObject switch Children = avaloniaObject switch
{ {
ILogical logical => new LogicalTreeNodeCollection(this, logical), ILogical logical => new LogicalTreeNodeCollection(this, logical),
Controls.Application host => new ApplicationHostLogical(this, host), Controls.TopLevelGroup host => new TopLevelGroupHostLogical(this, host),
_ => TreeNodeCollection.Empty _ => TreeNodeCollection.Empty
}; };
} }
@ -55,70 +55,55 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
internal class ApplicationHostLogical : TreeNodeCollection internal class TopLevelGroupHostLogical : TreeNodeCollection
{ {
readonly Controls.Application _application; private readonly Controls.TopLevelGroup _group;
CompositeDisposable _subscriptions = new CompositeDisposable(2); private readonly CompositeDisposable _subscriptions = new(1);
public ApplicationHostLogical(TreeNode owner, Controls.Application host) :
public TopLevelGroupHostLogical(TreeNode owner, Controls.TopLevelGroup host) :
base(owner) base(owner)
{ {
_application = host; _group = host;
} }
protected override void Initialize(AvaloniaList<TreeNode> nodes) protected override void Initialize(AvaloniaList<TreeNode> nodes)
{ {
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single && for (var i = 0; i < _group.Items.Count; i++)
single.MainView is not null)
{ {
nodes.Add(new LogicalTreeNode(single.MainView, Owner)); var window = _group.Items[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
} }
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic) void GroupOnAdded(object? sender, TopLevel e)
{ {
if (e is Views.MainWindow)
for (int i = 0; i < classic.Windows.Count; i++)
{ {
var window = classic.Windows[i]; return;
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
} }
_subscriptions = new CompositeDisposable(2)
nodes.Add(new LogicalTreeNode(e, Owner));
}
void GroupOnRemoved(object? sender, TopLevel e)
{
if (e is Views.MainWindow)
{ {
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> return;
{ }
if (s is Views.MainWindow)
{ nodes.Add(new LogicalTreeNode(e, Owner));
return;
}
nodes.Add(new LogicalTreeNode((AvaloniaObject)s!,Owner));
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s));
if(!(item is null))
{
nodes.Remove(item);
}
if(nodes.Count == 0)
{
if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
controller.Shutdown();
}
else
{
Environment.Exit(0);
}
}
}),
};
} }
_group.Added += GroupOnAdded;
_group.Removed += GroupOnRemoved;
_subscriptions.Add(new Disposable.AnonymousDisposable(() =>
{
_group.Added -= GroupOnAdded;
_group.Removed -= GroupOnRemoved;
}));
} }
public override void Dispose() public override void Dispose()

Loading…
Cancel
Save