Browse Source
* [DevTools] Allowed to take a screenshot of the currently selected control. * feat(DevTools): Allowed to customize screenshot root folder. * feat(DevTools): Allowed screenshot filename by convention * fixes(DevTools): Mark Shot as async and catch eventualy RenderTo excepion * fixes: allignment of code comment * fixes: CS0173 when build on linux and macOS * Refactoring Shot method * fixes: typo in DefaultScreenshotRoot * fixes: assembly detection * fixes: code comment * fixes: nullable warnings * fixes: merge issue * fixes: removed unnecessary using * fixes: removed Dispatcher.UIThread.InvokeAsync * fixes: unexpected to the user results after call RenderTo * fixes: screenshot Clip * fixes: typo * fixes: remove workaraund * Apply suggestions from code review * feat(DevTools): Add Shot Button * feat(DevTools): Custom Screenshot handler * fixes(DevTools): Renamed Screenshots.FileHandler to Screenshots.FileConvetionHandler * feat(DevTools): Screenshots.BaseRenderToStreamHandler * feat(DevTools): Screenshots.FilePickerHandler * fixes(DevTools): marked VisualExtensions as internal * fixes(DevTools): Strip out FileConvetionHandler * fixes(DevTools): FilePickerHandler promote Title and ScreenshotsRoot properties to ctor * fixes(DevTools): Remove screenshot HotKey description form status bar * fixes(DevTools): Xml Comment * fixes(DevTools): Moved Screenshot command to File menu * fixes(DevTools): Removed reference to Avalonia.Dialogs Co-authored-by: Giuseppe Lippolis <spaccabit@gmail.com> Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com> Co-authored-by: Max Katz <maxkatz6@outlook.com>pull/7285/head
committed by
GitHub
9 changed files with 295 additions and 2 deletions
@ -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,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,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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue