using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Mesen.Utilities; using System; using System.Collections.Generic; using System.Text; using Mesen.Config; using System.Text.RegularExpressions; using Mesen.Debugger.Utilities; using System.Globalization; namespace Mesen.Debugger.Controls { public partial class HexEditor : Control { public static readonly StyledProperty DataProviderProperty = AvaloniaProperty.Register(nameof(DataProvider)); public static readonly StyledProperty TopRowProperty = AvaloniaProperty.Register(nameof(TopRow), 0, false, Avalonia.Data.BindingMode.TwoWay); public static readonly StyledProperty BytesPerRowProperty = AvaloniaProperty.Register(nameof(BytesPerRow), 16); public static readonly StyledProperty SelectionStartProperty = AvaloniaProperty.Register(nameof(SelectionStart), 0, defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); public static readonly StyledProperty SelectionLengthProperty = AvaloniaProperty.Register(nameof(SelectionLength), 0, defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); public static readonly StyledProperty FontFamilyProperty = AvaloniaProperty.Register(nameof(FontFamily), new FontFamily(FontManager.Current.DefaultFontFamily.Name)); public static readonly StyledProperty FontSizeProperty = AvaloniaProperty.Register(nameof(FontSize), 12); public static readonly StyledProperty ShowStringViewProperty = AvaloniaProperty.Register(nameof(ShowStringView), false); public static readonly StyledProperty HighDensityModeProperty = AvaloniaProperty.Register(nameof(HighDensityMode), false); public static readonly StyledProperty LastNibbleProperty = AvaloniaProperty.Register(nameof(LastNibble), false); public static readonly StyledProperty SelectedRowColumnColorProperty = AvaloniaProperty.Register(nameof(SelectedRowColumnColor), new SolidColorBrush(0xFFF0F0F0)); public static readonly StyledProperty HeaderBackgroundProperty = AvaloniaProperty.Register(nameof(HeaderBackground), new SolidColorBrush(Color.FromRgb(235, 235, 235))); public static readonly StyledProperty HeaderForegroundProperty = AvaloniaProperty.Register(nameof(HeaderForeground), new SolidColorBrush(Colors.Gray)); public static readonly StyledProperty HeaderHighlightProperty = AvaloniaProperty.Register(nameof(HeaderHighlight), new SolidColorBrush(Colors.White)); public static readonly StyledProperty NewByteValueProperty = AvaloniaProperty.Register(nameof(NewByteValue), -1); public IHexEditorDataProvider DataProvider { get { return GetValue(DataProviderProperty); } set { SetValue(DataProviderProperty, value); this.InvalidateVisual(); } } public int TopRow { get { return GetValue(TopRowProperty); } set { SetValue(TopRowProperty, value); } } public int BytesPerRow { get { return GetValue(BytesPerRowProperty); } set { SetValue(BytesPerRowProperty, value); } } public int SelectionStart { get { return GetValue(SelectionStartProperty); } set { CommitByteChanges(); SetValue(SelectionStartProperty, value); } } public int SelectionLength { get { return GetValue(SelectionLengthProperty); } set { SetValue(SelectionLengthProperty, value); } } public bool ShowStringView { get { return GetValue(ShowStringViewProperty); } set { SetValue(ShowStringViewProperty, value); } } public bool HighDensityMode { get { return GetValue(HighDensityModeProperty); } set { SetValue(HighDensityModeProperty, value); } } public FontFamily FontFamily { get { return GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } public double FontSize { get { return GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } public SolidColorBrush SelectedRowColumnColor { get { return GetValue(SelectedRowColumnColorProperty); } set { SetValue(SelectedRowColumnColorProperty, value); } } public SolidColorBrush HeaderBackground { get { return GetValue(HeaderBackgroundProperty); } set { SetValue(HeaderBackgroundProperty, value); } } public SolidColorBrush HeaderForeground { get { return GetValue(HeaderForegroundProperty); } set { SetValue(HeaderForegroundProperty, value); } } public SolidColorBrush HeaderHighlight { get { return GetValue(HeaderHighlightProperty); } set { SetValue(HeaderHighlightProperty, value); } } public int NewByteValue { get { return GetValue(NewByteValueProperty); } set { SetValue(NewByteValueProperty, value); } } private bool LastNibble { get { return GetValue(LastNibbleProperty); } set { SetValue(LastNibbleProperty, value); } } public event EventHandler? ByteUpdated; private Typeface Font { get; set; } private Size LetterSize { get; set; } private double RowHeight => HighDensityMode ? LetterSize.Height * 0.8 : LetterSize.Height; private int HeaderCharLength => DataProvider.Length > 0 ? (DataProvider.Length - 1).ToString(HexFormat).Length : 0; private double RowHeaderWidth => HeaderCharLength * LetterSize.Width + 10; private double ContentLeftPadding => 5; private double RowWidth => LetterSize.Width * (3 * BytesPerRow - 1); private double StringViewMargin => 20; private double ColumnHeaderHeight => LetterSize.Height + 5; private int VisibleRows => Math.Max(0, (int)((Bounds.Height - ColumnHeaderHeight) / RowHeight) - 1); private string HexFormat => "X2"; private int _cursorPosition = 0; private int _lastClickedPosition = -1; private bool _inStringView = false; private float[] _startPositionByByte = Array.Empty(); private float[] _endPositionByByte = Array.Empty(); private FontAntialiasing _fontAntialiasing; private Point _pointerPressedPos; private double _scrollAccumulator = 0.0; static HexEditor() { AffectsRender( DataProviderProperty, TopRowProperty, BytesPerRowProperty, SelectionStartProperty, SelectionLengthProperty, SelectedRowColumnColorProperty, HeaderBackgroundProperty, HeaderForegroundProperty, HeaderHighlightProperty, IsFocusedProperty, HighDensityModeProperty, NewByteValueProperty, FontFamilyProperty, FontSizeProperty, LastNibbleProperty ); FontFamilyProperty.Changed.AddClassHandler((x, e) => { x.InitFontAndLetterSize(); }); FontSizeProperty.Changed.AddClassHandler((x, e) => { x.InitFontAndLetterSize(); }); DataProviderProperty.Changed.AddClassHandler((x, e) => { if(x.DataProvider != null) { x.TopRow = Math.Min((x.DataProvider.Length / x.BytesPerRow) - 1, Math.Max(0, x.TopRow)); } }); } public HexEditor() { Focusable = true; InitFontAndLetterSize(); ColorHelper.InvalidateControlOnThemeChange(this); _fontAntialiasing = ConfigManager.Config.Preferences.FontAntialiasing; } protected override void OnPointerEntered(PointerEventArgs e) { base.OnPointerEntered(e); Cursor = new Cursor(StandardCursorType.Ibeam); } protected override void OnPointerExited(PointerEventArgs e) { base.OnPointerExited(e); Cursor = null; } private void MoveCursor(int offset, bool nibbleMode = false, bool keepNibble = false) { if(nibbleMode) { if(LastNibble) { if(offset > 0) { MoveCursor(1); } else { LastNibble = false; } } else { if(offset > 0) { LastNibble = true; } else { MoveCursor(-1); LastNibble = true; } } } else { int pos = this.SelectionStart + offset; SetCursorPosition(pos, keepNibble); } } public void SetCursorPosition(int pos, bool keepNibble = false, bool scrollToTop = false) { if(this.DataProvider == null) { return; } this.SelectionStart = Math.Max(0, Math.Min(pos, this.DataProvider.Length - 1)); this.SelectionLength = 0; _cursorPosition = this.SelectionStart; _lastClickedPosition = _cursorPosition; if(!keepNibble) { LastNibble = false; } ScrollIntoView(_cursorPosition, scrollToTop); } private void ChangeSelectionLength(int offset) { LastNibble = false; int start = this.SelectionStart; int end = this.SelectionStart + this.SelectionLength; int length = this.SelectionLength; bool newAnchorEnd = false; if(this.SelectionStart == this._cursorPosition) { //Anchored at the start of the selection start += offset; if(end < start) { length = start - end; start = end; newAnchorEnd = true; } else { length -= offset; } } else { //Anchored at the end of the selection length += offset; if(length < 0) { start = start + length; length = -length; newAnchorEnd = false; } else { newAnchorEnd = true; } } if(start < 0) { length += start; start = 0; } this.SelectionStart = Math.Max(0, Math.Min(this.DataProvider.Length - 1, start)); this.SelectionLength = Math.Max(0, Math.Min(length, this.DataProvider.Length - this.SelectionStart)); if(newAnchorEnd) { this._cursorPosition = this.SelectionStart + this.SelectionLength; } else { this._cursorPosition = this.SelectionStart; } ScrollIntoView(this._cursorPosition); } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); bool ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); if(e.KeyModifiers.HasFlag(KeyModifiers.Shift)) { switch(e.Key) { case Key.Left: ChangeSelectionLength(-1); break; case Key.Right: ChangeSelectionLength(1); break; case Key.Up: ChangeSelectionLength(-BytesPerRow); break; case Key.Down: ChangeSelectionLength(BytesPerRow); break; case Key.PageUp: ChangeSelectionLength(-BytesPerRow * VisibleRows); break; case Key.PageDown: ChangeSelectionLength(BytesPerRow * VisibleRows); break; case Key.Home: { int anchor = _cursorPosition == SelectionStart ? _cursorPosition : SelectionStart + SelectionLength; ChangeSelectionLength(-(anchor % BytesPerRow)); break; } case Key.End: { int anchor = _cursorPosition == SelectionStart ? _cursorPosition : SelectionStart + SelectionLength; ChangeSelectionLength(BytesPerRow - (anchor % BytesPerRow)); break; } } } else { switch(e.Key) { case Key.Left: MoveCursor(-1, ctrl || LastNibble); break; case Key.Right: MoveCursor(1, ctrl); break; case Key.Up: MoveCursor(-BytesPerRow, keepNibble: true); break; case Key.Down: MoveCursor(BytesPerRow, keepNibble: true); break; case Key.PageUp: MoveCursor(-BytesPerRow * VisibleRows, keepNibble: true); break; case Key.PageDown: MoveCursor(BytesPerRow * VisibleRows, keepNibble: true); break; case Key.Home: if(ctrl) { SetCursorPosition(0); } else { MoveCursor(-(SelectionStart % BytesPerRow)); } break; case Key.End: if(ctrl) { SetCursorPosition(DataProvider.Length); } else { MoveCursor(BytesPerRow - (SelectionStart % BytesPerRow) - 1); } break; } } } protected override void OnTextInput(TextInputEventArgs e) { if(e.Text != null) { char c = e.Text[0]; if(_inStringView) { NewByteValue = DataProvider.ConvertCharToByte(c); CommitByteChanges(); MoveCursor(SelectionLength == 0 ? 1 : SelectionLength); } else { if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { int keyValue = Int32.Parse(c.ToString(), System.Globalization.NumberStyles.HexNumber); if(NewByteValue < 0) { NewByteValue = DataProvider.GetRawByte(SelectionStart); } if(LastNibble) { //Commit byte NewByteValue &= 0xF0; NewByteValue |= keyValue; CommitByteChanges(); MoveCursor(SelectionLength == 0 ? 1 : SelectionLength); } else { NewByteValue &= 0x0F; NewByteValue |= (keyValue << 4); LastNibble = true; } } } } base.OnTextInput(e); } public void CopySelection() { IHexEditorDataProvider dp = DataProvider; byte[] data = dp.GetRawBytes(SelectionStart, SelectionLength); StringBuilder sb = new StringBuilder(); int bytesPerRow = BytesPerRow; for(int i = 0; i < data.Length; i++) { if(_inStringView) { UInt64 tblKeyValue = (UInt64)data[i]; for(int j = 1; j < 8; j++) { if(i + j < data.Length) { tblKeyValue += (UInt64)data[i + j] << (8 * j); } } sb.Append(dp.ConvertValueToString(tblKeyValue, out int keyLength)); i += keyLength - 1; } else { sb.Append(data[i].ToString("X2")); if((i + 1) % bytesPerRow == 0) { sb.AppendLine(); } else if(i < data.Length - 1) { sb.Append(" "); } } } ApplicationHelper.GetMainWindow()?.Clipboard?.SetTextAsync(sb.ToString()); } public async void PasteSelection() { var clipboard = ApplicationHelper.GetMainWindow()?.Clipboard; if(clipboard != null) { string? text = await clipboard.GetTextAsync(); if(text != null) { text = text.Replace("\n", "").Replace("\r", ""); if(Regex.IsMatch(text, "^[ a-f0-9]+$", RegexOptions.IgnoreCase)) { byte[] pastedData = HexUtilities.HexToArray(text); ByteUpdated?.Invoke(this, new ByteUpdatedEventArgs() { ByteOffset = _cursorPosition, Length = pastedData.Length, Values = pastedData }); //Move cursor to the end of the pasted section SetCursorPosition(_cursorPosition + pastedData.Length, false); } } } } public void SelectAll() { SetCursorPosition(0); SelectionLength = DataProvider.Length; } private void CommitByteChanges() { if(NewByteValue >= 0) { RequestByteUpdate(SelectionStart, (byte)NewByteValue); LastNibble = false; NewByteValue = -1; } } private void RequestByteUpdate(int position, byte value) { if(position < DataProvider.Length) { ByteUpdated?.Invoke(this, new ByteUpdatedEventArgs() { ByteOffset = position, Length = SelectionLength == 0 ? 1 : SelectionLength, Value = value }); } } private bool IsPointInLeftMargin(Point p, bool inStringView) { if(inStringView) { return p.X + 3 < RowHeaderWidth + ContentLeftPadding + RowWidth + StringViewMargin; } else { return p.X + 3 < RowHeaderWidth + ContentLeftPadding; } } private GridPoint? GetGridPosition(Point p, bool allowLeftMargin = false, bool stringViewOnly = false) { //+ 5 pixels for gap between mouse position X vs mouse icon I-bar p = p.WithX(p.X + 3); if((allowLeftMargin || p.X >= RowHeaderWidth + ContentLeftPadding) && p.Y >= ColumnHeaderHeight) { int row = (int)((p.Y - ColumnHeaderHeight) / RowHeight); if(TopRow + row != 0 && (TopRow + row + 1) * BytesPerRow > DataProvider.Length + BytesPerRow) { //Out of range return null; } if(ShowStringView && ((stringViewOnly && allowLeftMargin) || p.X >= RowHeaderWidth + ContentLeftPadding + RowWidth + StringViewMargin)) { //String view try { int rowStart = row * BytesPerRow; float[] startPos = _startPositionByByte; float[] endPos = _endPositionByByte; double x = p.X - RowHeaderWidth - ContentLeftPadding - RowWidth - StringViewMargin; if(allowLeftMargin && x < 0) { x = 0; } int column = -1; if(endPos.Length != startPos.Length) { return null; } for(int i = 0, len = BytesPerRow; i < len; i++) { if(rowStart + i >= startPos.Length) { break; } else if(startPos[rowStart + i] <= x && endPos[rowStart + i] >= x) { column = i; break; } } if(column < 0 || row * BytesPerRow + column > DataProvider.Length) { //Out of range return null; } return new GridPoint { X = column, Y = row, LastNibble = false, InStringView = true }; } catch { return null; } } else { double column = (p.X - RowHeaderWidth - ContentLeftPadding) / (LetterSize.Width * 3); if(allowLeftMargin && column < 0) { column = 0; } if(column > BytesPerRow || column < 0) { return null; } bool middle = (column - Math.Floor(column)) >= 0.30; bool nextByte = (column - Math.Floor(column)) >= 0.80; if(nextByte && column < BytesPerRow) { middle = false; column++; } if(row * BytesPerRow + column > DataProvider.Length) { //Out of range return null; } return new GridPoint { X = (int)column, Y = row, LastNibble = middle, InStringView = false }; } } return null; } private void ScrollIntoView(int byteIndex, bool scrollToTop = false) { int topRow = TopRow; if(byteIndex < 0) { topRow = 0; } else if(byteIndex >= DataProvider.Length) { topRow = (DataProvider.Length / BytesPerRow) - VisibleRows; } else if(byteIndex < TopRow * BytesPerRow || scrollToTop) { //scroll up topRow = byteIndex / BytesPerRow; } else if(byteIndex > (TopRow + VisibleRows) * BytesPerRow) { topRow = byteIndex / BytesPerRow - VisibleRows; } TopRow = Math.Max(0, topRow); } protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { base.OnPointerWheelChanged(e); _scrollAccumulator += e.GetDeltaY() * 3; this.TopRow = Math.Min((DataProvider.Length / BytesPerRow) - 1, Math.Max(0, this.TopRow - (int) _scrollAccumulator)); _scrollAccumulator -= (int) _scrollAccumulator; } protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); PointerPointProperties props = e.GetCurrentPoint(this).Properties; Point p = e.GetPosition(this); _pointerPressedPos = p; GridPoint? gridPos = GetGridPosition(p); if(gridPos == null) { return; } if(props.IsLeftButtonPressed) { _inStringView = gridPos.Value.InStringView; LastNibble = e.KeyModifiers.HasFlag(KeyModifiers.Control) ? gridPos.Value.LastNibble : false; if(e.KeyModifiers.HasFlag(KeyModifiers.Shift)) { MoveSelectionWithMouse(gridPos.Value); } else { int pos = GetByteOffset(gridPos.Value); SetCursorPosition(pos, true); } } else if(props.IsRightButtonPressed) { int pos = GetByteOffset(gridPos.Value); if(pos >= SelectionStart && pos < SelectionStart + SelectionLength) { _inStringView = gridPos.Value.InStringView; } else { SetCursorPosition(pos); } } } private int GetByteOffset(GridPoint gridPos) { return Math.Max(0, Math.Min((TopRow + gridPos.Y) * BytesPerRow + gridPos.X, DataProvider.Length - 1)); } public int GetByteOffset(Point pos) { GridPoint? gridPoint = GetGridPosition(pos); return gridPoint != null ? GetByteOffset(gridPoint.Value) : -1; } private void MoveSelectionWithMouse(GridPoint gridPos) { MoveSelectionWithMouse(GetByteOffset(gridPos)); } private void MoveSelectionWithMouse(int currentPos) { if(currentPos < _lastClickedPosition) { this.SelectionStart = currentPos; this.SelectionLength = _lastClickedPosition - currentPos; } else { this.SelectionStart = _lastClickedPosition; this.SelectionLength = currentPos - _lastClickedPosition + 1; } this._cursorPosition = currentPos; ScrollIntoView(currentPos); } protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); if(!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || _lastClickedPosition < 0) { return; } Point p = e.GetPosition(this); if(Math.Abs(p.X - _pointerPressedPos.X) < 3 && Math.Abs(p.Y - _pointerPressedPos.Y) < 3) { //Mouse didn't move more than 3 pixels in any direction, ignore movement return; } if(p.Y < ColumnHeaderHeight) { //Allow auto-scroll up when mouse goes over header this.TopRow = Math.Max(0, this.TopRow - 1); p = p.WithY(ColumnHeaderHeight); } bool isMargin = IsPointInLeftMargin(p, _inStringView); GridPoint? gridPos = GetGridPosition(p, isMargin, _inStringView); LastNibble = false; if(gridPos != null) { if(gridPos.Value.InStringView != _inStringView) { return; } int offset = GetByteOffset(gridPos.Value); if(offset > SelectionStart + SelectionLength - 1 && !gridPos.Value.LastNibble && !_inStringView) { offset--; } else if(isMargin && _lastClickedPosition < offset) { offset--; } if(offset >= 0) { MoveSelectionWithMouse(offset); } } } public override void Render(DrawingContext context) { base.Render(context); IHexEditorDataProvider dataProvider = this.DataProvider; if(dataProvider == null) { return; } using var clipRect = context.PushClip(new Rect(this.Bounds.Size)); context.DrawRectangle(ColorHelper.GetBrush(Colors.White), null, new Rect(Bounds.Size)); //Draw column headers DrawColumnHeaders(context); using var columnHeaderTranslation = context.PushTransform(Matrix.CreateTranslation(0, ColumnHeaderHeight)); //Draw row headers DrawRowHeaders(context); using var rowHeaderTranslation = context.PushTransform(Matrix.CreateTranslation(RowHeaderWidth + ContentLeftPadding, 0)); //Precalculate some values for data draw int bytesPerRow = this.BytesPerRow; Rect bounds = this.Bounds; int position = this.TopRow * this.BytesPerRow; Brush selectedRowColumnColor = ColorHelper.GetBrush(SelectedRowColumnColor); int selectionStart = this.SelectionStart; int selectionLength = this.SelectionLength; int selectedColumn = this._cursorPosition % bytesPerRow; int selectedRow = this._cursorPosition / bytesPerRow; double rowWidth = RowWidth; double letterWidth = LetterSize.Width; //Init byte color information for the data we're about to draw dataProvider.Prepare(position, position + bytesPerRow * (VisibleRows + 3)); //Draw selected column background color if(IsFocused) { context.DrawRectangle(selectedRowColumnColor, null, new Rect(letterWidth * (3 * selectedColumn), 0, letterWidth * 2, bounds.Height)); } //Draw data int visibleRows = VisibleRows + 2; if(IsFocused && selectedRow >= TopRow && selectedRow < TopRow + visibleRows) { //Draw background color for current row context.DrawRectangle(selectedRowColumnColor, null, new Rect(0, (selectedRow - TopRow) * RowHeight, rowWidth, RowHeight)); } int bytesToDraw = bytesPerRow * visibleRows; List dataToDraw = new List(bytesToDraw); HashSet fgColors = new HashSet(); for(int i = 0; i < bytesToDraw; i++) { if(dataProvider.Length <= position) { break; } ByteInfo byteInfo = dataProvider.GetByte(position); byteInfo.Selected = selectionLength > 0 && position >= selectionStart && position < selectionStart + selectionLength; if(((SelectionLength == 0 && position == SelectionStart) || (SelectionLength > 0 && position >= SelectionStart && position < SelectionStart + SelectionLength)) && NewByteValue >= 0) { //About to draw the selected byte, draw anything that's pending, and then the current byte byteInfo.ForeColor = Colors.DarkOrange; byteInfo.Value = (byte)NewByteValue; dataToDraw.Add(byteInfo); } else { dataToDraw.Add(byteInfo); } fgColors.Add(byteInfo.ForeColor); position++; } context.Custom(new HexViewDrawOperation(this, dataToDraw, fgColors, _fontAntialiasing)); if(selectedRow >= TopRow && selectedRow < TopRow + visibleRows) { //Draw selected character/byte cursor + rectangle around correspond byte in other view PixelPoint cursorPos = new PixelPoint( (int)(letterWidth * selectedColumn * 3 + (!_inStringView && LastNibble ? letterWidth : 0)), (int)((selectedRow - TopRow) * RowHeight) ); Rect otherViewRect = new Rect(); if(ShowStringView) { float[] startPos = _startPositionByByte; float[] endPos = _endPositionByByte; int index = (selectedRow - TopRow) * bytesPerRow + selectedColumn; if(index >= 0 && index < startPos.Length) { if(_inStringView) { otherViewRect = new Rect( cursorPos.X, cursorPos.Y, (int)(letterWidth * 2) + 2, (int)RowHeight ); cursorPos = cursorPos.WithX((int)(RowWidth + StringViewMargin + startPos[index])); } else { otherViewRect = new Rect( RowWidth + StringViewMargin + startPos[index], cursorPos.Y, (int)endPos[index] - startPos[index], (int)RowHeight ); } } } if(cursorPos.X >= 0) { Pen cursorPen = ColorHelper.GetPen(Colors.Black); context.DrawRectangle(cursorPen, new Rect(cursorPos.X, cursorPos.Y, 1, (int)RowHeight)); } if(otherViewRect.Width > 0 && SelectionLength == 0) { Pen cursorPen = ColorHelper.GetPen(Colors.Black); cursorPen.DashStyle = DashStyle.Dash; context.DrawRectangle(cursorPen, otherViewRect.Inflate(new Thickness(0, -1, 0, 0))); } } } private void InitFontAndLetterSize() { this.Font = new Typeface(FontFamily); var text = new FormattedText("A", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Font, FontSize, null); this.LetterSize = new Size(text.Width, text.Height); } private void DrawRowHeaders(DrawingContext context) { //Draw background context.DrawRectangle(ColorHelper.GetBrush(HeaderBackground), null, new Rect(0, 0, RowHeaderWidth, Bounds.Height)); int selectedRow = _cursorPosition / BytesPerRow; if(selectedRow >= TopRow && selectedRow <= TopRow + VisibleRows) { context.DrawRectangle(ColorHelper.GetBrush(HeaderHighlight), null, new Rect(0, (selectedRow - TopRow) * RowHeight, RowHeaderWidth, RowHeight)); } //Draw row addresses context.Custom(new HexViewDrawRowHeaderOperation(this, _fontAntialiasing)); } private void DrawColumnHeaders(DrawingContext context) { context.DrawRectangle(ColorHelper.GetBrush(HeaderBackground), null, new Rect(0, 0, Bounds.Width, this.ColumnHeaderHeight)); double leftMargin = RowHeaderWidth + ContentLeftPadding; int selectedColumn = _cursorPosition % BytesPerRow; context.DrawRectangle(ColorHelper.GetBrush(HeaderHighlight), null, new Rect(leftMargin + selectedColumn * LetterSize.Width * 3 - LetterSize.Width / 2, 0, LetterSize.Width * 3, ColumnHeaderHeight)); StringBuilder sb = new StringBuilder(); for(int i = 0, len = BytesPerRow; i < len ; i++) { sb.Append(i.ToString(HexFormat) + " "); } var text = new FormattedText(sb.ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Font, FontSize, ColorHelper.GetBrush(HeaderForeground)); context.DrawText(text, new Point(leftMargin, (this.ColumnHeaderHeight - this.LetterSize.Height) / 2)); } } public interface IHexEditorDataProvider { void Prepare(int firstByteIndex, int lastByteIndex); ByteInfo GetByte(int byteIndex); byte GetRawByte(int byteIndex); int Length { get; } byte[] GetRawBytes(int start, int length); string ConvertValueToString(UInt64 val, out int keyLength); byte ConvertCharToByte(char c); } public struct GridPoint { public int X; public int Y; public bool LastNibble; public bool InStringView; } public class ByteUpdatedEventArgs : EventArgs { public int ByteOffset; public int Length; public byte? Value = null; public byte[]? Values = null; } public struct ByteInfo { public Color ForeColor { get; set; } public Color BackColor { get; set; } public Color BorderColor { get; set; } public bool Selected { get; set; } public byte Value { get; set; } public int StringValueKeyLength { get; set; } public string StringValue { get; set; } public bool UseAltFont { get; set; } public override bool Equals(object? obj) { if(obj == null || !(obj is ByteInfo)) { return false; } ByteInfo a = (ByteInfo)obj; return this.BackColor == a.BackColor && this.BorderColor == a.BorderColor && this.ForeColor == a.ForeColor && this.Selected == a.Selected; } public override int GetHashCode() { return (int)(this.BackColor.ToUInt32() ^ this.BorderColor.ToUInt32() ^ this.ForeColor.ToUInt32() ^ (this.Selected ? 1 : 0)); } } }