From acfbfcb6424bf1ec695c43d830b167c9fc574f49 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Fri, 12 Jul 2019 17:04:14 -0700 Subject: [PATCH] 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.) --- SourceGenWPF/DisplayList.cs | 5 + SourceGenWPF/MainController.cs | 45 +++- SourceGenWPF/SourceGenWPF.csproj | 8 + SourceGenWPF/Tests/WpfGui/GenTestRunner.xaml | 1 - SourceGenWPF/Tools/VirtualHexDump.cs | 199 ++++++++++++++++ SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml | 75 ++++++ .../Tools/WpfGui/HexDumpViewer.xaml.cs | 221 ++++++++++++++++++ SourceGenWPF/WpfGui/MainWindow.xaml | 5 +- SourceGenWPF/WpfGui/MainWindow.xaml.cs | 34 +-- 9 files changed, 570 insertions(+), 23 deletions(-) create mode 100644 SourceGenWPF/Tools/VirtualHexDump.cs create mode 100644 SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml create mode 100644 SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml.cs diff --git a/SourceGenWPF/DisplayList.cs b/SourceGenWPF/DisplayList.cs index 360c797..d057ce3 100644 --- a/SourceGenWPF/DisplayList.cs +++ b/SourceGenWPF/DisplayList.cs @@ -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 /// public class DisplayList : IList, IList, INotifyCollectionChanged, INotifyPropertyChanged { diff --git a/SourceGenWPF/MainController.cs b/SourceGenWPF/MainController.cs index 7d860be..a2cf846 100644 --- a/SourceGenWPF/MainController.cs +++ b/SourceGenWPF/MainController.cs @@ -63,6 +63,11 @@ namespace SourceGenWPF { /// private MainWindow mMainWin; + /// + /// Hex dump viewer window. + /// + private Tools.WpfGui.HexDumpViewer mHexDumpDialog; + /// /// List of recently-opened projects. /// @@ -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); } diff --git a/SourceGenWPF/SourceGenWPF.csproj b/SourceGenWPF/SourceGenWPF.csproj index caf86f3..b4c0e62 100644 --- a/SourceGenWPF/SourceGenWPF.csproj +++ b/SourceGenWPF/SourceGenWPF.csproj @@ -78,6 +78,7 @@ GenTestRunner.xaml + AboutBox.xaml @@ -114,6 +115,9 @@ GotoBox.xaml + + HexDumpViewer.xaml + WorkProgress.xaml @@ -257,6 +261,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/SourceGenWPF/Tests/WpfGui/GenTestRunner.xaml b/SourceGenWPF/Tests/WpfGui/GenTestRunner.xaml index 9af7930..e00f739 100644 --- a/SourceGenWPF/Tests/WpfGui/GenTestRunner.xaml +++ b/SourceGenWPF/Tests/WpfGui/GenTestRunner.xaml @@ -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"> diff --git a/SourceGenWPF/Tools/VirtualHexDump.cs b/SourceGenWPF/Tools/VirtualHexDump.cs new file mode 100644 index 0000000..53b5346 --- /dev/null +++ b/SourceGenWPF/Tools/VirtualHexDump.cs @@ -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 { + /// + /// 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. + /// + /// + /// Doing proper data virtualization in WPF is tricky and annoying. We just render on + /// demand and retain the results indefinitely. + /// + public class VirtualHexDump : IList, INotifyCollectionChanged, INotifyPropertyChanged { + /// + /// + /// Data to display. We currently require that the entire file fit in memory, + /// which is reasonable because we impose a 2^24 (16MB) limit. + /// + private byte[] mData; + + /// + /// 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. + /// + 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; + } + + /// + /// Causes all lines to be reformatted. Call this after changing the desired format. + /// Generates a collection-reset event, which may cause loss of selection. + /// + public void Reformat(Formatter newFormat) { + mFormatter = newFormat; + + for (int i = 0; i < mLines.Length; i++) { + mLines[i] = null; + } + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionReset(); + } + + /// + /// Returns the Nth line, generating it if it hasn't been yet. + /// + 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 + } +} diff --git a/SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml b/SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml new file mode 100644 index 0000000..a056347 --- /dev/null +++ b/SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml @@ -0,0 +1,75 @@ + + + + + + Basic ASCII + High/Low ASCII + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml.cs b/SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml.cs new file mode 100644 index 0000000..3e61d2a --- /dev/null +++ b/SourceGenWPF/Tools/WpfGui/HexDumpViewer.xaml.cs @@ -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 { + /// + /// Hex dump viewer. + /// + public partial class HexDumpViewer : Window, INotifyPropertyChanged { + /// + /// Maximum length of data we will display. + /// + public const int MAX_LENGTH = 1 << 24; + + /// + /// ItemsSource for list. + /// + public VirtualHexDump HexDumpLines { get; private set; } + + /// + /// Hex formatter. + /// + private Formatter mFormatter; + + + /// + /// If true, don't include non-ASCII characters in text area. (Without this we might + /// use Unicode bullets or other glyphs for unprintable text.) + /// + 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 + } + + /// + /// Character conversion combo box item. + /// + 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); + } + + /// + /// Sets the scroll position and selection to show the specified range. + /// + /// First offset to show. + /// Last offset to show. + 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 + /// + /// Generates a string for every selected line, then copies the full thing to the + /// clipboard. + /// + 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 + } +} diff --git a/SourceGenWPF/WpfGui/MainWindow.xaml b/SourceGenWPF/WpfGui/MainWindow.xaml index 29e9a8a..34ad23a 100644 --- a/SourceGenWPF/WpfGui/MainWindow.xaml +++ b/SourceGenWPF/WpfGui/MainWindow.xaml @@ -121,6 +121,7 @@ limitations under the License. Ctrl+A + Ctrl+D @@ -203,6 +204,8 @@ limitations under the License. + - + diff --git a/SourceGenWPF/WpfGui/MainWindow.xaml.cs b/SourceGenWPF/WpfGui/MainWindow.xaml.cs index b5330c2..cc14d81 100644 --- a/SourceGenWPF/WpfGui/MainWindow.xaml.cs +++ b/SourceGenWPF/WpfGui/MainWindow.xaml.cs @@ -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) {