1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-25 14:34:27 +00:00

Add visualization thumbnails

Thumbnails are now visible in the main list and in the visualization
set editor.  They're generated on first need, and regenerated when
the set of plugins changes.

Added a checkerboard background for the visualization editor bitmap
preview.  (It looks all official now.)
This commit is contained in:
Andy McFadden 2019-12-03 14:34:45 -08:00
parent 125080dbda
commit 2f56c56e5c
15 changed files with 244 additions and 66 deletions

View File

@ -39,10 +39,13 @@ namespace CommonUtil {
/// <summary>
/// Compares two Dictionaries to see if their contents are equal. Key and value types
/// must have correctly-implemented equality checks.
/// must have correctly-implemented equality checks. (I contend this works incorrectly
/// for float -- 5.0f is equal to the integer 5.)
/// </summary>
/// <remarks>
/// https://stackoverflow.com/q/3804367/294248
///
/// TODO: make this work right for float/int comparisons
/// </remarks>
/// <typeparam name="TKey">Dictionary key type.</typeparam>
/// <typeparam name="TValue">Dictionary value type.</typeparam>

View File

@ -29,15 +29,20 @@ namespace PluginCommon {
string Identifier { get; }
/// <summary>
/// Prepares the plugin for action. Called at the start of every code analysis pass.
/// Prepares the plugin for action. Called at the start of every code analysis pass
/// and when generating visualization images.
///
/// In the current implementation, the file data will be the same every time,
/// because it doesn't change after the project is opened. However, this could
/// change if we add a descramble feature.
/// change if we add a descramble feature. The IApplication and AddressTranslate
/// references will likely change between invocations.
///
/// This may be called even when the plugin won't be asked to do anything. Avoid
/// performing expensive operations here.
/// </summary>
/// <param name="appRef">Reference to application interface.</param>
/// <param name="fileData">65xx code and data.</param>
/// <param name="addrMap">Mapping between offsets and addresses.</param>
/// <param name="addrTrans">Mapping between offsets and addresses.</param>
void Prepare(IApplication appRef, byte[] fileData, AddressTranslate addrTrans);
/// <summary>
@ -236,12 +241,13 @@ namespace PluginCommon {
/// <summary>
/// Interfaces provided by the application for use by plugins. An IApplication instance
/// is passed to the plugin as an argument Prepare().
/// is passed to the plugin as an argument to Prepare().
/// </summary>
public interface IApplication {
/// <summary>
/// Sends a debug message to the application. This can be useful when debugging scripts.
/// (Use DEBUG > Show Analyzer Output to view it.)
/// (For example, DEBUG > Show Analyzer Output shows output generated while performing
/// code analysis.)
/// </summary>
/// <param name="msg">Message to send.</param>
void DebugLog(string msg);

View File

@ -22,6 +22,18 @@ limitations under the License.
<ResourceDictionary>
<FontFamily x:Key="GeneralMonoFont">Consolas</FontFamily>
<LinearGradientBrush x:Key="BitmapBackground" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#707070" Offset="0.0"/>
<GradientStop Color="#f0f0f0" Offset="1.0"/>
</LinearGradientBrush>
<!-- https://stackoverflow.com/a/47049174/294248 -->
<DrawingBrush x:Key="CheckerBackground" TileMode="Tile" Viewport="0,0,16,16" ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<GeometryDrawing Geometry="M0,0 H1 V1 H2 V2 H1 V1 H0Z" Brush="LightGray"/>
</DrawingBrush.Drawing>
</DrawingBrush>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Res/Strings.xaml"/>
</ResourceDictionary.MergedDictionaries>

View File

@ -2204,11 +2204,11 @@ namespace SourceGen {
}
break;
case UndoableChange.ChangeType.SetProjectProperties: {
bool needExternalFileReload = !CommonUtil.Container.StringListEquals(
bool needPlatformSymReload = !CommonUtil.Container.StringListEquals(
((ProjectProperties)oldValue).PlatformSymbolFileIdentifiers,
((ProjectProperties)newValue).PlatformSymbolFileIdentifiers,
null /*StringComparer.InvariantCulture*/);
needExternalFileReload |= !CommonUtil.Container.StringListEquals(
bool needExtScriptReload = !CommonUtil.Container.StringListEquals(
((ProjectProperties)oldValue).ExtensionScriptFileIdentifiers,
((ProjectProperties)newValue).ExtensionScriptFileIdentifiers,
null);
@ -2226,7 +2226,7 @@ namespace SourceGen {
Debug.WriteLine("Replacing CPU def object");
UpdateCpuDef();
if (needExternalFileReload) {
if (needPlatformSymReload || needExtScriptReload) {
string errMsgs = LoadExternalFiles();
// TODO(someday): if the plugin failed to compile, we will have
@ -2237,6 +2237,14 @@ namespace SourceGen {
// "reload all external files and plugins" command, which might
// run through here.)
}
if (needExtScriptReload) {
// Clear all the Visualization thumbnails. We don't do GUI
// stuff in here, so this just sets a flag.
foreach (KeyValuePair<int, VisualizationSet> kvp
in VisualizationSets) {
kvp.Value.RefreshNeeded();
}
}
}
// ignore affectedOffsets
Debug.Assert(uc.ReanalysisRequired ==

View File

@ -20,6 +20,7 @@ using System.Collections.Specialized;
using System.Diagnostics;
using System.ComponentModel;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SourceGen {
/// <summary>
@ -354,10 +355,13 @@ namespace SourceGen {
public string Operand { get; private set; }
public string Comment { get; private set; }
public bool IsLongComment { get; private set; }
public bool IsVisualizationSet { get; private set; }
public bool HasBackgroundColor { get; private set; }
public Brush BackgroundBrush { get; private set; }
public bool IsVisualizationSet { get; private set; }
public Visualization[] VisualizationSet { get; private set; }
// Set to true if we want to highlight the address and label fields.
public bool HasAddrLabelHighlight { get; private set; }
@ -452,8 +456,9 @@ namespace SourceGen {
public static FormattedParts CreateVisualizationSet(VisualizationSet visSet) {
FormattedParts parts = new FormattedParts();
if (visSet.Count == 0) {
// not expected
parts.Comment = "!EMPTY!";
// should not happen
parts.Comment = "!EMPTY VSET!";
parts.IsLongComment = true;
} else {
string fmt;
if (visSet.Count == 1) {
@ -461,10 +466,10 @@ namespace SourceGen {
} else {
fmt = Res.Strings.VIS_SET_MULTIPLE_FMT;
}
parts.Comment = string.Format(fmt, visSet[0].Tag, visSet.Count - 1);
}
// TODO(xyzzy): show image thumbnails
parts.Comment = string.Format(fmt, "Bitmap", visSet[0].Tag, visSet.Count - 1);
parts.VisualizationSet = visSet.ToArray();
parts.IsVisualizationSet = true;
}
return parts;
}

View File

@ -983,6 +983,10 @@ namespace SourceGen {
mReanalysisTimer.StartTask("Generate DisplayList");
CodeLineList.GenerateAll();
mReanalysisTimer.EndTask("Generate DisplayList");
mReanalysisTimer.StartTask("Refresh Visualization thumbnails");
VisualizationSet.RefreshAllThumbnails(mProject);
mReanalysisTimer.EndTask("Refresh Visualization thumbnails");
}
#endregion Project management

View File

@ -175,6 +175,6 @@ limitations under the License.
<system:String x:Key="str_TitleNewProject">[new project]</system:String>
<system:String x:Key="str_TitleReadOnly">*READ-ONLY*</system:String>
<system:String x:Key="str_Unset">[unset]</system:String>
<system:String x:Key="str_VisSetMultipleFmt">Vis: {0} (+{1} more)</system:String>
<system:String x:Key="str_VisSetSingleFmt">Vis: {0}</system:String>
<system:String x:Key="str_VisSetMultipleFmt">{0}: {1} (+{2} more)</system:String>
<system:String x:Key="str_VisSetSingleFmt">{0}: {1}</system:String>
</ResourceDictionary>

View File

@ -121,8 +121,8 @@ namespace RuntimeData.Apple {
bool isColor, isFirstOdd;
offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
byteWidth = Util.GetFromObjDict(parms, P_BYTE_WIDTH, 0); // width ignoring colStride
height = Util.GetFromObjDict(parms, P_HEIGHT, 0);
byteWidth = Util.GetFromObjDict(parms, P_BYTE_WIDTH, 1); // width ignoring colStride
height = Util.GetFromObjDict(parms, P_HEIGHT, 1);
colStride = Util.GetFromObjDict(parms, P_COL_STRIDE, 0);
rowStride = Util.GetFromObjDict(parms, P_ROW_STRIDE, 0);
isColor = Util.GetFromObjDict(parms, P_IS_COLOR, true);

View File

@ -18,10 +18,17 @@ using System.Collections.Generic;
using System.Text;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CommonUtil;
using PluginCommon;
namespace SourceGen {
/// <summary>
/// Graphical visualization object. Useful for displaying 2D bitmaps and 3D objects.
///
/// Treat this as generally immutable, i.e. don't modify VisGenParams. The CachedImage
/// field is mutable.
/// </summary>
public class Visualization {
/// <summary>
/// Unique user-specified tag. Contents are arbitrary, but may not be empty.
@ -39,13 +46,19 @@ namespace SourceGen {
public Dictionary<string, object> VisGenParams { get; private set; }
/// <summary>
/// Cached reference to thumbnail.
/// Cached reference to 2D image, useful for thumbnails. Not serialized.
/// </summary>
/// <remarks>
/// Because the underlying data never changes, we only need to regenerate the
/// thumbnail if the set of active plugins changes.
/// image if the set of active plugins changes.
/// </remarks>
private BitmapSource Thumbnail { get; set; } // TODO - 64x64(?) bitmap
public BitmapSource CachedImage { get; set; }
/// <summary>
/// Image to show when things are broken.
/// </summary>
public static readonly BitmapImage BROKEN_IMAGE =
new BitmapImage(new Uri("pack://application:,,,/Res/RedX.png"));
/// <summary>
@ -59,6 +72,19 @@ namespace SourceGen {
Tag = tag;
VisGenIdent = visGenIdent;
VisGenParams = visGenParams;
CachedImage = BROKEN_IMAGE;
}
/// <summary>
/// Updates the cached thumbnail image.
/// </summary>
/// <param name="vis2d">Visualization, or null to clear the thumbnail.</param>
public void SetThumbnail(IVisualization2d vis2d) {
if (vis2d == null) {
CachedImage = BROKEN_IMAGE;
} else {
CachedImage = ConvertToBitmapSource(vis2d);
}
}
/// <summary>
@ -110,31 +136,6 @@ namespace SourceGen {
return image;
}
/// <summary>
/// Finds a plugin that provides the named visualization generator.
/// </summary>
/// <param name="proj">Project with script manager.</param>
/// <param name="visGenIdent">Visualization generator identifier.</param>
/// <returns>A plugin that matches, or null if none found.</returns>
public static IPlugin_Visualizer FindPluginByVisGenIdent(DisasmProject proj,
string visGenIdent, out VisDescr visDescr) {
List<IPlugin> plugins = proj.GetActivePlugins();
foreach (IPlugin chkPlug in plugins) {
if (!(chkPlug is IPlugin_Visualizer)) {
continue;
}
IPlugin_Visualizer vplug = (IPlugin_Visualizer)chkPlug;
foreach (VisDescr descr in vplug.GetVisGenDescrs()) {
if (descr.Ident == visGenIdent) {
visDescr = descr;
return vplug;
}
}
}
visDescr = null;
return null;
}
public override string ToString() {
return "[Vis: " + Tag + " (" + VisGenIdent + ") count=" + VisGenParams.Count + "]";
@ -147,8 +148,8 @@ namespace SourceGen {
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) {
return false; // one is null
}
// All fields must be equal.
if (a.Tag != b.Tag || a.VisGenIdent != b.VisGenIdent || a.Thumbnail != b.Thumbnail) {
// All fields must be equal (but we ignore CachedImage).
if (a.Tag != b.Tag || a.VisGenIdent != b.VisGenIdent) {
return false;
}
// Compare the vis gen parameter lists.
@ -167,7 +168,7 @@ namespace SourceGen {
return obj is Visualization && this == (Visualization)obj;
}
public override int GetHashCode() {
// TODO(maybe): hash code should include up VisGenParams items
// TODO(maybe): hash code should factor in VisGenParams items
return Tag.GetHashCode() ^ VisGenIdent.GetHashCode() ^ VisGenParams.Count;
}
}

View File

@ -16,13 +16,16 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using PluginCommon;
namespace SourceGen {
/// <summary>
/// Ordered list of visualization objects.
/// </summary>
/// <remarks>
/// Right now the only thing separating this from a plain List<> is the operator== stuff.
/// There's not much separating this from a plain List<>, except perhaps the operator== stuff.
/// </remarks>
public class VisualizationSet : IEnumerable<Visualization> {
/// <summary>
@ -74,6 +77,122 @@ namespace SourceGen {
mList.Remove(vis);
}
public Visualization[] ToArray() {
Visualization[] arr = new Visualization[mList.Count];
for (int i = 0; i < mList.Count; i++) {
arr[i] = mList[i];
}
return arr;
}
#region Image generation
private class ScriptSupport : MarshalByRefObject, PluginCommon.IApplication {
public ScriptSupport() { }
public void DebugLog(string msg) {
Debug.WriteLine("Vis plugin: " + msg);
}
public bool SetOperandFormat(int offset, DataSubType subType, string label) {
throw new InvalidOperationException();
}
public bool SetInlineDataFormat(int offset, int length, DataType type,
DataSubType subType, string label) {
throw new InvalidOperationException();
}
}
/// <summary>
/// Informs all list elements that a refresh is needed. Call this when the set of active
/// plugins changes. The actual refresh will happen later.
/// </summary>
public void RefreshNeeded() {
foreach (Visualization vis in mList) {
vis.SetThumbnail(null);
}
}
/// <summary>
/// Attempts to refresh broken thumbnails across all visualization sets in the project.
/// </summary>
/// <param name="project">Project reference.</param>
public static void RefreshAllThumbnails(DisasmProject project) {
ScriptSupport iapp = null;
List<IPlugin> plugins = null;
foreach (KeyValuePair<int, VisualizationSet> kvp in project.VisualizationSets) {
VisualizationSet visSet = kvp.Value;
foreach (Visualization vis in visSet) {
if (vis.CachedImage != Visualization.BROKEN_IMAGE) {
continue;
}
Debug.WriteLine("Vis needs refresh: " + vis.Tag);
if (plugins == null) {
plugins = project.GetActivePlugins();
}
IPlugin_Visualizer vplug = FindPluginByVisGenIdent(plugins,
vis.VisGenIdent, out VisDescr visDescr);
if (vplug == null) {
Debug.WriteLine("Unable to referesh " + vis.Tag + ": plugin not found");
continue;
}
if (iapp == null) {
// Prep the plugins on first need.
iapp = new ScriptSupport();
project.PrepareScripts(iapp);
}
IVisualization2d vis2d;
try {
vis2d = vplug.Generate2d(visDescr, vis.VisGenParams);
if (vis2d == null) {
Debug.WriteLine("Vis generator returned null");
}
} catch (Exception ex) {
Debug.WriteLine("Vis generation failed: " + ex);
vis2d = null;
}
if (vis2d != null) {
Debug.WriteLine(" Rendered thumbnail: " + vis.Tag);
vis.SetThumbnail(vis2d);
}
}
}
if (iapp != null) {
project.UnprepareScripts();
}
}
/// <summary>
/// Finds a plugin that provides the named visualization generator.
/// </summary>
/// <param name="plugins">List of plugins, from project ScriptManager.</param>
/// <param name="visGenIdent">Visualization generator identifier.</param>
/// <returns>A plugin that matches, or null if none found.</returns>
private static IPlugin_Visualizer FindPluginByVisGenIdent(List<IPlugin> plugins,
string visGenIdent, out VisDescr visDescr) {
foreach (IPlugin chkPlug in plugins) {
if (!(chkPlug is IPlugin_Visualizer)) {
continue;
}
IPlugin_Visualizer vplug = (IPlugin_Visualizer)chkPlug;
foreach (VisDescr descr in vplug.GetVisGenDescrs()) {
if (descr.Ident == visGenIdent) {
visDescr = descr;
return vplug;
}
}
}
visDescr = null;
return null;
}
#endregion Image generation
public override string ToString() {
return "[VS: " + mList.Count + " items]";

View File

@ -92,16 +92,28 @@ See also https://github.com/fadden/DisasmUiTest
<GridViewColumn DisplayMemberBinding="{Binding Attr}" Width="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}
}, Path=View.Columns[4].ActualWidth}"/>
<!-- This column holds the long comment. There's no easy way to set its width, so we
have to let the main window take care of that. (It's tempting to just make its width
very large, but that causes the GridView contents to horizontally scroll independently
of the GridView header when you reach the edge of the "normal" column set.) -->
<!-- This column holds the visualization set. -->
<GridViewColumn Header="(visualization set)" Width="{Binding LongCommentWidth}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Comment}" VerticalAlignment="Center"/>
<Button Width="64" Height="64" Content="Whee"/>
<ItemsControl ItemsSource="{Binding VisualizationSet}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" Margin="8,0,0,0"
Background="{StaticResource BitmapBackground}">
<Image Width="64" Height="64" Source="{Binding CachedImage}"
RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>

View File

@ -127,7 +127,7 @@ limitations under the License.
<Border Grid.Row="1" BorderThickness="1" HorizontalAlignment="Center"
BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}"
Background="LightGray">
Background="{StaticResource CheckerBackground}">
<Image Name="previewImage" Width="320" Height="320" Source="/Res/RedX.png"
RenderOptions.BitmapScalingMode="NearestNeighbor"/>
<!--<Button Width="400" Height="400" Content="Test"/>-->

View File

@ -141,10 +141,9 @@ namespace SourceGen.WpfGui {
private ScriptSupport mScriptSupport;
/// <summary>
/// URI for the image we show when we fail to generate a visualization.
/// Image we show when we fail to generate a visualization.
/// </summary>
private static BitmapImage sBadParamsImage =
new BitmapImage(new Uri("pack://application:,,,/Res/RedX.png"));
private static BitmapImage sBadParamsImage = Visualization.BROKEN_IMAGE;
/// <summary>
@ -259,6 +258,7 @@ namespace SourceGen.WpfGui {
string trimTag = Visualization.TrimAndValidateTag(TagString, out bool isTagValid);
Debug.Assert(isTagValid);
NewVis = new Visualization(trimTag, item.VisDescriptor.Ident, valueDict);
NewVis.CachedImage = (BitmapSource)previewImage.Source;
DialogResult = true;
}

View File

@ -23,7 +23,7 @@ limitations under the License.
xmlns:local="clr-namespace:SourceGen.WpfGui"
mc:Ignorable="d"
Title="Edit Visualization Set"
Width="500" Height="400" ResizeMode="NoResize"
Width="560" Height="400" ResizeMode="NoResize"
ShowInTaskbar="False" WindowStartupLocation="CenterOwner"
Closing="Window_Closing">
@ -47,7 +47,7 @@ limitations under the License.
IsReadOnly="True"
FontFamily="{StaticResource GeneralMonoFont}"
SnapsToDevicePixels="True"
GridLinesVisibility="Vertical"
HorizontalGridLinesBrush="#FF7F7F7F"
VerticalGridLinesBrush="#FF7F7F7F"
AutoGenerateColumns="False"
HeadersVisibility="Column"
@ -64,7 +64,18 @@ limitations under the License.
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}"
Color="{x:Static SystemColors.HighlightTextColor}"/>
</DataGrid.Resources>
<!-- ItemsSource is a list of Visualization -->
<DataGrid.Columns>
<DataGridTemplateColumn Header="Img" Width="56">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="0" Background="{StaticResource BitmapBackground}">
<Image Source="{Binding CachedImage}" Width="48" Height="48"
RenderOptions.BitmapScalingMode="NearestNeighbor"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Tag" Width="155" Binding="{Binding Tag}"/>
<DataGridTextColumn Header="Visualization Generator" Width="220" Binding="{Binding VisGenIdent}"/>
</DataGrid.Columns>

View File

@ -22,12 +22,9 @@ using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Asm65;
using PluginCommon;
using SourceGen.Sandbox;
namespace SourceGen.WpfGui {
/// <summary>