2
0
Эх сурвалжийг харах

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.
BDisp 3 жил өмнө
parent
commit
b28e8186dc

+ 2 - 2
ReactiveExample/ReactiveExample.csproj

@@ -4,8 +4,8 @@
         <TargetFramework>net6.0</TargetFramework>
     </PropertyGroup>
     <ItemGroup>
-        <PackageReference Include="ReactiveUI.Fody" Version="17.1.9" />
-        <PackageReference Include="ReactiveUI" Version="17.1.9" />
+        <PackageReference Include="ReactiveUI.Fody" Version="17.1.17" />
+        <PackageReference Include="ReactiveUI" Version="17.1.17" />
         <PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.1.4" PrivateAssets="all" />
     </ItemGroup>
     <ItemGroup>

+ 322 - 78
Terminal.Gui/Views/HexView.cs

@@ -37,27 +37,44 @@ namespace Terminal.Gui {
 	public class HexView : View {
 		SortedDictionary<long, byte> edits = new SortedDictionary<long, byte> ();
 		Stream source;
-		long displayStart, position;
+		long displayStart, pos;
 		bool firstNibble, leftSide;
 
+		private long position {
+			get => pos;
+			set {
+				pos = value;
+				OnPositionChanged ();
+			}
+		}
+
 		/// <summary>
-		/// Initialzies a <see cref="HexView"/> class using <see cref="LayoutStyle.Computed"/> layout.
+		/// Initializes a <see cref="HexView"/> class using <see cref="LayoutStyle.Computed"/> layout.
 		/// </summary>
 		/// <param name="source">The <see cref="Stream"/> to view and edit as hex, this <see cref="Stream"/> must support seeking, or an exception will be thrown.</param>
 		public HexView (Stream source) : base ()
 		{
 			Source = source;
-			this.source = source;
 			CanFocus = true;
 			leftSide = true;
 			firstNibble = true;
 		}
 
 		/// <summary>
-		/// Initialzies a <see cref="HexView"/> class using <see cref="LayoutStyle.Computed"/> layout.
+		/// Initializes a <see cref="HexView"/> class using <see cref="LayoutStyle.Computed"/> layout.
 		/// </summary>
 		public HexView () : this (source: new MemoryStream ()) { }
 
+		/// <summary>
+		/// Event to be invoked when an edit is made on the <see cref="Stream"/>.
+		/// </summary>
+		public event Action<KeyValuePair<long, byte>> Edited;
+
+		/// <summary>
+		/// Event to be invoked when the position and cursor position changes.
+		/// </summary>
+		public event Action<HexViewEventArgs> PositionChanged;
+
 		/// <summary>
 		/// Sets or gets the <see cref="Stream"/> the <see cref="HexView"/> is operating on; the stream must support seeking (<see cref="Stream.CanSeek"/> == true).
 		/// </summary>
@@ -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 ();
+			}
+		}
 
 		/// <inheritdoc/>
 		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);
 				}
 			}
-
 		}
 
 		///<inheritdoc/>
@@ -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;
 		}
 
 		/// <inheritdoc/>
@@ -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<long, byte> (position, edits [position]));
 					} else {
 						b = (byte)(b & 0xf0 | value);
 						edits [position] = b;
-						CursorRight ();
+						OnEdited (new KeyValuePair<long, byte> (position, edits [position]));
+						MoveRight ();
 					}
 					return true;
 				} else
 					return false;
 			}
-			PositionCursor ();
+		}
+
+		/// <summary>
+		/// Method used to invoke the <see cref="Edited"/> event passing the <see cref="KeyValuePair{TKey, TValue}"/>.
+		/// </summary>
+		/// <param name="keyValuePair">The key value pair.</param>
+		public virtual void OnEdited (KeyValuePair<long, byte> keyValuePair)
+		{
+			Edited?.Invoke (keyValuePair);
+		}
+
+		/// <summary>
+		/// Method used to invoke the <see cref="PositionChanged"/> event passing the <see cref="HexViewEventArgs"/> arguments.
+		/// </summary>
+		public virtual void OnPositionChanged ()
+		{
+			PositionChanged?.Invoke (new HexViewEventArgs (Position, CursorPosition, BytesPerLine));
+		}
+
+		/// <inheritdoc/>
+		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 <see cref="Stream"/>.
 		/// </summary>
 		/// <value><c>true</c> if allow edits; otherwise, <c>false</c>.</value>
