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 4 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
46 changes: 46 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Convetions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.VisualTree;

namespace Avalonia.Diagnostics
{
static class Convetions
{
public static string DefaultScreenshotRoot =>
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create),
"ScreenShot");

/// <summary>
/// Return the path of the screenshot folder according to the rules indicated in issue <see href="https://github.com/AvaloniaUI/Avalonia/issues/4743">GH-4743</see>
/// </summary>
/// <param name="control"></param>
/// <returns>full file path</returns>
public static Func<IControl, string,string> DefaultScreenshotFileNameConvention = (control,screenshotRoot) =>
workgroupengineering marked this conversation as resolved.
Show resolved Hide resolved
{
IVisual root = control.VisualRoot is null
? control
: control.VisualRoot ?? control.GetVisualRoot();
var rootType = root.GetType();
var windowName = rootType.Name;
if (root is IControl rc && !string.IsNullOrWhiteSpace(rc.Name))
{
windowName = rc.Name;
}

var assembly = Assembly.GetExecutingAssembly();
var appName = Application.Current?.Name
?? assembly.GetCustomAttribute<AssemblyProductAttribute>()?.Product
?? assembly.GetName().Name;
var appVerions = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion
?? assembly.GetCustomAttribute<AssemblyVersionAttribute>().Version;
var folder = System.IO.Path.Combine(screenshotRoot
, appName
, appVerions
, windowName);

return System.IO.Path.Combine(folder
, $"{DateTime.Now:yyyyMMddhhmmssfff}.png");
};
}
}
5 changes: 4 additions & 1 deletion src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Avalonia.Diagnostics
{
public static class DevTools
{
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly Dictionary<TopLevel, MainWindow> s_open =
new Dictionary<TopLevel, MainWindow>();

public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
Expand Down Expand Up @@ -53,6 +54,8 @@ public static IDisposable Open(TopLevel root, DevToolsOptions options)
Height = options.Size.Height,
};

window.SetOptions(options);

window.Closed += DevToolsClosed;
s_open.Add(root, window);

Expand Down
16 changes: 15 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 All @@ -22,5 +23,18 @@ public class DevToolsOptions
/// Gets or sets the initial size of the DevTools window. The default value is 1280x720.
/// </summary>
public Size Size { get; set; } = new Size(1280, 720);


/// <summary>
/// Get or sets the root folder where screeshot well be stored.
/// The default root folder is MyPictures/ScreenShot.
/// </summary>
public string? ScreenshotRoot { get; set; }

/// <summary>
/// Get or sets conventin for screenshot fileName.
///For known default screen shot file name convection see <see href="https://github.com/AvaloniaUI/Avalonia/issues/4743">GH-4743</see>.
/// </summary>
public Func<Avalonia.Controls.IControl, string, string>? ScreenshotFileNameConvention { get; set; }
}
}
117 changes: 98 additions & 19 deletions src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using System;
using System.ComponentModel;

using System.Reactive.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading;

namespace Avalonia.Diagnostics.ViewModels
Expand All @@ -22,6 +26,9 @@ internal class MainViewModel : ViewModelBase, IDisposable
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
private IDisposable _selectedNodeChanged;
private string _screenshotRoot;
private Func<IControl, string, string> _getScreenshotFileName;

#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
Expand All @@ -46,7 +53,7 @@ public bool ShouldVisualizeMarginPadding
get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}

public bool ShouldVisualizeDirtyRects
{
get => _shouldVisualizeDirtyRects;
Expand Down Expand Up @@ -90,27 +97,43 @@ public ViewModelBase Content
// [MemberNotNull(nameof(_content))]
private set
{
if (_content is TreePageViewModel oldTree &&
value is TreePageViewModel newTree &&
oldTree?.SelectedNode?.Visual is IControl control)
TreePageViewModel oldTree = _content as TreePageViewModel;
TreePageViewModel newTree = value as TreePageViewModel;
if (oldTree != null)
{
// HACK: We want to select the currently selected control in the new tree, but
// to select nested nodes in TreeView, currently the TreeView has to be able to
// expand the parent nodes. Because at this point the TreeView isn't visible,
// this will fail unless we schedule the selection to run after layout.
DispatcherTimer.RunOnce(
() =>
{
try
_selectedNodeChanged?.Dispose();
_selectedNodeChanged = null;
}

if (newTree != null)
{
if (oldTree != null &&
oldTree?.SelectedNode?.Visual is IControl control)
{
// HACK: We want to select the currently selected control in the new tree, but
// to select nested nodes in TreeView, currently the TreeView has to be able to
// expand the parent nodes. Because at this point the TreeView isn't visible,
// this will fail unless we schedule the selection to run after layout.
DispatcherTimer.RunOnce(
() =>
{
newTree.SelectControl(control);
}
catch { }
},
TimeSpan.FromMilliseconds(0),
DispatcherPriority.ApplicationIdle);
try
{
newTree.SelectControl(control);
}
catch { }
},
TimeSpan.FromMilliseconds(0),
DispatcherPriority.ApplicationIdle);
}
_selectedNodeChanged = Observable
.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
x => newTree.PropertyChanged += x,
x => newTree.PropertyChanged -= x
).Subscribe(arg => RaisePropertyChanged(nameof(TreePageViewModel.SelectedNode)));
}


