Skip to content

Commit

Permalink
Try to only keep at most five active png files in the temp directory.…
Browse files Browse the repository at this point in the history
… Added External Viewer command. Adjusted transparency tiling background.
  • Loading branch information
mlptownsend committed Apr 19, 2024
1 parent 2ad2643 commit 70270d8
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 44 deletions.
4 changes: 2 additions & 2 deletions SkiaSharpVisualizer/SkiaSharpVisualizer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyVersion>1.0.2</AssemblyVersion>
<FileVersion>1.0.2</FileVersion>
<AssemblyVersion>1.0.3</AssemblyVersion>
<FileVersion>1.0.3</FileVersion>
<Title>SkiaSharp Visualizer</Title>
<Company>MapLarge</Company>
<PackageProjectUrl>https://github.com/MapLarge/SkiaSharpVisualizer</PackageProjectUrl>
Expand Down
54 changes: 29 additions & 25 deletions SkiaSharpVisualizer/SkiaSharpVisualizerControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
>
<DataTemplate.Resources>
<Style TargetType="Label" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}" />
<Style TargetType="Button" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ButtonStyleKey}}" />
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.TextBoxStyleKey}}" />
<Style TargetType="CheckBox" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.CheckBoxStyleKey}}" />
</DataTemplate.Resources>
Expand Down Expand Up @@ -39,10 +40,11 @@
<TextBox Name="ImageHeight" VerticalAlignment="Center" TextAlignment="Right" IsReadOnly="True" Text="{Binding Path=Height}" Grid.Row="2" Grid.Column="2" />
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<CheckBox Content="_Stretch?" IsChecked="{Binding IsStretched}" />
<CheckBox Margin="5px,0,0,0" Content="_Bordered?" IsChecked="{Binding IsBordered}" />
<CheckBox VerticalAlignment="Center" Content="_Stretch?" IsChecked="{Binding IsStretched}" />
<CheckBox VerticalAlignment="Center" Margin="5px,0,0,0" Content="_Bordered?" IsChecked="{Binding IsBordered}" />
<Button Content="Open In External Viewer" Margin="5px,0,0,0" MinWidth="100px" Command="{Binding OpenExternalCommand}" CommandParameter="{Binding FilePath}" />
</StackPanel>
<Border Margin="0,5,0,5" BorderThickness="2" Padding="0" Grid.Row="4">
<Border Margin="0,5,0,5" BorderThickness="0" Padding="0" Grid.Row="4">
<Border.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Yellow" Offset="0.0" />
Expand All @@ -51,26 +53,6 @@
<GradientStop Color="LimeGreen" Offset="1.0" />
</LinearGradientBrush>
</Border.BorderBrush>
<Border.Background>
<VisualBrush TileMode="Tile" Viewport="0, 0, 16, 16" ViewportUnits="Absolute">
<VisualBrush.Visual>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Grid.Column="0" Width="16" Height="16" Fill="#d7d7d7" />
<Rectangle Grid.Row="0" Grid.Column="1" Width="16" Height="16" Fill="#ffffff" />
<Rectangle Grid.Row="1" Grid.Column="0" Width="16" Height="16" Fill="#ffffff" />
<Rectangle Grid.Row="1" Grid.Column="1" Width="16" Height="16" Fill="#d7d7d7" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Border.Background>
<Border BorderThickness="{Binding BorderThickness}" HorizontalAlignment="Center" VerticalAlignment="Center">
<Border.BorderBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
Expand All @@ -80,8 +62,30 @@
<GradientStop Color="Yellow" Offset="1.0" />
</LinearGradientBrush>
</Border.BorderBrush>
<!-- BitmapScalingMode HighQuality and SnapsToDevicePixels False makes this look nicer w/ differing monitor DPIs / scale percentages -->
<Image Name="Image" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="False" Source="{Binding Path=FilePath}" HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="{Binding ImageStretch}" />
<Border BorderThickness="0" Padding="0" Margin="0">
<Border.Background>
<VisualBrush TileMode="Tile" Viewport="0, 0, 16, 16" ViewportUnits="Absolute">
<VisualBrush.Visual>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Grid.Column="0" Width="16" Height="16" Fill="#d7d7d7" />
<Rectangle Grid.Row="0" Grid.Column="1" Width="16" Height="16" Fill="#ffffff" />
<Rectangle Grid.Row="1" Grid.Column="0" Width="16" Height="16" Fill="#ffffff" />
<Rectangle Grid.Row="1" Grid.Column="1" Width="16" Height="16" Fill="#d7d7d7" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Border.Background>
<!-- BitmapScalingMode HighQuality and SnapsToDevicePixels False makes this look nicer w/ differing monitor DPIs / scale percentages -->
<Image Name="Image" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="False" Source="{Binding Path=FilePath}" HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="{Binding ImageStretch}" />
</Border>
</Border>
</Border>
</Grid>
Expand Down
157 changes: 140 additions & 17 deletions SkiaSharpVisualizer/SkiaSharpVisualizerDataContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
using Microsoft.VisualStudio.Extensibility.UI;
using Microsoft.VisualStudio.RpcContracts.DebuggerVisualizers;
using System;
Expand All @@ -19,6 +20,8 @@ public class SkiaSharpVisualizerDataContext : NotifyPropertyChangedObject, IDisp
public SkiaSharpVisualizerDataContext(VisualizerTarget visualizerTarget) {
this.visualizerTarget = visualizerTarget;
visualizerTarget.StateChanged += this.OnStateChangedAsync;

this.OpenExternalCommand = new OpenExternalCommand(this);
}

