From 11b4e45920dbea72694dcda8e37295cdd0493fc5 Mon Sep 17 00:00:00 2001 From: miguel Date: Thu, 10 May 2018 23:14:56 -0400 Subject: [PATCH] More HexEdit work --- Terminal.Gui/Views/HexView.cs | 216 +++++++++++++++++++++++++++----- Terminal.Gui/Views/TextField.cs | 2 +- 2 files changed, 186 insertions(+), 32 deletions(-) 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);