mirror of
https://github.com/fadden/6502bench.git
synced 2024-11-25 14:34:27 +00:00
Add Export feature to visualization editor
It's nice to be able to save images from the visualization editor for display elsewhere. This can be done during HTML export, but that's inconvenient when you just want one image, and doesn't allow the output size to be specified. This change adds an Export button to the Edit Visualization dialog. The current bitmap, wireframe, or wireframe animation can be saved to a GIF image. A handful of sizes can be selected from a pop-up menu.
This commit is contained in:
parent
b43fd07688
commit
c47beffcee
@ -17,10 +17,10 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace CommonWPF {
|
||||
/// <summary>
|
||||
@ -263,4 +263,38 @@ namespace CommonWPF {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BitmapSource extensions.
|
||||
/// </summary>
|
||||
public static class BitmapSourceExtensions {
|
||||
/// <summary>
|
||||
/// Creates a scaled copy of a BitmapSource. Only scales up, using nearest-neighbor.
|
||||
/// </summary>
|
||||
public static BitmapSource CreateScaledCopy(this BitmapSource src, int scale) {
|
||||
// Simple approach always does a "blurry" scale.
|
||||
//return new TransformedBitmap(src, new ScaleTransform(scale, scale));
|
||||
|
||||
// Adapted from https://weblogs.asp.net/bleroy/resizing-images-from-the-server-using-wpf-wic-instead-of-gdi
|
||||
// (found via https://stackoverflow.com/a/25570225/294248)
|
||||
BitmapScalingMode scalingMode = BitmapScalingMode.NearestNeighbor;
|
||||
|
||||
int newWidth = (int)src.Width * scale;
|
||||
int newHeight = (int)src.Height * scale;
|
||||
|
||||
var group = new DrawingGroup();
|
||||
RenderOptions.SetBitmapScalingMode(group, scalingMode);
|
||||
group.Children.Add(new ImageDrawing(src,
|
||||
new Rect(0, 0, newWidth, newHeight)));
|
||||
var targetVisual = new DrawingVisual();
|
||||
var targetContext = targetVisual.RenderOpen();
|
||||
targetContext.DrawDrawing(group);
|
||||
var target = new RenderTargetBitmap(
|
||||
newWidth, newHeight, 96, 96, PixelFormats.Default);
|
||||
targetContext.Close();
|
||||
target.Render(targetVisual);
|
||||
var targetFrame = BitmapFrame.Create(target);
|
||||
return targetFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ limitations under the License.
|
||||
<system:String x:Key="str_FileFilterCs">C# Source Files (*.cs)|*.cs</system:String>
|
||||
<system:String x:Key="str_FileFilterCsv">CSV files (*.csv)|*.csv</system:String>
|
||||
<system:String x:Key="str_FileFilterDis65">SourceGen projects (*.dis65)|*.dis65</system:String>
|
||||
<system:String x:Key="str_FileFilterGif">GIF images (*.gif)|*.gif</system:String>
|
||||
<system:String x:Key="str_FileFilterHtml">HTML files (*.html)|*.html</system:String>
|
||||
<system:String x:Key="str_FileFilterSgec">SGEC files (*.sgec)|*.sgec</system:String>
|
||||
<system:String x:Key="str_FileFilterSym65">SourceGen symbols (*.sym65)|*.sym65</system:String>
|
||||
|
@ -159,6 +159,8 @@ namespace SourceGen.Res {
|
||||
(string)Application.Current.FindResource("str_FileFilterCsv");
|
||||
public static string FILE_FILTER_DIS65 =
|
||||
(string)Application.Current.FindResource("str_FileFilterDis65");
|
||||
public static string FILE_FILTER_GIF =
|
||||
(string)Application.Current.FindResource("str_FileFilterGif");
|
||||
public static string FILE_FILTER_HTML =
|
||||
(string)Application.Current.FindResource("str_FileFilterHtml");
|
||||
public static string FILE_FILTER_SGEC =
|
||||
|
@ -101,6 +101,9 @@ the former a text entry field that accepts decimal and hexadecimal values.
|
||||
The range of allowable values is shown to the right of the entry field.
|
||||
If you enter an invalid value, the parameter description will turn red.</p>
|
||||
|
||||
<p>The "Export" button at the top right can be used to save a copy of
|
||||
the bitmap or wireframe rendering with the current parameters.</p>
|
||||
|
||||
<h5>Wireframe View Controls</h5>
|
||||
|
||||
<p>The wireframe generator may offer the choice of perspective vs.
|
||||
|
@ -155,6 +155,9 @@
|
||||
<Compile Include="WpfGui\Export.xaml.cs">
|
||||
<DependentUpon>Export.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="WpfGui\ExportVisualization.xaml.cs">
|
||||
<DependentUpon>ExportVisualization.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="WpfGui\FindBox.xaml.cs">
|
||||
<DependentUpon>FindBox.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@ -357,6 +360,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="WpfGui\ExportVisualization.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="WpfGui\FindBox.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
@ -17,11 +17,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
using CommonUtil;
|
||||
using PluginCommon;
|
||||
|
||||
|
@ -105,6 +105,7 @@ limitations under the License.
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="130"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
@ -119,6 +120,9 @@ limitations under the License.
|
||||
HorizontalAlignment="Left"
|
||||
ItemsSource="{Binding VisualizationList}" DisplayMemberPath="VisDescriptor.UiName"
|
||||
SelectionChanged="VisComboBox_SelectionChanged"/>
|
||||
<Button Grid.Column="2" Grid.Row="0" Width="70" Height="20"
|
||||
HorizontalAlignment="Right" Content="Export..."
|
||||
Click="ExportButton_Click"/>
|
||||
|
||||
<TextBlock Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right" Margin="0,0,4,0"
|
||||
Text="Tag:"/>
|
||||
|
@ -374,6 +374,24 @@ namespace SourceGen.WpfGui {
|
||||
}
|
||||
|
||||
private void OkButton_Click(object sender, RoutedEventArgs e) {
|
||||
NewVis = PrepResultVis();
|
||||
sLastVisIdent = NewVis.VisGenIdent;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void ExportButton_Click(object sender, RoutedEventArgs e) {
|
||||
Visualization vis = PrepResultVis();
|
||||
ExportVisualization dlg = new ExportVisualization(this, vis, mWireObj,
|
||||
"vis" + mSetOffset.ToString("x6")); // tag may not be valid filename, use offset
|
||||
dlg.ShowDialog();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the visualization that we will return as the result object, based on
|
||||
/// the currents state of the controls.
|
||||
/// </summary>
|
||||
/// <returns>New Visualization or VisWireframeAnimation object.</returns>
|
||||
private Visualization PrepResultVis() {
|
||||
VisualizationItem item = (VisualizationItem)visComboBox.SelectedItem;
|
||||
Debug.Assert(item != null);
|
||||
|
||||
@ -384,25 +402,25 @@ namespace SourceGen.WpfGui {
|
||||
|
||||
string trimTag = Visualization.TrimAndValidateTag(TagString, out bool isTagValid);
|
||||
Debug.Assert(isTagValid);
|
||||
Visualization vis;
|
||||
if (isWireframe && IsWireframeAnimated) {
|
||||
NewVis = new VisWireframeAnimation(trimTag, item.VisDescriptor.Ident, valueDict,
|
||||
vis = new VisWireframeAnimation(trimTag, item.VisDescriptor.Ident, valueDict,
|
||||
mOrigVis, mWireObj);
|
||||
} else {
|
||||
NewVis = new Visualization(trimTag, item.VisDescriptor.Ident, valueDict, mOrigVis);
|
||||
vis = new Visualization(trimTag, item.VisDescriptor.Ident, valueDict, mOrigVis);
|
||||
}
|
||||
|
||||
// Set the thumbnail image.
|
||||
if (isWireframe) {
|
||||
Debug.Assert(mWireObj != null);
|
||||
NewVis.CachedImage = Visualization.GenerateWireframeImage(mWireObj,
|
||||
vis.CachedImage = Visualization.GenerateWireframeImage(mWireObj,
|
||||
Visualization.THUMBNAIL_DIM, valueDict);
|
||||
} else {
|
||||
Debug.Assert(mThumbnail != null);
|
||||
NewVis.CachedImage = mThumbnail;
|
||||
vis.CachedImage = mThumbnail;
|
||||
}
|
||||
|
||||
sLastVisIdent = NewVis.VisGenIdent;
|
||||
DialogResult = true;
|
||||
return vis;
|
||||
}
|
||||
|
||||
private ReadOnlyDictionary<string, object> CreateVisGenParams(bool includeWire) {
|
||||
|
60
SourceGen/WpfGui/ExportVisualization.xaml
Normal file
60
SourceGen/WpfGui/ExportVisualization.xaml
Normal file
@ -0,0 +1,60 @@
|
||||
<!--
|
||||
Copyright 2019 faddenSoft
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<Window x:Class="SourceGen.WpfGui.ExportVisualization"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:SourceGen.WpfGui"
|
||||
mc:Ignorable="d"
|
||||
Title="Export Visualization"
|
||||
SizeToContent="WidthAndHeight" ResizeMode="NoResize"
|
||||
ShowInTaskbar="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock Text="This will save the bitmap to an image file."
|
||||
Visibility="{Binding IsBitmap, Converter={StaticResource BoolToVis}}"
|
||||
d:IsHidden="True"/>
|
||||
<TextBlock Text="This will render the wireframe and save the image to a file."
|
||||
Visibility="{Binding IsWireframe, Converter={StaticResource BoolToVis}}"
|
||||
d:IsHidden="False"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,8">
|
||||
<TextBlock Text="Scale output to:" Margin="0,3,8,0"/>
|
||||
<ComboBox Name="sizeComboBox" Width="100" ItemsSource="{Binding OutputSizeList}"/>
|
||||
</StackPanel>
|
||||
<!-- TODO(maybe): bitmap: transparency modifier? -->
|
||||
<!-- TODO(maybe): wireframe: combo box for color mode -->
|
||||
<!-- TODO(maybe): wireframe: checkbox to disable anti-aliasing -->
|
||||
</StackPanel>
|
||||
|
||||
<DockPanel Grid.Column="0" Grid.Row="1" Margin="0,8,0,0" LastChildFill="False">
|
||||
<Button DockPanel.Dock="Right" Content="Cancel" Width="70" Margin="8,0,0,0" IsCancel="True"/>
|
||||
<Button DockPanel.Dock="Right" Grid.Column="1" Content="Save" Width="70"
|
||||
IsDefault="True" Click="SaveButton_Click"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</Window>
|
175
SourceGen/WpfGui/ExportVisualization.xaml.cs
Normal file
175
SourceGen/WpfGui/ExportVisualization.xaml.cs
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2020 faddenSoft
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using CommonWPF;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace SourceGen.WpfGui {
|
||||
/// <summary>
|
||||
/// Export an image from the visualization editor.
|
||||
/// </summary>
|
||||
public partial class ExportVisualization : Window, INotifyPropertyChanged {
|
||||
private Visualization mVis;
|
||||
private WireframeObject mWireObj;
|
||||
private string mFileNameBase;
|
||||
|
||||
// INotifyPropertyChanged implementation
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public bool IsBitmap {
|
||||
get { return mIsBitmap; }
|
||||
set { mIsBitmap = value; OnPropertyChanged(); }
|
||||
}
|
||||
public bool IsWireframe {
|
||||
get { return !mIsBitmap; }
|
||||
}
|
||||
private bool mIsBitmap;
|
||||
|
||||
/// <summary>
|
||||
/// Item for output size combo box.
|
||||
/// </summary>
|
||||
public class OutputSize {
|
||||
public int Width { get; private set; }
|
||||
public int Height { get; private set; }
|
||||
public override string ToString() {
|
||||
return Width + "x" + Height;
|
||||
}
|
||||
|
||||
public OutputSize(int width, int height) {
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of output sizes, for combo box.
|
||||
/// </summary>
|
||||
public List<OutputSize> OutputSizeList { get; private set; }
|
||||
|
||||
|
||||
public ExportVisualization(Window owner, Visualization vis, WireframeObject wireObj,
|
||||
string fileNameBase) {
|
||||
InitializeComponent();
|
||||
Owner = owner;
|
||||
DataContext = this;
|
||||
|
||||
mVis = vis;
|
||||
mWireObj = wireObj;
|
||||
mFileNameBase = fileNameBase;
|
||||
|
||||
OutputSizeList = new List<OutputSize>();
|
||||
|
||||
// Normally, bitmap and wireframe visualizations don't really differ, because
|
||||
// we're just working off the cached image rendering. It matters for us though,
|
||||
// so we need to see if a wireframe-only parameter exists.
|
||||
bool isWireframe = (vis is VisWireframeAnimation) ||
|
||||
vis.VisGenParams.ContainsKey(VisWireframeAnimation.P_IS_ANIMATED);
|
||||
IsBitmap = !isWireframe;
|
||||
|
||||
if (isWireframe) {
|
||||
int dim = 64;
|
||||
while (dim <= 1024) {
|
||||
OutputSizeList.Add(new OutputSize(dim, dim));
|
||||
dim *= 2;
|
||||
}
|
||||
} else {
|
||||
int baseWidth = (int)vis.CachedImage.Width;
|
||||
int baseHeight = (int)vis.CachedImage.Height;
|
||||
// ensure there's at least one entry, then add other options
|
||||
OutputSizeList.Add(new OutputSize(baseWidth, baseHeight));
|
||||
int mult = 2;
|
||||
while (baseWidth * mult < 2048 && baseHeight * mult < 2048) {
|
||||
OutputSizeList.Add(new OutputSize(baseWidth * mult, baseHeight * mult));
|
||||
mult *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
sizeComboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs e) {
|
||||
SaveFileDialog fileDlg = new SaveFileDialog() {
|
||||
Filter = Res.Strings.FILE_FILTER_GIF + "|" + Res.Strings.FILE_FILTER_ALL,
|
||||
FilterIndex = 1,
|
||||
ValidateNames = true,
|
||||
AddExtension = true,
|
||||
FileName = mFileNameBase + ".gif"
|
||||
};
|
||||
if (fileDlg.ShowDialog() != true) {
|
||||
return;
|
||||
}
|
||||
string pathName = Path.GetFullPath(fileDlg.FileName);
|
||||
Debug.WriteLine("Save path: " + pathName);
|
||||
|
||||
try {
|
||||
OutputSize item = (OutputSize)sizeComboBox.SelectedItem;
|
||||
if (mVis is VisWireframeAnimation) {
|
||||
Debug.Assert(item.Width == item.Height);
|
||||
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
|
||||
((VisWireframeAnimation)mVis).EncodeGif(encoder, item.Width);
|
||||
|
||||
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
|
||||
encoder.Save(stream, out int dispWidth, out int dispHeight);
|
||||
}
|
||||
} else {
|
||||
BitmapSource outImage;
|
||||
if (IsBitmap) {
|
||||
int scale = item.Width / (int)mVis.CachedImage.Width;
|
||||
Debug.Assert(scale >= 1);
|
||||
if (scale == 1) {
|
||||
outImage = mVis.CachedImage;
|
||||
} else {
|
||||
outImage = mVis.CachedImage.CreateScaledCopy(scale);
|
||||
}
|
||||
} else {
|
||||
Debug.Assert(item.Width == item.Height);
|
||||
outImage = Visualization.GenerateWireframeImage(mWireObj,
|
||||
item.Width, mVis.VisGenParams);
|
||||
}
|
||||
|
||||
GifBitmapEncoder encoder = new GifBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(outImage));
|
||||
|
||||
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
|
||||
encoder.Save(stream);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Error handling is a little sloppy, but this shouldn't fail often.
|
||||
MessageBox.Show(ex.Message, Res.Strings.ERR_FILE_GENERIC_CAPTION,
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// After successful save, close dialog box.
|
||||
DialogResult = true;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user