mirror of
https://github.com/aaru-dps/Aaru.git
synced 2025-12-16 19:24:25 +00:00
[GUI] Add a hex viewer panel.
This commit is contained in:
474
Aaru.Gui/Controls/HexViewPanel.cs
Normal file
474
Aaru.Gui/Controls/HexViewPanel.cs
Normal file
@@ -0,0 +1,474 @@
|
||||
// /***************************************************************************
|
||||
// Aaru Data Preservation Suite
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Filename : HexViewPanel.cs
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// 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;
|
||||
|
||||
/// <summary>Represents a color range for highlighting bytes in the hex view.</summary>
|
||||
public class ColorRange
|
||||
{
|
||||
/// <summary>Gets or sets the starting byte index (inclusive).</summary>
|
||||
public int Start { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ending byte index (inclusive).</summary>
|
||||
public int End { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the color to apply to this range.</summary>
|
||||
public IBrush Color { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Hex view control with synchronized scrolling of offset, hex, and ASCII columns.</summary>
|
||||
public class HexViewPanel : UserControl
|
||||
{
|
||||
private const int BYTES_PER_LINE = 16;
|
||||
|
||||
public static readonly StyledProperty<byte[]> DataProperty =
|
||||
AvaloniaProperty.Register<HexViewPanel, byte[]>(nameof(Data));
|
||||
|
||||
public static readonly StyledProperty<string> OffsetHeaderProperty =
|
||||
AvaloniaProperty.Register<HexViewPanel, string>(nameof(OffsetHeader), "Offset");
|
||||
|
||||
public static readonly StyledProperty<string> AsciiHeaderProperty =
|
||||
AvaloniaProperty.Register<HexViewPanel, string>(nameof(AsciiHeader), "ASCII");
|
||||
|
||||
public static readonly StyledProperty<List<ColorRange>> ColorRangesProperty =
|
||||
AvaloniaProperty.Register<HexViewPanel, List<ColorRange>>(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<HexViewPanel>(static (sender, _) => sender.OnDataChanged());
|
||||
|
||||
OffsetHeaderProperty.Changed.AddClassHandler<HexViewPanel>(static (sender, _) =>
|
||||
sender.OnOffsetHeaderChanged());
|
||||
|
||||
AsciiHeaderProperty.Changed.AddClassHandler<HexViewPanel>(static (sender, _) => sender.OnAsciiHeaderChanged());
|
||||
|
||||
ColorRangesProperty.Changed.AddClassHandler<HexViewPanel>(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<ColorRange> ColorRanges
|
||||
{
|
||||
get => GetValue(ColorRangesProperty);
|
||||
set => SetValue(ColorRangesProperty, value);
|
||||
}
|
||||
|
||||
private void OnDataChanged()
|
||||
{
|
||||
_data = Data ?? Array.Empty<byte>();
|
||||
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<int, IBrush>();
|
||||
|
||||
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<int, IBrush> 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<int, IBrush> 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()
|
||||
{
|
||||
if(_offsetHeaderText != null) _offsetHeaderText.Text = OffsetHeader;
|
||||
}
|
||||
|
||||
private void OnAsciiHeaderChanged()
|
||||
{
|
||||
if(_asciiHeaderText != null) _asciiHeaderText.Text = AsciiHeader;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user