[DataMember]
Expand Down Expand Up @@ -68,22 +71,17 @@ public bool IsBordered {
[DataMember]
public int BorderThickness => _isBordered ? 3 : 0;

private async Task OnStateChangedAsync(object? sender, VisualizerTargetStateNotification args) {
var dataSource = await GetRequestAsync(args);
private const int MAXFILEPATHS = 5;
private SortedDictionary<string, string> byteFilePaths = new();
private SortedDictionary<string, DateTimeOffset> byteLastAccess = new();

//This is where we'd delete the previous image if WPF/VS didn't lock it.
var prevFilePath = this.FilePath;
this.FilePath = null;
[DataMember]
public IAsyncCommand OpenExternalCommand { get; }

//There seems to be a bug with the data template and using a BitmapSource from the data context, so we will write the png to a temp file since binding to a url works.
var tmpFilePath = System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), "png");
var pngBase64 = dataSource?.pngBase64;
var pngBytes = !string.IsNullOrWhiteSpace(pngBase64) ? System.Convert.FromBase64String(pngBase64) : Array.Empty<byte>();
await System.IO.File.WriteAllBytesAsync(tmpFilePath, pngBytes);
#if DEBUG
private readonly List<string> failedToDeleteFiles = new();
#endif

this.FilePath = tmpFilePath;
this.Model = dataSource;
}
private async Task<SkiaSharpVisualizerDataSource?> GetRequestAsync(VisualizerTargetStateNotification args) {
switch (args) {
case VisualizerTargetStateNotification.Available:
Expand All @@ -96,14 +94,139 @@ private async Task OnStateChangedAsync(object? sender, VisualizerTargetStateNoti
}
}

private async Task OnStateChangedAsync(object? sender, VisualizerTargetStateNotification args) {
var dataSource = await GetRequestAsync(args);

CleanupUsedFilePaths();

//No data.
var pngBase64 = dataSource?.pngBase64;
if (string.IsNullOrWhiteSpace(pngBase64)) {
ResetBindings();
return;
}

var pngBytes = System.Convert.FromBase64String(pngBase64);
try {
//Is this the same image we've shown already?
if (byteFilePaths.TryGetValue(pngBase64, out var fp) && System.IO.File.Exists(fp) && System.IO.File.ReadAllBytes(fp).SequenceEqual(pngBytes)) {
this.byteLastAccess[pngBase64] = DateTimeOffset.Now;
this.FilePath = fp;
this.Model = dataSource;
return;
}
} catch {
//Ignore any lookup errors.
}

//Using a BitmapSource on the data context is not serializable cross-process, so we will write the png to a temp file since binding to a url works.
try {
var tmpFilePath = System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), "png");
await System.IO.File.WriteAllBytesAsync(tmpFilePath, pngBytes);

this.byteFilePaths[pngBase64] = tmpFilePath;
this.byteLastAccess[pngBase64] = DateTimeOffset.Now;
this.FilePath = tmpFilePath;
this.Model = dataSource;
} catch {
//Something terrible happened.
ResetBindings();
}
}

private void CleanupUsedFilePaths() {
//Once we hit the max limit, remove the oldest tracked file.
if (byteLastAccess.Count < MAXFILEPATHS) {
return;
}

var oldestFile = byteLastAccess.First();
if (!byteFilePaths.TryGetValue(oldestFile.Key, out var filePath)) {
//Shouldn't happen.
byteLastAccess.Remove(oldestFile.Key);
return;
}

byteFilePaths.Remove(oldestFile.Key);
byteLastAccess.Remove(oldestFile.Key);
try {
//Make an attempt to remove the file.
System.IO.File.Delete(filePath);
} catch {
//Ignore IO errors
#if DEBUG
failedToDeleteFiles.Add(filePath);
#endif
}
}
private void ResetBindings() {
this.FilePath = null;
this.Model = null;
}
private void RemoveAllFiles() {
foreach (var kvp in byteFilePaths) {
try {
//Make an attempt to remove the file. VS locks them for a while, so we might not get them all.
System.IO.File.Delete(kvp.Value);
} catch {
//Ignore IO errors
#if DEBUG
failedToDeleteFiles.Add(kvp.Value);
#endif
}
}

this.byteFilePaths.Clear();
this.byteLastAccess.Clear();
}

public void Dispose() {
visualizerTarget.StateChanged -= this.OnStateChangedAsync;
this.visualizerTarget.Dispose();

//This is where we'd delete the previous image if WPF/VS didn't lock it.
var prevFilePath = this.FilePath;
this.FilePath = null;
this.ResetBindings();
this.RemoveAllFiles();
}

}

public class OpenExternalCommand : NotifyPropertyChangedObject, IAsyncCommand {

private bool executeFailed = false;
public bool CanExecute => !executeFailed && !string.IsNullOrWhiteSpace(context.FilePath);

private readonly SkiaSharpVisualizerDataContext context;
public OpenExternalCommand(SkiaSharpVisualizerDataContext context) {
this.context = context;
this.context.PropertyChanged += Context_PropertyChanged;
}

private void Context_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
switch (e.PropertyName) {
case nameof(context.FilePath):
this.RaiseNotifyPropertyChangedEvent("CanExecute");
break;
}
}

public Task ExecuteAsync(object? parameter, IClientContext clientContext, CancellationToken cancellationToken) {
var filePath = parameter as string;
if (string.IsNullOrWhiteSpace(filePath)) {
return Task.CompletedTask;
}

try {
//Need UseShellExecute to run an image file.
var info = new System.Diagnostics.ProcessStartInfo(filePath);
info.UseShellExecute = true;
using var _ = System.Diagnostics.Process.Start(info);
} catch {
//Hopefully doesn't happen.
this.executeFailed = true;
this.RaiseNotifyPropertyChangedEvent("CanExecute");
}
return Task.CompletedTask;
}
}

}

0 comments on commit 70270d8

Please sign in to comment.