Fixed #3786 - Focus

This commit is contained in:
Tig
2024-10-09 18:06:41 -06:00
parent 675b8bba2b
commit 573d6ee0ff
11 changed files with 547 additions and 372 deletions

View File

@@ -77,6 +77,7 @@ public class ApplicationNavigation
{
return;
}
Debug.Assert (value is null or { CanFocus: true, HasFocus: true });
_focused = value;

View File

@@ -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 ();

View File

@@ -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.
/// </summary>
/// <param name="previousFocusedView">
/// The previously focused view. If <see langword="null"/> there is no previously focused
/// <param name="currentFocusedView">
/// The currently focused view. If <see langword="null"/> there is no previously focused
/// view.
/// </param>
/// <param name="traversingUp"></param>
/// <returns><see langword="true"/> if <see cref="HasFocus"/> was changed to <see langword="true"/>.</returns>
/// <exception cref="InvalidOperationException"></exception>
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.
/// </param>
/// <param name="traversingDown">
/// 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().
/// </param>
/// <exception cref="InvalidOperationException"></exception>
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);

View File

@@ -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;
/// <summary>An hex viewer and editor <see cref="View"/> over a <see cref="System.IO.Stream"/></summary>
/// <summary>An hex viewer and editor <see cref="View"/> over a <see cref="Stream"/></summary>
/// <remarks>
/// <para>
/// <see cref="HexView"/> provides a hex editor on top of a seekable <see cref="Stream"/> with the left side
@@ -28,11 +30,12 @@ namespace Terminal.Gui;
/// </remarks>
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 ()) { }
/// <summary>
/// Gets or sets whether this <see cref="HexView"/> allow editing of the <see cref="Stream"/> of the underlying
/// Gets or sets whether this <see cref="HexView"/> allows editing of the <see cref="Stream"/> of the underlying
/// <see cref="Stream"/>.
/// </summary>
/// <value><c>true</c> if allow edits; otherwise, <c>false</c>.</value>
/// <value><c>true</c> to allow edits; otherwise, <c>false</c>.</value>
public bool AllowEdits { get; set; } = true;
/// <summary>Gets the current cursor position starting at one for both, line and column.</summary>
/// <summary>Gets the current cursor position.</summary>
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);
}
}
}
///<inheritdoc/>
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<long, byte> _edits = [];
/// <summary>
@@ -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?
/// <summary>Gets the current character position starting at one, related to the <see cref="Stream"/>.</summary>
public long Position => position + 1;
private long position
/// <summary>Gets or sets the current byte position in the <see cref="Stream"/>.</summary>
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;
/// <summary>
/// Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8.
/// </summary>
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
/// <summary>Raises the <see cref="PositionChanged"/> event.</summary>
protected void RaisePositionChanged ()
{
HexViewEventArgs args = new (Position, CursorPosition, BytesPerLine);
HexViewEventArgs args = new (Address, CursorPosition, BytesPerLine);
OnPositionChanged (args);
PositionChanged?.Invoke (this, args);
}
/// <summary>
/// Called when <see cref="Position"/> has changed.
/// Called when <see cref="Address"/> has changed.
/// </summary>
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;
}
///<inheritdoc/>
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;

View File

@@ -12,12 +12,12 @@ namespace Terminal.Gui;
public class HexViewEventArgs : EventArgs
{
/// <summary>Initializes a new instance of <see cref="HexViewEventArgs"/></summary>
/// <param name="pos">The character position.</param>
/// <param name="address">The byte position in the steam.</param>
/// <param name="cursor">The cursor position.</param>
/// <param name="lineLength">Line bytes length.</param>
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
/// <summary>Gets the current cursor position starting at one for both, line and column.</summary>
public Point CursorPosition { get; private set; }
/// <summary>Gets the current character position starting at one, related to the <see cref="Stream"/>.</summary>
public long Position { get; private set; }
/// <summary>Gets the byte position in the <see cref="Stream"/>.</summary>
public long Address { get; private set; }
}
/// <summary>Defines the event arguments for <see cref="HexView.Edited"/> event.</summary>
public class HexViewEditEventArgs : EventArgs
{
/// <summary>Creates a new instance of the <see cref="HexViewEditEventArgs"/> class.</summary>
/// <param name="position"></param>
/// <param name="address"></param>
/// <param name="newValue"></param>
public HexViewEditEventArgs (long position, byte newValue)
public HexViewEditEventArgs (long address, byte newValue)
{
Position = position;
Address = address;
NewValue = newValue;
}
/// <summary>Gets the new value for that <see cref="Position"/>.</summary>
/// <summary>Gets the new value for that <see cref="Address"/>.</summary>
public byte NewValue { get; }
/// <summary>Gets the location of the edit.</summary>
public long Position { get; }
/// <summary>Gets the adress of the edit in the stream.</summary>
public long Address { get; }
}

View File

@@ -58,7 +58,7 @@ public class Shortcut : View, IOrientation, IDesignable
/// </para>
/// </remarks>
/// <param name="targetView">
/// The View that <paramref name="command"/> will be invoked when user does something that causes the Shortcut's Accept
/// The View that <paramref name="command"/> will be invoked on when user does something that causes the Shortcut's Accept
/// event to be raised.
/// </param>
/// <param name="command">

View File

@@ -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<long> addressUpDown = new NumericUpDown<long>
{
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<long> 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 ()

View File

@@ -387,6 +387,7 @@ public class UICatalogApp
// 'app' closed cleanly.
foreach (Responder? inst in Responder.Instances)
{
Debug.Assert (inst.WasDisposed);
}

View File

@@ -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

View File

@@ -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<long, byte> 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); }
}
}

View File

@@ -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));