Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
32b45de
[DevTools] Allowed to take a screenshot of the currently selected con…
workgroupengineering Sep 28, 2020
5db991c
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jun 11, 2021
f99526f
feat(DevTools): Allowed to customize screenshot root folder.
workgroupengineering Jun 11, 2021
861fdb2
feat(DevTools): Allowed screenshot filename by convention
workgroupengineering Jun 11, 2021
1e2fd87
fixes(DevTools): Mark Shot as async and catch eventualy RenderTo exce…
workgroupengineering Jun 11, 2021
7c85b99
fixes: allignment of code comment
workgroupengineering Jun 11, 2021
5c2e066
fixes: CS0173 when build on linux and macOS
workgroupengineering Jun 11, 2021
af51871
Refactoring Shot method
workgroupengineering Jun 12, 2021
b5f30aa
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jun 12, 2021
25356a9
fixes: typo in DefaultScreenshotRoot
workgroupengineering Jun 12, 2021
dcdbe6e
fixes: assembly detection
workgroupengineering Jun 12, 2021
9cb6c5d
fixes: code comment
workgroupengineering Jun 12, 2021
0c1f6cc
fixes: nullable warnings
workgroupengineering Jun 12, 2021
d683a9d
fixes: merge issue
workgroupengineering Jun 12, 2021
de5a033
fixes: removed unnecessary using
workgroupengineering Jun 12, 2021
b715db1
fixes: removed Dispatcher.UIThread.InvokeAsync
spaccabit Jun 13, 2021
a82b861
fixes: unexpected to the user results after call RenderTo
spaccabit Jun 13, 2021
f00a101
fixes: screenshot Clip
workgroupengineering Jun 15, 2021
73b43d2
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jun 15, 2021
6173eca
fixes: typo
workgroupengineering Jun 15, 2021
e0549a1
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jun 16, 2021
f6e4b29
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jun 16, 2021
a6fe880
fixes: remove workaraund
workgroupengineering Jun 18, 2021
89f9d8d
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jun 18, 2021
5d6efb3
Merge branch 'master' into features/DevTools/Screenshot
workgroupengineering Jul 10, 2021
cd09d9a
Merge branch 'master' into features/DevTools/Screenshot
jmacato Aug 1, 2021
6836f95
Merge remote-tracking branch 'upstream/master' into features/DevTools…
workgroupengineering Aug 23, 2021
5cbb702
Apply suggestions from code review
maxkatz6 Dec 25, 2021
84e511c
fixes(DevTools): merge conflict with master branch
workgroupengineering Dec 27, 2021
8b810f2
feat(DevTools): Add Shot Button
workgroupengineering Dec 27, 2021
0422187
feat(DevTools): Custom Screenshot handler
workgroupengineering Dec 27, 2021
7540112
fixes(DevTools): Renamed Screenshots.FileHandler to Screenshots.FileC…
workgroupengineering Dec 27, 2021
1007ab2
feat(DevTools): Screenshots.BaseRenderToStreamHandler
workgroupengineering Dec 27, 2021
a7c3562
feat(DevTools): Screenshots.FilePickerHandler
workgroupengineering Dec 27, 2021
c36944a
fixes(DevTools): merge conflict with master branch
workgroupengineering Dec 27, 2021
5d246e7
fixes(DevTools): marked VisualExtensions as internal
workgroupengineering Dec 28, 2021
d7826e9
fixes(DevTools): Strip out FileConvetionHandler
workgroupengineering Dec 28, 2021
771238b
fixes(DevTools): FilePickerHandler promote Title and ScreenshotsRoot …
workgroupengineering Dec 28, 2021
3591daf
fixes(DevTools): Remove screenshot HotKey description form status bar
workgroupengineering Dec 28, 2021
4414a43
fixes(DevTools): Xml Comment
workgroupengineering Dec 28, 2021
08236df
fixes(DevTools): Moved Screenshot command to File menu
workgroupengineering Dec 28, 2021
98a8564
fixes(DevTools): Removed reference to Avalonia.Dialogs
workgroupengineering Dec 29, 2021
a5a3ddb
Merge branch 'master' into features/DevTools/Screenshot
maxkatz6 Dec 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
workgroupengineering marked this conversation as resolved.
Show resolved Hide resolved
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
Expand Down
17 changes: 17 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Convetions.cs
Original file line number Diff line number Diff line change
@@ -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 =>
workgroupengineering marked this conversation as resolved.
Show resolved Hide resolved
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create),
"Screenshots");

public static IScreenshotHandler DefaultScreenshotHandler { get; } =
new Screenshots.FilePickerHandler();
}
}
10 changes: 9 additions & 1 deletion src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia.Input;
using System;
using Avalonia.Input;

namespace Avalonia.Diagnostics
{
Expand Down Expand Up @@ -28,5 +29,12 @@ public class DevToolsOptions
/// 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 changes: 17 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,7 +29,9 @@ internal class MainViewModel : ViewModelBase, IDisposable
private bool _freezePopups;
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
private IScreenshotHandler? _screenshotHandler;
private bool _showPropertyType;

public MainViewModel(AvaloniaObject root)
{
_root = root;
Expand Down Expand Up @@ -285,9 +290,38 @@ public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
}

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);
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
//TODO: Notify error
}
}
}

public void SetOptions(DevToolsOptions options)
{
_screenshotHandler = options.ScreenshotHandler;
StartupScreenIndex = options.StartupScreenIndex;
}

Expand Down
30 changes: 30 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}">
Expand Down
4 changes: 3 additions & 1 deletion src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 71 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}