RaiseAndSetIfChanged(ref _content, value);
}
}
Expand Down Expand Up @@ -181,6 +204,7 @@ public void Dispose()
{
KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;
_pointerOverSubscription.Dispose();
_selectedNodeChanged?.Dispose();
_logicalTree.Dispose();
_visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false;
Expand Down Expand Up @@ -213,5 +237,60 @@ public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
tree.SelectControl(control);
}
}

[DependsOn(nameof(TreePageViewModel.SelectedNode))]
[DependsOn(nameof(Content))]
bool CanShot(object? paramter)
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
{
return Content is TreePageViewModel tree
&& tree.SelectedNode != null
&& tree.SelectedNode.Visual != null
&& tree.SelectedNode.Visual.VisualRoot != null;
}

void Shot(object? parameter)
{
// This is a workaround because MethodToCommand does not support the asynchronous method.
workgroupengineering marked this conversation as resolved.
Show resolved Hide resolved
Task.Factory.StartNew(arg =>
{
if (arg is IControl control)
{
try
{
var filePath = _getScreenshotFileName(control, _screenshotRoot);
var folder = System.IO.Path.GetDirectoryName(filePath);
if (System.IO.Directory.Exists(folder) == false)
{
System.IO.Directory.CreateDirectory(folder);
}

var output = new System.IO.FileStream(filePath, System.IO.FileMode.Create);
Dispatcher.UIThread.Post(() =>
{
control.RenderTo(output);
output.Dispose();
}
);

}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
//TODO: Notify error
}

}
}, (Content as TreePageViewModel)?.SelectedNode?.Visual);
}

public void SetOptions(DevToolsOptions options)
{
_screenshotRoot = string.IsNullOrWhiteSpace(options.ScreenshotRoot)
? Convetions.DefaultScreenshotRoot
: options.ScreenshotRoot!;

_getScreenshotFileName = options.ScreenshotFileNameConvention
?? Convetions.DefaultScreenshotFileNameConvention;
}
}
}
2 changes: 2 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0">
<StackPanel Spacing="4" Orientation="Horizontal">
<TextBlock>Hold F8 to take a screenshot.</TextBlock>
<Separator Width="8"/>
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
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>
3 changes: 3 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,8 @@ private void RawKeyDown(RawKeyEventArgs e)
}

private void RootClosed(object? sender, EventArgs e) => Close();

public void SetOptions(DevToolsOptions options) =>
(DataContext as MainViewModel)?.SetOptions(options);
}
}
63 changes: 63 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.IO;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;

namespace Avalonia.Diagnostics
{
static class VisualExtensions
{
/// <summary>
/// Rendered control to stream
/// </summary>
/// <param name="source">the control I want to render in the stream</param>
/// <param name="destination">destination destina</param>
/// <param name="dpi">(optional) dpi quality default 96</param>
public static void RenderTo(this IControl source, Stream destination, double dpi = 96)
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
{
var rect = source.Bounds;
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;

// Backup current vaules
var oldgeometry = root.Clip;
var oldClipToBounds = root.ClipToBounds;
var oldRenderTransformOrigin = root.RenderTransformOrigin;
var oldrenderTransform = root.RenderTransform;
workgroupengineering marked this conversation as resolved.
Show resolved Hide resolved

try
{
// Translate coordinate clinet to root
var top = source.TranslatePoint(new Point(0,0), root);
var bottomRight = source.TranslatePoint(rect.BottomRight, root);

// Set clip region
var clipRegion = new Media.RectangleGeometry(new Rect(top.Value, bottomRight.Value));
root.ClipToBounds = true;
root.Clip = clipRegion;
// Traslate origin
root.RenderTransformOrigin = new RelativePoint(top.Value, RelativeUnit.Absolute);
root.RenderTransform = new Media.TranslateTransform(-top.Value.X, -top.Value.Y);
using (var bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
{
bitmap.Render(root);
bitmap.Save(destination);
}

}
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
finally
{
// Restore current vaules
root.ClipToBounds = oldClipToBounds;
root.Clip = oldgeometry;
root.RenderTransformOrigin = oldRenderTransformOrigin;
root.RenderTransform = oldrenderTransform;
}
}
}
}