diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs index 23cb2ba11..5835957a2 100644 --- a/Terminal.Gui/Application/ApplicationNavigation.cs +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -27,7 +27,19 @@ public class ApplicationNavigation /// /// Gets the most focused in the application, if there is one. /// - public View? GetFocused () { return _focused; } + public View? GetFocused () + { + return _focused; + + if (_focused is { CanFocus: true, HasFocus: true }) + { + return _focused; + } + + _focused = null; + + return null; + } /// /// Gets whether is in the Subview hierarchy of . diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index 1268a576d..2cf77699b 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -97,6 +97,9 @@ public class GlyphDefinitions /// Dot. Default is (U+2219) - ∙. public Rune Dot { get; set; } = (Rune)'∙'; + /// Dotted Square - ⬚ U+02b1a┝ + public Rune DottedSquare { get; set; } = (Rune)'⬚'; + /// Black Circle . Default is (U+025cf) - ●. public Rune BlackCircle { get; set; } = (Rune)'●'; // Black Circle - ● U+025cf diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 43af4e906..e0af3c2af 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -166,6 +166,11 @@ public enum Command /// EnableOverwrite, + /// + /// Inserts a character. + /// + Insert, + /// Disables overwrite mode () DisableOverwrite, diff --git a/Terminal.Gui/View/CancelEventArgs.cs b/Terminal.Gui/View/CancelEventArgs.cs index 74e23ce2f..46c781b98 100644 --- a/Terminal.Gui/View/CancelEventArgs.cs +++ b/Terminal.Gui/View/CancelEventArgs.cs @@ -27,6 +27,8 @@ public class CancelEventArgs : CancelEventArgs where T : notnull NewValue = newValue; } + protected CancelEventArgs () { } + /// The current value of the property. public T CurrentValue { get; } diff --git a/Terminal.Gui/View/Navigation/AdvanceFocusEventArgs.cs b/Terminal.Gui/View/Navigation/AdvanceFocusEventArgs.cs new file mode 100644 index 000000000..c8dbfd9a4 --- /dev/null +++ b/Terminal.Gui/View/Navigation/AdvanceFocusEventArgs.cs @@ -0,0 +1,18 @@ +namespace Terminal.Gui; + +/// The event arguments for events. +public class AdvanceFocusEventArgs : CancelEventArgs +{ + /// Initializes a new instance. + public AdvanceFocusEventArgs (NavigationDirection direction, TabBehavior? behavior) + { + Direction = direction; + Behavior = behavior; + } + + /// Gets or sets the view that is losing focus. + public NavigationDirection Direction { get; set; } + + /// Gets or sets the view that is gaining focus. + public TabBehavior? Behavior { get; set; } +} diff --git a/Terminal.Gui/View/Navigation/FocusEventArgs.cs b/Terminal.Gui/View/Navigation/FocusEventArgs.cs index 63c38bbe2..c895e4e88 100644 --- a/Terminal.Gui/View/Navigation/FocusEventArgs.cs +++ b/Terminal.Gui/View/Navigation/FocusEventArgs.cs @@ -20,4 +20,4 @@ public class HasFocusEventArgs : CancelEventArgs /// Gets or sets the view that is gaining focus. public View NewFocused { get; set; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index 0141f5f87..e68b3c7d3 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -1,6 +1,5 @@ #nullable enable using System.Diagnostics; -using System.Reflection.PortableExecutable; namespace Terminal.Gui; @@ -18,7 +17,8 @@ public partial class View // Focus and cross-view navigation management (TabStop /// If there is no next/previous view to advance to, the focus is set to the view itself. /// /// - /// See the View Navigation Deep Dive for more information: + /// See the View Navigation Deep Dive for more information: + /// /// /// /// @@ -34,6 +34,11 @@ public partial class View // Focus and cross-view navigation management (TabStop return false; } + if (RaiseAdvancingFocus (direction, behavior)) + { + return true; + } + View? focused = Focused; if (focused is { } && focused.AdvanceFocus (direction, behavior)) @@ -128,6 +133,14 @@ public partial class View // Focus and cross-view navigation management (TabStop if (view.HasFocus) { // We could not advance + if (view != this) + { + // Tell it to try the other way. + return view.RaiseAdvancingFocus ( + direction == NavigationDirection.Forward ? NavigationDirection.Backward : NavigationDirection.Forward, + behavior); + } + return view == this; } @@ -137,10 +150,61 @@ public partial class View // Focus and cross-view navigation management (TabStop return focusSet; } + private bool RaiseAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) + { + // Call the virtual method + if (OnAdvancingFocus (direction, behavior)) + { + // The event was cancelled + return true; + } + + var args = new AdvanceFocusEventArgs (direction, behavior); + AdvancingFocus?.Invoke (this, args); + + if (args.Cancel) + { + // The event was cancelled + return true; + } + + return false; + } + + /// + /// Called when is about to advance focus. + /// + /// + /// + /// If a view cancels the event and the focus could not otherwise advance, the Navigation direction will be + /// reversed and the event will be raised again. + /// + /// + /// + /// , if the focus advance is to be cancelled, + /// otherwise. + /// + protected virtual bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) { return false; } + + /// + /// Raised when is about to advance focus. + /// + /// + /// + /// Cancel the event to prevent the focus from advancing. + /// + /// + /// If a view cancels the event and the focus could not otherwise advance, the Navigation direction will be + /// reversed and the event will be raised again. + /// + /// + public event EventHandler? AdvancingFocus; + /// Gets or sets a value indicating whether this can be focused. /// /// - /// See the View Navigation Deep Dive for more information: + /// See the View Navigation Deep Dive for more information: + /// /// /// /// must also have set to . @@ -180,8 +244,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 - // Set traverssingdown so we don't go back up the hierachy... - SetHasFocusFalse (null, traversingDown: false); + // Set transversing down so we don't go back up the hierarchy... + SetHasFocusFalse (null, false); } if (_canFocus && !HasFocus && Visible && SuperView is { Focused: null }) @@ -330,7 +394,8 @@ public partial class View // Focus and cross-view navigation management (TabStop /// /// /// - /// See the View Navigation Deep Dive for more information: + /// See the View Navigation Deep Dive for more information: + /// /// /// /// Only Views that are visible, enabled, and have set to are @@ -381,6 +446,8 @@ public partial class View // Focus and cross-view navigation management (TabStop { SetHasFocusFalse (null); + Debug.Assert (!_hasFocus); + if (_hasFocus) { // force it. @@ -397,7 +464,8 @@ public partial class View // Focus and cross-view navigation management (TabStop /// /// /// - /// See the View Navigation Deep Dive for more information: + /// See the View Navigation Deep Dive for more information: + /// /// /// /// if the focus changed; false otherwise. @@ -462,7 +530,7 @@ public partial class View // Focus and cross-view navigation management (TabStop bool previousValue = HasFocus; - bool cancelled = NotifyFocusChanging (false, true, currentFocusedView, this); + bool cancelled = RaiseFocusChanging (false, true, currentFocusedView, this); if (cancelled) { @@ -502,7 +570,6 @@ public partial class View // Focus and cross-view navigation management (TabStop // Restore focus to the previously focused subview, if any if (!RestoreFocus ()) { - 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); } @@ -525,7 +592,7 @@ public partial class View // Focus and cross-view navigation management (TabStop } // Focus work is done. Notify. - NotifyFocusChanged (HasFocus, currentFocusedView, this); + RaiseFocusChanged (HasFocus, currentFocusedView, this); SetNeedsDisplay (); @@ -538,7 +605,7 @@ public partial class View // Focus and cross-view navigation management (TabStop return (true, false); } - private bool NotifyFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) + private bool RaiseFocusChanging (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 }); @@ -563,7 +630,14 @@ public partial class View // Focus and cross-view navigation management (TabStop if (appFocused == currentFocused) { - Application.Navigation?.SetFocused (null); + if (newFocused is { HasFocus: true }) + { + Application.Navigation?.SetFocused (newFocused); + } + else + { + Application.Navigation?.SetFocused (null); + } } return false; @@ -610,7 +684,8 @@ public partial class View // Focus and cross-view navigation management (TabStop /// /// /// 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(). + /// 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) @@ -632,23 +707,29 @@ public partial class View // Focus and cross-view navigation management (TabStop // If newFocusedVew is null, we need to find the view that should get focus, and SetFocus on it. if (!traversingDown && newFocusedView is null) { + // Restore focus? if (superViewOrParent?._previouslyFocused is { CanFocus: true }) { + // TODO: Why don't we call RestoreFocus here? if (superViewOrParent._previouslyFocused != this && superViewOrParent._previouslyFocused.SetFocus ()) { // The above will cause SetHasFocusFalse, so we can return Debug.Assert (!_hasFocus); + return; } } + // AdvanceFocus? if (superViewOrParent is { CanFocus: true }) { if (superViewOrParent.AdvanceFocus (NavigationDirection.Forward, TabStop)) { - // The above will cause SetHasFocusFalse, so we can return - Debug.Assert (!_hasFocus); - return; + // The above might have SetHasFocusFalse, so we can return + if (!_hasFocus) + { + return; + } } if (superViewOrParent is { HasFocus: true, CanFocus: true }) @@ -657,24 +738,53 @@ public partial class View // Focus and cross-view navigation management (TabStop } } - if (Application.Navigation is { } && Application.Navigation.GetFocused () is { CanFocus: true }) + // Application.Navigation.GetFocused? + View? applicationFocused = Application.Navigation?.GetFocused (); + + if (newFocusedView is null && applicationFocused != this && applicationFocused is { CanFocus: true }) { // Temporarily ensure this view can't get focus bool prevCanFocus = _canFocus; _canFocus = false; - bool restoredFocus = Application.Navigation.GetFocused ()!.RestoreFocus (); + bool restoredFocus = applicationFocused!.RestoreFocus (); _canFocus = prevCanFocus; if (restoredFocus) { // The above caused SetHasFocusFalse, so we can return Debug.Assert (!_hasFocus); + return; } } + + // Application.Top? + if (newFocusedView is null && Application.Top is { CanFocus: true, HasFocus: false }) + { + // Temporarily ensure this view can't get focus + bool prevCanFocus = _canFocus; + _canFocus = false; + bool restoredFocus = Application.Top.RestoreFocus (); + _canFocus = prevCanFocus; + + if (Application.Top is { CanFocus: true, HasFocus: true }) + { + newFocusedView = Application.Top; + } + else if (restoredFocus) + { + // The above caused SetHasFocusFalse, so we can return + Debug.Assert (!_hasFocus); + + return; + } + } + // No other focusable view to be found. Just "leave" us... } + Debug.Assert (_hasFocus); + // Before we can leave focus, we need to make sure that all views down the subview-hierarchy have left focus. View? mostFocused = MostFocused; @@ -705,18 +815,25 @@ public partial class View // Focus and cross-view navigation management (TabStop bool previousValue = HasFocus; - // Note, can't be cancelled. - NotifyFocusChanging (HasFocus, !HasFocus, this, newFocusedView); - Debug.Assert (_hasFocus); + // Note, can't be cancelled. + RaiseFocusChanging (HasFocus, !HasFocus, this, newFocusedView); + + // Even though the change can't be cancelled, some listener may have changed the focus to another view. + if (!_hasFocus) + { + // Notify caused HasFocus to change to false. + return; + } + // Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused View? focusedPeer = superViewOrParent?.Focused; // Set HasFocus false _hasFocus = false; - NotifyFocusChanged (HasFocus, this, newFocusedView); + RaiseFocusChanged (HasFocus, this, newFocusedView); if (_hasFocus) { @@ -733,7 +850,7 @@ public partial class View // Focus and cross-view navigation management (TabStop SetNeedsDisplay (); } - private void NotifyFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) + private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { if (newHasFocus && focusedVew?.Focused is null) { @@ -775,7 +892,8 @@ public partial class View // Focus and cross-view navigation management (TabStop #region Tab/Focus Handling /// - /// Gets the subviews and Adornments of this view that are scoped to the specified behavior and direction. If behavior is null, all focusable subviews and + /// Gets the subviews and Adornments of this view that are scoped to the specified behavior and direction. If behavior + /// is null, all focusable subviews and /// Adornments are returned. /// /// @@ -794,7 +912,6 @@ public partial class View // Focus and cross-view navigation management (TabStop filteredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); } - // How about in Adornments? if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior) { @@ -825,11 +942,14 @@ public partial class View // Focus and cross-view navigation management (TabStop /// Gets or sets the behavior of for keyboard navigation. /// /// - /// + /// + /// + /// See the View Navigation Deep Dive for more information: + /// + /// + /// + /// /// /// - /// See the View Navigation Deep Dive for more information: - /// - /// /// /// If the tab stop has not been set and setting to true will set it /// to /// . diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index 8aed1c56d..130a12e0a 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -1,21 +1,22 @@ #nullable enable -using System.Diagnostics; - // // HexView.cs: A hexadecimal viewer // // TODO: Support searching and highlighting of the search result -// TODO: Support growing/shrinking the stream (e.g. del/backspace should work). +// TODO: Support shrinking the stream (e.g. del/backspace should work). // +using System.Buffers; + namespace Terminal.Gui; -/// An hex viewer and editor over a +/// Hex viewer and editor over a /// /// /// provides a hex editor on top of a seekable with the left side -/// showing an hex dump of the values in the and the right side showing the contents (filtered +/// showing the hex values of the bytes in the and the right side showing the contents +/// (filtered /// to non-control sequence ASCII characters). /// /// Users can switch from one side to the other by using the tab key. @@ -26,6 +27,10 @@ namespace Terminal.Gui; /// changes were made and the new values. A convenience method, will apply the edits to /// the . /// +/// +/// Control the byte at the caret for editing by setting the property to an offset in the +/// stream. +/// /// Control the first byte shown by setting the property to an offset in the stream. /// public class HexView : View, IDesignable @@ -37,7 +42,8 @@ public class HexView : View, IDesignable private bool _firstNibble; private bool _leftSideHasFocus; private static readonly Rune _spaceCharRune = new (' '); - private static readonly Rune _periodCharRune = new ('.'); + private static readonly Rune _periodCharRune = Glyphs.DottedSquare; + private static readonly Rune _columnSeparatorRune = Glyphs.VLineDa4; /// Initializes a class. /// @@ -55,26 +61,25 @@ public class HexView : View, IDesignable // PERF: Closure capture of 'this' creates a lot of overhead. // BUG: Closure capture of 'this' may have unexpected results depending on how this is called. - // The above two comments apply to all of the lambdas passed to all calls to AddCommand below. - // Things this view knows how to do + // The above two comments apply to all the lambdas passed to all calls to AddCommand below. AddCommand (Command.Left, () => MoveLeft ()); AddCommand (Command.Right, () => MoveRight ()); AddCommand (Command.Down, () => MoveDown (BytesPerLine)); AddCommand (Command.Up, () => MoveUp (BytesPerLine)); - AddCommand (Command.Tab, () => Navigate (NavigationDirection.Forward)); - AddCommand (Command.BackTab, () => Navigate (NavigationDirection.Backward)); - AddCommand (Command.PageUp, () => MoveUp (BytesPerLine * Frame.Height)); - AddCommand (Command.PageDown, () => MoveDown (BytesPerLine * Frame.Height)); + AddCommand (Command.PageUp, () => MoveUp (BytesPerLine * Viewport.Height)); + AddCommand (Command.PageDown, () => MoveDown (BytesPerLine * Viewport.Height)); AddCommand (Command.Start, () => MoveHome ()); AddCommand (Command.End, () => MoveEnd ()); AddCommand (Command.LeftStart, () => MoveLeftStart ()); AddCommand (Command.RightEnd, () => MoveEndOfLine ()); AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - _displayStart) / BytesPerLine))); - AddCommand ( Command.EndOfPage, - () => MoveDown (BytesPerLine * (Frame.Height - 1 - (int)(Address - _displayStart) / BytesPerLine)) + () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - _displayStart) / BytesPerLine)) ); + AddCommand (Command.DeleteCharLeft, () => true); + AddCommand (Command.DeleteCharRight, () => true); + AddCommand (Command.Insert, () => true); KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Add (Key.CursorRight, Command.Right); @@ -92,8 +97,9 @@ public class HexView : View, IDesignable KeyBindings.Add (Key.CursorUp.WithCtrl, Command.StartOfPage); KeyBindings.Add (Key.CursorDown.WithCtrl, Command.EndOfPage); - KeyBindings.Add (Key.Tab, Command.Tab); - KeyBindings.Add (Key.Tab.WithShift, Command.BackTab); + KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (Key.Delete, Command.DeleteCharRight); + KeyBindings.Add (Key.InsertChar, Command.Insert); KeyBindings.Remove (Key.Space); KeyBindings.Remove (Key.Enter); @@ -111,8 +117,8 @@ public class HexView : View, IDesignable /// true to allow edits; otherwise, false. public bool AllowEdits { get; set; } = true; - /// Gets the current cursor position. - public Point CursorPosition + /// Gets the current edit position. + public Point Position { get { @@ -120,26 +126,16 @@ public class HexView : View, IDesignable { return Point.Empty; } + var delta = (int)Address; - if (_leftSideHasFocus) - { - int line = delta / BytesPerLine; - int item = delta % BytesPerLine; + int line = delta / BytesPerLine; + int item = delta % BytesPerLine; - return new (item, line); - } - else - { - int line = delta / BytesPerLine; - int item = delta % BytesPerLine; - - return new (item, line); - } + return new (item, line); } } - /// public override Point? PositionCursor () { @@ -162,7 +158,6 @@ public class HexView : View, IDesignable return new (x, y); } - private SortedDictionary _edits = []; /// @@ -172,7 +167,6 @@ public class HexView : View, IDesignable /// The edits. public IReadOnlyDictionary Edits => _edits; - private Stream? _source; /// @@ -221,7 +215,6 @@ public class HexView : View, IDesignable } } - private long _address; /// Gets or sets the current byte position in the . @@ -264,7 +257,7 @@ public class HexView : View, IDesignable 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. + /// Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8. /// public int AddressWidth { @@ -275,15 +268,13 @@ public class HexView : View, IDesignable { return; } + _addressWidth = value; SetNeedsDisplay (); } } - private int GetLeftSideStartColumn () - { - return AddressWidth == 0 ? 0 : AddressWidth + 1; - } + private int GetLeftSideStartColumn () { return AddressWidth == 0 ? 0 : AddressWidth + 1; } internal void SetDisplayStart (long value) { @@ -303,7 +294,6 @@ public class HexView : View, IDesignable SetNeedsDisplay (); } - /// /// Applies and edits made to the and resets the contents of the /// property. @@ -359,7 +349,7 @@ public class HexView : View, IDesignable if (me.Flags == MouseFlags.WheeledDown) { - DisplayStart = Math.Min (DisplayStart + BytesPerLine, _source.Length); + DisplayStart = Math.Min (DisplayStart + BytesPerLine, GetEditedSize()); return true; } @@ -402,11 +392,11 @@ public class HexView : View, IDesignable if (clickIsOnLeftSide) { - Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, _source.Length - 1); + Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, GetEditedSize ()); } else { - Address = Math.Min (lineStart + item, _source.Length - 1); + Address = Math.Min (lineStart + item, GetEditedSize ()); } if (me.Flags == MouseFlags.Button1DoubleClicked) @@ -461,7 +451,7 @@ public class HexView : View, IDesignable Move (0, line); currentAttribute = GetHotNormalColor (); Driver.SetAttribute (currentAttribute); - string address = $"{_displayStart + line * nblocks * NUM_BYTES_PER_HEX_COLUMN:x8}"; + var address = $"{_displayStart + line * nblocks * NUM_BYTES_PER_HEX_COLUMN:x8}"; Driver.AddStr ($"{address.Substring (8 - AddressWidth)}"); if (AddressWidth > 0) @@ -492,7 +482,7 @@ public class HexView : View, IDesignable Driver.AddRune (_spaceCharRune); } - Driver.AddStr (block + 1 == nblocks ? " " : "| "); + Driver.AddStr (block + 1 == nblocks ? " " : $"{_columnSeparatorRune} "); } for (var bitem = 0; bitem < nblocks * NUM_BYTES_PER_HEX_COLUMN; bitem++) @@ -501,23 +491,37 @@ public class HexView : View, IDesignable byte b = GetData (data, offset, out bool edited); Rune c; + var utf8BytesConsumed = 0; + if (offset >= n && !edited) { c = _spaceCharRune; } else { - if (b < 32) + switch (b) { - c = _periodCharRune; - } - else if (b > 127) - { - c = _periodCharRune; - } - else - { - Rune.DecodeFromUtf8 (new (ref b), out c, out _); + //case < 32: + // c = _periodCharRune; + + // break; + case > 127: + { + var utf8 = GetData (data, offset, 4, out bool _); + + OperationStatus status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed); + + while (status == OperationStatus.NeedMoreData) + { + status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed); + } + + break; + } + default: + Rune.DecodeFromUtf8 (new (ref b), out c, out _); + + break; } } @@ -531,6 +535,12 @@ public class HexView : View, IDesignable } Driver.AddRune (c); + + for (var i = 1; i < utf8BytesConsumed; i++) + { + bitem++; + Driver.AddRune (_periodCharRune); + } } } @@ -547,7 +557,7 @@ public class HexView : View, IDesignable /// Raises the event. protected void RaiseEdited (HexViewEditEventArgs e) { - OnEditied (e); + OnEdited (e); Edited?.Invoke (this, e); } @@ -555,25 +565,27 @@ public class HexView : View, IDesignable public event EventHandler? Edited; /// - /// /// /// - protected virtual void OnEditied (HexViewEditEventArgs e) { } + protected virtual void OnEdited (HexViewEditEventArgs e) { } - /// Raises the event. + /// + /// Call this when (and ) has changed. Raises the + /// event. + /// protected void RaisePositionChanged () { - HexViewEventArgs args = new (Address, CursorPosition, BytesPerLine); + HexViewEventArgs args = new (Address, Position, BytesPerLine); OnPositionChanged (args); PositionChanged?.Invoke (this, args); } /// - /// Called when has changed. + /// Called when (and ) has changed. /// protected virtual void OnPositionChanged (HexViewEventArgs e) { } - /// Event to be invoked when the position and cursor position changes. + /// Raised when (and ) has changed. public event EventHandler? PositionChanged; /// @@ -584,17 +596,16 @@ public class HexView : View, IDesignable return false; } - // Ignore control characters and other special keys - if (keyEvent < Key.Space || keyEvent.KeyCode > KeyCode.CharMask) - { - return false; - } - if (_leftSideHasFocus) { int value; var k = (char)keyEvent.KeyCode; + if (!char.IsAsciiHexDigit ((char)keyEvent.KeyCode)) + { + return false; + } + if (k is >= 'A' and <= 'F') { value = k - 'A' + 10; @@ -612,9 +623,7 @@ public class HexView : View, IDesignable return false; } - byte b; - - if (!_edits.TryGetValue (Address, out b)) + if (!_edits.TryGetValue (Address, out byte b)) { _source.Position = Address; b = (byte)_source.ReadByte (); @@ -640,25 +649,40 @@ public class HexView : View, IDesignable return true; } - else + + keyEvent = keyEvent.NoAlt.NoCtrl; + Rune r = keyEvent.AsRune; + + if (Rune.IsControl (r)) { - 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])); + return false; } - return false; + var utf8 = new byte [4]; + + // If the rune is a wide char, encode as utf8 + if (r.TryEncodeToUtf8 (utf8, out int bytesWritten)) + { + if (bytesWritten > 1) + { + bytesWritten = 4; + } + + for (var utfIndex = 0; utfIndex < bytesWritten; utfIndex++) + { + _edits [Address] = utf8 [utfIndex]; + RaiseEdited (new (Address, _edits [Address])); + MoveRight (); + } + } + else + { + _edits [Address] = (byte)r.Value; + RaiseEdited (new (Address, _edits [Address])); + MoveRight (); + } + + return true; } // @@ -684,6 +708,31 @@ public class HexView : View, IDesignable return buffer [offset]; } + private byte [] GetData (byte [] buffer, int offset, int count, out bool edited) + { + var returnBytes = new byte [count]; + edited = false; + + long pos = DisplayStart + offset; + for (long i = pos; i < pos + count; i++) + { + if (_edits.TryGetValue (i, out byte v)) + { + edited = true; + returnBytes [i - pos] = v; + } + else + { + if (pos < buffer.Length - 1) + { + returnBytes [i - pos] = buffer [pos]; + } + } + } + + return returnBytes; + } + private void HexView_LayoutComplete (object? sender, LayoutEventArgs e) { // Small buffers will just show the position, with the bsize field value (4 bytes) @@ -691,7 +740,9 @@ public class HexView : View, IDesignable if (Viewport.Width - GetLeftSideStartColumn () >= HEX_COLUMN_WIDTH) { - BytesPerLine = NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / 18); + BytesPerLine = Math.Max ( + NUM_BYTES_PER_HEX_COLUMN, + NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / (HEX_COLUMN_WIDTH + NUM_BYTES_PER_HEX_COLUMN))); } } @@ -699,8 +750,9 @@ public class HexView : View, IDesignable { RedisplayLine (Address); - if (Address + bytes < _source.Length) + if (Address + bytes < GetEditedSize ()) { + // We can move down lines cleanly (without extending stream) Address += bytes; } else if ((bytes == BytesPerLine * Viewport.Height && _source.Length >= DisplayStart + BytesPerLine * Viewport.Height) @@ -709,7 +761,8 @@ public class HexView : View, IDesignable { long p = Address; - while (p + BytesPerLine < _source.Length) + // This lets address go past the end of the stream one, enabling adding to the stream. + while (p + BytesPerLine <= GetEditedSize ()) { p += BytesPerLine; } @@ -732,7 +785,8 @@ public class HexView : View, IDesignable private bool MoveEnd () { - Address = _source!.Length; + // This lets address go past the end of the stream one, enabling adding to the stream. + Address = GetEditedSize (); if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { @@ -749,7 +803,8 @@ public class HexView : View, IDesignable private bool MoveEndOfLine () { - Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, _source!.Length); + // This lets address go past the end of the stream one, enabling adding to the stream. + Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, GetEditedSize ()); SetNeedsDisplay (); return true; @@ -815,7 +870,8 @@ public class HexView : View, IDesignable _firstNibble = true; } - if (Address < _source.Length - 1) + // This lets address go past the end of the stream one, enabling adding to the stream. + if (Address < GetEditedSize ()) { Address++; } @@ -833,6 +889,18 @@ public class HexView : View, IDesignable return true; } + private long GetEditedSize () + { + if (_edits.Count == 0) + { + return _source!.Length; + } + + long maxEditAddress = _edits.Keys.Max (); + + return Math.Max (_source!.Length, maxEditAddress + 1); + } + private bool MoveLeftStart () { Address = Address / BytesPerLine * BytesPerLine; @@ -876,23 +944,30 @@ public class HexView : View, IDesignable SetNeedsDisplay (new (0, line, Viewport.Width, 1)); } - private bool Navigate (NavigationDirection direction) + /// + protected override bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) { - switch (direction) + if (behavior is { } && behavior != TabStop) { - case NavigationDirection.Forward: - _leftSideHasFocus = !_leftSideHasFocus; - RedisplayLine (Address); - _firstNibble = true; + return false; + } - return true; + if (direction == NavigationDirection.Forward && _leftSideHasFocus) + { + _leftSideHasFocus = !_leftSideHasFocus; + RedisplayLine (Address); + _firstNibble = true; - case NavigationDirection.Backward: - _leftSideHasFocus = !_leftSideHasFocus; - RedisplayLine (Address); - _firstNibble = true; + return true; + } - return true; + if (direction == NavigationDirection.Backward && !_leftSideHasFocus) + { + _leftSideHasFocus = !_leftSideHasFocus; + RedisplayLine (Address); + _firstNibble = true; + + return true; } return false; diff --git a/Terminal.Gui/Views/HexViewEventArgs.cs b/Terminal.Gui/Views/HexViewEventArgs.cs index f3f6ddb92..49586aac9 100644 --- a/Terminal.Gui/Views/HexViewEventArgs.cs +++ b/Terminal.Gui/Views/HexViewEventArgs.cs @@ -13,20 +13,20 @@ public class HexViewEventArgs : EventArgs { /// Initializes a new instance of /// The byte position in the steam. - /// The cursor position. + /// The edit position. /// Line bytes length. - public HexViewEventArgs (long address, Point cursor, int lineLength) + public HexViewEventArgs (long address, Point position, int lineLength) { Address = address; - CursorPosition = cursor; + Position = position; BytesPerLine = lineLength; } /// The bytes length per line. public int BytesPerLine { get; private set; } - /// Gets the current cursor position starting at one for both, line and column. - public Point CursorPosition { get; private set; } + /// Gets the current edit position. + public Point Position { get; private set; } /// Gets the byte position in the . public long Address { get; private set; } @@ -47,6 +47,6 @@ public class HexViewEditEventArgs : EventArgs /// Gets the new value for that . public byte NewValue { get; } - /// Gets the adress of the edit in the stream. + /// Gets the address of the edit in the stream. public long Address { get; } } diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index 096c83e6c..134f8d410 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -39,7 +39,7 @@ public class HexEditor : Scenario Width = Dim.Fill (), Height = Dim.Fill (1), Title = _fileName ?? "Untitled", - BorderStyle = LineStyle.Rounded + BorderStyle = LineStyle.Rounded, }; _hexView.Edited += _hexView_Edited; _hexView.PositionChanged += _hexView_PositionChanged; @@ -161,7 +161,7 @@ public class HexEditor : Scenario _scInfo.Title = $"Bytes: {_hexView.Source!.Length}"; _scPosition.Title = - $"L: {obj.CursorPosition.Y} C: {obj.CursorPosition.X} Per Line: {obj.BytesPerLine}"; + $"L: {obj.Position.Y} C: {obj.Position.X} Per Line: {obj.BytesPerLine}"; if (_scAddress.CommandView is NumericUpDown addrNumericUpDown) { diff --git a/UICatalog/Scenarios/Text.cs b/UICatalog/Scenarios/Text.cs index 9c5ef0979..84d6fd01c 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -177,7 +177,7 @@ public class Text : Scenario new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!")) ) { - X = Pos.Right (label) + 1, Y = Pos.Bottom (chxMultiline) + 1, Width = Dim.Percent (50) - 1, Height = Dim.Percent (30) + X = Pos.Right (label) + 1, Y = Pos.Bottom (chxMultiline) + 1, Width = Dim.Percent (50) - 1, Height = Dim.Percent (30), }; win.Add (hexEditor); diff --git a/UnitTests/View/Navigation/NavigationTests.cs b/UnitTests/View/Navigation/NavigationTests.cs index 12b49c129..e0b666c7f 100644 --- a/UnitTests/View/Navigation/NavigationTests.cs +++ b/UnitTests/View/Navigation/NavigationTests.cs @@ -60,13 +60,6 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews case TabBehavior.NoStop: case TabBehavior.TabGroup: Application.OnKeyDown (key); - - if (view.HasFocus) - { - // Try once more (HexView) - Application.OnKeyDown (key); - } - break; default: Application.OnKeyDown (Key.Tab); @@ -78,12 +71,11 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews { left = true; _output.WriteLine ($"{view.GetType ().Name} - {key} Left."); - view.SetFocus (); - } - else - { - _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); + + break; } + + _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); } top.Dispose (); diff --git a/UnitTests/Views/HexViewTests.cs b/UnitTests/Views/HexViewTests.cs index d6f4e00a4..6c43ad229 100644 --- a/UnitTests/Views/HexViewTests.cs +++ b/UnitTests/Views/HexViewTests.cs @@ -8,66 +8,58 @@ public class HexViewTests { [Theory] [InlineData (0, 4)] - [InlineData (9, 4)] - [InlineData (20, 4)] - [InlineData (24, 4)] - [InlineData (30, 4)] - [InlineData (31, 4)] - [InlineData (32, 4)] - [InlineData (33, 4)] - [InlineData (34, 4)] + [InlineData (4, 4)] + [InlineData (8, 4)] [InlineData (35, 4)] - [InlineData (36, 4)] - [InlineData (37, 4)] - [InlineData (50, 8)] - [InlineData (51, 8)] + [InlineData (36, 8)] + [InlineData (37, 8)] + [InlineData (41, 8)] + [InlineData (54, 12)] + [InlineData (55, 12)] + [InlineData (71, 12)] + [InlineData (72, 16)] + [InlineData (73, 16)] public void BytesPerLine_Calculates_Correctly (int width, int expectedBpl) { - var hv = new HexView (LoadStream (null, out long _)) { Width = width, Height = 10 }; + var hv = new HexView (LoadStream (null, out long _)) { Width = width, Height = 10, AddressWidth = 0 }; 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 (null, out _, true)) { Width = 20, Height = 20 }; + Application.Navigation = new ApplicationNavigation (); + Application.Top = new Toplevel (); + Application.Top.Add (hv); + Application.Top.SetFocus (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); + Assert.True (Application.OnKeyDown (Key.Tab)); // Move to left side + Assert.Empty (hv.Edits); hv.AllowEdits = false; - Assert.True (hv.NewKeyDownEvent (Key.Home)); - Assert.False (hv.NewKeyDownEvent (Key.A)); + Assert.True (Application.OnKeyDown (Key.Home)); + Assert.False (Application.OnKeyDown (Key.A)); Assert.Empty (hv.Edits); - Assert.Equal (126, hv.Source.Length); + Assert.Equal (126, hv.Source!.Length); hv.AllowEdits = true; - Assert.True (hv.NewKeyDownEvent (Key.D4)); - Assert.True (hv.NewKeyDownEvent (Key.D1)); + Assert.True (Application.OnKeyDown (Key.D4)); + Assert.True (Application.OnKeyDown (Key.D1)); 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.NewKeyDownEvent (Key.End)); - Assert.True (hv.NewKeyDownEvent (Key.D4)); - Assert.True (hv.NewKeyDownEvent (Key.D2)); + Assert.True (Application.OnKeyDown (Key.End)); + Assert.True (Application.OnKeyDown (Key.D4)); + Assert.True (Application.OnKeyDown (Key.D2)); Assert.Equal (2, hv.Edits.Count); Assert.Equal (66, hv.Edits.ToList () [1].Value); Assert.Equal ('B', (char)hv.Edits.ToList () [1].Value); @@ -76,11 +68,18 @@ public class HexViewTests hv.ApplyEdits (); Assert.Empty (hv.Edits); Assert.Equal (127, hv.Source.Length); + + Application.Top.Dispose (); + Application.ResetState (true); + } [Fact] public void ApplyEdits_With_Argument () { + Application.Navigation = new ApplicationNavigation (); + Application.Top = new Toplevel (); + byte [] buffer = Encoding.Default.GetBytes ("Fest"); var original = new MemoryStream (); original.Write (buffer, 0, buffer.Length); @@ -90,28 +89,40 @@ public class HexViewTests original.CopyTo (copy); copy.Flush (); var hv = new HexView (copy) { Width = Dim.Fill (), Height = Dim.Fill () }; + Application.Top.Add (hv); + Application.Top.SetFocus (); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubviews (); - var readBuffer = new byte [hv.Source.Length]; + var readBuffer = new byte [hv.Source!.Length]; hv.Source.Position = 0; hv.Source.Read (readBuffer); Assert.Equal ("Fest", Encoding.Default.GetString (readBuffer)); - Assert.True (hv.NewKeyDownEvent (Key.D5)); - Assert.True (hv.NewKeyDownEvent (Key.D4)); + Assert.True (Application.OnKeyDown (Key.Tab)); // Move to left side + Assert.True (Application.OnKeyDown (Key.D5)); + Assert.True (Application.OnKeyDown (Key.D4)); readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value; Assert.Equal ("Test", Encoding.Default.GetString (readBuffer)); + Assert.True (Application.OnKeyDown (Key.Tab)); // Move to right side + Assert.True (Application.OnKeyDown (Key.CursorLeft)); + Assert.True (Application.OnKeyDown (Key.Z.WithShift)); + readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value; + Assert.Equal ("Zest", 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 ("Zest", Encoding.Default.GetString (buffer)); + Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer)); Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer)); + + Application.Top.Dispose (); + Application.ResetState (true); } [Fact] @@ -131,67 +142,76 @@ public class HexViewTests } [Fact] - public void CursorPosition_Encoding_Default () + public void Position_Encoding_Default () { + Application.Navigation = new ApplicationNavigation (); + var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 }; Application.Top = new Toplevel (); Application.Top.Add (hv); Application.Top.LayoutSubviews (); - Assert.Equal (new (0, 0), hv.CursorPosition); + Assert.Equal (63, hv.Source!.Length); Assert.Equal (20, hv.BytesPerLine); - Assert.True (hv.NewKeyDownEvent (Key.Tab)); - Assert.Equal (new (0, 0), hv.CursorPosition); + Assert.Equal (new (0, 0), hv.Position); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl)); - Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine - 1); - Assert.True (hv.NewKeyDownEvent (Key.Home)); + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.Equal (new (0, 0), hv.Position); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (new (1, 0), hv.CursorPosition); + Assert.True (Application.OnKeyDown (Key.CursorRight.WithCtrl)); + Assert.Equal (hv.BytesPerLine - 1, hv.Position.X); - Assert.True (hv.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (new (1, 1), hv.CursorPosition); + Assert.True (Application.OnKeyDown (Key.Home)); - 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 * (hv.BytesPerLine - col); - Assert.Equal (hv.Address, col * line + offset); + Assert.True (Application.OnKeyDown (Key.CursorRight)); + Assert.Equal (new (1, 0), hv.Position); + + Assert.True (Application.OnKeyDown (Key.CursorDown)); + Assert.Equal (new (1, 1), hv.Position); + + Assert.True (Application.OnKeyDown (Key.End)); + Assert.Equal (new (3, 3), hv.Position); + + Assert.Equal (hv.Source!.Length, hv.Address); Application.Top.Dispose (); Application.ResetState (true); } [Fact] - public void CursorPosition_Encoding_Unicode () + public void Position_Encoding_Unicode () { - var hv = new HexView (LoadStream (null, out _, true)) { Width = Dim.Fill (), Height = Dim.Fill () }; + Application.Navigation = new ApplicationNavigation (); + + var hv = new HexView (LoadStream (null, out _, unicode: true)) { Width = 100, Height = 100 }; Application.Top = new Toplevel (); Application.Top.Add (hv); hv.LayoutSubviews (); - Assert.Equal (new (0, 0), hv.CursorPosition); + Assert.Equal (126, hv.Source!.Length); + Assert.Equal (20, hv.BytesPerLine); - Assert.True (hv.NewKeyDownEvent (Key.Tab)); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl)); - Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine); - Assert.True (hv.NewKeyDownEvent (Key.Home)); + Assert.Equal (new (0, 0), hv.Position); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (new (2, 1), hv.CursorPosition); + Assert.True (Application.OnKeyDown (Key.Tab)); - Assert.True (hv.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (new (2, 2), hv.CursorPosition); + Assert.True (Application.OnKeyDown (Key.CursorRight.WithCtrl)); + Assert.Equal (hv.BytesPerLine - 1, hv.Position.X); - Assert.True (hv.NewKeyDownEvent (Key.End)); - int col = hv.CursorPosition.X; - int line = hv.CursorPosition.Y; - int offset = (line - 1) * (hv.BytesPerLine - col); - Assert.Equal (hv.Address, col * line + offset); + Assert.True (Application.OnKeyDown (Key.Home)); + + Assert.True (Application.OnKeyDown (Key.CursorRight)); + Assert.Equal (new (1, 0), hv.Position); + + Assert.True (Application.OnKeyDown (Key.CursorDown)); + Assert.Equal (new (1, 1), hv.Position); + + Assert.True (Application.OnKeyDown (Key.End)); + Assert.Equal (new (6, 6), hv.Position); + + Assert.Equal (hv.Source!.Length, hv.Address); Application.Top.Dispose (); Application.ResetState (true); } @@ -209,7 +229,7 @@ public class HexViewTests 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); + Assert.Equal (126, hv.Source!.Length); hv.DiscardEdits (); Assert.Empty (hv.Edits); @@ -227,7 +247,7 @@ public class HexViewTests Assert.True (hv.NewKeyDownEvent (Key.PageDown)); Assert.Equal (4 * hv.Frame.Height, hv.DisplayStart); - Assert.Equal (hv.Source.Length, hv.Source.Position); + Assert.Equal (hv.Source!.Length, hv.Source.Position); Assert.True (hv.NewKeyDownEvent (Key.End)); @@ -265,127 +285,59 @@ public class HexViewTests [Fact] public void KeyBindings_Test_Movement_LeftSide () { - var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; + Application.Navigation = new ApplicationNavigation (); Application.Top = new Toplevel (); + var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; Application.Top.Add (hv); hv.LayoutSubviews (); - Assert.Equal (MEM_STRING_LENGTH, hv.Source.Length); + 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)); + // Default internal focus is on right side. Move back to left. + Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); + Assert.True (Application.OnKeyDown (Key.CursorRight)); Assert.Equal (1, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorLeft)); + Assert.True (Application.OnKeyDown (Key.CursorLeft)); Assert.Equal (0, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.Equal (4, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorUp)); + Assert.True (Application.OnKeyDown (Key.CursorUp)); Assert.Equal (0, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.PageDown)); + Assert.True (Application.OnKeyDown (Key.PageDown)); Assert.Equal (40, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.PageUp)); + Assert.True (Application.OnKeyDown (Key.PageUp)); Assert.Equal (0, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.End)); + Assert.True (Application.OnKeyDown (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.Home)); + Assert.True (Application.OnKeyDown (Key.Home)); Assert.Equal (0, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.CursorRight.WithCtrl)); Assert.Equal (3, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorLeft.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.CursorLeft.WithCtrl)); Assert.Equal (0, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorDown.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.CursorDown.WithCtrl)); Assert.Equal (36, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorUp.WithCtrl)); + Assert.True (Application.OnKeyDown (Key.CursorUp.WithCtrl)); Assert.Equal (0, hv.Address); Application.Top.Dispose (); Application.ResetState (true); } - [Fact] - public void Position_Using_Encoding_Default () - { - 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 (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 (MEM_STRING_LENGTH, hv.Source.Position); - Assert.Equal (1, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - 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 (MEM_STRING_LENGTH, hv.Source.Position); - Assert.Equal (2, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorLeft)); - 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 (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 (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.Address); - - // left side needed to press twice - Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (126, hv.Source.Position); - Assert.Equal (1, hv.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (126, hv.Source.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.Address); - Assert.True (hv.NewKeyDownEvent (Key.CursorLeft)); - Assert.Equal (126, hv.Source.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.Address); - Assert.Equal (hv.Address - 1, hv.Source.Length); - } - [Fact] public void PositionChanged_Event () { @@ -395,18 +347,18 @@ public class HexViewTests Application.Top.LayoutSubviews (); - HexViewEventArgs hexViewEventArgs = null; + HexViewEventArgs hexViewEventArgs = null!; hv.PositionChanged += (s, e) => hexViewEventArgs = e; - Assert.Equal (12, hv.BytesPerLine); + Assert.Equal (4, hv.BytesPerLine); Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); // left side must press twice Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); Assert.True (hv.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (12, hexViewEventArgs.BytesPerLine); - Assert.Equal (new (2, 2), hexViewEventArgs.CursorPosition); - Assert.Equal (14, hexViewEventArgs.Address); + Assert.Equal (4, hexViewEventArgs.BytesPerLine); + Assert.Equal (new (1, 1), hexViewEventArgs.Position); + Assert.Equal (5, hexViewEventArgs.Address); Application.Top.Dispose (); Application.ResetState (true); } @@ -421,27 +373,27 @@ public class HexViewTests hv.LayoutSubviews (); Assert.True (hv.NewKeyDownEvent (Key.End)); - Assert.Equal (62, hv.DisplayStart); - Assert.Equal (64, hv.Address); + Assert.Equal (MEM_STRING_LENGTH - 1, hv.DisplayStart); + Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (0, hv.Address - 1); + Assert.Equal (0, hv.Address); hv.Source = LoadStream (null, out _); hv.Width = Dim.Fill (); hv.Height = Dim.Fill (); Application.Top.LayoutSubviews (); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (0, hv.Address - 1); + Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (64, hv.Address); + Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); Assert.Equal (0, hv.DisplayStart); - Assert.Equal (0, hv.Address - 1); + Assert.Equal (0, hv.Address); Application.Top.Dispose (); Application.ResetState (true); } diff --git a/docfx/docs/cursor.md b/docfx/docs/cursor.md index 7b6de830f..8e7299396 100644 --- a/docfx/docs/cursor.md +++ b/docfx/docs/cursor.md @@ -4,19 +4,19 @@ See end for list of issues this design addresses. ## Tenets for Cursor Support (Unless you know better ones...) -1. **More GUI than Command Line**. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keybaord import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a `ColorPicker`). Terminal.Gui's cursor system is biased towards the broader TUI experiences. +1. **More GUI than Command Line**. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keyboard import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a `ColorPicker`). Terminal.Gui's cursor system is biased towards the broader TUI experiences. 2. **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and the cursor should behave in a way consistent with the terminal. ## Lexicon & Taxonomy - Navigation - Refers to the user-experience for moving Focus between views in the application view-hierarchy. See [Navigation](navigation.md) for a deep-dive. -- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-heirachy in an applicstion can have focus (`view.HasFocus == true`), and there is only one View in a focused heirarchy that is the most-focused; the one recieving keyboard input. See [Navigation](navigation.md) for a deep-dive. -- Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal sesssion. +- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-hexarchy in an application can have focus (`view.HasFocus == true`), and there is only one View in a focused hierarchy that is the most-focused; the one receiving keyboard input. See [Navigation](navigation.md) for a deep-dive. +- Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal session. - Cursor Location - The top-left corner of the Cursor. In text entry scenarios, new text will be inserted to the left/top of the Cursor Location. - Cursor Size - The width and height of the cursor. Currently the size is limited to 1x1. - Cursor Style - How the cursor renders. Some terminals support various cursor styles such as Block and Underline. -- Cursor Visibilty - Whether the cursor is visible to the user or not. NOTE: Some ConsoleDrivers overload Cursor Style and Cursor Visibility, making "invisible" a style. Terminal.Gui HIDES this from developers and changing the visibilty of the cursor does NOT change the style. +- Cursor Visibility - Whether the cursor is visible to the user or not. NOTE: Some ConsoleDrivers overload Cursor Style and Cursor Visibility, making "invisible" a style. Terminal.Gui HIDES this from developers and changing the visibility of the cursor does NOT change the style. - Caret - Visual indicator that where text entry will occur. - Selection - A visual indicator to the user that something is selected. It is common for the Selection and Cursor to be the same. It is also common for the Selection and Cursor to be distinct. In a `ListView` the Cursor and Selection (`SelectedItem`) are the same, but the `Cursor` is not visible. In a `TextView` with text selected, the `Cursor` is at either the start or end of the `Selection`. A `TableView' supports mutliple things being selected at once.