From b28e8186dc44f7cb1a2d406a3bab70d2cc25412b Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 6 Feb 2022 22:34:15 +0000 Subject: [PATCH] Added mouse support and more features to the HexView. (#1571) * Added mouse support and more features. * Updating NuGet packages. * Putting text on the same line. * Added a read only Position, CursorPosition properties and events. * Added a stream argument to ApplyEdits to only save the edits. * Ignore control characters and other special keys. --- ReactiveExample/ReactiveExample.csproj | 4 +- Terminal.Gui/Views/HexView.cs | 400 ++++++++++++++++++++----- UICatalog/Scenarios/HexEditor.cs | 97 ++++-- UnitTests/HexViewTests.cs | 399 ++++++++++++++++++++++++ UnitTests/UnitTests.csproj | 2 +- 5 files changed, 792 insertions(+), 110 deletions(-) create mode 100644 UnitTests/HexViewTests.cs diff --git a/ReactiveExample/ReactiveExample.csproj b/ReactiveExample/ReactiveExample.csproj index b57731a0f..30716dd37 100644 --- a/ReactiveExample/ReactiveExample.csproj +++ b/ReactiveExample/ReactiveExample.csproj @@ -4,8 +4,8 @@ net6.0 - - + + diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index 631fe10cd..fd086c3b2 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -37,27 +37,44 @@ namespace Terminal.Gui { public class HexView : View { SortedDictionary edits = new SortedDictionary (); Stream source; - long displayStart, position; + long displayStart, pos; bool firstNibble, leftSide; + private long position { + get => pos; + set { + pos = value; + OnPositionChanged (); + } + } + /// - /// Initialzies a class using layout. + /// Initializes a class using layout. /// /// The to view and edit as hex, this must support seeking, or an exception will be thrown. public HexView (Stream source) : base () { Source = source; - this.source = source; CanFocus = true; leftSide = true; firstNibble = true; } /// - /// Initialzies a class using layout. + /// Initializes a class using layout. /// public HexView () : this (source: new MemoryStream ()) { } + /// + /// Event to be invoked when an edit is made on the . + /// + public event Action> Edited; + + /// + /// Event to be invoked when the position and cursor position changes. + /// + public event Action PositionChanged; + /// /// Sets or gets the the is operating on; the stream must support seeking ( == true). /// @@ -71,13 +88,17 @@ namespace Terminal.Gui { throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source"); source = value; + if (displayStart > source.Length) + DisplayStart = 0; + if (position > source.Length) + position = 0; SetNeedsDisplay (); } } internal void SetDisplayStart (long value) { - if (value >= source.Length) + if (value > 0 && value >= source.Length) displayStart = source.Length - 1; else if (value < 0) displayStart = 0; @@ -101,7 +122,14 @@ namespace Terminal.Gui { const int displayWidth = 9; const int bsize = 4; - int bytesPerLine; + int bpl; + private int bytesPerLine { + get => bpl; + set { + bpl = value; + OnPositionChanged (); + } + } /// public override Rect Frame { @@ -109,10 +137,10 @@ namespace Terminal.Gui { set { base.Frame = value; - // Small buffers will just show the position, with 4 bytes - bytesPerLine = 4; + // Small buffers will just show the position, with the bsize field value (4 bytes) + bytesPerLine = bsize; if (value.Width - displayWidth > 17) - bytesPerLine = 4 * ((value.Width - displayWidth) / 18); + bytesPerLine = bsize * ((value.Width - displayWidth) / 18); } } @@ -144,8 +172,8 @@ namespace Terminal.Gui { var frame = Frame; - var nblocks = bytesPerLine / 4; - var data = new byte [nblocks * 4 * frame.Height]; + var nblocks = bytesPerLine / bsize; + var data = new byte [nblocks * bsize * frame.Height]; Source.Position = displayStart; var n = source.Read (data, 0, data.Length); @@ -159,38 +187,34 @@ namespace Terminal.Gui { Move (0, line); Driver.SetAttribute (ColorScheme.HotNormal); - Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * 4)); + Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * bsize)); currentAttribute = ColorScheme.HotNormal; SetAttribute (GetNormalColor ()); for (int block = 0; block < nblocks; block++) { - for (int b = 0; b < 4; b++) { - var offset = (line * nblocks * 4) + block * 4 + b; - bool edited; - var value = GetData (data, offset, out edited); + for (int b = 0; b < bsize; b++) { + var offset = (line * nblocks * bsize) + block * bsize + b; + var value = GetData (data, offset, out bool edited); if (offset + displayStart == position || edited) SetAttribute (leftSide ? activeColor : trackingColor); else SetAttribute (GetNormalColor ()); - Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2}", value)); + Driver.AddStr (offset >= n && !edited ? " " : string.Format ("{0:x2}", value)); SetAttribute (GetNormalColor ()); Driver.AddRune (' '); } Driver.AddStr (block + 1 == nblocks ? " " : "| "); } - - for (int bitem = 0; bitem < nblocks * 4; bitem++) { - var offset = line * nblocks * 4 + bitem; - - bool edited = false; - Rune c = ' '; - if (offset >= n) + for (int bitem = 0; bitem < nblocks * bsize; bitem++) { + var offset = line * nblocks * bsize + bitem; + var b = GetData (data, offset, out bool edited); + Rune c; + if (offset >= n && !edited) c = ' '; else { - var b = GetData (data, offset, out edited); if (b < 32) c = '.'; else if (b > 127) @@ -214,7 +238,6 @@ namespace Terminal.Gui { Driver.SetAttribute (attribute); } } - } /// @@ -223,13 +246,13 @@ namespace Terminal.Gui { var delta = (int)(position - displayStart); var line = delta / bytesPerLine; var item = delta % bytesPerLine; - var block = item / 4; - var column = (item % 4) * 3; + var block = item / bsize; + var column = (item % bsize) * 3; if (leftSide) Move (displayWidth + block * 14 + column + (firstNibble ? 0 : 1), line); else - Move (displayWidth + (bytesPerLine / 4) * 14 + item - 1, line); + Move (displayWidth + (bytesPerLine / bsize) * 14 + item - 1, line); } void RedisplayLine (long pos) @@ -240,13 +263,80 @@ namespace Terminal.Gui { SetNeedsDisplay (new Rect (0, line, Frame.Width, 1)); } - void CursorRight () + bool MoveEndOfLine () + { + position = Math.Min ((position / bytesPerLine * bytesPerLine) + bytesPerLine - 1, source.Length); + SetNeedsDisplay (); + + return true; + } + + bool MoveStartOfLine () + { + position = position / bytesPerLine * bytesPerLine; + SetNeedsDisplay (); + + return true; + } + + bool MoveEnd () + { + position = source.Length; + if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { + SetDisplayStart (position); + SetNeedsDisplay (); + } else + RedisplayLine (position); + + return true; + } + + bool MoveHome () + { + DisplayStart = 0; + SetNeedsDisplay (); + + return true; + } + + bool ToggleSide () + { + leftSide = !leftSide; + RedisplayLine (position); + firstNibble = true; + + return true; + } + + bool MoveLeft () + { + 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--; + + return true; + } + + bool MoveRight () { RedisplayLine (position); if (leftSide) { if (firstNibble) { firstNibble = false; - return; + return true; } else firstNibble = true; } @@ -257,32 +347,44 @@ namespace Terminal.Gui { SetNeedsDisplay (); } else RedisplayLine (position); + + return true; } - void MoveUp (int bytes) + bool MoveUp (int bytes) { RedisplayLine (position); - position -= bytes; - if (position < 0) - position = 0; + if (position - bytes > -1) + position -= bytes; if (position < DisplayStart) { SetDisplayStart (DisplayStart - bytes); SetNeedsDisplay (); } else RedisplayLine (position); + return true; } - void MoveDown (int bytes) + bool MoveDown (int bytes) { RedisplayLine (position); if (position + bytes < source.Length) position += bytes; + else if ((bytes == bytesPerLine * Frame.Height && source.Length >= (DisplayStart + bytesPerLine * Frame.Height)) + || (bytes <= (bytesPerLine * Frame.Height - bytesPerLine) && source.Length <= (DisplayStart + bytesPerLine * Frame.Height))) { + var p = position; + while (p + bytesPerLine < source.Length) { + p += bytesPerLine; + } + position = p; + } if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { SetDisplayStart (DisplayStart + bytes); SetNeedsDisplay (); } else RedisplayLine (position); + + return true; } /// @@ -290,52 +392,43 @@ namespace Terminal.Gui { { 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; + return MoveLeft (); case Key.CursorRight: - CursorRight (); - break; + return MoveRight (); case Key.CursorDown: - MoveDown (bytesPerLine); - break; + return MoveDown (bytesPerLine); case Key.CursorUp: - MoveUp (bytesPerLine); - break; + return MoveUp (bytesPerLine); case Key.Enter: - leftSide = !leftSide; - RedisplayLine (position); - firstNibble = true; - break; + return ToggleSide (); case ((int)'v' + Key.AltMask): case Key.PageUp: - MoveUp (bytesPerLine * Frame.Height); - break; + return MoveUp (bytesPerLine * Frame.Height); case Key.V | Key.CtrlMask: case Key.PageDown: - MoveDown (bytesPerLine * Frame.Height); - break; + return MoveDown (bytesPerLine * Frame.Height); case Key.Home: - DisplayStart = 0; - SetNeedsDisplay (); - break; + return MoveHome (); + case Key.End: + return MoveEnd (); + case Key.CursorLeft | Key.CtrlMask: + return MoveStartOfLine (); + case Key.CursorRight | Key.CtrlMask: + return MoveEndOfLine (); + case Key.CursorUp | Key.CtrlMask: + return MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine)); + case Key.CursorDown | Key.CtrlMask: + return MoveDown (bytesPerLine * (Frame.Height - 1 - ((int)(position - displayStart) / bytesPerLine))); default: + if (!AllowEdits) + return false; + + // Ignore control characters and other special keys + if (keyEvent.Key < Key.Space || keyEvent.Key > Key.CharMask) + return false; + if (leftSide) { - int value = -1; + int value; var k = (char)keyEvent.Key; if (k >= 'A' && k <= 'F') value = k - 'A' + 10; @@ -354,18 +447,89 @@ namespace Terminal.Gui { RedisplayLine (position); if (firstNibble) { firstNibble = false; - b = (byte)(b & 0xf | (value << 4)); + b = (byte)(b & 0xf | (value << bsize)); edits [position] = b; + OnEdited (new KeyValuePair (position, edits [position])); } else { b = (byte)(b & 0xf0 | value); edits [position] = b; - CursorRight (); + OnEdited (new KeyValuePair (position, edits [position])); + MoveRight (); } return true; } else return false; } - PositionCursor (); + } + + /// + /// Method used to invoke the event passing the . + /// + /// The key value pair. + public virtual void OnEdited (KeyValuePair keyValuePair) + { + Edited?.Invoke (keyValuePair); + } + + /// + /// Method used to invoke the event passing the arguments. + /// + public virtual void OnPositionChanged () + { + PositionChanged?.Invoke (new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) + && !me.Flags.HasFlag (MouseFlags.WheeledDown) && !me.Flags.HasFlag (MouseFlags.WheeledUp)) + return false; + + if (!HasFocus) + SetFocus (); + + if (me.Flags == MouseFlags.WheeledDown) { + DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length); + return true; + } + + if (me.Flags == MouseFlags.WheeledUp) { + DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0); + return true; + } + + if (me.X < displayWidth) + return true; + var nblocks = bytesPerLine / bsize; + var blocksSize = nblocks * 14; + var blocksRightOffset = displayWidth + blocksSize - 1; + if (me.X > blocksRightOffset + bytesPerLine - 1) + return true; + leftSide = me.X >= blocksRightOffset; + var lineStart = (me.Y * bytesPerLine) + displayStart; + var x = me.X - displayWidth + 1; + var block = x / 14; + x -= block * 2; + var empty = x % 3; + var item = x / 3; + if (!leftSide && item > 0 && (empty == 0 || x == (block * 14) + 14 - 1 - (block * 2))) + return true; + firstNibble = true; + if (leftSide) + position = Math.Min (lineStart + me.X - blocksRightOffset, source.Length); + else + position = Math.Min (lineStart + item, source.Length); + + if (me.Flags == MouseFlags.Button1DoubleClicked) { + leftSide = !leftSide; + if (leftSide) + firstNibble = empty == 1; + else + firstNibble = true; + } + SetNeedsDisplay (); + return true; } @@ -374,7 +538,7 @@ namespace Terminal.Gui { /// of the underlying . /// /// true if allow edits; otherwise, false. - public bool AllowEdits { get; set; } + public bool AllowEdits { get; set; } = true; /// /// Gets a describing the edits done to the . @@ -384,16 +548,56 @@ namespace Terminal.Gui { public IReadOnlyDictionary Edits => edits; /// - /// This method applies andy edits made to the and resets the - /// contents of the property + /// Gets the current character position starting at one, related to the . /// - public void ApplyEdits () + public long Position => position + 1; + + /// + /// Gets the current cursor position starting at one for both, line and column. + /// + public Point CursorPosition { + get { + var delta = (int)position; + var line = delta / bytesPerLine + 1; + var item = delta % bytesPerLine + 1; + + return new Point (item, line); + } + } + + /// + /// The bytes length per line. + /// + public int BytesPerLine => bytesPerLine; + + /// + /// This method applies and edits made to the and resets the + /// contents of the property. + /// + /// If provided also applies the changes to the passed . + public void ApplyEdits (Stream stream = null) { foreach (var kv in edits) { source.Position = kv.Key; source.WriteByte (kv.Value); + source.Flush (); + if (stream != null) { + stream.Position = kv.Key; + stream.WriteByte (kv.Value); + stream.Flush (); + } } edits = new SortedDictionary (); + SetNeedsDisplay (); + } + + /// + /// This method discards the edits made to the by resetting the + /// contents of the property. + /// + public void DiscardEdits () + { + edits = new SortedDictionary (); } private CursorVisibility desiredCursorVisibility = CursorVisibility.Default; @@ -411,5 +615,45 @@ namespace Terminal.Gui { desiredCursorVisibility = value; } } + + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (DesiredCursorVisibility); + + return base.OnEnter (view); + } + + /// + /// Defines the event arguments for event. + /// + public class HexViewEventArgs : EventArgs { + /// + /// Gets the current character position starting at one, related to the . + /// + public long Position { get; private set; } + /// + /// Gets the current cursor position starting at one for both, line and column. + /// + public Point CursorPosition { get; private set; } + + /// + /// The bytes length per line. + /// + public int BytesPerLine { get; private set; } + + /// + /// Initializes a new instance of + /// + /// The character position. + /// The cursor position. + /// Line bytes length. + public HexViewEventArgs (long pos, Point cursor, int lineLength) + { + Position = pos; + CursorPosition = cursor; + BytesPerLine = lineLength; + } + } } } diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index f48b6c241..b3dc3d488 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -14,13 +14,26 @@ namespace UICatalog.Scenarios { private string _fileName = "demo.bin"; private HexView _hexView; private bool _saved = true; + private MenuItem miAllowEdits; + private StatusItem siPositionChanged; + private StatusBar statusBar; public override void Setup () { - Win.Title = this.GetName() + "-" + _fileName ?? "Untitled"; - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar - Top.LayoutSubviews (); + Win.Title = this.GetName () + "-" + _fileName ?? "Untitled"; + + CreateDemoFile (_fileName); + //CreateUnicodeDemoFile (_fileName); + + _hexView = new HexView (LoadFile ()) { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (), + }; + _hexView.Edited += _hexView_Edited; + _hexView.PositionChanged += _hexView_PositionChanged; + Win.Add (_hexView); var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { @@ -35,48 +48,61 @@ namespace UICatalog.Scenarios { new MenuItem ("C_ut", "", () => Cut()), new MenuItem ("_Paste", "", () => Paste()) }), + new MenuBarItem ("_Options", new MenuItem [] { + miAllowEdits = new MenuItem ("_AllowEdits", "", () => ToggleAllowEdits ()){Checked = _hexView.AllowEdits, CheckType = MenuItemCheckStyle.Checked} + }) }); Top.Add (menu); - var statusBar = new StatusBar (new StatusItem [] { - //new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }), + statusBar = new StatusBar (new StatusItem [] { new StatusItem(Key.F2, "~F2~ Open", () => Open()), new StatusItem(Key.F3, "~F3~ Save", () => Save()), new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), + siPositionChanged = new StatusItem(Key.Null, + $"Position: {_hexView.Position} Line: {_hexView.CursorPosition.Y} Col: {_hexView.CursorPosition.X} Line length: {_hexView.BytesPerLine}", () => {}) }); Top.Add (statusBar); + } - CreateDemoFile (_fileName); + private void _hexView_PositionChanged (HexView.HexViewEventArgs obj) + { + siPositionChanged.Title = $"Position: {obj.Position} Line: {obj.CursorPosition.Y} Col: {obj.CursorPosition.X} Line length: {obj.BytesPerLine}"; + statusBar.SetNeedsDisplay (); + } - _hexView = new HexView (LoadFile()) { - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill (), - }; - _hexView.CanFocus = true; - Win.Add (_hexView); + private void ToggleAllowEdits () + { + _hexView.AllowEdits = miAllowEdits.Checked = !miAllowEdits.Checked; + } + + private void _hexView_Edited (System.Collections.Generic.KeyValuePair obj) + { + _saved = false; } private void New () { _fileName = null; - Win.Title = this.GetName () + "-" + _fileName ?? "Untitled"; - throw new NotImplementedException (); + _hexView.Source = LoadFile (); } private Stream LoadFile () { - MemoryStream stream = null; - if (!_saved) { - MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); + MemoryStream stream = new MemoryStream (); + if (!_saved && _hexView != null && _hexView.Edits.Count > 0) { + if (MessageBox.ErrorQuery ("Save", "The changes were not saved. Want to open without saving?", "Yes", "No") == 1) + return _hexView.Source; + _hexView.DiscardEdits (); + _saved = true; } if (_fileName != null) { var bin = System.IO.File.ReadAllBytes (_fileName); - stream = new MemoryStream (bin); + stream.Write (bin); Win.Title = this.GetName () + "-" + _fileName; _saved = true; + } else { + Win.Title = this.GetName () + "-" + (_fileName ?? "Untitled"); } return stream; } @@ -94,9 +120,6 @@ namespace UICatalog.Scenarios { private void Copy () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); - //if (_textView != null && _textView.SelectedLength != 0) { - // _textView.Copy (); - //} } private void Open () @@ -115,11 +138,14 @@ namespace UICatalog.Scenarios { { if (_fileName != null) { using (FileStream fs = new FileStream (_fileName, FileMode.OpenOrCreate)) { - _hexView.ApplyEdits (); - _hexView.Source.CopyTo (fs); - fs.Flush (); + _hexView.ApplyEdits (fs); + //_hexView.Source.Position = 0; + //_hexView.Source.CopyTo (fs); + //fs.Flush (); } _saved = true; + } else { + _hexView.ApplyEdits (); } } @@ -128,10 +154,9 @@ namespace UICatalog.Scenarios { Application.RequestStop (); } - private void CreateDemoFile(string fileName) + private void CreateDemoFile (string fileName) { var sb = new StringBuilder (); - // BUGBUG: #279 TextView does not know how to deal with \r\n, only \r sb.Append ("Hello world.\n"); sb.Append ("This is a test of the Emergency Broadcast System.\n"); @@ -139,5 +164,19 @@ namespace UICatalog.Scenarios { sw.Write (sb.ToString ()); sw.Close (); } + + private void CreateUnicodeDemoFile (string fileName) + { + var sb = new StringBuilder (); + sb.Append ("Hello world.\n"); + sb.Append ("This is a test of the Emergency Broadcast System.\n"); + + byte [] buffer = Encoding.Unicode.GetBytes (sb.ToString()); + MemoryStream ms = new MemoryStream (buffer); + FileStream file = new FileStream (fileName, FileMode.Create, FileAccess.Write); + ms.WriteTo (file); + file.Close (); + ms.Close (); + } } } diff --git a/UnitTests/HexViewTests.cs b/UnitTests/HexViewTests.cs new file mode 100644 index 000000000..89d1dbaf8 --- /dev/null +++ b/UnitTests/HexViewTests.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class HexViewTests { + [Fact] + public void Constructors_Defaults () + { + var hv = new HexView (); + Assert.NotNull (hv.Source); + Assert.IsAssignableFrom (hv.Source); + Assert.True (hv.CanFocus); + Assert.True (hv.AllowEdits); + + hv = new HexView (new System.IO.MemoryStream ()); + Assert.NotNull (hv.Source); + Assert.IsAssignableFrom (hv.Source); + Assert.True (hv.CanFocus); + Assert.True (hv.AllowEdits); + } + + private Stream LoadStream (bool unicode = false) + { + MemoryStream stream = new MemoryStream (); + byte [] bArray; + string memString = "Hello world.\nThis is a test of the Emergency Broadcast System.\n"; + + Assert.Equal (63, memString.Length); + + if (unicode) { + bArray = Encoding.Unicode.GetBytes (memString); + Assert.Equal (126, bArray.Length); + } else { + bArray = Encoding.Default.GetBytes (memString); + Assert.Equal (63, bArray.Length); + } + stream.Write (bArray); + + return stream; + } + + [Fact] + public void AllowEdits_Edits_ApplyEdits () + { + var hv = new HexView (LoadStream (true)) { + Width = 20, + Height = 20 + }; + + Assert.Empty (hv.Edits); + hv.AllowEdits = false; + Assert.True (hv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.False (hv.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ()))); + Assert.Empty (hv.Edits); + Assert.Equal (126, hv.Source.Length); + + hv.AllowEdits = true; + Assert.True (hv.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ()))); + Assert.Single (hv.Edits); + Assert.Equal (65, hv.Edits.ToList () [0].Value); + Assert.Equal ('A', (char)hv.Edits.ToList () [0].Value); + Assert.Equal (126, hv.Source.Length); + + // Appends byte + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.D2, new KeyModifiers ()))); + Assert.Equal (2, hv.Edits.Count); + Assert.Equal (66, hv.Edits.ToList () [1].Value); + Assert.Equal ('B', (char)hv.Edits.ToList () [1].Value); + Assert.Equal (126, hv.Source.Length); + + hv.ApplyEdits (); + Assert.Empty (hv.Edits); + Assert.Equal (127, hv.Source.Length); + } + + [Fact] + public void DisplayStart_Source () + { + var hv = new HexView (LoadStream (true)) { + Width = 20, + Height = 20 + }; + + Assert.Equal (0, hv.DisplayStart); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (4 * hv.Frame.Height, hv.DisplayStart); + Assert.Equal (hv.Source.Length, hv.Source.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + // already on last page and so the DisplayStart is the same as before + Assert.Equal (4 * hv.Frame.Height, hv.DisplayStart); + Assert.Equal (hv.Source.Length, hv.Source.Position); + } + + [Fact] + public void Edited_Event () + { + var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + KeyValuePair keyValuePair = default; + hv.Edited += (e) => keyValuePair = e; + + Assert.True (hv.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.D6, new KeyModifiers ()))); + + Assert.Equal (0, (int)keyValuePair.Key); + Assert.Equal (70, (int)keyValuePair.Value); + Assert.Equal ('F', (char)keyValuePair.Value); + } + + [Fact] + public void DiscardEdits_Method () + { + var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + Assert.True (hv.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ()))); + Assert.Single (hv.Edits); + Assert.Equal (65, hv.Edits.ToList () [0].Value); + Assert.Equal ('A', (char)hv.Edits.ToList () [0].Value); + Assert.Equal (126, hv.Source.Length); + + hv.DiscardEdits (); + Assert.Empty (hv.Edits); + } + + [Fact] + public void Position_Using_Encoding_Unicode () + { + var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + Assert.Equal (126, hv.Source.Length); + Assert.Equal (126, hv.Source.Position); + Assert.Equal (1, hv.Position); + + // left side needed to press twice + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (126, hv.Source.Position); + Assert.Equal (1, hv.Position); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (126, hv.Source.Position); + Assert.Equal (2, hv.Position); + + // right side only needed to press one time + Assert.True (hv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal (126, hv.Source.Position); + Assert.Equal (2, hv.Position); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (126, hv.Source.Position); + Assert.Equal (1, hv.Position); + + // last position is equal to the source length + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (126, hv.Source.Position); + Assert.Equal (127, hv.Position); + Assert.Equal (hv.Position - 1, hv.Source.Length); + } + + [Fact] + public void Position_Using_Encoding_Default () + { + var hv = new HexView (LoadStream ()) { Width = 20, Height = 20 }; + Assert.Equal (63, hv.Source.Length); + Assert.Equal (63, hv.Source.Position); + Assert.Equal (1, hv.Position); + + // left side needed to press twice + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (63, hv.Source.Position); + Assert.Equal (1, hv.Position); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (63, hv.Source.Position); + Assert.Equal (2, hv.Position); + + // right side only needed to press one time + Assert.True (hv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal (63, hv.Source.Position); + Assert.Equal (2, hv.Position); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (63, hv.Source.Position); + Assert.Equal (1, hv.Position); + + // last position is equal to the source length + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (63, hv.Source.Position); + Assert.Equal (64, hv.Position); + Assert.Equal (hv.Position - 1, hv.Source.Length); + } + + [Fact] + [AutoInitShutdown] + public void CursorPosition_Encoding_Unicode () + { + var hv = new HexView (LoadStream (true)) { Width = Dim.Fill (), Height = Dim.Fill () }; + Application.Top.Add (hv); + Application.Begin (Application.Top); + + Assert.Equal (new Point (1, 1), hv.CursorPosition); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine); + Assert.True (hv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (2, 1), hv.CursorPosition); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (2, 2), hv.CursorPosition); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + var col = hv.CursorPosition.X; + var line = hv.CursorPosition.Y; + var offset = (line - 1) * (hv.BytesPerLine - col); + Assert.Equal (hv.Position, col * line + offset); + } + + [Fact] + [AutoInitShutdown] + public void CursorPosition_Encoding_Default () + { + var hv = new HexView (LoadStream ()) { Width = Dim.Fill (), Height = Dim.Fill () }; + Application.Top.Add (hv); + Application.Begin (Application.Top); + + Assert.Equal (new Point (1, 1), hv.CursorPosition); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine); + Assert.True (hv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (2, 1), hv.CursorPosition); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (2, 2), hv.CursorPosition); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + var col = hv.CursorPosition.X; + var line = hv.CursorPosition.Y; + var offset = (line - 1) * (hv.BytesPerLine - col); + Assert.Equal (hv.Position, col * line + offset); + } + + [Fact] + [AutoInitShutdown] + public void PositionChanged_Event () + { + var hv = new HexView (LoadStream ()) { Width = Dim.Fill (), Height = Dim.Fill () }; + HexView.HexViewEventArgs hexViewEventArgs = null; + hv.PositionChanged += (e) => hexViewEventArgs = e; + Application.Top.Add (hv); + Application.Begin (Application.Top); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); // left side must press twice + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + + Assert.Equal (12, hexViewEventArgs.BytesPerLine); + Assert.Equal (new Point (2, 2), hexViewEventArgs.CursorPosition); + Assert.Equal (14, hexViewEventArgs.Position); + } + + private class NonSeekableStream : Stream { + Stream m_stream; + public NonSeekableStream (Stream baseStream) + { + m_stream = baseStream; + } + public override bool CanRead { + get { return m_stream.CanRead; } + } + + public override bool CanSeek { + get { return false; } + } + + public override bool CanWrite { + get { return m_stream.CanWrite; } + } + + public override void Flush () + { + m_stream.Flush (); + } + + public override long Length { + get { throw new NotSupportedException (); } + } + + public override long Position { + get { + return m_stream.Position; + } + set { + throw new NotSupportedException (); + } + } + + public override int Read (byte [] buffer, int offset, int count) + { + return m_stream.Read (buffer, offset, count); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotImplementedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public override void Write (byte [] buffer, int offset, int count) + { + m_stream.Write (buffer, offset, count); + } + } + + [Fact] + public void Exceptions_Tests () + { + Assert.Throws (() => new HexView (null)); + Assert.Throws (() => new HexView (new NonSeekableStream (new MemoryStream ()))); + } + + [Fact] + [AutoInitShutdown] + public void Source_Sets_DisplayStart_And_Position_To_Zero_If_Greater_Than_Source_Length () + { + var hv = new HexView (LoadStream ()) { Width = 10, Height = 5 }; + Application.Top.Add (hv); + Application.Begin (Application.Top); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (62, hv.DisplayStart); + Assert.Equal (64, hv.Position); + + hv.Source = new MemoryStream (); + Assert.Equal (0, hv.DisplayStart); + Assert.Equal (0, hv.Position - 1); + + hv.Source = LoadStream (); + hv.Width = Dim.Fill (); + hv.Height = Dim.Fill (); + Application.Top.LayoutSubviews (); + Assert.Equal (0, hv.DisplayStart); + Assert.Equal (0, hv.Position - 1); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (0, hv.DisplayStart); + Assert.Equal (64, hv.Position); + + hv.Source = new MemoryStream (); + Assert.Equal (0, hv.DisplayStart); + Assert.Equal (0, hv.Position - 1); + } + + [Fact] + public void ApplyEdits_With_Argument () + { + byte [] buffer = Encoding.Default.GetBytes ("Fest"); + var original = new MemoryStream (); + original.Write (buffer, 0, buffer.Length); + original.Flush (); + var copy = new MemoryStream (); + original.Position = 0; + original.CopyTo (copy); + copy.Flush (); + var hv = new HexView (copy) { Width = Dim.Fill (), Height = Dim.Fill () }; + byte [] readBuffer = new byte [hv.Source.Length]; + hv.Source.Position = 0; + hv.Source.Read (readBuffer); + Assert.Equal ("Fest", Encoding.Default.GetString (readBuffer)); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.D5, new KeyModifiers ()))); + Assert.True (hv.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers ()))); + readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value; + Assert.Equal ("Test", Encoding.Default.GetString (readBuffer)); + + hv.ApplyEdits (original); + original.Position = 0; + original.Read (buffer); + copy.Position = 0; + copy.Read (readBuffer); + Assert.Equal ("Test", Encoding.Default.GetString (buffer)); + Assert.Equal ("Test", Encoding.Default.GetString (readBuffer)); + Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer)); + } + } +} diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index d56bf8b73..62a83c34b 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -16,7 +16,7 @@ - +