More HexEdit work

This commit is contained in:
miguel
2018-05-10 23:14:56 -04:00
parent 33502735d3
commit 11b4e45920
2 changed files with 186 additions and 32 deletions

View File

@@ -2,21 +2,43 @@
// HexView.cs: A hexadecimal viewer
//
// TODO:
// - Support an operation to switch between hex and values
// - Tab perhaps to switch?
// - Support nibble-based navigation
// - Support editing, perhaps via list of changes?
// - Support selection with highlighting
// - Redraw should support just repainted affected region
// - Process Key needs to just queue affected region for cursor changes (as we repaint the text)
// - Support searching and highlighting of the search result
// - Support PageUp/PageDown/Home/End
//
using System;
using System.Collections.Generic;
using System.IO;
namespace Terminal.Gui {
/// <summary>
/// An Hex viewer an editor view over a System.IO.Stream
/// </summary>
/// <remarks>
/// <para>
/// This provides a hex editor on top of a seekable stream with the left side showing an hex
/// dump of the values in the stream and the right side showing the contents (filterd to
/// non-control sequence ascii characters).
/// </para>
/// <para>
/// Users can switch from one side to the other by using the tab key.
/// </para>
/// <para>
/// If you want to enable editing, set the AllowsEdits property, once that is done, the user
/// can make changes to the hexadecimal values of the stream. Any changes done are tracked
/// in the Edits property which is a sorted dictionary indicating the position where the
/// change was made and the new value. A convenience ApplyEdits method can be used to c
/// apply the methods to the underlying stream.
/// </para>
/// <para>
/// It is possible to control the first byte shown by setting the DisplayStart property
/// to the offset that you want to start viewing.
/// </para>
/// </remarks>
public class HexView : View {
SortedDictionary<long, byte> edits = new SortedDictionary<long, byte> ();
Stream source;
long displayStart, position;
bool firstNibble, leftSide;
/// <summary>
/// Creates and instance of the HexView that will render a seekable stream in hex on the allocated view region.
@@ -27,6 +49,8 @@ namespace Terminal.Gui {
Source = source;
this.source = source;
CanFocus = true;
leftSide = true;
firstNibble = true;
}
/// <summary>
@@ -86,6 +110,24 @@ namespace Terminal.Gui {
}
}
//
// This is used to support editing of the buffer on a peer List<>,
// the offset corresponds to an offset relative to DisplayStart, and
// the buffer contains the contents of a screenful of data, so the
// offset is relative to the buffer.
//
//
byte GetData (byte [] buffer, int offset, out bool edited)
{
var pos = DisplayStart + offset;
if (edits.TryGetValue (pos, out byte v)) {
edited = true;
return v;
}
edited = false;
return buffer [offset];
}
public override void Redraw (Rect region)
{
Attribute currentAttribute;
@@ -100,7 +142,14 @@ namespace Terminal.Gui {
Source.Position = displayStart;
var n = source.Read (data, 0, data.Length);
int activeColor = ColorScheme.HotNormal;
int trackingColor = ColorScheme.HotFocus;
for (int line = 0; line < frame.Height; line++) {
var lineRect = new Rect (0, line, frame.Width, 1);
if (!region.Contains (lineRect))
continue;
Move (0, line);
Driver.SetAttribute (ColorScheme.HotNormal);
Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * 4));
@@ -111,28 +160,30 @@ namespace Terminal.Gui {
for (int block = 0; block < nblocks; block++) {
for (int b = 0; b < 4; b++) {
var offset = (line * nblocks * 4) + block * 4 + b;
if (offset + displayStart == position)
SetAttribute (ColorScheme.HotNormal);
bool edited;
var value = GetData (data, offset, out edited);
if (offset + displayStart == position || edited)
SetAttribute (leftSide ? activeColor : trackingColor);
else
SetAttribute (ColorScheme.Normal);
Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2} ", data [offset]));
Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2}", value));
SetAttribute (ColorScheme.Normal);
Driver.AddRune (' ');
}
Driver.AddStr (block + 1 == nblocks ? " " : "| ");
}
for (int bitem = 0; bitem < nblocks * 4; bitem++) {
var offset = line * nblocks * 4 + bitem;
if (offset + displayStart == position)
SetAttribute (ColorScheme.HotFocus);
else
SetAttribute (ColorScheme.Normal);
bool edited = false;
Rune c = ' ';
if (offset >= n)
c = ' ';
else {
var b = data [offset];
var b = GetData (data, offset, out edited);
if (b < 32)
c = '.';
else if (b > 127)
@@ -140,6 +191,11 @@ namespace Terminal.Gui {
else
c = b;
}
if (offset + displayStart == position || edited)
SetAttribute (leftSide ? trackingColor : activeColor);
else
SetAttribute (ColorScheme.Normal);
Driver.AddRune (c);
}
}
@@ -154,6 +210,9 @@ namespace Terminal.Gui {
}
/// <summary>
/// Positions the cursor based for the hex view
/// </summary>
public override void PositionCursor ()
{
var delta = (int)(position - displayStart);
@@ -162,53 +221,148 @@ namespace Terminal.Gui {
var block = item / 4;
var column = (item % 4) * 3;
Move (displayWidth + block * 14 + column, line);
if (leftSide)
Move (displayWidth + block * 14 + column + (firstNibble ? 0 : 1), line);
else
Move (displayWidth + (bytesPerLine / 4) * 14 + item - 1, line);
}
void RedisplayLine (long pos)
{
var delta = (int) (pos - DisplayStart);
var line = delta / bytesPerLine;
SetNeedsDisplay (new Rect (0, line, Frame.Width, 1));
}
void CursorRight ()
{
RedisplayLine (position);
if (leftSide) {
if (firstNibble) {
firstNibble = false;
return;
} else
firstNibble = true;
}
if (position < source.Length)
position++;
if (position >= (DisplayStart + bytesPerLine * Frame.Height)) {
SetDisplayStart (DisplayStart + bytesPerLine);
SetNeedsDisplay ();
} else
RedisplayLine (position);
}
public override bool ProcessKey (KeyEvent keyEvent)
{
switch (keyEvent.Key) {
case Key.CursorLeft:
RedisplayLine (position);
if (leftSide) {
if (!firstNibble) {
firstNibble = true;
return true;
}
firstNibble = false;
}
if (position == 0)
return true;
if (position - 1 < DisplayStart) {
SetDisplayStart (displayStart - bytesPerLine);
SetNeedsDisplay ();
}
} else
RedisplayLine (position);
position--;
break;
case Key.CursorRight:
if (position < source.Length)
position++;
if (position >= (DisplayStart + bytesPerLine * Frame.Height)) {
SetDisplayStart (DisplayStart + bytesPerLine);
SetNeedsDisplay ();
}
CursorRight ();
break;
case Key.CursorDown:
RedisplayLine (position);
if (position + bytesPerLine < source.Length)
position += bytesPerLine;
if (position >= (DisplayStart + bytesPerLine * Frame.Height)) {
SetDisplayStart (DisplayStart + bytesPerLine);
SetNeedsDisplay ();
}
} else
RedisplayLine (position);
break;
case Key.CursorUp:
RedisplayLine (position);
position -= bytesPerLine;
if (position < 0)
position = 0;
if (position < DisplayStart) {
SetDisplayStart (DisplayStart - bytesPerLine);
SetNeedsDisplay ();
}
} else
RedisplayLine (position);
break;
case Key.Tab:
leftSide = !leftSide;
RedisplayLine (position);
firstNibble = true;
break;
default:
return false;
if (leftSide) {
int value = -1;
var k = (char)keyEvent.Key;
if (k >= 'A' && k <= 'F')
value = k - 'A' + 10;
else if (k >= 'a' && k <= 'f')
value = k - 'a' + 10;
else if (k >= '0' && k <= '9')
value = k - '0';
else
return false;
byte b;
if (!edits.TryGetValue (position, out b)) {
source.Position = position;
b = (byte)source.ReadByte ();
}
RedisplayLine (position);
if (firstNibble) {
firstNibble = false;
b = (byte)(b & 0xf | (value << 4));
edits [position] = b;
} else {
b = (byte)(b & 0xf0 | value);
edits [position] = b;
CursorRight ();
}
return true;
} else
return false;
}
// TODO: just se the NeedDispay for the affected region, not all
SetNeedsDisplay ();
PositionCursor ();
return false;
return true;
}
/// <summary>
/// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.HexView"/> allow editing of the contents of the underlying stream.
/// </summary>
/// <value><c>true</c> if allow edits; otherwise, <c>false</c>.</value>
public bool AllowEdits { get; set; }
/// <summary>
/// Gets a list of the edits done to the buffer which is a sorted dictionary with the positions where the edit took place and the value that was set.
/// </summary>
/// <value>The edits.</value>
public IReadOnlyDictionary<long,byte> Edits => edits;
/// <summary>
/// This method applies the edits to the stream and resets the contents of the Edits property
/// </summary>
public void ApplyEdits ()
{
foreach (var kv in edits) {
source.Position = kv.Key;
source.WriteByte (kv.Value);
}
edits = new SortedDictionary<long, byte> ();
}
}
}

View File

@@ -254,7 +254,7 @@ namespace Terminal.Gui {
default:
// Ignore other control characters.
if (kb.Key < Key.Space || kb.Key > Key.CharMask)
if (kb.Key < Key.Space || kb.Key > Key.CharMask)
return false;
var kbstr = ustring.Make ((uint)kb.Key);