committed by
GitHub
169 changed files with 2810 additions and 2735 deletions
@ -1,35 +0,0 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
|
|||
<!-- Ensure that code generator is actually built --> |
|||
<ItemGroup> |
|||
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\src\tools\MicroComGenerator\MicroComGenerator.csproj"> |
|||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly> |
|||
<ExcludeAssets>all</ExcludeAssets> |
|||
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties> |
|||
<SetTargetFramework>TargetFramework=net6.0</SetTargetFramework> |
|||
</ProjectReference> |
|||
</ItemGroup> |
|||
|
|||
<Target Name="GenerateAvaloniaNativeComInterop" |
|||
BeforeTargets="CoreCompile" |
|||
DependsOnTargets="ResolveReferences" |
|||
Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs" |
|||
Outputs="%(AvnComIdl.OutputFile)"> |
|||
<Message Importance="high" Text="Generating file %(AvnComIdl.OutputFile) from @(AvnComIdl)" /> |
|||
<Exec Command="dotnet "$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/bin/$(Configuration)/net6.0/MicroComGenerator.dll" -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)" |
|||
LogStandardErrorAsError="true" /> |
|||
<ItemGroup> |
|||
<!-- Remove and re-add generated file, this is needed for the clean build --> |
|||
<Compile Remove="%(AvnComIdl.OutputFile)"/> |
|||
<Compile Include="%(AvnComIdl.OutputFile)"/> |
|||
</ItemGroup> |
|||
</Target> |
|||
<ItemGroup> |
|||
<UpToDateCheckInput Include="@(AvnComIdl)"/> |
|||
<UpToDateCheckInput Include="$(MSBuildThisFileDirectory)/../src/tools/MicroComGenerator/**/*.cs"/> |
|||
</ItemGroup> |
|||
<PropertyGroup> |
|||
<_AvaloniaPatchComInterop>true</_AvaloniaPatchComInterop> |
|||
</PropertyGroup> |
|||
<Import Project="$(MSBuildThisFileDirectory)/BuildTargets.targets" /> |
|||
</Project> |
|||
@ -1,14 +1,14 @@ |
|||
using System.IO; |
|||
using MicroComGenerator; |
|||
using MicroCom.CodeGenerator; |
|||
using Nuke.Common; |
|||
|
|||
partial class Build : NukeBuild |
|||
{ |
|||
Target GenerateCppHeaders => _ => _.Executes(() => |
|||
{ |
|||
var text = File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"); |
|||
var ast = AstParser.Parse(text); |
|||
var file = MicroComCodeGenerator.Parse( |
|||
File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); |
|||
File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", |
|||
CppGen.GenerateCpp(ast)); |
|||
file.GenerateCppHeader()); |
|||
}); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
Compat issues with assembly Avalonia.Base: |
|||
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post<T>(System.Action<T>, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. |
|||
Total Issues: 1 |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Utilities; |
|||
|
|||
/// <summary>
|
|||
/// Defines a listener to a event subscribed vis the <see cref="WeakEvent{TTarget, TEventArgs}"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
|
|||
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs |
|||
{ |
|||
void OnEvent(object? sender, WeakEvent ev, TEventArgs e); |
|||
} |
|||
@ -0,0 +1,187 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Utilities; |
|||
|
|||
/// <summary>
|
|||
/// Manages subscriptions to events using weak listeners.
|
|||
/// </summary>
|
|||
public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : EventArgs where TSender : class |
|||
{ |
|||
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe; |
|||
|
|||
readonly ConditionalWeakTable<object, Subscription> _subscriptions = new(); |
|||
|
|||
internal WeakEvent( |
|||
Action<TSender, EventHandler<TEventArgs>> subscribe, |
|||
Action<TSender, EventHandler<TEventArgs>> unsubscribe) |
|||
{ |
|||
_subscribe = (t, s) => |
|||
{ |
|||
subscribe(t, s); |
|||
return () => unsubscribe(t, s); |
|||
}; |
|||
} |
|||
|
|||
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe) |
|||
{ |
|||
_subscribe = subscribe; |
|||
} |
|||
|
|||
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) |
|||
{ |
|||
if (!_subscriptions.TryGetValue(target, out var subscription)) |
|||
_subscriptions.Add(target, subscription = new Subscription(this, target)); |
|||
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber)); |
|||
} |
|||
|
|||
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) |
|||
{ |
|||
if (_subscriptions.TryGetValue(target, out var subscription)) |
|||
subscription.Remove(subscriber); |
|||
} |
|||
|
|||
private class Subscription |
|||
{ |
|||
private readonly WeakEvent<TSender, TEventArgs> _ev; |
|||
private readonly TSender _target; |
|||
private readonly Action _compact; |
|||
|
|||
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data = |
|||
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16]; |
|||
private int _count; |
|||
private readonly Action _unsubscribe; |
|||
private bool _compactScheduled; |
|||
|
|||
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target) |
|||
{ |
|||
_ev = ev; |
|||
_target = target; |
|||
_compact = Compact; |
|||
_unsubscribe = ev._subscribe(target, OnEvent); |
|||
} |
|||
|
|||
void Destroy() |
|||
{ |
|||
_unsubscribe(); |
|||
_ev._subscriptions.Remove(_target); |
|||
} |
|||
|
|||
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s) |
|||
{ |
|||
if (_count == _data.Length) |
|||
{ |
|||
//Extend capacity
|
|||
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2]; |
|||
Array.Copy(_data, extendedData, _data.Length); |
|||
_data = extendedData; |
|||
} |
|||
|
|||
_data[_count] = s; |
|||
_count++; |
|||
} |
|||
|
|||
public void Remove(IWeakEventSubscriber<TEventArgs> s) |
|||
{ |
|||
var removed = false; |
|||
|
|||
for (int c = 0; c < _count; ++c) |
|||
{ |
|||
var reference = _data[c]; |
|||
|
|||
if (reference != null && reference.TryGetTarget(out var instance) && instance == s) |
|||
{ |
|||
_data[c] = null; |
|||
removed = true; |
|||
} |
|||
} |
|||
|
|||
if (removed) |
|||
{ |
|||
ScheduleCompact(); |
|||
} |
|||
} |
|||
|
|||
void ScheduleCompact() |
|||
{ |
|||
if(_compactScheduled) |
|||
return; |
|||
_compactScheduled = true; |
|||
Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); |
|||
} |
|||
|
|||
void Compact() |
|||
{ |
|||
_compactScheduled = false; |
|||
int empty = -1; |
|||
for (var c = 0; c < _count; c++) |
|||
{ |
|||
var r = _data[c]; |
|||
//Mark current index as first empty
|
|||
if (r == null && empty == -1) |
|||
empty = c; |
|||
//If current element isn't null and we have an empty one
|
|||
if (r != null && empty != -1) |
|||
{ |
|||
_data[c] = null; |
|||
_data[empty] = r; |
|||
empty++; |
|||
} |
|||
} |
|||
|
|||
if (empty != -1) |
|||
_count = empty; |
|||
if (_count == 0) |
|||
Destroy(); |
|||
} |
|||
|
|||
void OnEvent(object? sender, TEventArgs eventArgs) |
|||
{ |
|||
var needCompact = false; |
|||
for (var c = 0; c < _count; c++) |
|||
{ |
|||
var r = _data[c]; |
|||
if (r?.TryGetTarget(out var sub) == true) |
|||
sub!.OnEvent(_target, _ev, eventArgs); |
|||
else |
|||
needCompact = true; |
|||
} |
|||
|
|||
if (needCompact) |
|||
ScheduleCompact(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
public class WeakEvent |
|||
{ |
|||
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>( |
|||
Action<TSender, EventHandler<TEventArgs>> subscribe, |
|||
Action<TSender, EventHandler<TEventArgs>> unsubscribe) where TSender : class where TEventArgs : EventArgs |
|||
{ |
|||
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe); |
|||
} |
|||
|
|||
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>( |
|||
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs |
|||
{ |
|||
return new WeakEvent<TSender, TEventArgs>(subscribe); |
|||
} |
|||
|
|||
public static WeakEvent<TSender, EventArgs> Register<TSender>( |
|||
Action<TSender, EventHandler> subscribe, |
|||
Action<TSender, EventHandler> unsubscribe) where TSender : class |
|||
{ |
|||
return Register<TSender, EventArgs>((s, h) => |
|||
{ |
|||
EventHandler handler = (_, e) => h(s, e); |
|||
subscribe(s, handler); |
|||
return () => unsubscribe(s, handler); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
using System.Windows.Input; |
|||
|
|||
namespace Avalonia.Utilities; |
|||
|
|||
public class WeakEvents |
|||
{ |
|||
/// <summary>
|
|||
/// Represents CollectionChanged event from <see cref="INotifyCollectionChanged"/>
|
|||
/// </summary>
|
|||
public static readonly WeakEvent<INotifyCollectionChanged, NotifyCollectionChangedEventArgs> |
|||
CollectionChanged = WeakEvent.Register<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>( |
|||
(c, s) => |
|||
{ |
|||
NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e); |
|||
c.CollectionChanged += handler; |
|||
return () => c.CollectionChanged -= handler; |
|||
}); |
|||
|
|||
/// <summary>
|
|||
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/>
|
|||
/// </summary>
|
|||
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs> |
|||
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>( |
|||
(s, h) => |
|||
{ |
|||
PropertyChangedEventHandler handler = (_, e) => h(s, e); |
|||
s.PropertyChanged += handler; |
|||
return () => s.PropertyChanged -= handler; |
|||
}); |
|||
|
|||
/// <summary>
|
|||
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
|
|||
/// </summary>
|
|||
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged = |
|||
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h, |
|||
(s, h) => s.CanExecuteChanged -= h); |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
namespace Avalonia.Diagnostics.Behaviors |
|||
{ |
|||
/// <summary>
|
|||
/// See discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773
|
|||
/// </summary>
|
|||
static class ColumnDefinition |
|||
{ |
|||
private readonly static Avalonia.Controls.GridLength ZeroWidth = |
|||
new Avalonia.Controls.GridLength(0, Avalonia.Controls.GridUnitType.Pixel); |
|||
|
|||
private readonly static AttachedProperty<Avalonia.Controls.GridLength?> LastWidthProperty = |
|||
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, Avalonia.Controls.GridLength?>("LastWidth" |
|||
, typeof(ColumnDefinition) |
|||
, default); |
|||
|
|||
public readonly static AttachedProperty<bool> IsVisibleProperty = |
|||
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, bool>("IsVisible" |
|||
, typeof(ColumnDefinition) |
|||
, true |
|||
, coerce: (element, visibility) => |
|||
{ |
|||
|
|||
var lastWidth = element.GetValue(LastWidthProperty); |
|||
if (visibility == true && lastWidth is { }) |
|||
{ |
|||
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, lastWidth); |
|||
} |
|||
else if (visibility == false) |
|||
{ |
|||
element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty)); |
|||
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth); |
|||
} |
|||
return visibility; |
|||
} |
|||
); |
|||
|
|||
public static bool GetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition) |
|||
{ |
|||
return columnDefinition.GetValue(IsVisibleProperty); |
|||
} |
|||
|
|||
public static void SetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition, bool visibility) |
|||
{ |
|||
columnDefinition.SetValue(IsVisibleProperty, visibility); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,119 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Lifetimes = Avalonia.Controls.ApplicationLifetimes; |
|||
using App = Avalonia.Application; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
class Application : AvaloniaObject |
|||
, Input.ICloseable |
|||
|
|||
{ |
|||
private readonly App _application; |
|||
private static readonly Version s_version = typeof(IAvaloniaObject).Assembly?.GetName()?.Version |
|||
?? Version.Parse("0.0.00"); |
|||
public event EventHandler? Closed; |
|||
|
|||
public Application(App application) |
|||
{ |
|||
_application = application; |
|||
|
|||
if (_application.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller) |
|||
{ |
|||
EventHandler<Lifetimes.ControlledApplicationLifetimeExitEventArgs> eh = default!; |
|||
eh = (s, e) => |
|||
{ |
|||
controller.Exit -= eh; |
|||
Closed?.Invoke(s, e); |
|||
}; |
|||
controller.Exit += eh; |
|||
} |
|||
RendererRoot = application.ApplicationLifetime switch |
|||
{ |
|||
Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow.Renderer, |
|||
Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as VisualTree.IVisual)?.VisualRoot?.Renderer, |
|||
_ => null |
|||
}; |
|||
} |
|||
|
|||
internal App Instance => _application; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="DataContext"/> property.
|
|||
/// </summary>
|
|||
public object? DataContext => |
|||
_application.DataContext; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the application's global data templates.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's global data templates.
|
|||
/// </value>
|
|||
public Avalonia.Controls.Templates.DataTemplates DataTemplates => |
|||
_application.DataTemplates; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's focus manager.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's focus manager.
|
|||
/// </value>
|
|||
public Input.IFocusManager? FocusManager => |
|||
_application.FocusManager; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's input manager.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's input manager.
|
|||
/// </value>
|
|||
public Input.InputManager? InputManager => |
|||
_application.InputManager; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application clipboard.
|
|||
/// </summary>
|
|||
public Input.Platform.IClipboard? Clipboard => |
|||
_application.Clipboard; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's global resource dictionary.
|
|||
/// </summary>
|
|||
public IResourceDictionary Resources => |
|||
_application.Resources; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's global styles.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's global styles.
|
|||
/// </value>
|
|||
/// <remarks>
|
|||
/// Global styles apply to all windows in the application.
|
|||
/// </remarks>
|
|||
public Styling.Styles Styles => |
|||
_application.Styles; |
|||
|
|||
/// <summary>
|
|||
/// Application lifetime, use it for things like setting the main window and exiting the app from code
|
|||
/// Currently supported lifetimes are:
|
|||
/// - <see cref="Lifetimes.IClassicDesktopStyleApplicationLifetime"/>
|
|||
/// - <see cref="Lifetimes.ISingleViewApplicationLifetime"/>
|
|||
/// - <see cref="Lifetimes.IControlledApplicationLifetime"/>
|
|||
/// </summary>
|
|||
public Lifetimes.IApplicationLifetime? ApplicationLifetime => |
|||
_application.ApplicationLifetime; |
|||
|
|||
/// <summary>
|
|||
/// Application name to be used for various platform-specific purposes
|
|||
/// </summary>
|
|||
public string? Name => |
|||
_application.Name; |
|||
|
|||
/// <summary>
|
|||
/// Gets the root of the visual tree, if the control is attached to a visual tree.
|
|||
/// </summary>
|
|||
internal Rendering.IRenderer? RendererRoot { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
|
|||
namespace Avalonia.Diagnostics.Converters |
|||
{ |
|||
internal class GetTypeNameConverter : IValueConverter |
|||
{ |
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (value is Type type) |
|||
{ |
|||
return type.GetTypeName(); |
|||
} |
|||
return BindingOperations.DoNothing; |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return BindingOperations.DoNothing; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using Avalonia.Controls; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
static class Convetions |
|||
{ |
|||
public static string DefaultScreenshotsRoot => |
|||
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create), |
|||
"Screenshots"); |
|||
|
|||
public static IScreenshotHandler DefaultScreenshotHandler { get; } = |
|||
new Screenshots.FilePickerHandler(); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
/// <summary>
|
|||
/// Allowed to define custom handler for Shreeshot
|
|||
/// </summary>
|
|||
public interface IScreenshotHandler |
|||
{ |
|||
/// <summary>
|
|||
/// Handle the Screenshot
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task Take(IControl control); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
static class KeyGestureExtesions |
|||
{ |
|||
public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) => |
|||
keyEvent != null && |
|||
(KeyModifiers)(keyEvent.Modifiers & RawInputModifiers.KeyboardMask) == gesture.KeyModifiers && |
|||
ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(gesture.Key); |
|||
|
|||
private static Key ResolveNumPadOperationKey(Key key) |
|||
{ |
|||
switch (key) |
|||
{ |
|||
case Key.Add: |
|||
return Key.OemPlus; |
|||
case Key.Subtract: |
|||
return Key.OemMinus; |
|||
case Key.Decimal: |
|||
return Key.OemPeriod; |
|||
default: |
|||
return key; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.Diagnostics.Screenshots |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for render Screenshto to stream
|
|||
/// </summary>
|
|||
public abstract class BaseRenderToStreamHandler : IScreenshotHandler |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Get stream
|
|||
/// </summary>
|
|||
/// <param name="control"></param>
|
|||
/// <returns>stream to render the control</returns>
|
|||
protected abstract Task<System.IO.Stream?> GetStream(IControl control); |
|||
|
|||
public async Task Take(IControl control) |
|||
{ |
|||
using var output = await GetStream(control); |
|||
if (output is { }) |
|||
{ |
|||
control.RenderTo(output); |
|||
await output.FlushAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Lifetimes = Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Diagnostics.Screenshots |
|||
{ |
|||
/// <summary>
|
|||
/// Show a FileSavePicker to select where save screenshot
|
|||
/// </summary>
|
|||
public sealed class FilePickerHandler : BaseRenderToStreamHandler |
|||
{ |
|||
/// <summary>
|
|||
/// Instance FilePickerHandler
|
|||
/// </summary>
|
|||
public FilePickerHandler() |
|||
{ |
|||
|
|||
} |
|||
/// <summary>
|
|||
/// Instance FilePickerHandler with specificated parameter
|
|||
/// </summary>
|
|||
/// <param name="title">SaveFilePicker Title</param>
|
|||
/// <param name="screenshotRoot"></param>
|
|||
public FilePickerHandler(string? title |
|||
, string? screenshotRoot = default |
|||
) |
|||
{ |
|||
if (title is { }) |
|||
Title = title; |
|||
if (screenshotRoot is { }) |
|||
ScreenshotsRoot = screenshotRoot; |
|||
} |
|||
/// <summary>
|
|||
/// Get the root folder where screeshots well be stored.
|
|||
/// The default root folder is [Environment.SpecialFolder.MyPictures]/Screenshots.
|
|||
/// </summary>
|
|||
public string ScreenshotsRoot { get; } |
|||
= Convetions.DefaultScreenshotsRoot; |
|||
|
|||
/// <summary>
|
|||
/// SaveFilePicker Title
|
|||
/// </summary>
|
|||
public string Title { get; } = "Save Screenshot to ..."; |
|||
|
|||
Window GetWindow(IControl control) |
|||
{ |
|||
var window = control.VisualRoot as Window; |
|||
var app = Application.Current; |
|||
if (app?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop) |
|||
{ |
|||
window = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow); |
|||
} |
|||
return window!; |
|||
} |
|||
|
|||
protected async override Task<Stream?> GetStream(IControl control) |
|||
{ |
|||
Stream? output = default; |
|||
var result = await new SaveFileDialog() |
|||
{ |
|||
Title = Title, |
|||
Filters = new() { new FileDialogFilter() { Name = "PNG", Extensions = new() { "png" } } }, |
|||
Directory = ScreenshotsRoot, |
|||
}.ShowAsync(GetWindow(control)); |
|||
if (!string.IsNullOrWhiteSpace(result)) |
|||
{ |
|||
var foldler = Path.GetDirectoryName(result); |
|||
// Directory information for path, or null if path denotes a root directory or is
|
|||
// null. Returns System.String.Empty if path does not contain directory information.
|
|||
if (!string.IsNullOrWhiteSpace(foldler)) |
|||
{ |
|||
if (!Directory.Exists(foldler)) |
|||
{ |
|||
Directory.CreateDirectory(foldler); |
|||
} |
|||
output = new FileStream(result, FileMode.Create); |
|||
} |
|||
} |
|||
return output; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal static class TypeExtesnions |
|||
{ |
|||
private static readonly ConditionalWeakTable<Type, string> s_getTypeNameCache = |
|||
new ConditionalWeakTable<Type, string>(); |
|||
|
|||
public static string GetTypeName(this Type type) |
|||
{ |
|||
if (!s_getTypeNameCache.TryGetValue(type, out var name)) |
|||
{ |
|||
name = type.Name; |
|||
if (Nullable.GetUnderlyingType(type) is Type nullable) |
|||
{ |
|||
name = nullable.Name + "?"; |
|||
} |
|||
else if (type.IsGenericType) |
|||
{ |
|||
var definition = type.GetGenericTypeDefinition(); |
|||
var arguments = type.GetGenericArguments(); |
|||
name = definition.Name.Substring(0, definition.Name.IndexOf('`')); |
|||
name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; |
|||
} |
|||
s_getTypeNameCache.Add(type, name); |
|||
} |
|||
return name; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
using System; |
|||
using System.IO; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal static class VisualExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Render control to the destination stream.
|
|||
/// </summary>
|
|||
/// <param name="source">Control to be rendered.</param>
|
|||
/// <param name="destination">Destination stream.</param>
|
|||
/// <param name="dpi">Dpi quality.</param>
|
|||
public static void RenderTo(this IControl source, Stream destination, double dpi = 96) |
|||
{ |
|||
if (source.TransformedBounds == null) |
|||
{ |
|||
return; |
|||
} |
|||
var rect = source.TransformedBounds.Value.Clip; |
|||
var top = rect.TopLeft; |
|||
var pixelSize = new PixelSize((int)rect.Width, (int)rect.Height); |
|||
var dpiVector = new Vector(dpi, dpi); |
|||
|
|||
// get Visual root
|
|||
var root = (source.VisualRoot |
|||
?? source.GetVisualRoot()) |
|||
as IControl ?? source; |
|||
|
|||
IDisposable? clipSetter = default; |
|||
IDisposable? clipToBoundsSetter = default; |
|||
IDisposable? renderTransformOriginSetter = default; |
|||
IDisposable? renderTransformSetter = default; |
|||
try |
|||
{ |
|||
// Set clip region
|
|||
var clipRegion = new Media.RectangleGeometry(rect); |
|||
clipToBoundsSetter = root.SetValue(Visual.ClipToBoundsProperty, true, BindingPriority.Animation); |
|||
clipSetter = root.SetValue(Visual.ClipProperty, clipRegion, BindingPriority.Animation); |
|||
|
|||
// Translate origin
|
|||
renderTransformOriginSetter = root.SetValue(Visual.RenderTransformOriginProperty, |
|||
new RelativePoint(top, RelativeUnit.Absolute), |
|||
BindingPriority.Animation); |
|||
|
|||
renderTransformSetter = root.SetValue(Visual.RenderTransformProperty, |
|||
new Media.TranslateTransform(-top.X, -top.Y), |
|||
BindingPriority.Animation); |
|||
|
|||
using (var bitmap = new RenderTargetBitmap(pixelSize, dpiVector)) |
|||
{ |
|||
bitmap.Render(root); |
|||
bitmap.Save(destination); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
// Restore values before trasformation
|
|||
renderTransformSetter?.Dispose(); |
|||
renderTransformOriginSetter?.Dispose(); |
|||
clipSetter?.Dispose(); |
|||
clipToBoundsSetter?.Dispose(); |
|||
source?.InvalidateVisual(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
Compat issues with assembly Avalonia.Dialogs: |
|||
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Dialogs.AboutAvaloniaDialog' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract. |
|||
Total Issues: 1 |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue