/*
* Copyright 2018 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.Diagnostics;
using System.Text;
using System.Windows.Forms;
using Asm65;
using CommonWinForms;
namespace SourceGen.Tools {
///
/// Display a hex dump.
///
public partial class HexDumpViewer : Form {
///
/// Maximum length of data we will display.
///
public const int MAX_LENGTH = 1 << 24;
///
/// Character conversion mode. The enum must match the items in the combo box.
///
private enum CharConvMode {
PlainAscii = 0,
HighLowAscii
}
///
/// 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;
///
/// If true, don't include non-ASCII characters in text area.
///
private bool mAsciiOnlyDump;
///
/// Subscribe to this to be notified when the dialog closes.
///
public event WindowClosing OnWindowClosing;
public delegate void WindowClosing(object sender, EventArgs e);
public HexDumpViewer(byte[] data, Formatter formatter) {
InitializeComponent();
hexDumpListView.SetDoubleBuffered(true);
Debug.Assert(data.Length <= MAX_LENGTH);
mData = data;
mFormatter = formatter;
hexDumpListView.VirtualListSize = (mData.Length + 15) / 16;
}
private void HexDumpViewer_Load(object sender, EventArgs e) {
topMostCheckBox.Checked = TopMost;
// Configure ASCII-only mode. Note this causes the CheckedChange callback to
// fire, which sets the field and replaces the formatter.
bool asciiOnly = AppSettings.Global.GetBool(AppSettings.HEXD_ASCII_ONLY, false);
asciiOnlyCheckBox.Checked = asciiOnly;
// Just save and restore the combo box index. This might come up wrong after
// an upgrade that shuffles the options, but doing it right isn't worth the effort.
int charConv = AppSettings.Global.GetInt(AppSettings.HEXD_CHAR_CONV, 0);
if (charConv >= 0 && charConv <= charConvComboBox.Items.Count) {
charConvComboBox.SelectedIndex = charConv;
}
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData) {
if (keyData == (Keys.Control | Keys.A)) {
hexDumpListView.SelectAll();
return true;
} else if (keyData == (Keys.Control | Keys.C)) {
CopySelectionToClipboard();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void asciiOnlyCheckBox_CheckedChanged(object sender, EventArgs e) {
mAsciiOnlyDump = asciiOnlyCheckBox.Checked;
AppSettings.Global.SetBool(AppSettings.HEXD_ASCII_ONLY, mAsciiOnlyDump);
ReplaceFormatter();
InvalidateListView();
}
private void topMostCheckBox_CheckedChanged(object sender, EventArgs e) {
TopMost = topMostCheckBox.Checked;
}
private void charConvComboBox_SelectedIndexChanged(object sender, EventArgs e) {
Debug.WriteLine("charConvCombBox selected: " + charConvComboBox.SelectedIndex);
AppSettings.Global.SetInt(AppSettings.HEXD_CHAR_CONV, charConvComboBox.SelectedIndex);
ReplaceFormatter();
InvalidateListView();
}
private void HexDumpViewer_FormClosed(object sender, FormClosedEventArgs e) {
if (OnWindowClosing != null) {
OnWindowClosing(this, e);
}
}
///
/// Replaces the Formatter with a new one, using the current dialog configuration.
///
private void ReplaceFormatter() {
Formatter.FormatConfig config = mFormatter.Config;
config.mHexDumpAsciiOnly = mAsciiOnlyDump;
CharConvMode mode = (CharConvMode)charConvComboBox.SelectedIndex;
switch (mode) {
case CharConvMode.PlainAscii:
config.mHexDumpCharConvMode = Formatter.FormatConfig.CharConvMode.PlainAscii;
break;
case CharConvMode.HighLowAscii:
config.mHexDumpCharConvMode = Formatter.FormatConfig.CharConvMode.HighLowAscii;
break;
case (CharConvMode)(-1):
// this happens during dialog init, before combo box is configured
break;
default:
Debug.Assert(false);
break;
}
mFormatter = new Formatter(config);
}
///
/// 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);
}
///
/// Sets the scroll position 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;
// TODO(someday): instead of selecting the lines, highlight the individual
// bytes. This requires an owner-drawn ListView.
hexDumpListView.SelectedIndices.Clear();
for (int i = startLine; i <= endLine; i++) {
hexDumpListView.SelectedIndices.Add(i);
}
hexDumpListView.EnsureVisible(endLine);
hexDumpListView.EnsureVisible(startLine);
}
#region Virtual List View
// Using a virtual list view means we're allocating objects frequently when the
// list is being scrolled, but get to avoid massive trauma when opening a large file,
// and don't have to turn a 16MB file into a 74+MB string collection.
///
/// Cache of previously-constructed ListViewItems. The ListView will request items
/// continuously as they are moused-over, so this is fairly important.
///
private ListViewItem[] mItemCache;
private int mItemCacheFirst;
private void hexDumpListView_RetrieveVirtualItem(object sender,
RetrieveVirtualItemEventArgs e) {
// Is item cached?
if (mItemCache != null && e.ItemIndex >= mItemCacheFirst &&
e.ItemIndex < mItemCacheFirst + mItemCache.Length) {
// Yes, return existing item.
e.Item = mItemCache[e.ItemIndex - mItemCacheFirst];
} else {
// No, create item.
e.Item = CreateListViewItem(e.ItemIndex);
}
}
private void hexDumpListView_CacheVirtualItems(object sender,
CacheVirtualItemsEventArgs e) {
if (mItemCache != null && e.StartIndex >= mItemCacheFirst &&
e.EndIndex <= mItemCacheFirst + mItemCache.Length) {
// Already have this span cached.
return;
}
// Discard old cache, create new one, populate it.
mItemCacheFirst = e.StartIndex;
int len = e.EndIndex - e.StartIndex + 1; // end is inclusive
mItemCache = new ListViewItem[len];
for (int i = 0; i < len; i++) {
mItemCache[i] = CreateListViewItem(e.StartIndex + i);
}
}
private ListViewItem CreateListViewItem(int index) {
string fmtd = mFormatter.FormatHexDump(mData, index * 16);
return new ListViewItem(fmtd);
}
///
/// Invalidates the contents of the list view, forcing a redraw. Useful when
/// the desired output format changes.
///
private void InvalidateListView() {
hexDumpListView.BeginUpdate();
mItemCache = null;
mItemCacheFirst = -1;
hexDumpListView.EndUpdate();
}
#endregion Virtual List View
}
}