-		public bool AllowEdits { get; set; }
+		public bool AllowEdits { get; set; } = true;
 
 		/// <summary>
 		/// Gets a <see cref="SortedDictionary{TKey, TValue}"/> describing the edits done to the <see cref="HexView"/>. 
@@ -384,16 +548,56 @@ namespace Terminal.Gui {
 		public IReadOnlyDictionary<long, byte> Edits => edits;
 
 		/// <summary>
-		/// This method applies andy edits made to the <see cref="Stream"/> and resets the 
-		/// contents of the <see cref="Edits"/> property
+		/// Gets the current character position starting at one, related to the <see cref="Stream"/>.
+		/// </summary>
+		public long Position => position + 1;
+
+		/// <summary>
+		/// Gets the current cursor position starting at one for both, line and column.
+		/// </summary>
+		public Point CursorPosition {
+			get {
+				var delta = (int)position;
+				var line = delta / bytesPerLine + 1;
+				var item = delta % bytesPerLine + 1;
+
+				return new Point (item, line);
+			}
+		}
+
+		/// <summary>
+		/// The bytes length per line.
 		/// </summary>
-		public void ApplyEdits ()
+		public int BytesPerLine => bytesPerLine;
+
+		/// <summary>
+		/// This method applies and edits made to the <see cref="Stream"/> and resets the 
+		/// contents of the <see cref="Edits"/> property.
+		/// </summary>
+		/// <param name="stream">If provided also applies the changes to the passed <see cref="Stream"/></param>.
+		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<long, byte> ();
+			SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// This method discards the edits made to the <see cref="Stream"/> by resetting the 
+		/// contents of the <see cref="Edits"/> property.
+		/// </summary>
+		public void DiscardEdits ()
+		{
+			edits = new SortedDictionary<long, byte> ();
 		}
 
 		private CursorVisibility desiredCursorVisibility = CursorVisibility.Default;
@@ -411,5 +615,45 @@ namespace Terminal.Gui {
 				desiredCursorVisibility = value;
 			}
 		}
+
+		///<inheritdoc/>
+		public override bool OnEnter (View view)
+		{
+			Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
+
+			return base.OnEnter (view);
+		}
+
+		/// <summary>
+		/// Defines the event arguments for <see cref="PositionChanged"/> event.
+		/// </summary>
+		public class HexViewEventArgs : EventArgs {
+			/// <summary>
+			/// Gets the current character position starting at one, related to the <see cref="Stream"/>.
+			/// </summary>
+			public long Position { get; private set; }
+			/// <summary>
+			/// Gets the current cursor position starting at one for both, line and column.
+			/// </summary>
+			public Point CursorPosition { get; private set; }
+
+			/// <summary>
+			/// The bytes length per line.
+			/// </summary>
+			public int BytesPerLine { get; private set; }
+
+			/// <summary>
+			/// Initializes a new instance of <see cref="HexViewEventArgs"/>
+			/// </summary>
+			/// <param name="pos">The character position.</param>
+			/// <param name="cursor">The cursor position.</param>
+			/// <param name="lineLength">Line bytes length.</param>
+			public HexViewEventArgs (long pos, Point cursor, int lineLength)
+			{
+				Position = pos;
+				CursorPosition = cursor;
+				BytesPerLine = lineLength;
+			}
+		}
 	}
 }

+ 68 - 29
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<long, byte> 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 ();
+		}
 	}
 }

+ 399 - 0
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<System.IO.MemoryStream> (hv.Source);
+			Assert.True (hv.CanFocus);
+			Assert.True (hv.AllowEdits);
+
+			hv = new HexView (new System.IO.MemoryStream ());
+			Assert.NotNull (hv.Source);
+			Assert.IsAssignableFrom<System.IO.Stream> (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<long, byte> 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<ArgumentNullException> (() => new HexView (null));
+			Assert.Throws<ArgumentException> (() => 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));
+		}
+	}
+}

+ 1 - 1
UnitTests/UnitTests.csproj

@@ -16,7 +16,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
-    <PackageReference Include="ReportGenerator" Version="5.0.0" />
+    <PackageReference Include="ReportGenerator" Version="5.0.2" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">