Browse Source

[DevTools] Allowed to take a screenshot of the currently selected control. (#4762)

* [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
workgroupengineering 5 years ago
committed by GitHub
parent
commit
eab9659487
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      src/Avalonia.Diagnostics/Diagnostics/Convetions.cs
  2. 10
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  3. 17
      src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs
  4. 29
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs
  5. 85
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
  6. 34
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  7. 30
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  8. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
  9. 71
      src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs

17
src/Avalonia.Diagnostics/Diagnostics/Convetions.cs

@ -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();
}
}

10
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -1,4 +1,5 @@
using Avalonia.Input;
using System;
using Avalonia.Input;
namespace Avalonia.Diagnostics
{
@ -28,5 +29,12 @@ namespace Avalonia.Diagnostics
/// Get or set the startup screen index where the DevTools window will be displayed.
/// </summary>
public int? StartupScreenIndex { get; set; }
/// <summary>
/// Allow to customizze SreenshotHandler
/// </summary>
/// <remarks>Default handler is <see cref="Screenshots.FilePickerHandler"/></remarks>
public IScreenshotHandler ScreenshotHandler { get; set; }
= Convetions.DefaultScreenshotHandler;
}
}

17
src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs

@ -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);
}
}

29
src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs

@ -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();
}
}
}
}

85
src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs

@ -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;
}
}
}

34
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,8 +1,11 @@
using System;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq;
@ -26,7 +29,9 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _freezePopups;
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
private IScreenshotHandler? _screenshotHandler;
private bool _showPropertyType;
public MainViewModel(AvaloniaObject root)
{
_root = root;
@ -285,9 +290,38 @@ namespace Avalonia.Diagnostics.ViewModels
}
public int? StartupScreenIndex { get; private set; } = default;
[DependsOn(nameof(TreePageViewModel.SelectedNode))]
[DependsOn(nameof(Content))]
bool CanShot(object? parameter)
{
return Content is TreePageViewModel tree
&& tree.SelectedNode != null
&& tree.SelectedNode.Visual is VisualTree.IVisual visual
&& visual.VisualRoot != null;
}
async void Shot(object? parameter)
{
if ((Content as TreePageViewModel)?.SelectedNode?.Visual is IControl control
&& _screenshotHandler is { }
)
{
try
{
await _screenshotHandler.Take(control);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
//TODO: Notify error
}
}
}
public void SetOptions(DevToolsOptions options)
{
_screenshotHandler = options.ScreenshotHandler;
StartupScreenIndex = options.StartupScreenIndex;
}

30
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -6,6 +6,36 @@
<Menu>
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}" />
<MenuItem Header="Screenshot" Command="{Binding Shot}" HotKey="F8">
<MenuItem.Icon>
<Image>
<DrawingImage>
<DrawingGroup>
<GeometryDrawing Geometry="F1 M 13.4533,17.56C 13.4533,19.8827 15.344,21.772 17.6653,21.772L 17.6653,21.772C 19.988,21.772 21.8773,19.8827 21.8773,17.56L 21.8773,17.56C 21.8773,15.2373 19.988,13.348 17.6653,13.348L 17.6653,13.348C 15.344,13.348 13.4533,15.2373 13.4533,17.56 Z ">
<GeometryDrawing.Brush>
<RadialGradientBrush Center="0.245696,0.288009" GradientOrigin="0.245696,0.288009" Radius="0.499952">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF878A8C" Offset="0" />
<GradientStop Color="#FF544A4C" Offset="0.991379" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Geometry="F1 M 13.332,6.22803L 10.2227,9.72668L 8.49866,9.72668L 8.49866,7.56136L 5.33333,7.56136L 5.33333,9.72668L 3.33333,9.72668L 3.33333,24.3947L 13.1213,24.3947C 14.424,25.264 15.9853,25.772 17.6653,25.772L 17.6653,25.772C 19.344,25.772 20.9067,25.264 22.2094,24.3947L 28.6667,24.3947L 28.6667,9.72668L 24.944,9.72668L 21.8333,6.22803M 12.12,17.56C 12.12,14.5013 14.608,12.0147 17.6653,12.0147L 17.6653,12.0147C 20.7227,12.0147 23.2107,14.5013 23.2107,17.56L 23.2107,17.56C 23.2107,20.6174 20.7227,23.104 17.6653,23.104L 17.6653,23.104C 14.608,23.104 12.12,20.6174 12.12,17.56 Z ">
<GeometryDrawing.Brush>
<RadialGradientBrush Center="0.196943,0.216757" GradientOrigin="0.196943,0.216757" Radius="0.44654">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF87898C" Offset="0" />
<GradientStop Color="#FF544A4C" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage>
</Image>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_View">
<MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml

@ -18,6 +18,8 @@
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
</Window.Styles>
<Window.KeyBindings>
<KeyBinding Gesture="F8" Command="{Binding Shot}"/>
</Window.KeyBindings>
<views:MainView/>
</Window>

71
src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs

@ -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…
Cancel
Save