HexView.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. //
  2. // HexView.cs: A hexadecimal viewer
  3. //
  4. // TODO:
  5. // - Support searching and highlighting of the search result
  6. // - Bug showing the last line
  7. //
  8. namespace Terminal.Gui;
  9. /// <summary>An hex viewer and editor <see cref="View"/> over a <see cref="System.IO.Stream"/></summary>
  10. /// <remarks>
  11. /// <para>
  12. /// <see cref="HexView"/> provides a hex editor on top of a seekable <see cref="Stream"/> with the left side
  13. /// showing an hex dump of the values in the <see cref="Stream"/> and the right side showing the contents (filtered
  14. /// to non-control sequence ASCII characters).
  15. /// </para>
  16. /// <para>Users can switch from one side to the other by using the tab key.</para>
  17. /// <para>
  18. /// To enable editing, set <see cref="AllowEdits"/> to true. When <see cref="AllowEdits"/> is true the user can
  19. /// make changes to the hexadecimal values of the <see cref="Stream"/>. Any changes are tracked in the
  20. /// <see cref="Edits"/> property (a <see cref="SortedDictionary{TKey, TValue}"/>) indicating the position where the
  21. /// changes were made and the new values. A convenience method, <see cref="ApplyEdits"/> will apply the edits to
  22. /// the <see cref="Stream"/>.
  23. /// </para>
  24. /// <para>Control the first byte shown by setting the <see cref="DisplayStart"/> property to an offset in the stream.</para>
  25. /// </remarks>
  26. public class HexView : View, IDesignable
  27. {
  28. private const int bsize = 4;
  29. private const int displayWidth = 9;
  30. private int bpl;
  31. private long displayStart, pos;
  32. private SortedDictionary<long, byte> edits = [];
  33. private bool firstNibble;
  34. private bool leftSide;
  35. private Stream source;
  36. private static readonly Rune SpaceCharRune = new (' ');
  37. private static readonly Rune PeriodCharRune = new ('.');
  38. /// <summary>Initializes a <see cref="HexView"/> class.</summary>
  39. /// <param name="source">
  40. /// The <see cref="Stream"/> to view and edit as hex, this <see cref="Stream"/> must support seeking,
  41. /// or an exception will be thrown.
  42. /// </param>
  43. public HexView (Stream source)
  44. {
  45. Source = source;
  46. CanFocus = true;
  47. CursorVisibility = CursorVisibility.Default;
  48. leftSide = true;
  49. firstNibble = true;
  50. // PERF: Closure capture of 'this' creates a lot of overhead.
  51. // BUG: Closure capture of 'this' may have unexpected results depending on how this is called.
  52. // The above two comments apply to all of the lambdas passed to all calls to AddCommand below.
  53. // Things this view knows how to do
  54. AddCommand (Command.Left, () => MoveLeft ());
  55. AddCommand (Command.Right, () => MoveRight ());
  56. AddCommand (Command.Down, () => MoveDown (bytesPerLine));
  57. AddCommand (Command.Up, () => MoveUp (bytesPerLine));
  58. AddCommand (Command.Tab, () => Navigate (NavigationDirection.Forward));
  59. AddCommand (Command.BackTab, () => Navigate (NavigationDirection.Backward));
  60. AddCommand (Command.PageUp, () => MoveUp (bytesPerLine * Frame.Height));
  61. AddCommand (Command.PageDown, () => MoveDown (bytesPerLine * Frame.Height));
  62. AddCommand (Command.Start, () => MoveHome ());
  63. AddCommand (Command.End, () => MoveEnd ());
  64. AddCommand (Command.LeftStart, () => MoveLeftStart ());
  65. AddCommand (Command.RightEnd, () => MoveEndOfLine ());
  66. AddCommand (Command.StartOfPage, () => MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine)));
  67. AddCommand (
  68. Command.EndOfPage,
  69. () => MoveDown (bytesPerLine * (Frame.Height - 1 - (int)(position - displayStart) / bytesPerLine))
  70. );
  71. // Default keybindings for this view
  72. KeyBindings.Add (Key.CursorLeft, Command.Left);
  73. KeyBindings.Add (Key.CursorRight, Command.Right);
  74. KeyBindings.Add (Key.CursorDown, Command.Down);
  75. KeyBindings.Add (Key.CursorUp, Command.Up);
  76. KeyBindings.Add (Key.V.WithAlt, Command.PageUp);
  77. KeyBindings.Add (Key.PageUp, Command.PageUp);
  78. KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
  79. KeyBindings.Add (Key.PageDown, Command.PageDown);
  80. KeyBindings.Add (Key.Home, Command.Start);
  81. KeyBindings.Add (Key.End, Command.End);
  82. KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.LeftStart);
  83. KeyBindings.Add (Key.CursorRight.WithCtrl, Command.RightEnd);
  84. KeyBindings.Add (Key.CursorUp.WithCtrl, Command.StartOfPage);
  85. KeyBindings.Add (Key.CursorDown.WithCtrl, Command.EndOfPage);
  86. KeyBindings.Add (Key.Tab, Command.Tab);
  87. KeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
  88. LayoutComplete += HexView_LayoutComplete;
  89. }
  90. /// <summary>Initializes a <see cref="HexView"/> class.</summary>
  91. public HexView () : this (new MemoryStream ()) { }
  92. /// <summary>
  93. /// Gets or sets whether this <see cref="HexView"/> allow editing of the <see cref="Stream"/> of the underlying
  94. /// <see cref="Stream"/>.
  95. /// </summary>
  96. /// <value><c>true</c> if allow edits; otherwise, <c>false</c>.</value>
  97. public bool AllowEdits { get; set; } = true;
  98. /// <summary>The bytes length per line.</summary>
  99. public int BytesPerLine => bytesPerLine;
  100. /// <summary>Gets the current cursor position starting at one for both, line and column.</summary>
  101. public Point CursorPosition
  102. {
  103. get
  104. {
  105. if (!IsInitialized)
  106. {
  107. return Point.Empty;
  108. }
  109. var delta = (int)position;
  110. int line = delta / bytesPerLine + 1;
  111. int item = delta % bytesPerLine + 1;
  112. return new Point (item, line);
  113. }
  114. }
  115. /// <summary>
  116. /// Sets or gets the offset into the <see cref="Stream"/> that will be displayed at the top of the
  117. /// <see cref="HexView"/>
  118. /// </summary>
  119. /// <value>The display start.</value>
  120. public long DisplayStart
  121. {
  122. get => displayStart;
  123. set
  124. {
  125. position = value;
  126. SetDisplayStart (value);
  127. }
  128. }
  129. /// <summary>
  130. /// Gets a <see cref="SortedDictionary{TKey, TValue}"/> describing the edits done to the <see cref="HexView"/>.
  131. /// Each Key indicates an offset where an edit was made and the Value is the changed byte.
  132. /// </summary>
  133. /// <value>The edits.</value>
  134. public IReadOnlyDictionary<long, byte> Edits => edits;
  135. /// <summary>Gets the current character position starting at one, related to the <see cref="Stream"/>.</summary>
  136. public long Position => position + 1;
  137. /// <summary>
  138. /// Sets or gets the <see cref="Stream"/> the <see cref="HexView"/> is operating on; the stream must support
  139. /// seeking ( <see cref="Stream.CanSeek"/> == true).
  140. /// </summary>
  141. /// <value>The source.</value>
  142. public Stream Source
  143. {
  144. get => source;
  145. set
  146. {
  147. if (value is null)
  148. {
  149. throw new ArgumentNullException ("source");
  150. }
  151. if (!value.CanSeek)
  152. {
  153. throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source");
  154. }
  155. source = value;
  156. if (displayStart > source.Length)
  157. {
  158. DisplayStart = 0;
  159. }
  160. if (position > source.Length)
  161. {
  162. position = 0;
  163. }
  164. SetNeedsDisplay ();
  165. }
  166. }
  167. private int bytesPerLine
  168. {
  169. get => bpl;
  170. set
  171. {
  172. bpl = value;
  173. OnPositionChanged ();
  174. }
  175. }
  176. private long position
  177. {
  178. get => pos;
  179. set
  180. {
  181. pos = value;
  182. OnPositionChanged ();
  183. }
  184. }
  185. /// <summary>
  186. /// This method applies and edits made to the <see cref="Stream"/> and resets the contents of the
  187. /// <see cref="Edits"/> property.
  188. /// </summary>
  189. /// <param name="stream">If provided also applies the changes to the passed <see cref="Stream"/></param>
  190. /// .
  191. public void ApplyEdits (Stream stream = null)
  192. {
  193. foreach (KeyValuePair<long, byte> kv in edits)
  194. {
  195. source.Position = kv.Key;
  196. source.WriteByte (kv.Value);
  197. source.Flush ();
  198. if (stream is { })
  199. {
  200. stream.Position = kv.Key;
  201. stream.WriteByte (kv.Value);
  202. stream.Flush ();
  203. }
  204. }
  205. edits = new SortedDictionary<long, byte> ();
  206. SetNeedsDisplay ();
  207. }
  208. /// <summary>
  209. /// This method discards the edits made to the <see cref="Stream"/> by resetting the contents of the
  210. /// <see cref="Edits"/> property.
  211. /// </summary>
  212. public void DiscardEdits () { edits = new SortedDictionary<long, byte> (); }
  213. /// <summary>Event to be invoked when an edit is made on the <see cref="Stream"/>.</summary>
  214. public event EventHandler<HexViewEditEventArgs> Edited;
  215. /// <inheritdoc/>
  216. protected override bool OnMouseEvent (MouseEvent me)
  217. {
  218. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
  219. && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  220. && !me.Flags.HasFlag (MouseFlags.WheeledDown)
  221. && !me.Flags.HasFlag (MouseFlags.WheeledUp))
  222. {
  223. return false;
  224. }
  225. if (!HasFocus)
  226. {
  227. SetFocus ();
  228. }
  229. if (me.Flags == MouseFlags.WheeledDown)
  230. {
  231. DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length);
  232. return true;
  233. }
  234. if (me.Flags == MouseFlags.WheeledUp)
  235. {
  236. DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0);
  237. return true;
  238. }
  239. if (me.Position.X < displayWidth)
  240. {
  241. return true;
  242. }
  243. int nblocks = bytesPerLine / bsize;
  244. int blocksSize = nblocks * 14;
  245. int blocksRightOffset = displayWidth + blocksSize - 1;
  246. if (me.Position.X > blocksRightOffset + bytesPerLine - 1)
  247. {
  248. return true;
  249. }
  250. leftSide = me.Position.X >= blocksRightOffset;
  251. long lineStart = me.Position.Y * bytesPerLine + displayStart;
  252. int x = me.Position.X - displayWidth + 1;
  253. int block = x / 14;
  254. x -= block * 2;
  255. int empty = x % 3;
  256. int item = x / 3;
  257. if (!leftSide && item > 0 && (empty == 0 || x == block * 14 + 14 - 1 - block * 2))
  258. {
  259. return true;
  260. }
  261. firstNibble = true;
  262. if (leftSide)
  263. {
  264. position = Math.Min (lineStart + me.Position.X - blocksRightOffset, source.Length);
  265. }
  266. else
  267. {
  268. position = Math.Min (lineStart + item, source.Length);
  269. }
  270. if (me.Flags == MouseFlags.Button1DoubleClicked)
  271. {
  272. leftSide = !leftSide;
  273. if (leftSide)
  274. {
  275. firstNibble = empty == 1;
  276. }
  277. else
  278. {
  279. firstNibble = true;
  280. }
  281. }
  282. SetNeedsDisplay ();
  283. return true;
  284. }
  285. ///<inheritdoc/>
  286. public override void OnDrawContent (Rectangle viewport)
  287. {
  288. Attribute currentAttribute;
  289. Attribute current = ColorScheme.Focus;
  290. Driver.SetAttribute (current);
  291. Move (0, 0);
  292. int nblocks = bytesPerLine / bsize;
  293. var data = new byte [nblocks * bsize * viewport.Height];
  294. Source.Position = displayStart;
  295. int n = source.Read (data, 0, data.Length);
  296. Attribute activeColor = ColorScheme.HotNormal;
  297. Attribute trackingColor = ColorScheme.HotFocus;
  298. for (var line = 0; line < viewport.Height; line++)
  299. {
  300. Rectangle lineRect = new (0, line, viewport.Width, 1);
  301. if (!Viewport.Contains (lineRect))
  302. {
  303. continue;
  304. }
  305. Move (0, line);
  306. Driver.SetAttribute (ColorScheme.HotNormal);
  307. Driver.AddStr ($"{displayStart + line * nblocks * bsize:x8} ");
  308. currentAttribute = ColorScheme.HotNormal;
  309. SetAttribute (GetNormalColor ());
  310. for (var block = 0; block < nblocks; block++)
  311. {
  312. for (var b = 0; b < bsize; b++)
  313. {
  314. int offset = line * nblocks * bsize + block * bsize + b;
  315. byte value = GetData (data, offset, out bool edited);
  316. if (offset + displayStart == position || edited)
  317. {
  318. SetAttribute (leftSide ? activeColor : trackingColor);
  319. }
  320. else
  321. {
  322. SetAttribute (GetNormalColor ());
  323. }
  324. Driver.AddStr (offset >= n && !edited ? " " : $"{value:x2}");
  325. SetAttribute (GetNormalColor ());
  326. Driver.AddRune (SpaceCharRune);
  327. }
  328. Driver.AddStr (block + 1 == nblocks ? " " : "| ");
  329. }
  330. for (var bitem = 0; bitem < nblocks * bsize; bitem++)
  331. {
  332. int offset = line * nblocks * bsize + bitem;
  333. byte b = GetData (data, offset, out bool edited);
  334. Rune c;
  335. if (offset >= n && !edited)
  336. {
  337. c = SpaceCharRune;
  338. }
  339. else
  340. {
  341. if (b < 32)
  342. {
  343. c = PeriodCharRune;
  344. }
  345. else if (b > 127)
  346. {
  347. c = PeriodCharRune;
  348. }
  349. else
  350. {
  351. Rune.DecodeFromUtf8 (new ReadOnlySpan<byte> (ref b), out c, out _);
  352. }
  353. }
  354. if (offset + displayStart == position || edited)
  355. {
  356. SetAttribute (leftSide ? trackingColor : activeColor);
  357. }
  358. else
  359. {
  360. SetAttribute (GetNormalColor ());
  361. }
  362. Driver.AddRune (c);
  363. }
  364. }
  365. void SetAttribute (Attribute attribute)
  366. {
  367. if (currentAttribute != attribute)
  368. {
  369. currentAttribute = attribute;
  370. Driver.SetAttribute (attribute);
  371. }
  372. }
  373. }
  374. /// <summary>Method used to invoke the <see cref="Edited"/> event passing the <see cref="KeyValuePair{TKey, TValue}"/>.</summary>
  375. /// <param name="e">The key value pair.</param>
  376. public virtual void OnEdited (HexViewEditEventArgs e) { Edited?.Invoke (this, e); }
  377. /// <summary>
  378. /// Method used to invoke the <see cref="PositionChanged"/> event passing the <see cref="HexViewEventArgs"/>
  379. /// arguments.
  380. /// </summary>
  381. public virtual void OnPositionChanged () { PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); }
  382. /// <inheritdoc/>
  383. protected override bool OnKeyDownNotHandled (Key keyEvent)
  384. {
  385. if (!AllowEdits)
  386. {
  387. return false;
  388. }
  389. // Ignore control characters and other special keys
  390. if (keyEvent < Key.Space || keyEvent.KeyCode > KeyCode.CharMask)
  391. {
  392. return false;
  393. }
  394. if (leftSide)
  395. {
  396. int value;
  397. var k = (char)keyEvent.KeyCode;
  398. if (k >= 'A' && k <= 'F')
  399. {
  400. value = k - 'A' + 10;
  401. }
  402. else if (k >= 'a' && k <= 'f')
  403. {
  404. value = k - 'a' + 10;
  405. }
  406. else if (k >= '0' && k <= '9')
  407. {
  408. value = k - '0';
  409. }
  410. else
  411. {
  412. return false;
  413. }
  414. byte b;
  415. if (!edits.TryGetValue (position, out b))
  416. {
  417. source.Position = position;
  418. b = (byte)source.ReadByte ();
  419. }
  420. RedisplayLine (position);
  421. if (firstNibble)
  422. {
  423. firstNibble = false;
  424. b = (byte)((b & 0xf) | (value << bsize));
  425. edits [position] = b;
  426. OnEdited (new HexViewEditEventArgs (position, edits [position]));
  427. }
  428. else
  429. {
  430. b = (byte)((b & 0xf0) | value);
  431. edits [position] = b;
  432. OnEdited (new HexViewEditEventArgs (position, edits [position]));
  433. MoveRight ();
  434. }
  435. return true;
  436. }
  437. return false;
  438. }
  439. /// <summary>Event to be invoked when the position and cursor position changes.</summary>
  440. public event EventHandler<HexViewEventArgs> PositionChanged;
  441. ///<inheritdoc/>
  442. public override Point? PositionCursor ()
  443. {
  444. var delta = (int)(position - displayStart);
  445. int line = delta / bytesPerLine;
  446. int item = delta % bytesPerLine;
  447. int block = item / bsize;
  448. int column = item % bsize * 3;
  449. int x = displayWidth + block * 14 + column + (firstNibble ? 0 : 1);
  450. int y = line;
  451. if (!leftSide)
  452. {
  453. x = displayWidth + bytesPerLine / bsize * 14 + item - 1;
  454. }
  455. Move (x, y);
  456. return new (x, y);
  457. }
  458. internal void SetDisplayStart (long value)
  459. {
  460. if (value > 0 && value >= source.Length)
  461. {
  462. displayStart = source.Length - 1;
  463. }
  464. else if (value < 0)
  465. {
  466. displayStart = 0;
  467. }
  468. else
  469. {
  470. displayStart = value;
  471. }
  472. SetNeedsDisplay ();
  473. }
  474. //
  475. // This is used to support editing of the buffer on a peer List<>,
  476. // the offset corresponds to an offset relative to DisplayStart, and
  477. // the buffer contains the contents of a screenful of data, so the
  478. // offset is relative to the buffer.
  479. //
  480. //
  481. private byte GetData (byte [] buffer, int offset, out bool edited)
  482. {
  483. long pos = DisplayStart + offset;
  484. if (edits.TryGetValue (pos, out byte v))
  485. {
  486. edited = true;
  487. return v;
  488. }
  489. edited = false;
  490. return buffer [offset];
  491. }
  492. private void HexView_LayoutComplete (object sender, LayoutEventArgs e)
  493. {
  494. // Small buffers will just show the position, with the bsize field value (4 bytes)
  495. bytesPerLine = bsize;
  496. if (Viewport.Width - displayWidth > 17)
  497. {
  498. bytesPerLine = bsize * ((Viewport.Width - displayWidth) / 18);
  499. }
  500. }
  501. private bool MoveDown (int bytes)
  502. {
  503. RedisplayLine (position);
  504. if (position + bytes < source.Length)
  505. {
  506. position += bytes;
  507. }
  508. else if ((bytes == bytesPerLine * Viewport.Height && source.Length >= DisplayStart + bytesPerLine * Viewport.Height)
  509. || (bytes <= bytesPerLine * Viewport.Height - bytesPerLine
  510. && source.Length <= DisplayStart + bytesPerLine * Viewport.Height))
  511. {
  512. long p = position;
  513. while (p + bytesPerLine < source.Length)
  514. {
  515. p += bytesPerLine;
  516. }
  517. position = p;
  518. }
  519. if (position >= DisplayStart + bytesPerLine * Viewport.Height)
  520. {
  521. SetDisplayStart (DisplayStart + bytes);
  522. SetNeedsDisplay ();
  523. }
  524. else
  525. {
  526. RedisplayLine (position);
  527. }
  528. return true;
  529. }
  530. private bool MoveEnd ()
  531. {
  532. position = source.Length;
  533. if (position >= DisplayStart + bytesPerLine * Viewport.Height)
  534. {
  535. SetDisplayStart (position);
  536. SetNeedsDisplay ();
  537. }
  538. else
  539. {
  540. RedisplayLine (position);
  541. }
  542. return true;
  543. }
  544. private bool MoveEndOfLine ()
  545. {
  546. position = Math.Min (position / bytesPerLine * bytesPerLine + bytesPerLine - 1, source.Length);
  547. SetNeedsDisplay ();
  548. return true;
  549. }
  550. private bool MoveHome ()
  551. {
  552. DisplayStart = 0;
  553. SetNeedsDisplay ();
  554. return true;
  555. }
  556. private bool MoveLeft ()
  557. {
  558. RedisplayLine (position);
  559. if (leftSide)
  560. {
  561. if (!firstNibble)
  562. {
  563. firstNibble = true;
  564. return true;
  565. }
  566. firstNibble = false;
  567. }
  568. if (position == 0)
  569. {
  570. return true;
  571. }
  572. if (position - 1 < DisplayStart)
  573. {
  574. SetDisplayStart (displayStart - bytesPerLine);
  575. SetNeedsDisplay ();
  576. }
  577. else
  578. {
  579. RedisplayLine (position);
  580. }
  581. position--;
  582. return true;
  583. }
  584. private bool MoveRight ()
  585. {
  586. RedisplayLine (position);
  587. if (leftSide)
  588. {
  589. if (firstNibble)
  590. {
  591. firstNibble = false;
  592. return true;
  593. }
  594. firstNibble = true;
  595. }
  596. if (position < source.Length)
  597. {
  598. position++;
  599. }
  600. if (position >= DisplayStart + bytesPerLine * Viewport.Height)
  601. {
  602. SetDisplayStart (DisplayStart + bytesPerLine);
  603. SetNeedsDisplay ();
  604. }
  605. else
  606. {
  607. RedisplayLine (position);
  608. }
  609. return true;
  610. }
  611. private bool MoveLeftStart ()
  612. {
  613. position = position / bytesPerLine * bytesPerLine;
  614. SetNeedsDisplay ();
  615. return true;
  616. }
  617. private bool MoveUp (int bytes)
  618. {
  619. RedisplayLine (position);
  620. if (position - bytes > -1)
  621. {
  622. position -= bytes;
  623. }
  624. if (position < DisplayStart)
  625. {
  626. SetDisplayStart (DisplayStart - bytes);
  627. SetNeedsDisplay ();
  628. }
  629. else
  630. {
  631. RedisplayLine (position);
  632. }
  633. return true;
  634. }
  635. private void RedisplayLine (long pos)
  636. {
  637. if (bytesPerLine == 0)
  638. {
  639. return;
  640. }
  641. var delta = (int)(pos - DisplayStart);
  642. int line = delta / bytesPerLine;
  643. SetNeedsDisplay (new (0, line, Viewport.Width, 1));
  644. }
  645. private bool Navigate (NavigationDirection direction)
  646. {
  647. switch (direction)
  648. {
  649. case NavigationDirection.Forward:
  650. if (leftSide)
  651. {
  652. leftSide = false;
  653. RedisplayLine (position);
  654. firstNibble = true;
  655. return true;
  656. }
  657. break;
  658. case NavigationDirection.Backward:
  659. if (!leftSide)
  660. {
  661. leftSide = true;
  662. RedisplayLine (position);
  663. firstNibble = true;
  664. return true;
  665. }
  666. break;
  667. }
  668. return false;
  669. }
  670. /// <inheritdoc />
  671. bool IDesignable.EnableForDesign ()
  672. {
  673. Source = new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"));
  674. return true;
  675. }
  676. }