diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs
index 2662155fb..ec3558073 100644
--- a/Terminal.Gui/Views/HexView.cs
+++ b/Terminal.Gui/Views/HexView.cs
@@ -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 {
+ ///
+ /// An Hex viewer an editor view over a System.IO.Stream
+ ///
+ ///
+ ///
+ /// 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).
+ ///
+ ///
+ /// Users can switch from one side to the other by using the tab key.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// It is possible to control the first byte shown by setting the DisplayStart property
+ /// to the offset that you want to start viewing.
+ ///
+ ///
public class HexView : View {
+ SortedDictionary edits = new SortedDictionary ();
Stream source;
long displayStart, position;
+ bool firstNibble, leftSide;
///
/// 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;
}
///
@@ -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 {
}
+ ///
+ /// Positions the cursor based for the hex view
+ ///
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;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this allow editing of the contents of the underlying stream.
+ ///
+ /// true if allow edits; otherwise, false.
+ public bool AllowEdits { get; set; }
+
+ ///
+ /// 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.
+ ///
+ /// The edits.
+ public IReadOnlyDictionary Edits => edits;
+
+ ///
+ /// This method applies the edits to the stream and resets the contents of the Edits property
+ ///
+ public void ApplyEdits ()
+ {
+ foreach (var kv in edits) {
+ source.Position = kv.Key;
+ source.WriteByte (kv.Value);
+ }
+ edits = new SortedDictionary ();
}
}
}
diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs
index 8b1317a69..11b5b6f26 100644
--- a/Terminal.Gui/Views/TextField.cs
+++ b/Terminal.Gui/Views/TextField.cs
@@ -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);