From 573d6ee0ffc56cd0343030b9366d7b5d8ac2774b Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 9 Oct 2024 18:06:41 -0600 Subject: [PATCH] Fixed #3786 - Focus --- .../Application/ApplicationNavigation.cs | 1 + Terminal.Gui/View/View.Hierarchy.cs | 7 + Terminal.Gui/View/View.Navigation.cs | 117 ++++--- Terminal.Gui/Views/HexView.cs | 317 +++++++++++------- Terminal.Gui/Views/HexViewEventArgs.cs | 22 +- Terminal.Gui/Views/Shortcut.cs | 2 +- UICatalog/Scenarios/HexEditor.cs | 183 ++++++---- UICatalog/UICatalog.cs | 1 + .../Application.NavigationTests.cs | 6 +- UnitTests/Views/HexViewTests.cs | 260 ++++++++------ UnitTests/Views/ScrollViewTests.cs | 3 +- 11 files changed, 547 insertions(+), 372 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs index 2523709d0..23cb2ba11 100644 --- a/Terminal.Gui/Application/ApplicationNavigation.cs +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -77,6 +77,7 @@ public class ApplicationNavigation { return; } + Debug.Assert (value is null or { CanFocus: true, HasFocus: true }); _focused = value; diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs index 02b737896..01cf01beb 100644 --- a/Terminal.Gui/View/View.Hierarchy.cs +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -169,6 +169,13 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, Debug.Assert (!view.HasFocus); _subviews.Remove (view); + + // Clean up focus stuff + _previouslyFocused = null; + if (view._superView is { } && view._superView._previouslyFocused == this) + { + view._superView._previouslyFocused = null; + } view._superView = null; SetNeedsLayout (); diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index a8a2022aa..0141f5f87 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -180,7 +180,8 @@ public partial class View // Focus and cross-view navigation management (TabStop if (!_canFocus && HasFocus) { // If CanFocus is set to false and this view has focus, make it leave focus - HasFocus = false; + // Set traverssingdown so we don't go back up the hierachy... + SetHasFocusFalse (null, traversingDown: false); } if (_canFocus && !HasFocus && Visible && SuperView is { Focused: null }) @@ -296,7 +297,12 @@ public partial class View // Focus and cross-view navigation management (TabStop if (Focused is null && _previouslyFocused is { } && indicies.Contains (_previouslyFocused)) { - return _previouslyFocused.SetFocus (); + if (_previouslyFocused.SetFocus ()) + { + return true; + } + + _previouslyFocused = null; } return false; @@ -412,14 +418,14 @@ public partial class View // Focus and cross-view navigation management (TabStop /// other methods that /// set or remove focus from a view. /// - /// - /// The previously focused view. If there is no previously focused + /// + /// The currently focused view. If there is no previously focused /// view. /// /// /// if was changed to . /// - private (bool focusSet, bool cancelled) SetHasFocusTrue (View? previousFocusedView, bool traversingUp = false) + private (bool focusSet, bool cancelled) SetHasFocusTrue (View? currentFocusedView, bool traversingUp = false) { Debug.Assert (SuperView is null || ApplicationNavigation.IsInHierarchy (SuperView, this)); @@ -429,6 +435,11 @@ public partial class View // Focus and cross-view navigation management (TabStop return (false, false); } + if (currentFocusedView is { HasFocus: false }) + { + throw new ArgumentException ("SetHasFocusTrue: currentFocusedView must HasFocus."); + } + var thisAsAdornment = this as Adornment; View? superViewOrParent = thisAsAdornment?.Parent ?? SuperView; @@ -451,7 +462,7 @@ public partial class View // Focus and cross-view navigation management (TabStop bool previousValue = HasFocus; - bool cancelled = NotifyFocusChanging (false, true, previousFocusedView, this); + bool cancelled = NotifyFocusChanging (false, true, currentFocusedView, this); if (cancelled) { @@ -462,7 +473,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // Any of them may cancel gaining focus. In which case we need to back out. if (superViewOrParent is { HasFocus: false } sv) { - (bool focusSet, bool svCancelled) = sv.SetHasFocusTrue (previousFocusedView, true); + (bool focusSet, bool svCancelled) = sv.SetHasFocusTrue (currentFocusedView, true); if (!focusSet) { @@ -488,31 +499,33 @@ public partial class View // Focus and cross-view navigation management (TabStop if (!traversingUp) { - // Restore focus to the previously focused subview + // Restore focus to the previously focused subview, if any if (!RestoreFocus ()) { - // Couldn't restore focus, so use Advance to navigate to the next focusable subview - if (!AdvanceFocus (NavigationDirection.Forward, null)) - { - // Couldn't advance, so we're the most focused view in the application - Application.Navigation?.SetFocused (this); - } + Debug.Assert (_previouslyFocused is null); + // Couldn't restore focus, so use Advance to navigate to the next focusable subview, if any + AdvanceFocus (NavigationDirection.Forward, null); } } - if (previousFocusedView is { HasFocus: true } && GetFocusChain (NavigationDirection.Forward, TabStop).Contains (previousFocusedView)) + // Now make sure the old focused view loses focus + if (currentFocusedView is { HasFocus: true } && GetFocusChain (NavigationDirection.Forward, TabStop).Contains (currentFocusedView)) { - previousFocusedView.SetHasFocusFalse (this); + currentFocusedView.SetHasFocusFalse (this); } - _previouslyFocused = null; + if (_previouslyFocused is { }) + { + _previouslyFocused = null; + } if (Arrangement.HasFlag (ViewArrangement.Overlapped)) { SuperView?.MoveSubviewToEnd (this); } - NotifyFocusChanged (HasFocus, previousFocusedView, this); + // Focus work is done. Notify. + NotifyFocusChanged (HasFocus, currentFocusedView, this); SetNeedsDisplay (); @@ -527,6 +540,9 @@ public partial class View // Focus and cross-view navigation management (TabStop private bool NotifyFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { + Debug.Assert (currentFocused is null || currentFocused is { HasFocus: true }); + Debug.Assert (newFocused is null || newFocused is { CanFocus: true }); + // Call the virtual method if (OnHasFocusChanging (currentHasFocus, newHasFocus, currentFocused, newFocused)) { @@ -543,6 +559,13 @@ public partial class View // Focus and cross-view navigation management (TabStop return true; } + View? appFocused = Application.Navigation?.GetFocused (); + + if (appFocused == currentFocused) + { + Application.Navigation?.SetFocused (null); + } + return false; } @@ -586,8 +609,8 @@ public partial class View // Focus and cross-view navigation management (TabStop /// focused. /// /// - /// Set to true to indicate method is being called recurively, traversing down the focus - /// chain. + /// Set to true to traverse down the focus + /// chain only. If false, the method will attempt to AdvanceFocus on the superview or restorefocus on Application.Navigation.GetFocused(). /// /// private void SetHasFocusFalse (View? newFocusedView, bool traversingDown = false) @@ -598,53 +621,60 @@ public partial class View // Focus and cross-view navigation management (TabStop throw new InvalidOperationException ("SetHasFocusFalse should not be called if the view does not have focus."); } + if (newFocusedView is { HasFocus: false }) + { + throw new InvalidOperationException ("SetHasFocusFalse new focused view does not have focus."); + } + var thisAsAdornment = this as Adornment; View? superViewOrParent = thisAsAdornment?.Parent ?? SuperView; // If newFocusedVew is null, we need to find the view that should get focus, and SetFocus on it. if (!traversingDown && newFocusedView is null) { - if (superViewOrParent?._previouslyFocused is { }) + if (superViewOrParent?._previouslyFocused is { CanFocus: true }) { - if (superViewOrParent._previouslyFocused != this) + if (superViewOrParent._previouslyFocused != this && superViewOrParent._previouslyFocused.SetFocus ()) { - superViewOrParent?._previouslyFocused?.SetFocus (); - // The above will cause SetHasFocusFalse, so we can return + Debug.Assert (!_hasFocus); return; } } - if (superViewOrParent is { }) + if (superViewOrParent is { CanFocus: true }) { if (superViewOrParent.AdvanceFocus (NavigationDirection.Forward, TabStop)) { // The above will cause SetHasFocusFalse, so we can return + Debug.Assert (!_hasFocus); return; } - newFocusedView = superViewOrParent; + if (superViewOrParent is { HasFocus: true, CanFocus: true }) + { + newFocusedView = superViewOrParent; + } } - if (Application.Navigation is { } && Application.Top is { }) + if (Application.Navigation is { } && Application.Navigation.GetFocused () is { CanFocus: true }) { // Temporarily ensure this view can't get focus bool prevCanFocus = _canFocus; _canFocus = false; - bool restoredFocus = Application.Top!.RestoreFocus (); + bool restoredFocus = Application.Navigation.GetFocused ()!.RestoreFocus (); _canFocus = prevCanFocus; if (restoredFocus) { // The above caused SetHasFocusFalse, so we can return + Debug.Assert (!_hasFocus); return; } } - // No other focusable view to be found. Just "leave" us... } - // Before we can leave focus, we need to make sure that all views down the subview-hierarchy have left focus. View? mostFocused = MostFocused; @@ -665,15 +695,7 @@ public partial class View // Focus and cross-view navigation management (TabStop bottom = bottom.SuperView; } - if (bottom == this && bottom.SuperView is Adornment a) - { - //a.SetHasFocusFalse (newFocusedView, true); - - Debug.Assert (_hasFocus); - } - Debug.Assert (_hasFocus); - } if (superViewOrParent is { }) @@ -684,7 +706,9 @@ public partial class View // Focus and cross-view navigation management (TabStop bool previousValue = HasFocus; // Note, can't be cancelled. - NotifyFocusChanging (HasFocus, !HasFocus, newFocusedView, this); + NotifyFocusChanging (HasFocus, !HasFocus, this, newFocusedView); + + Debug.Assert (_hasFocus); // Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused View? focusedPeer = superViewOrParent?.Focused; @@ -692,16 +716,6 @@ public partial class View // Focus and cross-view navigation management (TabStop // Set HasFocus false _hasFocus = false; - if (Application.Navigation is { }) - { - View? appFocused = Application.Navigation.GetFocused (); - - if (appFocused is { } || appFocused == this) - { - Application.Navigation.SetFocused (newFocusedView ?? superViewOrParent); - } - } - NotifyFocusChanged (HasFocus, this, newFocusedView); if (_hasFocus) @@ -721,6 +735,11 @@ public partial class View // Focus and cross-view navigation management (TabStop private void NotifyFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { + if (newHasFocus && focusedVew?.Focused is null) + { + Application.Navigation?.SetFocused (focusedVew); + } + // Call the virtual method OnHasFocusChanged (newHasFocus, previousFocusedView, focusedVew); diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index fb382ab9e..8e3cd335d 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -1,15 +1,17 @@ #nullable enable +using System.Diagnostics; + // // HexView.cs: A hexadecimal viewer // -// TODO: -// - Support searching and highlighting of the search result +// TODO: Support searching and highlighting of the search result +// TODO: Support growing/shrinking the stream (e.g. del/backspace should work). // namespace Terminal.Gui; -/// An hex viewer and editor over a +/// An hex viewer and editor over a /// /// /// provides a hex editor on top of a seekable with the left side @@ -28,11 +30,12 @@ namespace Terminal.Gui; /// public class HexView : View, IDesignable { - private const int BSIZE = 4; - private const int DISPLAY_WIDTH = 9; + private const int DEFAULT_ADDRESS_WIDTH = 8; // The default value for AddressWidth + private const int NUM_BYTES_PER_HEX_COLUMN = 4; + private const int HEX_COLUMN_WIDTH = NUM_BYTES_PER_HEX_COLUMN * 3 + 2; // 3 cols per byte + 1 for vert separator + right space private bool _firstNibble; - private bool _leftSide; + private bool _leftSideHasFocus; private static readonly Rune _spaceCharRune = new (' '); private static readonly Rune _periodCharRune = new ('.'); @@ -47,7 +50,7 @@ public class HexView : View, IDesignable CanFocus = true; CursorVisibility = CursorVisibility.Default; - _leftSide = true; + _leftSideHasFocus = true; _firstNibble = true; // PERF: Closure capture of 'this' creates a lot of overhead. @@ -66,23 +69,20 @@ public class HexView : View, IDesignable AddCommand (Command.End, () => MoveEnd ()); AddCommand (Command.LeftStart, () => MoveLeftStart ()); AddCommand (Command.RightEnd, () => MoveEndOfLine ()); - AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(position - _displayStart) / BytesPerLine))); + AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - _displayStart) / BytesPerLine))); AddCommand ( Command.EndOfPage, - () => MoveDown (BytesPerLine * (Frame.Height - 1 - (int)(position - _displayStart) / BytesPerLine)) + () => MoveDown (BytesPerLine * (Frame.Height - 1 - (int)(Address - _displayStart) / BytesPerLine)) ); - // Default keybindings for this view KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Add (Key.CursorDown, Command.Down); KeyBindings.Add (Key.CursorUp, Command.Up); - KeyBindings.Add (Key.V.WithAlt, Command.PageUp); KeyBindings.Add (Key.PageUp, Command.PageUp); - KeyBindings.Add (Key.V.WithCtrl, Command.PageDown); KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.Home, Command.Start); @@ -95,6 +95,9 @@ public class HexView : View, IDesignable KeyBindings.Add (Key.Tab, Command.Tab); KeyBindings.Add (Key.Tab.WithShift, Command.BackTab); + KeyBindings.Remove (Key.Space); + KeyBindings.Remove (Key.Enter); + LayoutComplete += HexView_LayoutComplete; } @@ -102,30 +105,64 @@ public class HexView : View, IDesignable public HexView () : this (new MemoryStream ()) { } /// - /// Gets or sets whether this allow editing of the of the underlying + /// Gets or sets whether this allows editing of the of the underlying /// . /// - /// true if allow edits; otherwise, false. + /// true to allow edits; otherwise, false. public bool AllowEdits { get; set; } = true; - /// Gets the current cursor position starting at one for both, line and column. + /// Gets the current cursor position. public Point CursorPosition { get { - if (!IsInitialized) + if (_source is null || BytesPerLine == 0) { return Point.Empty; } + var delta = (int)Address; - var delta = (int)position; - int line = delta / BytesPerLine + 1; - int item = delta % BytesPerLine + 1; + if (_leftSideHasFocus) + { + int line = delta / BytesPerLine; + int item = delta % BytesPerLine; - return new (item, line); + return new (item, line); + } + else + { + int line = delta / BytesPerLine; + int item = delta % BytesPerLine; + + return new (item, line); + } } } + + /// + public override Point? PositionCursor () + { + var delta = (int)(Address - _displayStart); + int line = delta / BytesPerLine; + int item = delta % BytesPerLine; + int block = item / NUM_BYTES_PER_HEX_COLUMN; + int column = item % NUM_BYTES_PER_HEX_COLUMN * 3; + + int x = GetLeftSideStartColumn () + block * HEX_COLUMN_WIDTH + column + (_firstNibble ? 0 : 1); + int y = line; + + if (!_leftSideHasFocus) + { + x = GetLeftSideStartColumn () + BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH + item - 1; + } + + Move (x, y); + + return new (x, y); + } + + private SortedDictionary _edits = []; /// @@ -162,9 +199,9 @@ public class HexView : View, IDesignable DisplayStart = 0; } - if (position > _source.Length) + if (Address > _source.Length) { - position = 0; + Address = 0; } SetNeedsDisplay (); @@ -185,18 +222,22 @@ public class HexView : View, IDesignable } - private long _pos; + private long _address; - // TODO: Why is this "starting at one"? How does that make sense? - /// Gets the current character position starting at one, related to the . - public long Position => position + 1; - - private long position + /// Gets or sets the current byte position in the . + public long Address { - get => _pos; + get => _address; set { - _pos = value; + if (_address == value) + { + return; + } + + //ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual (value, Source!.Length, $"Position"); + + _address = value; RaisePositionChanged (); } } @@ -214,15 +255,39 @@ public class HexView : View, IDesignable get => _displayStart; set { - position = value; + Address = value; SetDisplayStart (value); } } + private int _addressWidth = DEFAULT_ADDRESS_WIDTH; + + /// + /// Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8. + /// + public int AddressWidth + { + get => _addressWidth; + set + { + if (_addressWidth == value) + { + return; + } + _addressWidth = value; + SetNeedsDisplay (); + } + } + + private int GetLeftSideStartColumn () + { + return AddressWidth == 0 ? 0 : AddressWidth + 1; + } + internal void SetDisplayStart (long value) { - if (value > 0 && value >= _source.Length) + if (value > 0 && value >= _source?.Length) { _displayStart = _source.Length - 1; } @@ -306,49 +371,49 @@ public class HexView : View, IDesignable return true; } - if (me.Position.X < DISPLAY_WIDTH) + if (me.Position.X < GetLeftSideStartColumn ()) { return true; } - int nblocks = BytesPerLine / BSIZE; - int blocksSize = nblocks * 14; - int blocksRightOffset = DISPLAY_WIDTH + blocksSize - 1; + int nblocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN; + int blocksSize = nblocks * HEX_COLUMN_WIDTH; + int blocksRightOffset = GetLeftSideStartColumn () + blocksSize - 1; if (me.Position.X > blocksRightOffset + BytesPerLine - 1) { return true; } - _leftSide = me.Position.X >= blocksRightOffset; + bool clickIsOnLeftSide = me.Position.X >= blocksRightOffset; long lineStart = me.Position.Y * BytesPerLine + _displayStart; - int x = me.Position.X - DISPLAY_WIDTH + 1; - int block = x / 14; + int x = me.Position.X - GetLeftSideStartColumn () + 1; + int block = x / HEX_COLUMN_WIDTH; x -= block * 2; int empty = x % 3; int item = x / 3; - if (!_leftSide && item > 0 && (empty == 0 || x == block * 14 + 14 - 1 - block * 2)) + if (!clickIsOnLeftSide && item > 0 && (empty == 0 || x == block * HEX_COLUMN_WIDTH + HEX_COLUMN_WIDTH - 1 - block * 2)) { return true; } _firstNibble = true; - if (_leftSide) + if (clickIsOnLeftSide) { - position = Math.Min (lineStart + me.Position.X - blocksRightOffset, _source.Length); + Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, _source.Length - 1); } else { - position = Math.Min (lineStart + item, _source.Length); + Address = Math.Min (lineStart + item, _source.Length - 1); } if (me.Flags == MouseFlags.Button1DoubleClicked) { - _leftSide = !_leftSide; + _leftSideHasFocus = !clickIsOnLeftSide; - if (_leftSide) + if (_leftSideHasFocus) { _firstNibble = empty == 1; } @@ -376,8 +441,8 @@ public class HexView : View, IDesignable Driver.SetAttribute (current); Move (0, 0); - int nblocks = BytesPerLine / BSIZE; - var data = new byte [nblocks * BSIZE * viewport.Height]; + int nblocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN; + var data = new byte [nblocks * NUM_BYTES_PER_HEX_COLUMN * viewport.Height]; Source.Position = _displayStart; int n = _source.Read (data, 0, data.Length); @@ -396,20 +461,26 @@ public class HexView : View, IDesignable Move (0, line); currentAttribute = GetHotNormalColor (); Driver.SetAttribute (currentAttribute); - Driver.AddStr ($"{_displayStart + line * nblocks * BSIZE:x8} "); + string address = $"{_displayStart + line * nblocks * NUM_BYTES_PER_HEX_COLUMN:x8}"; + Driver.AddStr ($"{address.Substring (8 - AddressWidth)}"); + + if (AddressWidth > 0) + { + Driver.AddStr (" "); + } SetAttribute (GetNormalColor ()); for (var block = 0; block < nblocks; block++) { - for (var b = 0; b < BSIZE; b++) + for (var b = 0; b < NUM_BYTES_PER_HEX_COLUMN; b++) { - int offset = line * nblocks * BSIZE + block * BSIZE + b; + int offset = line * nblocks * NUM_BYTES_PER_HEX_COLUMN + block * NUM_BYTES_PER_HEX_COLUMN + b; byte value = GetData (data, offset, out bool edited); - if (offset + _displayStart == position || edited) + if (offset + _displayStart == Address || edited) { - SetAttribute (_leftSide ? activeColor : trackingColor); + SetAttribute (_leftSideHasFocus ? activeColor : trackingColor); } else { @@ -424,9 +495,9 @@ public class HexView : View, IDesignable Driver.AddStr (block + 1 == nblocks ? " " : "| "); } - for (var bitem = 0; bitem < nblocks * BSIZE; bitem++) + for (var bitem = 0; bitem < nblocks * NUM_BYTES_PER_HEX_COLUMN; bitem++) { - int offset = line * nblocks * BSIZE + bitem; + int offset = line * nblocks * NUM_BYTES_PER_HEX_COLUMN + bitem; byte b = GetData (data, offset, out bool edited); Rune c; @@ -450,9 +521,9 @@ public class HexView : View, IDesignable } } - if (offset + _displayStart == position || edited) + if (offset + _displayStart == Address || edited) { - SetAttribute (_leftSide ? trackingColor : activeColor); + SetAttribute (_leftSideHasFocus ? trackingColor : activeColor); } else { @@ -492,13 +563,13 @@ public class HexView : View, IDesignable /// Raises the event. protected void RaisePositionChanged () { - HexViewEventArgs args = new (Position, CursorPosition, BytesPerLine); + HexViewEventArgs args = new (Address, CursorPosition, BytesPerLine); OnPositionChanged (args); PositionChanged?.Invoke (this, args); } /// - /// Called when has changed. + /// Called when has changed. /// protected virtual void OnPositionChanged (HexViewEventArgs e) { } @@ -519,7 +590,7 @@ public class HexView : View, IDesignable return false; } - if (_leftSide) + if (_leftSideHasFocus) { int value; var k = (char)keyEvent.KeyCode; @@ -543,55 +614,51 @@ public class HexView : View, IDesignable byte b; - if (!_edits.TryGetValue (position, out b)) + if (!_edits.TryGetValue (Address, out b)) { - _source.Position = position; + _source.Position = Address; b = (byte)_source.ReadByte (); } - RedisplayLine (position); + // BUGBUG: This makes no sense here. + RedisplayLine (Address); if (_firstNibble) { _firstNibble = false; - b = (byte)((b & 0xf) | (value << BSIZE)); - _edits [position] = b; - RaiseEdited (new (position, _edits [position])); + b = (byte)((b & 0xf) | (value << NUM_BYTES_PER_HEX_COLUMN)); + _edits [Address] = b; + RaiseEdited (new (Address, _edits [Address])); } else { b = (byte)((b & 0xf0) | value); - _edits [position] = b; - RaiseEdited (new (position, _edits [position])); + _edits [Address] = b; + RaiseEdited (new (Address, _edits [Address])); MoveRight (); } return true; } - - return false; - } - - /// - public override Point? PositionCursor () - { - var delta = (int)(position - _displayStart); - int line = delta / BytesPerLine; - int item = delta % BytesPerLine; - int block = item / BSIZE; - int column = item % BSIZE * 3; - - int x = DISPLAY_WIDTH + block * 14 + column + (_firstNibble ? 0 : 1); - int y = line; - - if (!_leftSide) + else { - x = DISPLAY_WIDTH + BytesPerLine / BSIZE * 14 + item - 1; + Rune r = keyEvent.AsRune; + + // TODO: Enable entering Tab char - somehow disable Tab for navigation + + _edits [Address] = (byte)(r.Value & 0x00FF); + MoveRight (); + + if ((byte)(r.Value & 0xFF00) > 0) + { + _edits [Address] = (byte)(r.Value & 0xFF00); + MoveRight (); + } + + //RaiseEdited (new (Address, _edits [Address])); } - Move (x, y); - - return new (x, y); + return false; } // @@ -620,44 +687,44 @@ public class HexView : View, IDesignable private void HexView_LayoutComplete (object? sender, LayoutEventArgs e) { // Small buffers will just show the position, with the bsize field value (4 bytes) - BytesPerLine = BSIZE; + BytesPerLine = NUM_BYTES_PER_HEX_COLUMN; - if (Viewport.Width - DISPLAY_WIDTH > 17) + if (Viewport.Width - GetLeftSideStartColumn () > 17) { - BytesPerLine = BSIZE * ((Viewport.Width - DISPLAY_WIDTH) / 18); + BytesPerLine = NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / 18); } } private bool MoveDown (int bytes) { - RedisplayLine (position); + RedisplayLine (Address); - if (position + bytes < _source.Length) + if (Address + bytes < _source.Length) { - position += bytes; + Address += bytes; } else if ((bytes == BytesPerLine * Viewport.Height && _source.Length >= DisplayStart + BytesPerLine * Viewport.Height) || (bytes <= BytesPerLine * Viewport.Height - BytesPerLine && _source.Length <= DisplayStart + BytesPerLine * Viewport.Height)) { - long p = position; + long p = Address; while (p + BytesPerLine < _source.Length) { p += BytesPerLine; } - position = p; + Address = p; } - if (position >= DisplayStart + BytesPerLine * Viewport.Height) + if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (DisplayStart + bytes); SetNeedsDisplay (); } else { - RedisplayLine (position); + RedisplayLine (Address); } return true; @@ -665,16 +732,16 @@ public class HexView : View, IDesignable private bool MoveEnd () { - position = _source.Length; + Address = _source!.Length; - if (position >= DisplayStart + BytesPerLine * Viewport.Height) + if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { - SetDisplayStart (position); + SetDisplayStart (Address); SetNeedsDisplay (); } else { - RedisplayLine (position); + RedisplayLine (Address); } return true; @@ -682,7 +749,7 @@ public class HexView : View, IDesignable private bool MoveEndOfLine () { - position = Math.Min (position / BytesPerLine * BytesPerLine + BytesPerLine - 1, _source.Length); + Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, _source!.Length); SetNeedsDisplay (); return true; @@ -698,9 +765,9 @@ public class HexView : View, IDesignable private bool MoveLeft () { - RedisplayLine (position); + RedisplayLine (Address); - if (_leftSide) + if (_leftSideHasFocus) { if (!_firstNibble) { @@ -712,31 +779,31 @@ public class HexView : View, IDesignable _firstNibble = false; } - if (position == 0) + if (Address == 0) { return true; } - if (position - 1 < DisplayStart) + if (Address - 1 < DisplayStart) { SetDisplayStart (_displayStart - BytesPerLine); SetNeedsDisplay (); } else { - RedisplayLine (position); + RedisplayLine (Address); } - position--; + Address--; return true; } private bool MoveRight () { - RedisplayLine (position); + RedisplayLine (Address); - if (_leftSide) + if (_leftSideHasFocus) { if (_firstNibble) { @@ -748,19 +815,19 @@ public class HexView : View, IDesignable _firstNibble = true; } - if (position < _source.Length) + if (Address < _source.Length - 1) { - position++; + Address++; } - if (position >= DisplayStart + BytesPerLine * Viewport.Height) + if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (DisplayStart + BytesPerLine); SetNeedsDisplay (); } else { - RedisplayLine (position); + RedisplayLine (Address); } return true; @@ -768,7 +835,7 @@ public class HexView : View, IDesignable private bool MoveLeftStart () { - position = position / BytesPerLine * BytesPerLine; + Address = Address / BytesPerLine * BytesPerLine; SetNeedsDisplay (); return true; @@ -776,21 +843,21 @@ public class HexView : View, IDesignable private bool MoveUp (int bytes) { - RedisplayLine (position); + RedisplayLine (Address); - if (position - bytes > -1) + if (Address - bytes > -1) { - position -= bytes; + Address -= bytes; } - if (position < DisplayStart) + if (Address < DisplayStart) { SetDisplayStart (DisplayStart - bytes); SetNeedsDisplay (); } else { - RedisplayLine (position); + RedisplayLine (Address); } return true; @@ -814,15 +881,15 @@ public class HexView : View, IDesignable switch (direction) { case NavigationDirection.Forward: - _leftSide = !_leftSide; - RedisplayLine (position); + _leftSideHasFocus = !_leftSideHasFocus; + RedisplayLine (Address); _firstNibble = true; return true; case NavigationDirection.Backward: - _leftSide = !_leftSide; - RedisplayLine (position); + _leftSideHasFocus = !_leftSideHasFocus; + RedisplayLine (Address); _firstNibble = true; return true; diff --git a/Terminal.Gui/Views/HexViewEventArgs.cs b/Terminal.Gui/Views/HexViewEventArgs.cs index 662d5cc2d..f3f6ddb92 100644 --- a/Terminal.Gui/Views/HexViewEventArgs.cs +++ b/Terminal.Gui/Views/HexViewEventArgs.cs @@ -12,12 +12,12 @@ namespace Terminal.Gui; public class HexViewEventArgs : EventArgs { /// Initializes a new instance of - /// The character position. + /// The byte position in the steam. /// The cursor position. /// Line bytes length. - public HexViewEventArgs (long pos, Point cursor, int lineLength) + public HexViewEventArgs (long address, Point cursor, int lineLength) { - Position = pos; + Address = address; CursorPosition = cursor; BytesPerLine = lineLength; } @@ -28,25 +28,25 @@ public class HexViewEventArgs : EventArgs /// Gets the current cursor position starting at one for both, line and column. public Point CursorPosition { get; private set; } - /// Gets the current character position starting at one, related to the . - public long Position { get; private set; } + /// Gets the byte position in the . + public long Address { get; private set; } } /// Defines the event arguments for event. public class HexViewEditEventArgs : EventArgs { /// Creates a new instance of the class. - /// + /// /// - public HexViewEditEventArgs (long position, byte newValue) + public HexViewEditEventArgs (long address, byte newValue) { - Position = position; + Address = address; NewValue = newValue; } - /// Gets the new value for that . + /// Gets the new value for that . public byte NewValue { get; } - /// Gets the location of the edit. - public long Position { get; } + /// Gets the adress of the edit in the stream. + public long Address { get; } } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index c1aa85ea4..f23ec23e3 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -58,7 +58,7 @@ public class Shortcut : View, IOrientation, IDesignable /// /// /// - /// The View that will be invoked when user does something that causes the Shortcut's Accept + /// The View that will be invoked on when user does something that causes the Shortcut's Accept /// event to be raised. /// /// diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index 08029a0e7..096c83e6c 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -16,27 +16,30 @@ public class HexEditor : Scenario private HexView _hexView; private MenuItem _miAllowEdits; private bool _saved = true; - private Shortcut _siPositionChanged; + private Shortcut _scAddress; + private Shortcut _scInfo; + private Shortcut _scPosition; private StatusBar _statusBar; public override void Main () { Application.Init (); - Toplevel app = new Toplevel () + + var app = new Toplevel { ColorScheme = Colors.ColorSchemes ["Base"] }; CreateDemoFile (_fileName); - _hexView = new HexView (new MemoryStream (Encoding.UTF8.GetBytes ("Demo text."))) + _hexView = new (new MemoryStream (Encoding.UTF8.GetBytes ("Demo text."))) { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), Title = _fileName ?? "Untitled", - BorderStyle = LineStyle.Rounded, + BorderStyle = LineStyle.Rounded }; _hexView.Edited += _hexView_Edited; _hexView.PositionChanged += _hexView_PositionChanged; @@ -46,71 +49,97 @@ public class HexEditor : Scenario { Menus = [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new ("_New", "", () => New ()), - new ("_Open", "", () => Open ()), - new ("_Save", "", () => Save ()), - null, - new ("_Quit", "", () => Quit ()) - } - ), - new MenuBarItem ( - "_Edit", - new MenuItem [] - { - new ("_Copy", "", () => Copy ()), - new ("C_ut", "", () => Cut ()), - new ("_Paste", "", () => Paste ()) - } - ), - new MenuBarItem ( - "_Options", - new [] - { - _miAllowEdits = new MenuItem ( - "_AllowEdits", - "", - () => ToggleAllowEdits () - ) - { - Checked = _hexView.AllowEdits, - CheckType = MenuItemCheckStyle - .Checked - } - } - ) + new ( + "_File", + new MenuItem [] + { + new ("_New", "", () => New ()), + new ("_Open", "", () => Open ()), + new ("_Save", "", () => Save ()), + null, + new ("_Quit", "", () => Quit ()) + } + ), + new ( + "_Edit", + new MenuItem [] + { + new ("_Copy", "", () => Copy ()), + new ("C_ut", "", () => Cut ()), + new ("_Paste", "", () => Paste ()) + } + ), + new ( + "_Options", + new [] + { + _miAllowEdits = new ( + "_AllowEdits", + "", + () => ToggleAllowEdits () + ) + { + Checked = _hexView.AllowEdits, + CheckType = MenuItemCheckStyle + .Checked + } + } + ) ] }; app.Add (menu); - _statusBar = new StatusBar ( - new [] - { - new (Key.F2, "Open", () => Open ()), - new (Key.F3, "Save", () => Save ()), - new ( - Application.QuitKey, - $"Quit", - () => Quit () - ), - _siPositionChanged = new Shortcut ( - Key.Empty, - $"Position: { - _hexView.Position - } Line: { - _hexView.CursorPosition.Y - } Col: { - _hexView.CursorPosition.X - } Line length: { - _hexView.BytesPerLine - }", - () => { } - ) - } - ) + var addressWidthUpDown = new NumericUpDown + { + Value = _hexView.AddressWidth + }; + + NumericUpDown addressUpDown = new NumericUpDown + { + Value = _hexView.Address, + Format = $"0x{{0:X{_hexView.AddressWidth}}}" + }; + + addressWidthUpDown.ValueChanging += (sender, args) => + { + args.Cancel = args.NewValue is < 0 or > 8; + + if (!args.Cancel) + { + _hexView.AddressWidth = args.NewValue; + + // ReSharper disable once AccessToDisposedClosure + addressUpDown.Format = $"0x{{0:X{_hexView.AddressWidth}}}"; + } + }; + + addressUpDown.ValueChanging += (sender, args) => + { + args.Cancel = args.NewValue is < 0; + + if (!args.Cancel) + { + _hexView.Address = args.NewValue; + } + }; + + _statusBar = new ( + [ + new (Key.F2, "Open", Open), + new (Key.F3, "Save", Save), + new () + { + CommandView = addressWidthUpDown, + HelpText = "Address Width" + }, + _scAddress = new () + { + CommandView = addressUpDown, + HelpText = "Address:" + }, + _scInfo = new (Key.Empty, string.Empty, () => { }), + _scPosition = new (Key.Empty, string.Empty, () => { }) + ]) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; @@ -119,6 +148,8 @@ public class HexEditor : Scenario _hexView.Source = LoadFile (); Application.Run (app); + addressUpDown.Dispose (); + addressWidthUpDown.Dispose (); app.Dispose (); Application.Shutdown (); } @@ -127,8 +158,15 @@ public class HexEditor : Scenario private void _hexView_PositionChanged (object sender, HexViewEventArgs obj) { - _siPositionChanged.Title = - $"Position: {obj.Position} Line: {obj.CursorPosition.Y} Col: {obj.CursorPosition.X} Line length: {obj.BytesPerLine}"; + _scInfo.Title = + $"Bytes: {_hexView.Source!.Length}"; + _scPosition.Title = + $"L: {obj.CursorPosition.Y} C: {obj.CursorPosition.X} Per Line: {obj.BytesPerLine}"; + + if (_scAddress.CommandView is NumericUpDown addrNumericUpDown) + { + addrNumericUpDown.Value = obj.Address; + } } private void Copy () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } @@ -147,7 +185,7 @@ public class HexEditor : Scenario private void CreateUnicodeDemoFile (string fileName) { var sb = new StringBuilder (); - sb.Append ("Hello world.\n"); + sb.Append ("Hello world with wide codepoints: 𝔹Aℝ𝔽.\n"); sb.Append ("This is a test of the Emergency Broadcast System.\n"); byte [] buffer = Encoding.Unicode.GetBytes (sb.ToString ()); @@ -169,8 +207,8 @@ public class HexEditor : Scenario if (MessageBox.ErrorQuery ( "Save", "The changes were not saved. Want to open without saving?", - "Yes", - "No" + "_Yes", + "_No" ) == 1) { @@ -190,7 +228,7 @@ public class HexEditor : Scenario } else { - _hexView.Title = (_fileName ?? "Untitled"); + _hexView.Title = _fileName ?? "Untitled"; } return stream; @@ -213,10 +251,11 @@ public class HexEditor : Scenario _hexView.Source = LoadFile (); _hexView.DisplayStart = 0; } + d.Dispose (); } - private void Paste () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } + private void Paste () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "_Ok"); } private void Quit () { Application.RequestStop (); } private void Save () diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 975fcc048..f0b84d866 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -387,6 +387,7 @@ public class UICatalogApp // 'app' closed cleanly. foreach (Responder? inst in Responder.Instances) { + Debug.Assert (inst.WasDisposed); } diff --git a/UnitTests/Application/Application.NavigationTests.cs b/UnitTests/Application/Application.NavigationTests.cs index 2f5768ace..26732eaca 100644 --- a/UnitTests/Application/Application.NavigationTests.cs +++ b/UnitTests/Application/Application.NavigationTests.cs @@ -68,7 +68,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output) Application.Navigation.FocusedChanged += ApplicationNavigationOnFocusedChanged; - Application.Navigation.SetFocused (new ()); + Application.Navigation.SetFocused (new () { CanFocus = true, HasFocus = true }); Assert.True (raised); @@ -89,7 +89,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output) { Application.Navigation = new (); - Application.Top = new() + Application.Top = new () { Id = "top", CanFocus = true @@ -125,7 +125,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output) { Application.Navigation = new (); - Application.Top = new() + Application.Top = new () { Id = "top", CanFocus = true diff --git a/UnitTests/Views/HexViewTests.cs b/UnitTests/Views/HexViewTests.cs index 566c4117a..4c5d23081 100644 --- a/UnitTests/Views/HexViewTests.cs +++ b/UnitTests/Views/HexViewTests.cs @@ -1,13 +1,42 @@ -using System.Text; +#nullable enable +using System.Text; +using JetBrains.Annotations; namespace Terminal.Gui.ViewsTests; public class HexViewTests { + [Theory] + [InlineData (0, 4)] + [InlineData (9, 4)] + [InlineData (20, 4)] + [InlineData (24, 4)] + [InlineData (30, 4)] + [InlineData (50, 4)] + public void BytesPerLine_Calculates_Correctly (int width, int expectedBPL) + { + var hv = new HexView (LoadStream (null, out long _)) { Width = width, Height = 10 }; + hv.LayoutSubviews (); + + Assert.Equal (expectedBPL, hv.BytesPerLine); + } + + [Theory] + [InlineData ("01234", 20, 4)] + [InlineData ("012345", 20, 4)] + public void xuz (string str, int width, int expectedBPL) + { + var hv = new HexView (LoadStream (str, out long _)) { Width = width, Height = 10 }; + hv.LayoutSubviews (); + + Assert.Equal (expectedBPL, hv.BytesPerLine); + } + + [Fact] public void AllowEdits_Edits_ApplyEdits () { - var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); @@ -94,45 +123,50 @@ public class HexViewTests } [Fact] - [AutoInitShutdown] public void CursorPosition_Encoding_Default () { - var hv = new HexView (LoadStream ()) { Width = Dim.Fill (), Height = Dim.Fill () }; - var top = new Toplevel (); - top.Add (hv); - Application.Begin (top); + var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 }; + Application.Top = new Toplevel (); + Application.Top.Add (hv); - Assert.Equal (new (1, 1), hv.CursorPosition); + Application.Top.LayoutSubviews (); + + Assert.Equal (new (0, 0), hv.CursorPosition); + Assert.Equal (20, hv.BytesPerLine); Assert.True (hv.NewKeyDownEvent (Key.Tab)); + Assert.Equal (new (0, 0), hv.CursorPosition); + Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl)); - Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine); + Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine - 1); Assert.True (hv.NewKeyDownEvent (Key.Home)); Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (new (2, 1), hv.CursorPosition); + Assert.Equal (new (1, 0), hv.CursorPosition); Assert.True (hv.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (new (2, 2), hv.CursorPosition); + Assert.Equal (new (1, 1), hv.CursorPosition); Assert.True (hv.NewKeyDownEvent (Key.End)); + Assert.Equal (new (2, 2), hv.CursorPosition); int col = hv.CursorPosition.X; int line = hv.CursorPosition.Y; - int offset = (line - 1) * (hv.BytesPerLine - col); - Assert.Equal (hv.Position, col * line + offset); - top.Dispose (); + int offset = line * (hv.BytesPerLine - col); + Assert.Equal (hv.Address, col * line + offset); + Application.Top.Dispose (); + Application.ResetState (true); } [Fact] - [AutoInitShutdown] public void CursorPosition_Encoding_Unicode () { - var hv = new HexView (LoadStream (true)) { Width = Dim.Fill (), Height = Dim.Fill () }; - var top = new Toplevel (); - top.Add (hv); - Application.Begin (top); + var hv = new HexView (LoadStream (null, out _, true)) { Width = Dim.Fill (), Height = Dim.Fill () }; + Application.Top = new Toplevel (); + Application.Top.Add (hv); - Assert.Equal (new (1, 1), hv.CursorPosition); + hv.LayoutSubviews (); + + Assert.Equal (new (0, 0), hv.CursorPosition); Assert.True (hv.NewKeyDownEvent (Key.Tab)); Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl)); @@ -149,14 +183,15 @@ public class HexViewTests int col = hv.CursorPosition.X; int line = hv.CursorPosition.Y; int offset = (line - 1) * (hv.BytesPerLine - col); - Assert.Equal (hv.Position, col * line + offset); - top.Dispose (); + Assert.Equal (hv.Address, col * line + offset); + Application.Top.Dispose (); + Application.ResetState (true); } [Fact] public void DiscardEdits_Method () { - var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); @@ -175,7 +210,7 @@ public class HexViewTests [Fact] public void DisplayStart_Source () { - var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); @@ -196,13 +231,13 @@ public class HexViewTests [Fact] public void Edited_Event () { - var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); KeyValuePair keyValuePair = default; - hv.Edited += (s, e) => keyValuePair = new (e.Position, e.NewValue); + hv.Edited += (s, e) => keyValuePair = new (e.Address, e.NewValue); Assert.True (hv.NewKeyDownEvent (Key.D4)); Assert.True (hv.NewKeyDownEvent (Key.D6)); @@ -220,143 +255,142 @@ public class HexViewTests } [Fact] - [AutoInitShutdown] - public void KeyBindings_Command () + public void KeyBindings_Test_Movement_LeftSide () { - var hv = new HexView (LoadStream ()) { Width = 20, Height = 10 }; - var top = new Toplevel (); - top.Add (hv); - Application.Begin (top); + var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; + Application.Top = new Toplevel (); + Application.Top.Add (hv); - Assert.Equal (63, hv.Source.Length); - Assert.Equal (1, hv.Position); + hv.LayoutSubviews (); + + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Length); + Assert.Equal (0, hv.Address); Assert.Equal (4, hv.BytesPerLine); // right side only needed to press one time Assert.True (hv.NewKeyDownEvent (Key.Tab)); Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (2, hv.Position); + Assert.Equal (1, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorLeft)); - Assert.Equal (1, hv.Position); + Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (5, hv.Position); + Assert.Equal (4, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorUp)); - Assert.Equal (1, hv.Position); - - Assert.True (hv.NewKeyDownEvent (Key.V.WithCtrl)); - Assert.Equal (41, hv.Position); - - Assert.True (hv.NewKeyDownEvent (new (Key.V.WithAlt))); - Assert.Equal (1, hv.Position); + Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.PageDown)); - Assert.Equal (41, hv.Position); + Assert.Equal (40, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.PageUp)); - Assert.Equal (1, hv.Position); + Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.End)); - Assert.Equal (64, hv.Position); + Assert.Equal (MEM_STRING_LENGTH, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.Home)); - Assert.Equal (1, hv.Position); + Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl)); - Assert.Equal (4, hv.Position); + Assert.Equal (3, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorLeft.WithCtrl)); - Assert.Equal (1, hv.Position); + Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorDown.WithCtrl)); - Assert.Equal (37, hv.Position); + Assert.Equal (36, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorUp.WithCtrl)); - Assert.Equal (1, hv.Position); - top.Dispose (); + Assert.Equal (0, hv.Address); + Application.Top.Dispose (); + Application.ResetState (true); } [Fact] public void Position_Using_Encoding_Default () { - var hv = new HexView (LoadStream ()) { Width = 20, Height = 20 }; + var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 20 }; + hv.LayoutSubviews (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); - Assert.Equal (63, hv.Source.Length); - Assert.Equal (63, hv.Source.Position); - Assert.Equal (1, hv.Position); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Length); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position); + Assert.Equal (0, hv.Address); // left side needed to press twice Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (63, hv.Source.Position); - Assert.Equal (1, hv.Position); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position); + Assert.Equal (1, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (63, hv.Source.Position); - Assert.Equal (2, hv.Position); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position); + Assert.Equal (2, hv.Address); // right side only needed to press one time Assert.True (hv.NewKeyDownEvent (Key.Tab)); - Assert.Equal (63, hv.Source.Position); - Assert.Equal (2, hv.Position); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position); + Assert.Equal (2, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorLeft)); - Assert.Equal (63, hv.Source.Position); - Assert.Equal (1, hv.Position); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position); + Assert.Equal (1, hv.Address); // last position is equal to the source length Assert.True (hv.NewKeyDownEvent (Key.End)); - Assert.Equal (63, hv.Source.Position); - Assert.Equal (64, hv.Position); - Assert.Equal (hv.Position - 1, hv.Source.Length); + Assert.Equal (MEM_STRING_LENGTH, hv.Source.Position); + Assert.Equal (64, hv.Address); + Assert.Equal (hv.Address - 1, hv.Source.Length); } [Fact] public void Position_Using_Encoding_Unicode () { - var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 }; + var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); Assert.Equal (126, hv.Source.Length); Assert.Equal (126, hv.Source.Position); - Assert.Equal (1, hv.Position); + Assert.Equal (1, hv.Address); // left side needed to press twice Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); Assert.Equal (126, hv.Source.Position); - Assert.Equal (1, hv.Position); + Assert.Equal (1, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); Assert.Equal (126, hv.Source.Position); - Assert.Equal (2, hv.Position); + Assert.Equal (2, hv.Address); // right side only needed to press one time Assert.True (hv.NewKeyDownEvent (Key.Tab)); Assert.Equal (126, hv.Source.Position); - Assert.Equal (2, hv.Position); + Assert.Equal (2, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.CursorLeft)); Assert.Equal (126, hv.Source.Position); - Assert.Equal (1, hv.Position); + Assert.Equal (1, hv.Address); // last position is equal to the source length Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (126, hv.Source.Position); - Assert.Equal (127, hv.Position); - Assert.Equal (hv.Position - 1, hv.Source.Length); + Assert.Equal (127, hv.Address); + Assert.Equal (hv.Address - 1, hv.Source.Length); } [Fact] - [AutoInitShutdown] public void PositionChanged_Event () { - var hv = new HexView (LoadStream ()) { Width = Dim.Fill (), Height = Dim.Fill () }; + var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; + Application.Top = new Toplevel (); + Application.Top.Add (hv); + + Application.Top.LayoutSubviews (); + HexViewEventArgs hexViewEventArgs = null; hv.PositionChanged += (s, e) => hexViewEventArgs = e; - var top = new Toplevel (); - top.Add (hv); - Application.Begin (top); + + Assert.Equal (12, hv.BytesPerLine); Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); // left side must press twice Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); @@ -364,87 +398,93 @@ public class HexViewTests Assert.Equal (12, hexViewEventArgs.BytesPerLine); Assert.Equal (new (2, 2), hexViewEventArgs.CursorPosition); - Assert.Equal (14, hexViewEventArgs.Position); - top.Dispose (); + Assert.Equal (14, hexViewEventArgs.Address); + Application.Top.Dispose (); + Application.ResetState (true); } [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 }; - var top = new Toplevel (); - top.Add (hv); - Application.Begin (top); + var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 }; + Application.Top = new Toplevel (); + Application.Top.Add (hv); + + hv.LayoutSubviews (); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (62, hv.DisplayStart); - Assert.Equal (64, hv.Position); + Assert.Equal (64, hv.Address); hv.Source = new MemoryStream (); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (0, hv.Position - 1); + Assert.Equal (0, hv.Address - 1); - hv.Source = LoadStream (); + hv.Source = LoadStream (null, out _); hv.Width = Dim.Fill (); hv.Height = Dim.Fill (); - top.LayoutSubviews (); + Application.Top.LayoutSubviews (); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (0, hv.Position - 1); + Assert.Equal (0, hv.Address - 1); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (64, hv.Position); + Assert.Equal (64, hv.Address); hv.Source = new MemoryStream (); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (0, hv.Position - 1); - top.Dispose (); + Assert.Equal (0, hv.Address - 1); + Application.Top.Dispose (); + Application.ResetState (true); } - private Stream LoadStream (bool unicode = false) + private const string MEM_STRING = "Hello world.\nThis is a test of the Emergency Broadcast System.\n"; + private const int MEM_STRING_LENGTH = 63; + + private Stream LoadStream (string? memString, out long numBytesInMemString, bool unicode = false) { var stream = new MemoryStream (); byte [] bArray; - var memString = "Hello world.\nThis is a test of the Emergency Broadcast System.\n"; - Assert.Equal (63, memString.Length); + Assert.Equal (MEM_STRING_LENGTH, MEM_STRING.Length); + + if (memString is null) + { + memString = MEM_STRING; + } if (unicode) { bArray = Encoding.Unicode.GetBytes (memString); - Assert.Equal (126, bArray.Length); } else { bArray = Encoding.Default.GetBytes (memString); - Assert.Equal (63, bArray.Length); } + numBytesInMemString = bArray.Length; stream.Write (bArray); return stream; } - private class NonSeekableStream : Stream + private class NonSeekableStream (Stream baseStream) : Stream { - private readonly Stream m_stream; - public NonSeekableStream (Stream baseStream) { m_stream = baseStream; } - public override bool CanRead => m_stream.CanRead; + public override bool CanRead => baseStream.CanRead; public override bool CanSeek => false; - public override bool CanWrite => m_stream.CanWrite; + public override bool CanWrite => baseStream.CanWrite; public override long Length => throw new NotSupportedException (); public override long Position { - get => m_stream.Position; + get => baseStream.Position; set => throw new NotSupportedException (); } - public override void Flush () { m_stream.Flush (); } - public override int Read (byte [] buffer, int offset, int count) { return m_stream.Read (buffer, offset, count); } + public override void Flush () { baseStream.Flush (); } + public override int Read (byte [] buffer, int offset, int count) { return baseStream.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); } + public override void Write (byte [] buffer, int offset, int count) { baseStream.Write (buffer, offset, count); } } } diff --git a/UnitTests/Views/ScrollViewTests.cs b/UnitTests/Views/ScrollViewTests.cs index fd207e28f..149ad2c1d 100644 --- a/UnitTests/Views/ScrollViewTests.cs +++ b/UnitTests/Views/ScrollViewTests.cs @@ -874,7 +874,8 @@ public class ScrollViewTests (ITestOutputHelper output) X = 3, Y = 3, Width = 10, - Height = 10 + Height = 10, + TabStop = TabBehavior.TabStop }; sv.SetContentSize (new (50, 50));