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 System.IO; |
||||
using MicroComGenerator; |
using MicroCom.CodeGenerator; |
||||
using Nuke.Common; |
using Nuke.Common; |
||||
|
|
||||
partial class Build : NukeBuild |
partial class Build : NukeBuild |
||||
{ |
{ |
||||
Target GenerateCppHeaders => _ => _.Executes(() => |
Target GenerateCppHeaders => _ => _.Executes(() => |
||||
{ |
{ |
||||
var text = File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"); |
var file = MicroComCodeGenerator.Parse( |
||||
var ast = AstParser.Parse(text); |
File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); |
||||
File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", |
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