// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : HexViewPanel.cs // Author(s) : Natalia Portillo // // Component : GUI custom controls. // // --[ Description ] ---------------------------------------------------------- // // A hex view control that displays data in three synchronized columns: // offset, hexadecimal bytes, and ASCII representation. // // --[ License ] -------------------------------------------------------------- // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Collections.Generic; using System.Text; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; namespace Aaru.Gui.Controls; /// Represents a color range for highlighting bytes in the hex view. public class ColorRange { /// Gets or sets the starting byte index (inclusive). public int Start { get; set; } /// Gets or sets the ending byte index (inclusive). public int End { get; set; } /// Gets or sets the color to apply to this range. public IBrush Color { get; set; } } /// Hex view control with synchronized scrolling of offset, hex, and ASCII columns. public class HexViewPanel : UserControl { private const int BYTES_PER_LINE = 16; public static readonly StyledProperty DataProperty = AvaloniaProperty.Register(nameof(Data)); public static readonly StyledProperty OffsetHeaderProperty = AvaloniaProperty.Register(nameof(OffsetHeader), "Offset"); public static readonly StyledProperty AsciiHeaderProperty = AvaloniaProperty.Register(nameof(AsciiHeader), "ASCII"); public static readonly StyledProperty> ColorRangesProperty = AvaloniaProperty.Register>(nameof(ColorRanges)); private readonly TextBlock _asciiContent; private readonly TextBlock _asciiHeaderText; private readonly TextBlock _hexContent; private readonly TextBlock _offsetContent; private readonly TextBlock _offsetHeaderText; private readonly ScrollViewer _scrollViewer; private byte[] _data = []; static HexViewPanel() { DataProperty.Changed.AddClassHandler(static (sender, _) => sender.OnDataChanged()); OffsetHeaderProperty.Changed.AddClassHandler(static (sender, _) => sender.OnOffsetHeaderChanged()); AsciiHeaderProperty.Changed.AddClassHandler(static (sender, _) => sender.OnAsciiHeaderChanged()); ColorRangesProperty.Changed.AddClassHandler(static (sender, _) => sender.OnColorRangesChanged()); } public HexViewPanel() { // Create header for hex column with byte positions var hexHeader = new StringBuilder(); for(var i = 0; i < BYTES_PER_LINE; i++) { if(i > 0) hexHeader.Append(' '); hexHeader.Append(i.ToString("X2")); } // Create the three content TextBlocks _offsetContent = new TextBlock { FontFamily = new FontFamily("Courier New"), FontSize = 12, TextWrapping = TextWrapping.NoWrap, Foreground = Brushes.White, Padding = new Thickness(5) }; _hexContent = new TextBlock { FontFamily = new FontFamily("Courier New"), FontSize = 12, TextWrapping = TextWrapping.NoWrap, Foreground = Brushes.White, Padding = new Thickness(5), HorizontalAlignment = HorizontalAlignment.Center }; _asciiContent = new TextBlock { FontFamily = new FontFamily("Courier New"), FontSize = 12, TextWrapping = TextWrapping.NoWrap, Foreground = Brushes.White, Padding = new Thickness(5) }; // Create a shared grid that contains both headers and content with matching column definitions var sharedGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"), RowDefinitions = new RowDefinitions("Auto,*") }; // Offset column header _offsetHeaderText = new TextBlock { Text = OffsetHeader, FontFamily = new FontFamily("Courier New"), FontSize = 12, FontWeight = FontWeight.Bold, Foreground = Brushes.White, Padding = new Thickness(5), HorizontalAlignment = HorizontalAlignment.Center }; var offsetHeader = new Border { Background = new SolidColorBrush(Color.Parse("#2D2D30")), BorderBrush = new SolidColorBrush(Color.Parse("#555555")), BorderThickness = new Thickness(0, 0, 1, 1), Child = _offsetHeaderText }; Grid.SetColumn(offsetHeader, 0); Grid.SetRow(offsetHeader, 0); sharedGrid.Children.Add(offsetHeader); // Hex column header var hexHeaderBlock = new Border { Background = new SolidColorBrush(Color.Parse("#2D2D30")), BorderBrush = new SolidColorBrush(Color.Parse("#555555")), BorderThickness = new Thickness(0, 0, 1, 1), Child = new TextBlock { Text = hexHeader.ToString(), FontFamily = new FontFamily("Courier New"), FontSize = 12, FontWeight = FontWeight.Bold, Foreground = Brushes.White, Padding = new Thickness(5), HorizontalAlignment = HorizontalAlignment.Center } }; Grid.SetColumn(hexHeaderBlock, 1); Grid.SetRow(hexHeaderBlock, 0); sharedGrid.Children.Add(hexHeaderBlock); // ASCII column header _asciiHeaderText = new TextBlock { Text = AsciiHeader, FontFamily = new FontFamily("Courier New"), FontSize = 12, FontWeight = FontWeight.Bold, Foreground = Brushes.White, Padding = new Thickness(5), HorizontalAlignment = HorizontalAlignment.Center }; var asciiHeader = new Border { Background = new SolidColorBrush(Color.Parse("#2D2D30")), BorderBrush = new SolidColorBrush(Color.Parse("#555555")), BorderThickness = new Thickness(0, 0, 0, 1), Child = _asciiHeaderText }; Grid.SetColumn(asciiHeader, 2); Grid.SetRow(asciiHeader, 0); sharedGrid.Children.Add(asciiHeader); // Offset content column (directly in the shared grid) var offsetBorder = new Border { Background = new SolidColorBrush(Color.Parse("#1E1E1E")), BorderBrush = new SolidColorBrush(Color.Parse("#555555")), BorderThickness = new Thickness(0, 0, 1, 0), Child = _offsetContent }; Grid.SetColumn(offsetBorder, 0); Grid.SetRow(offsetBorder, 1); sharedGrid.Children.Add(offsetBorder); // Hex content column (directly in the shared grid) var hexBorder = new Border { Background = Brushes.Black, BorderBrush = new SolidColorBrush(Color.Parse("#555555")), BorderThickness = new Thickness(0, 0, 1, 0), Child = _hexContent }; Grid.SetColumn(hexBorder, 1); Grid.SetRow(hexBorder, 1); sharedGrid.Children.Add(hexBorder); // ASCII content column (directly in the shared grid) var asciiBorder = new Border { Background = Brushes.Black, Child = _asciiContent }; Grid.SetColumn(asciiBorder, 2); Grid.SetRow(asciiBorder, 1); sharedGrid.Children.Add(asciiBorder); // ScrollViewer that wraps the entire shared grid _scrollViewer = new ScrollViewer { Content = sharedGrid, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto }; // Main border var mainBorder = new Border { Child = _scrollViewer, BorderBrush = new SolidColorBrush(Color.Parse("#555555")), BorderThickness = new Thickness(1), Background = Brushes.Black }; Content = mainBorder; } public byte[] Data { get => GetValue(DataProperty); set => SetValue(DataProperty, value); } public string OffsetHeader { get => GetValue(OffsetHeaderProperty); set => SetValue(OffsetHeaderProperty, value); } public string AsciiHeader { get => GetValue(AsciiHeaderProperty); set => SetValue(AsciiHeaderProperty, value); } public List ColorRanges { get => GetValue(ColorRangesProperty); set => SetValue(ColorRangesProperty, value); } private void OnDataChanged() { _data = Data ?? []; UpdateDisplay(); } private void UpdateDisplay() { if(_data.Length == 0) { _offsetContent.Text = string.Empty; _hexContent.Inlines?.Clear(); _asciiContent.Inlines?.Clear(); return; } var offsetSb = new StringBuilder(); int totalLines = (_data.Length + BYTES_PER_LINE - 1) / BYTES_PER_LINE; // Clear existing inlines _hexContent.Inlines?.Clear(); _asciiContent.Inlines?.Clear(); // Build color lookup for quick access (byte index -> color) var colorLookup = new Dictionary(); if(ColorRanges != null) { foreach(ColorRange range in ColorRanges) { for(int i = range.Start; i <= range.End && i < _data.Length; i++) { if(!colorLookup.ContainsKey(i)) colorLookup[i] = range.Color; } } } for(var lineIndex = 0; lineIndex < totalLines; lineIndex++) { int offset = lineIndex * BYTES_PER_LINE; if(offset >= _data.Length) break; // Offset column (simple text) offsetSb.AppendLine(offset.ToString("X8")); // Hex column - build runs based on color ranges int endIdx = Math.Min(offset + BYTES_PER_LINE, _data.Length); BuildHexRuns(offset, endIdx, colorLookup); // Add line break for hex if(lineIndex < totalLines - 1) _hexContent.Inlines?.Add(new Run("\n")); // ASCII column - build runs based on color ranges BuildAsciiRuns(offset, endIdx, colorLookup); // Add line break for ASCII if(lineIndex < totalLines - 1) _asciiContent.Inlines?.Add(new Run("\n")); } _offsetContent.Text = offsetSb.ToString(); } private void BuildHexRuns(int offset, int endIdx, Dictionary colorLookup) { var currentRun = new StringBuilder(); IBrush currentColor = null; for(int i = offset; i < endIdx; i++) { // Get the color for this byte IBrush byteColor = colorLookup.TryGetValue(i, out IBrush value) ? value : Brushes.White; // If this is the first byte or color changed, flush previous run if(currentColor == null) currentColor = byteColor; else if(!Equals(byteColor, currentColor) && currentRun.Length > 0) { // Flush current run _hexContent.Inlines?.Add(new Run(currentRun.ToString()) { Foreground = currentColor }); currentRun.Clear(); currentColor = byteColor; } if(i > offset) currentRun.Append(' '); currentRun.Append(_data[i].ToString("X2")); } // Flush remaining run if(currentRun.Length > 0 && currentColor != null) { _hexContent.Inlines?.Add(new Run(currentRun.ToString()) { Foreground = currentColor }); } // Padding for hex column to maintain alignment int bytesWritten = endIdx - offset; if(bytesWritten >= BYTES_PER_LINE) return; var padding = new StringBuilder(); for(int i = bytesWritten; i < BYTES_PER_LINE; i++) padding.Append(" "); _hexContent.Inlines?.Add(new Run(padding.ToString()) { Foreground = Brushes.White }); } private void BuildAsciiRuns(int offset, int endIdx, Dictionary colorLookup) { var currentRun = new StringBuilder(); IBrush currentColor = null; for(int i = offset; i < endIdx; i++) { // Get the color for this byte IBrush byteColor = colorLookup.ContainsKey(i) ? colorLookup[i] : Brushes.White; // If this is the first byte or color changed, flush previous run if(currentColor == null) currentColor = byteColor; else if(!Equals(byteColor, currentColor) && currentRun.Length > 0) { // Flush current run _asciiContent.Inlines?.Add(new Run(currentRun.ToString()) { Foreground = currentColor }); currentRun.Clear(); currentColor = byteColor; } byte c = _data[i]; if(c is >= 32 and <= 126) currentRun.Append((char)c); else currentRun.Append('.'); } // Flush remaining run if(currentRun.Length > 0 && currentColor != null) { _asciiContent.Inlines?.Add(new Run(currentRun.ToString()) { Foreground = currentColor }); } } private void OnColorRangesChanged() { UpdateDisplay(); } private void OnOffsetHeaderChanged() { _offsetHeaderText?.Text = OffsetHeader; } private void OnAsciiHeaderChanged() { _asciiHeaderText?.Text = AsciiHeader; } }