mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-28 00:38:00 +01:00
778 lines
22 KiB
C#
778 lines
22 KiB
C#
//
|
|
// HexView.cs: A hexadecimal viewer
|
|
//
|
|
// TODO:
|
|
// - Support searching and highlighting of the search result
|
|
// - Bug showing the last line
|
|
//
|
|
|
|
namespace Terminal.Gui;
|
|
|
|
/// <summary>An hex viewer and editor <see cref="View"/> over a <see cref="System.IO.Stream"/></summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <see cref="HexView"/> provides a hex editor on top of a seekable <see cref="Stream"/> with the left side
|
|
/// showing an hex dump of the values in the <see cref="Stream"/> and the right side showing the contents (filtered
|
|
/// to non-control sequence ASCII characters).
|
|
/// </para>
|
|
/// <para>Users can switch from one side to the other by using the tab key.</para>
|
|
/// <para>
|
|
/// To enable editing, set <see cref="AllowEdits"/> to true. When <see cref="AllowEdits"/> is true the user can
|
|
/// make changes to the hexadecimal values of the <see cref="Stream"/>. Any changes are tracked in the
|
|
/// <see cref="Edits"/> property (a <see cref="SortedDictionary{TKey, TValue}"/>) indicating the position where the
|
|
/// changes were made and the new values. A convenience method, <see cref="ApplyEdits"/> will apply the edits to
|
|
/// the <see cref="Stream"/>.
|
|
/// </para>
|
|
/// <para>Control the first byte shown by setting the <see cref="DisplayStart"/> property to an offset in the stream.</para>
|
|
/// </remarks>
|
|
public class HexView : View
|
|
{
|
|
private const int bsize = 4;
|
|
private const int displayWidth = 9;
|
|
|
|
private int bpl;
|
|
private long displayStart, pos;
|
|
private SortedDictionary<long, byte> edits = [];
|
|
private bool firstNibble, leftSide;
|
|
private Stream source;
|
|
private static readonly Rune SpaceCharRune = new (' ');
|
|
private static readonly Rune PeriodCharRune = new ('.');
|
|
|
|
/// <summary>Initializes a <see cref="HexView"/> class.</summary>
|
|
/// <param name="source">
|
|
/// The <see cref="Stream"/> to view and edit as hex, this <see cref="Stream"/> must support seeking,
|
|
/// or an exception will be thrown.
|
|
/// </param>
|
|
public HexView (Stream source)
|
|
{
|
|
Source = source;
|
|
// BUG: This will always call the most-derived definition of CanFocus.
|
|
// Either seal it or don't set it here.
|
|
CanFocus = true;
|
|
CursorVisibility = CursorVisibility.Default;
|
|
leftSide = true;
|
|
firstNibble = true;
|
|
|
|
// 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
|
|
AddCommand (Command.Left, () => MoveLeft ());
|
|
AddCommand (Command.Right, () => MoveRight ());
|
|
AddCommand (Command.LineDown, () => MoveDown (bytesPerLine));
|
|
AddCommand (Command.LineUp, () => MoveUp (bytesPerLine));
|
|
AddCommand (Command.Accept, () => ToggleSide ());
|
|
AddCommand (Command.PageUp, () => MoveUp (bytesPerLine * Frame.Height));
|
|
AddCommand (Command.PageDown, () => MoveDown (bytesPerLine * Frame.Height));
|
|
AddCommand (Command.TopHome, () => MoveHome ());
|
|
AddCommand (Command.BottomEnd, () => MoveEnd ());
|
|
AddCommand (Command.StartOfLine, () => MoveStartOfLine ());
|
|
AddCommand (Command.EndOfLine, () => MoveEndOfLine ());
|
|
AddCommand (Command.StartOfPage, () => MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine)));
|
|
|
|
AddCommand (
|
|
Command.EndOfPage,
|
|
() => MoveDown (bytesPerLine * (Frame.Height - 1 - (int)(position - displayStart) / bytesPerLine))
|
|
);
|
|
|
|
// Default keybindings for this view
|
|
KeyBindings.Add (Key.CursorLeft, Command.Left);
|
|
KeyBindings.Add (Key.CursorRight, Command.Right);
|
|
KeyBindings.Add (Key.CursorDown, Command.LineDown);
|
|
KeyBindings.Add (Key.CursorUp, Command.LineUp);
|
|
KeyBindings.Add (Key.Enter, Command.Accept);
|
|
|
|
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.TopHome);
|
|
KeyBindings.Add (Key.End, Command.BottomEnd);
|
|
KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.StartOfLine);
|
|
KeyBindings.Add (Key.CursorRight.WithCtrl, Command.EndOfLine);
|
|
KeyBindings.Add (Key.CursorUp.WithCtrl, Command.StartOfPage);
|
|
KeyBindings.Add (Key.CursorDown.WithCtrl, Command.EndOfPage);
|
|
|
|
LayoutComplete += HexView_LayoutComplete;
|
|
}
|
|
|
|
/// <summary>Initializes a <see cref="HexView"/> class.</summary>
|
|
public HexView () : this (new MemoryStream ()) { }
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether this <see cref="HexView"/> allow 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>
|
|
public bool AllowEdits { get; set; } = true;
|
|
|
|
/// <summary>The bytes length per line.</summary>
|
|
public int BytesPerLine => bytesPerLine;
|
|
|
|
/// <summary>Gets the current cursor position starting at one for both, line and column.</summary>
|
|
public Point CursorPosition
|
|
{
|
|
get
|
|
{
|
|
if (!IsInitialized)
|
|
{
|
|
return Point.Empty;
|
|
}
|
|
|
|
var delta = (int)position;
|
|
int line = delta / bytesPerLine + 1;
|
|
int item = delta % bytesPerLine + 1;
|
|
|
|
return new Point (item, line);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or gets the offset into the <see cref="Stream"/> that will be displayed at the top of the
|
|
/// <see cref="HexView"/>
|
|
/// </summary>
|
|
/// <value>The display start.</value>
|
|
public long DisplayStart
|
|
{
|
|
get => displayStart;
|
|
set
|
|
{
|
|
position = value;
|
|
|
|
SetDisplayStart (value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="SortedDictionary{TKey, TValue}"/> describing the edits done to the <see cref="HexView"/>.
|
|
/// Each Key indicates an offset where an edit was made and the Value is the changed byte.
|
|
/// </summary>
|
|
/// <value>The edits.</value>
|
|
public IReadOnlyDictionary<long, byte> Edits => edits;
|
|
|
|
/// <summary>Gets the current character position starting at one, related to the <see cref="Stream"/>.</summary>
|
|
public long Position => position + 1;
|
|
|
|
/// <summary>
|
|
/// Sets or gets the <see cref="Stream"/> the <see cref="HexView"/> is operating on; the stream must support
|
|
/// seeking ( <see cref="Stream.CanSeek"/> == true).
|
|
/// </summary>
|
|
/// <value>The source.</value>
|
|
public Stream Source
|
|
{
|
|
get => source;
|
|
set
|
|
{
|
|
if (value is null)
|
|
{
|
|
throw new ArgumentNullException ("source");
|
|
}
|
|
|
|
if (!value.CanSeek)
|
|
{
|
|
throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source");
|
|
}
|
|
|
|
source = value;
|
|
|
|
if (displayStart > source.Length)
|
|
{
|
|
DisplayStart = 0;
|
|
}
|
|
|
|
if (position > source.Length)
|
|
{
|
|
position = 0;
|
|
}
|
|
|
|
SetNeedsDisplay ();
|
|
}
|
|
}
|
|
|
|
private int bytesPerLine
|
|
{
|
|
get => bpl;
|
|
set
|
|
{
|
|
bpl = value;
|
|
OnPositionChanged ();
|
|
}
|
|
}
|
|
|
|
private long position
|
|
{
|
|
get => pos;
|
|
set
|
|
{
|
|
pos = value;
|
|
OnPositionChanged ();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method applies and edits made to the <see cref="Stream"/> and resets the contents of the
|
|
/// <see cref="Edits"/> property.
|
|
/// </summary>
|
|
/// <param name="stream">If provided also applies the changes to the passed <see cref="Stream"/></param>
|
|
/// .
|
|
public void ApplyEdits (Stream stream = null)
|
|
{
|
|
foreach (KeyValuePair<long, byte> kv in edits)
|
|
{
|
|
source.Position = kv.Key;
|
|
source.WriteByte (kv.Value);
|
|
source.Flush ();
|
|
|
|
if (stream is { })
|
|
{
|
|
stream.Position = kv.Key;
|
|
stream.WriteByte (kv.Value);
|
|
stream.Flush ();
|
|
}
|
|
}
|
|
|
|
edits = new SortedDictionary<long, byte> ();
|
|
SetNeedsDisplay ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method discards the edits made to the <see cref="Stream"/> by resetting the contents of the
|
|
/// <see cref="Edits"/> property.
|
|
/// </summary>
|
|
public void DiscardEdits () { edits = new SortedDictionary<long, byte> (); }
|
|
|
|
/// <summary>Event to be invoked when an edit is made on the <see cref="Stream"/>.</summary>
|
|
public event EventHandler<HexViewEditEventArgs> Edited;
|
|
|
|
/// <inheritdoc/>
|
|
protected internal override bool OnMouseEvent (MouseEvent me)
|
|
{
|
|
if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
|
|
&& !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
|
|
&& !me.Flags.HasFlag (MouseFlags.WheeledDown)
|
|
&& !me.Flags.HasFlag (MouseFlags.WheeledUp))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!HasFocus)
|
|
{
|
|
SetFocus ();
|
|
}
|
|
|
|
if (me.Flags == MouseFlags.WheeledDown)
|
|
{
|
|
DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (me.Flags == MouseFlags.WheeledUp)
|
|
{
|
|
DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (me.Position.X < displayWidth)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
int nblocks = bytesPerLine / bsize;
|
|
int blocksSize = nblocks * 14;
|
|
int blocksRightOffset = displayWidth + blocksSize - 1;
|
|
|
|
if (me.Position.X > blocksRightOffset + bytesPerLine - 1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
leftSide = me.Position.X >= blocksRightOffset;
|
|
long lineStart = me.Position.Y * bytesPerLine + displayStart;
|
|
int x = me.Position.X - displayWidth + 1;
|
|
int block = x / 14;
|
|
x -= block * 2;
|
|
int empty = x % 3;
|
|
int item = x / 3;
|
|
|
|
if (!leftSide && item > 0 && (empty == 0 || x == block * 14 + 14 - 1 - block * 2))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
firstNibble = true;
|
|
|
|
if (leftSide)
|
|
{
|
|
position = Math.Min (lineStart + me.Position.X - blocksRightOffset, source.Length);
|
|
}
|
|
else
|
|
{
|
|
position = Math.Min (lineStart + item, source.Length);
|
|
}
|
|
|
|
if (me.Flags == MouseFlags.Button1DoubleClicked)
|
|
{
|
|
leftSide = !leftSide;
|
|
|
|
if (leftSide)
|
|
{
|
|
firstNibble = empty == 1;
|
|
}
|
|
else
|
|
{
|
|
firstNibble = true;
|
|
}
|
|
}
|
|
|
|
SetNeedsDisplay ();
|
|
|
|
return true;
|
|
}
|
|
|
|
///<inheritdoc/>
|
|
public override void OnDrawContent (Rectangle viewport)
|
|
{
|
|
Attribute currentAttribute;
|
|
Attribute current = ColorScheme.Focus;
|
|
Driver.SetAttribute (current);
|
|
Move (0, 0);
|
|
|
|
int nblocks = bytesPerLine / bsize;
|
|
var data = new byte [nblocks * bsize * viewport.Height];
|
|
Source.Position = displayStart;
|
|
int n = source.Read (data, 0, data.Length);
|
|
|
|
Attribute activeColor = ColorScheme.HotNormal;
|
|
Attribute trackingColor = ColorScheme.HotFocus;
|
|
|
|
for (var line = 0; line < viewport.Height; line++)
|
|
{
|
|
Rectangle lineRect = new (0, line, viewport.Width, 1);
|
|
|
|
if (!Viewport.Contains (lineRect))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Move (0, line);
|
|
Driver.SetAttribute (ColorScheme.HotNormal);
|
|
Driver.AddStr ($"{displayStart + line * nblocks * bsize:x8} ");
|
|
|
|
currentAttribute = ColorScheme.HotNormal;
|
|
SetAttribute (GetNormalColor ());
|
|
|
|
for (var block = 0; block < nblocks; block++)
|
|
{
|
|
for (var b = 0; b < bsize; b++)
|
|
{
|
|
int offset = line * nblocks * bsize + block * bsize + b;
|
|
byte value = GetData (data, offset, out bool edited);
|
|
|
|
if (offset + displayStart == position || edited)
|
|
{
|
|
SetAttribute (leftSide ? activeColor : trackingColor);
|
|
}
|
|
else
|
|
{
|
|
SetAttribute (GetNormalColor ());
|
|
}
|
|
|
|
Driver.AddStr (offset >= n && !edited ? " " : $"{value:x2}");
|
|
SetAttribute (GetNormalColor ());
|
|
Driver.AddRune (SpaceCharRune);
|
|
}
|
|
|
|
Driver.AddStr (block + 1 == nblocks ? " " : "| ");
|
|
}
|
|
|
|
for (var bitem = 0; bitem < nblocks * bsize; bitem++)
|
|
{
|
|
int offset = line * nblocks * bsize + bitem;
|
|
byte b = GetData (data, offset, out bool edited);
|
|
Rune c;
|
|
|
|
if (offset >= n && !edited)
|
|
{
|
|
c = SpaceCharRune;
|
|
}
|
|
else
|
|
{
|
|
if (b < 32)
|
|
{
|
|
c = PeriodCharRune;
|
|
}
|
|
else if (b > 127)
|
|
{
|
|
c = PeriodCharRune;
|
|
}
|
|
else
|
|
{
|
|
Rune.DecodeFromUtf8 (new ReadOnlySpan<byte> (ref b), out c, out _);
|
|
}
|
|
}
|
|
|
|
if (offset + displayStart == position || edited)
|
|
{
|
|
SetAttribute (leftSide ? trackingColor : activeColor);
|
|
}
|
|
else
|
|
{
|
|
SetAttribute (GetNormalColor ());
|
|
}
|
|
|
|
Driver.AddRune (c);
|
|
}
|
|
}
|
|
|
|
void SetAttribute (Attribute attribute)
|
|
{
|
|
if (currentAttribute != attribute)
|
|
{
|
|
currentAttribute = attribute;
|
|
Driver.SetAttribute (attribute);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Method used to invoke the <see cref="Edited"/> event passing the <see cref="KeyValuePair{TKey, TValue}"/>.</summary>
|
|
/// <param name="e">The key value pair.</param>
|
|
public virtual void OnEdited (HexViewEditEventArgs e) { Edited?.Invoke (this, e); }
|
|
|
|
/// <summary>
|
|
/// Method used to invoke the <see cref="PositionChanged"/> event passing the <see cref="HexViewEventArgs"/>
|
|
/// arguments.
|
|
/// </summary>
|
|
public virtual void OnPositionChanged () { PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); }
|
|
|
|
/// <inheritdoc/>
|
|
public override bool OnProcessKeyDown (Key keyEvent)
|
|
{
|
|
if (!AllowEdits)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ignore control characters and other special keys
|
|
if (keyEvent < Key.Space || keyEvent.KeyCode > KeyCode.CharMask)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (leftSide)
|
|
{
|
|
int value;
|
|
var k = (char)keyEvent.KeyCode;
|
|
|
|
if (k >= 'A' && k <= 'F')
|
|
{
|
|
value = k - 'A' + 10;
|
|
}
|
|
else if (k >= 'a' && k <= 'f')
|
|
{
|
|
value = k - 'a' + 10;
|
|
}
|
|
else if (k >= '0' && k <= '9')
|
|
{
|
|
value = k - '0';
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
byte b;
|
|
|
|
if (!edits.TryGetValue (position, out b))
|
|
{
|
|
source.Position = position;
|
|
b = (byte)source.ReadByte ();
|
|
}
|
|
|
|
RedisplayLine (position);
|
|
|
|
if (firstNibble)
|
|
{
|
|
firstNibble = false;
|
|
b = (byte)((b & 0xf) | (value << bsize));
|
|
edits [position] = b;
|
|
OnEdited (new HexViewEditEventArgs (position, edits [position]));
|
|
}
|
|
else
|
|
{
|
|
b = (byte)((b & 0xf0) | value);
|
|
edits [position] = b;
|
|
OnEdited (new HexViewEditEventArgs (position, edits [position]));
|
|
MoveRight ();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Event to be invoked when the position and cursor position changes.</summary>
|
|
public event EventHandler<HexViewEventArgs> PositionChanged;
|
|
|
|
///<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 = displayWidth + block * 14 + column + (firstNibble ? 0 : 1);
|
|
int y = line;
|
|
if (!leftSide)
|
|
{
|
|
x = displayWidth + bytesPerLine / bsize * 14 + item - 1;
|
|
}
|
|
|
|
Move (x, y);
|
|
return new (x, y);
|
|
}
|
|
|
|
internal void SetDisplayStart (long value)
|
|
{
|
|
if (value > 0 && value >= source.Length)
|
|
{
|
|
displayStart = source.Length - 1;
|
|
}
|
|
else if (value < 0)
|
|
{
|
|
displayStart = 0;
|
|
}
|
|
else
|
|
{
|
|
displayStart = value;
|
|
}
|
|
|
|
SetNeedsDisplay ();
|
|
}
|
|
|
|
//
|
|
// This is used to support editing of the buffer on a peer List<>,
|
|
// the offset corresponds to an offset relative to DisplayStart, and
|
|
// the buffer contains the contents of a screenful of data, so the
|
|
// offset is relative to the buffer.
|
|
//
|
|
//
|
|
private byte GetData (byte [] buffer, int offset, out bool edited)
|
|
{
|
|
long pos = DisplayStart + offset;
|
|
|
|
if (edits.TryGetValue (pos, out byte v))
|
|
{
|
|
edited = true;
|
|
|
|
return v;
|
|
}
|
|
|
|
edited = false;
|
|
|
|
return buffer [offset];
|
|
}
|
|
|
|
private void HexView_LayoutComplete (object sender, LayoutEventArgs e)
|
|
{
|
|
// Small buffers will just show the position, with the bsize field value (4 bytes)
|
|
bytesPerLine = bsize;
|
|
|
|
if (Viewport.Width - displayWidth > 17)
|
|
{
|
|
bytesPerLine = bsize * ((Viewport.Width - displayWidth) / 18);
|
|
}
|
|
}
|
|
|
|
private bool MoveDown (int bytes)
|
|
{
|
|
RedisplayLine (position);
|
|
|
|
if (position + bytes < source.Length)
|
|
{
|
|
position += 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;
|
|
|
|
while (p + bytesPerLine < source.Length)
|
|
{
|
|
p += bytesPerLine;
|
|
}
|
|
|
|
position = p;
|
|
}
|
|
|
|
if (position >= DisplayStart + bytesPerLine * Viewport.Height)
|
|
{
|
|
SetDisplayStart (DisplayStart + bytes);
|
|
SetNeedsDisplay ();
|
|
}
|
|
else
|
|
{
|
|
RedisplayLine (position);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveEnd ()
|
|
{
|
|
position = source.Length;
|
|
|
|
if (position >= DisplayStart + bytesPerLine * Viewport.Height)
|
|
{
|
|
SetDisplayStart (position);
|
|
SetNeedsDisplay ();
|
|
}
|
|
else
|
|
{
|
|
RedisplayLine (position);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveEndOfLine ()
|
|
{
|
|
position = Math.Min (position / bytesPerLine * bytesPerLine + bytesPerLine - 1, source.Length);
|
|
SetNeedsDisplay ();
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveHome ()
|
|
{
|
|
DisplayStart = 0;
|
|
SetNeedsDisplay ();
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveLeft ()
|
|
{
|
|
RedisplayLine (position);
|
|
|
|
if (leftSide)
|
|
{
|
|
if (!firstNibble)
|
|
{
|
|
firstNibble = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
firstNibble = false;
|
|
}
|
|
|
|
if (position == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (position - 1 < DisplayStart)
|
|
{
|
|
SetDisplayStart (displayStart - bytesPerLine);
|
|
SetNeedsDisplay ();
|
|
}
|
|
else
|
|
{
|
|
RedisplayLine (position);
|
|
}
|
|
|
|
position--;
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveRight ()
|
|
{
|
|
RedisplayLine (position);
|
|
|
|
if (leftSide)
|
|
{
|
|
if (firstNibble)
|
|
{
|
|
firstNibble = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
firstNibble = true;
|
|
}
|
|
|
|
if (position < source.Length)
|
|
{
|
|
position++;
|
|
}
|
|
|
|
if (position >= DisplayStart + bytesPerLine * Viewport.Height)
|
|
{
|
|
SetDisplayStart (DisplayStart + bytesPerLine);
|
|
SetNeedsDisplay ();
|
|
}
|
|
else
|
|
{
|
|
RedisplayLine (position);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveStartOfLine ()
|
|
{
|
|
position = position / bytesPerLine * bytesPerLine;
|
|
SetNeedsDisplay ();
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool MoveUp (int bytes)
|
|
{
|
|
RedisplayLine (position);
|
|
|
|
if (position - bytes > -1)
|
|
{
|
|
position -= bytes;
|
|
}
|
|
|
|
if (position < DisplayStart)
|
|
{
|
|
SetDisplayStart (DisplayStart - bytes);
|
|
SetNeedsDisplay ();
|
|
}
|
|
else
|
|
{
|
|
RedisplayLine (position);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void RedisplayLine (long pos)
|
|
{
|
|
var delta = (int)(pos - DisplayStart);
|
|
int line = delta / bytesPerLine;
|
|
|
|
SetNeedsDisplay (new (0, line, Viewport.Width, 1));
|
|
}
|
|
|
|
private bool ToggleSide ()
|
|
{
|
|
leftSide = !leftSide;
|
|
RedisplayLine (position);
|
|
firstNibble = true;
|
|
|
|
return true;
|
|
}
|
|
}
|