mirror of
https://github.com/aaru-dps/Aaru.git
synced 2025-12-16 11:14:25 +00:00
474 lines
16 KiB
C#
474 lines
16 KiB
C#
// /***************************************************************************
|
|
// 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 ?? [];
|
|
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()
|
|
{
|
|
_offsetHeaderText?.Text = OffsetHeader;
|
|
}
|
|
|
|
private void OnAsciiHeaderChanged()
|
|
{
|
|
_asciiHeaderText?.Text = AsciiHeader;
|
|
}
|
|
} |