1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-06-24 08:29:29 +00:00

Implement Show Hex Dump

Because of the way data virtualization works (or doesn't) in WPF,
this was a whole lot different from WinForms.  What I'm doing is
pretty simple, so I avoided the complexity (and quirks) inherent
to more complete data virtualization solutions.  (All I really want
is to not render the entire thing up front.)
This commit is contained in:
Andy McFadden 2019-07-12 17:04:14 -07:00
parent b1f84864d6
commit acfbfcb642
9 changed files with 570 additions and 23 deletions

View File

@ -44,6 +44,11 @@ namespace SourceGenWPF {
/// NOTE: it may or may not be possible to implement this trivially with an
/// ObservableCollection. At an earlier iteration it wasn't, and I'd like to keep this
/// around even if it is now possible, in case the pendulum swings back the other way.
///
/// Additional reading on data virtualization:
/// https://www.codeproject.com/Articles/34405/WPF-Data-Virtualization?msg=5635751
/// https://web.archive.org/web/20121216034305/http://www.zagstudio.com/blog/498
/// https://web.archive.org/web/20121107200359/http://www.zagstudio.com/blog/378
/// </remarks>
public class DisplayList : IList<DisplayList.FormattedParts>, IList,
INotifyCollectionChanged, INotifyPropertyChanged {

View File

@ -63,6 +63,11 @@ namespace SourceGenWPF {
/// </summary>
private MainWindow mMainWin;
/// <summary>
/// Hex dump viewer window.
/// </summary>
private Tools.WpfGui.HexDumpViewer mHexDumpDialog;
/// <summary>
/// List of recently-opened projects.
/// </summary>
@ -1138,10 +1143,10 @@ namespace SourceGenWPF {
if (mShowAnalyzerOutputDialog != null) {
mShowAnalyzerOutputDialog.Close();
}
#endif
if (mHexDumpDialog != null) {
mHexDumpDialog.Close();
}
#endif
// Discard all project state.
if (mProject != null) {
@ -1371,11 +1376,7 @@ namespace SourceGenWPF {
}
break;
case CodeListColumn.Bytes:
#if false
if (showHexDumpToolStripMenuItem.Enabled) {
ShowHexDump_Click(sender, e);
}
#endif
ShowHexDump();
break;
case CodeListColumn.Flags:
if (CanEditStatusFlags()) {
@ -2210,6 +2211,38 @@ namespace SourceGenWPF {
return -1;
}
public void ShowHexDump() {
if (mHexDumpDialog == null) {
// Create and show modeless dialog. This one is "always on top" by default,
// to allow the user to click around to various points.
mHexDumpDialog = new Tools.WpfGui.HexDumpViewer(mMainWin,
mProject.FileData, mOutputFormatter);
mHexDumpDialog.Closing += (sender, e) => {
Debug.WriteLine("Hex dump dialog closed");
//showHexDumpToolStripMenuItem.Checked = false;
mHexDumpDialog = null;
};
mHexDumpDialog.Topmost = true;
mHexDumpDialog.Show();
}
// Bring it to the front of the window stack. This also transfers focus to the
// window.
mHexDumpDialog.Activate();
// Set the dialog's position.
if (mMainWin.CodeListView_GetSelectionCount() > 0) {
int firstIndex = mMainWin.CodeListView_GetFirstSelectedIndex();
int lastIndex = mMainWin.CodeListView_GetLastSelectedIndex();
// offsets can be < 0 if they've selected EQU statements
int firstOffset = Math.Max(0, CodeLineList[firstIndex].FileOffset);
int lastOffset = Math.Max(firstOffset, CodeLineList[lastIndex].FileOffset +
CodeLineList[lastIndex].OffsetSpan - 1);
mHexDumpDialog.ShowOffsetRange(firstOffset, lastOffset);
}
}
public bool CanUndo() {
return (mProject != null && mProject.CanUndo);
}

View File

@ -78,6 +78,7 @@
<Compile Include="Tests\WpfGui\GenTestRunner.xaml.cs">
<DependentUpon>GenTestRunner.xaml</DependentUpon>
</Compile>
<Compile Include="Tools\VirtualHexDump.cs" />
<Compile Include="WpfGui\AboutBox.xaml.cs">
<DependentUpon>AboutBox.xaml</DependentUpon>
</Compile>
@ -114,6 +115,9 @@
<Compile Include="WpfGui\GotoBox.xaml.cs">
<DependentUpon>GotoBox.xaml</DependentUpon>
</Compile>
<Compile Include="Tools\WpfGui\HexDumpViewer.xaml.cs">
<DependentUpon>HexDumpViewer.xaml</DependentUpon>
</Compile>
<Compile Include="WpfGui\WorkProgress.xaml.cs">
<DependentUpon>WorkProgress.xaml</DependentUpon>
</Compile>
@ -257,6 +261,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Tools\WpfGui\HexDumpViewer.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="WpfGui\WorkProgress.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@ -23,7 +23,6 @@ limitations under the License.
xmlns:local="clr-namespace:SourceGenWPF.Tests.WpfGui"
mc:Ignorable="d"
Title="Source Generation Test"
Icon="/SourceGenWPF;component/Res/SourceGenIcon.ico"
Width="640" Height="480" MinWidth="640" MinHeight="480"
ShowInTaskbar="False" WindowStartupLocation="CenterOwner"
Closing="Window_Closing">

View File

@ -0,0 +1,199 @@
/*
* 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.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Asm65;
namespace SourceGenWPF.Tools {
/// <summary>
/// Generates formatted hex dump lines, and makes them available as a list. The result
/// is suitable for use with WPF ItemsSource. Items are generated on demand, providing
/// data virtualization.
/// </summary>
/// <remarks>
/// Doing proper data virtualization in WPF is tricky and annoying. We just render on
/// demand and retain the results indefinitely.
/// </remarks>
public class VirtualHexDump : IList, INotifyCollectionChanged, INotifyPropertyChanged {
/// <summary>
/// <summary>
/// Data to display. We currently require that the entire file fit in memory,
/// which is reasonable because we impose a 2^24 (16MB) limit.
/// </summary>
private byte[] mData;
/// <summary>
/// Data formatter object.
///
/// There's currently no way to update this after the dialog is opened, which means
/// we won't track changes to hex case preference. I'm okay with that.
/// </summary>
private Formatter mFormatter;
private string[] mLines;
private int mDebugGenLineCount;
public VirtualHexDump(byte[] data, Formatter formatter) {
mData = data;
mFormatter = formatter;
Count = (mData.Length + 15) / 16;
mLines = new string[Count];
mDebugGenLineCount = 0;
}
/// <summary>
/// Causes all lines to be reformatted. Call this after changing the desired format.
/// Generates a collection-reset event, which may cause loss of selection.
/// </summary>
public void Reformat(Formatter newFormat) {
mFormatter = newFormat;
for (int i = 0; i < mLines.Length; i++) {
mLines[i] = null;
}
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionReset();
}
/// <summary>
/// Returns the Nth line, generating it if it hasn't been yet.
/// </summary>
private string GetLine(int index) {
if (mLines[index] == null) {
mLines[index] = mFormatter.FormatHexDump(mData, index * 16);
//if ((++mDebugGenLineCount % 1000) == 0) {
// Debug.WriteLine("DebugGenLineCount: " + mDebugGenLineCount);
//}
}
//Debug.WriteLine("GET LINE " + index + ": " + mLines[index]);
return mLines[index];
}
#region Property / Collection Changed
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
private const string CountString = "Count";
private const string IndexerName = "Item[]";
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
PropertyChanged?.Invoke(this, e);
}
private void OnPropertyChanged(string propertyName) {
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
CollectionChanged?.Invoke(this, e);
}
private void OnCollectionReset() {
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion Property / Collection Changed
#region IList
public bool IsReadOnly => true;
public bool IsFixedSize => true;
public int Count { get; private set; }
public object SyncRoot => throw new NotImplementedException();
public bool IsSynchronized => false;
public object this[int index] {
get => GetLine(index);
set => throw new NotImplementedException();
}
public int Add(object value) {
throw new NotImplementedException();
}
public bool Contains(object value) {
return (IndexOf(value) >= 0);
}
public void Clear() {
throw new NotImplementedException();
}
public int IndexOf(object value) {
//Debug.WriteLine("VHD IndexOf " + value);
// This gets called sometimes when the selection changes, because the selection
// mechanism tracks objects rather than indices. Fortunately we can convert the
// value string to an index by parsing the first six characters.
int offset = Convert.ToInt32(((string)value).Substring(0, 6), 16);
int index = offset / 16;
// Either the object at the target location matches, or it doesn't; no need to
// search. We'll get requests for nonexistent objects after we reformat the
// collection.
//
// Object equality is what's desired; no need for string comparison
if ((object)mLines[index] == value) {
return index;
}
//Debug.WriteLine(" IndexOf not found");
return -1;
}
public void Insert(int index, object value) {
throw new NotImplementedException();
}
public void Remove(object value) {
throw new NotImplementedException();
}
public void RemoveAt(int index) {
throw new NotImplementedException();
}
public void CopyTo(Array array, int index) {
throw new NotImplementedException();
}
public IEnumerator GetEnumerator() {
// Use the indexer, rather than mLines's enumerator, to get on-demand string gen.
for (int i = 0; i < mLines.Length; i++) {
yield return this[i];
}
}
#endregion IList
}
}

View File

@ -0,0 +1,75 @@
<!--
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="SourceGenWPF.Tools.WpfGui.HexDumpViewer"
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:system="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SourceGenWPF.WpfGui"
mc:Ignorable="d"
Title="Hex Dump Viewer"
Icon="/SourceGenWPF;component/Res/SourceGenIcon.ico"
Width="542" Height="600" MinWidth="542" MinHeight="180"
ShowInTaskbar="False" WindowStartupLocation="CenterOwner"
Loaded="Window_Loaded">
<Window.Resources>
<system:String x:Key="str_PlainAscii">Basic ASCII</system:String>
<system:String x:Key="str_HighLowAscii">High/Low ASCII</system:String>
</Window.Resources>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- DataGrid gives us Ctrl+A select-all and Ctrl+C text-copy built in. -->
<DataGrid Name="hexDumpData" Grid.Row="0"
IsReadOnly="True"
ItemsSource="{Binding HexDumpLines}"
FontFamily="{StaticResource GeneralMonoFont}"
SnapsToDevicePixels="True"
GridLinesVisibility="None"
AutoGenerateColumns="False"
HeadersVisibility="Column"
CanUserReorderColumns="False"
CanUserSortColumns="False"
SelectionMode="Extended"
EnableRowVirtualization="True"
ScrollViewer.CanContentScroll="True"
VerticalScrollBarVisibility="Visible">
<DataGrid.Columns>
<DataGridTextColumn Header="Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F ASCII"
Width="491" Binding="{Binding}"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="1" Margin="0,5,0,0" Orientation="Horizontal">
<TextBlock Text="Character conversion:" Margin="0,3,0,0"/>
<ComboBox Name="charConvComboBox" Width="120" Margin="4,0,0,0"
ItemsSource="{Binding CharConvItems}" DisplayMemberPath="Name"
SelectionChanged="CharConvComboBox_SelectionChanged"/>
<CheckBox Content="ASCII-only dump" Margin="16,4,0,0" IsChecked="{Binding AsciiOnlyDump}"/>
<!-- Bind the checkbox directly to the window's Topmost property. -->
<CheckBox Content="Always on top" Margin="16,4,0,0" IsChecked="{Binding Path=Topmost}"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,221 @@
/*
* 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.
*/
using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Asm65;
namespace SourceGenWPF.Tools.WpfGui {
/// <summary>
/// Hex dump viewer.
/// </summary>
public partial class HexDumpViewer : Window, INotifyPropertyChanged {
/// <summary>
/// Maximum length of data we will display.
/// </summary>
public const int MAX_LENGTH = 1 << 24;
/// <summary>
/// ItemsSource for list.
/// </summary>
public VirtualHexDump HexDumpLines { get; private set; }
/// <summary>
/// Hex formatter.
/// </summary>
private Formatter mFormatter;
/// <summary>
/// If true, don't include non-ASCII characters in text area. (Without this we might
/// use Unicode bullets or other glyphs for unprintable text.)
/// </summary>
public bool AsciiOnlyDump {
get { return mAsciiOnlyDump; }
set {
mAsciiOnlyDump = value;
OnPropertyChanged();
ReplaceFormatter();
}
}
private bool mAsciiOnlyDump;
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public enum CharConvMode {
Unknown = 0,
PlainAscii,
HighLowAscii
}
/// <summary>
/// Character conversion combo box item.
/// </summary>
public class CharConvItem {
public string Name { get; private set; }
public CharConvMode Mode { get; private set; }
public CharConvItem(string name, CharConvMode mode) {
Name = name;
Mode = mode;
}
}
public CharConvItem[] CharConvItems { get; private set; }
public HexDumpViewer(Window owner, byte[] data, Formatter formatter) {
InitializeComponent();
Owner = owner;
DataContext = this;
Debug.Assert(data.Length <= MAX_LENGTH);
HexDumpLines = new VirtualHexDump(data, formatter);
mFormatter = formatter;
CharConvItems = new CharConvItem[] {
new CharConvItem((string)FindResource("str_PlainAscii"),
CharConvMode.PlainAscii),
new CharConvItem((string)FindResource("str_HighLowAscii"),
CharConvMode.HighLowAscii),
};
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
// Restore ASCII-only setting.
AsciiOnlyDump = AppSettings.Global.GetBool(AppSettings.HEXD_ASCII_ONLY, false);
// Restore conv mode setting.
CharConvMode mode = (CharConvMode)AppSettings.Global.GetEnum(
AppSettings.HEXD_CHAR_CONV, typeof(CharConvMode), (int)CharConvMode.PlainAscii);
int index = 0;
for (int i = 0; i < CharConvItems.Length; i++) {
if (CharConvItems[i].Mode == mode) {
index = i;
break;
}
}
charConvComboBox.SelectedIndex = index;
}
//private void Window_Closing(object sender, EventArgs e) {
// Debug.WriteLine("Window width: " + ActualWidth);
// Debug.WriteLine("Column width: " + hexDumpData.Columns[0].ActualWidth);
//}
private void CharConvComboBox_SelectionChanged(object sender,
SelectionChangedEventArgs e) {
ReplaceFormatter();
}
private void ReplaceFormatter() {
Formatter.FormatConfig config = mFormatter.Config;
CharConvItem item = (CharConvItem)charConvComboBox.SelectedItem;
if (item == null) {
// initializing
return;
}
switch (item.Mode) {
case CharConvMode.PlainAscii:
config.mHexDumpCharConvMode = Formatter.FormatConfig.CharConvMode.PlainAscii;
break;
case CharConvMode.HighLowAscii:
config.mHexDumpCharConvMode = Formatter.FormatConfig.CharConvMode.HighLowAscii;
break;
default:
Debug.Assert(false);
break;
}
config.mHexDumpAsciiOnly = AsciiOnlyDump;
// Keep app settings up to date.
AppSettings.Global.SetBool(AppSettings.HEXD_ASCII_ONLY, mAsciiOnlyDump);
AppSettings.Global.SetEnum(AppSettings.HEXD_CHAR_CONV, typeof(CharConvMode),
(int)item.Mode);
mFormatter = new Formatter(config);
HexDumpLines.Reformat(mFormatter);
}
/// <summary>
/// Sets the scroll position and selection to show the specified range.
/// </summary>
/// <param name="startOffset">First offset to show.</param>
/// <param name="endOffset">Last offset to show.</param>
public void ShowOffsetRange(int startOffset, int endOffset) {
Debug.WriteLine("HexDumpViewer: show +" + startOffset.ToString("x6") + " - +" +
endOffset.ToString("x6"));
int startLine = startOffset / 16;
int endLine = endOffset / 16;
hexDumpData.SelectedItems.Clear();
for (int i = startLine; i <= endLine; i++) {
hexDumpData.SelectedItems.Add(HexDumpLines[i]);
}
// Make sure it's visible.
hexDumpData.ScrollIntoView(HexDumpLines[endLine]);
hexDumpData.ScrollIntoView(HexDumpLines[startLine]);
}
#if false // DataGrid provides this automatically
/// <summary>
/// Generates a string for every selected line, then copies the full thing to the
/// clipboard.
/// </summary>
private void CopySelectionToClipboard() {
ListView.SelectedIndexCollection indices = hexDumpListView.SelectedIndices;
if (indices.Count == 0) {
Debug.WriteLine("Nothing selected");
return;
}
// Try to make the initial allocation big enough to hold the full thing.
// Each line is currently 73 bytes, plus we throw in a CRLF. Doesn't have to
// be exact. With a 16MB max file size we're creating a ~75MB string for the
// clipboard, which .NET and Win10-64 seem to be able to handle.
StringBuilder sb = new StringBuilder(indices.Count * (73 + 2));
try {
Application.UseWaitCursor = true;
Cursor.Current = Cursors.WaitCursor;
foreach (int index in indices) {
mFormatter.FormatHexDump(mData, index * 16, sb);
sb.Append("\r\n");
}
} finally {
Application.UseWaitCursor = false;
Cursor.Current = Cursors.Arrow;
}
Clipboard.SetText(sb.ToString(), TextDataFormat.Text);
}
#endif
}
}

View File

@ -121,6 +121,7 @@ limitations under the License.
<KeyGesture>Ctrl+A</KeyGesture>
</RoutedUICommand.InputGestures>
</RoutedUICommand>
<RoutedUICommand x:Key="ShowHexDumpCmd" Text="Show Hex Dump"/>
<RoutedUICommand x:Key="ToggleDataScanCmd" Text="Toggle Data Scan">
<RoutedUICommand.InputGestures>
<KeyGesture>Ctrl+D</KeyGesture>
@ -203,6 +204,8 @@ limitations under the License.
<!-- ListView has a built-in Ctrl+A handler; this only fires when codeListView is not in focus -->
<CommandBinding Command="{StaticResource SelectAllCmd}"
CanExecute="IsProjectOpen" Executed="SelectAllCmd_Executed"/>
<CommandBinding Command="{StaticResource ShowHexDumpCmd}"
CanExecute="IsProjectOpen" Executed="ShowHexDumpCmd_Executed"/>
<CommandBinding Command="{StaticResource ToggleDataScanCmd}"
CanExecute="IsProjectOpen" Executed="ToggleDataScanCmd_Executed"/>
<CommandBinding Command="{StaticResource UndoCmd}"
@ -266,7 +269,7 @@ limitations under the License.
<MenuItem Header="Format As Word"/> <!-- Ctrl+W -->
<MenuItem Header="Delete Note/Long Comment"/> <!-- Del -->
<Separator/>
<MenuItem Header="Show Hex Dump"/>
<MenuItem Command="{StaticResource ShowHexDumpCmd}"/>
</MenuItem>
<MenuItem Header="_Tools">
<MenuItem Header="Hex Dump..."/>

View File

@ -935,6 +935,23 @@ namespace SourceGenWPF.WpfGui {
mMainCtrl.EditProjectProperties();
}
private void RecentProjectCmd_Executed(object sender, ExecutedRoutedEventArgs e) {
int recentIndex;
if (e.Parameter is int) {
recentIndex = (int)e.Parameter;
} else if (e.Parameter is string) {
recentIndex = int.Parse((string)e.Parameter);
} else {
throw new Exception("Bad parameter: " + e.Parameter);
}
if (recentIndex < 0 || recentIndex >= MainController.MAX_RECENT_PROJECTS) {
throw new Exception("Bad parameter: " + e.Parameter);
}
Debug.WriteLine("Recent project #" + recentIndex);
mMainCtrl.OpenRecentProject(recentIndex);
}
private void RemoveHintsCmd_Executed(object sender, ExecutedRoutedEventArgs e) {
Debug.WriteLine("remove hints");
mMainCtrl.MarkAsType(CodeAnalysis.TypeHint.NoHint, false);
@ -970,21 +987,8 @@ namespace SourceGenWPF.WpfGui {
Debug.WriteLine("Select All cmd: " + (DateTime.Now - start).TotalMilliseconds + " ms");
}
private void RecentProjectCmd_Executed(object sender, ExecutedRoutedEventArgs e) {
int recentIndex;
if (e.Parameter is int) {
recentIndex = (int)e.Parameter;
} else if (e.Parameter is string) {
recentIndex = int.Parse((string)e.Parameter);
} else {
throw new Exception("Bad parameter: " + e.Parameter);
}
if (recentIndex < 0 || recentIndex >= MainController.MAX_RECENT_PROJECTS) {
throw new Exception("Bad parameter: " + e.Parameter);
}
Debug.WriteLine("Recent project #" + recentIndex);
mMainCtrl.OpenRecentProject(recentIndex);
private void ShowHexDumpCmd_Executed(object sender, ExecutedRoutedEventArgs e) {
mMainCtrl.ShowHexDump();
}
private void ToggleDataScanCmd_Executed(object sender, ExecutedRoutedEventArgs e) {