XPlor/app/hexviewerwidget.cpp

606 lines
18 KiB
C++
Raw Normal View History

#include "hexviewerwidget.h"
#include <QHeaderView>
#include <QFontDatabase>
#include <QScrollBar>
#include <QPainter>
#include <QResizeEvent>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QClipboard>
#include <QApplication>
// ============================================================================
// HexView - Virtualized hex viewer with direct painting
// ============================================================================
HexView::HexView(QWidget *parent)
: QAbstractScrollArea(parent)
{
// Set up monospace font
mMonoFont = QFont("Consolas", 10);
if (!QFontDatabase::hasFamily("Consolas")) {
mMonoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
mMonoFont.setPointSize(10);
}
QFontMetrics fm(mMonoFont);
mCharWidth = fm.horizontalAdvance('0');
mLineHeight = fm.height();
// Default dark theme colors
mBgColor = QColor("#1e1e1e");
mTextColor = QColor("#888888");
mOffsetColor = QColor("#ad0c0c");
mNullColor = QColor("#505050");
mHighColor = QColor("#ad0c0c");
mPrintableColor = QColor("#c0c0c0");
mControlColor = QColor("#707070");
mNonPrintableColor = QColor("#909090");
mBorderColor = QColor("#333333");
mSelectionColor = QColor("#264f78"); // Blue selection highlight
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
viewport()->setAutoFillBackground(false);
viewport()->setMouseTracking(true);
setFocusPolicy(Qt::StrongFocus);
}
void HexView::setData(const QByteArray &data)
{
mData = data;
recalculateBytesPerLine();
updateScrollBars();
viewport()->update();
}
void HexView::setTheme(const Theme &theme)
{
mBgColor = QColor(theme.backgroundColor);
mTextColor = QColor(theme.textColorMuted);
mOffsetColor = QColor(theme.accentColor);
mHighColor = QColor(theme.accentColor);
mBorderColor = QColor(theme.borderColor);
// Derived colors based on theme brightness
int brightness = mBgColor.lightness();
if (brightness < 128) {
// Dark theme
mNullColor = QColor("#505050");
mPrintableColor = QColor("#c0c0c0");
mControlColor = QColor("#707070");
mNonPrintableColor = QColor("#909090");
} else {
// Light theme
mNullColor = QColor("#a0a0a0");
mPrintableColor = QColor("#303030");
mControlColor = QColor("#808080");
mNonPrintableColor = QColor("#606060");
}
viewport()->update();
}
void HexView::setBytesPerLine(int bytes)
{
if (bytes != mBytesPerLine && bytes >= 8) {
mBytesPerLine = bytes;
updateScrollBars();
viewport()->update();
}
}
void HexView::updateScrollBars()
{
if (mData.isEmpty()) {
verticalScrollBar()->setRange(0, 0);
horizontalScrollBar()->setRange(0, 0);
return;
}
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
// Account for header (1 line) + gap (4px ~= 0.25 lines)
int headerHeight = mLineHeight + 4;
int availableHeight = viewport()->height() - headerHeight;
int visibleLines = qMax(1, availableHeight / mLineHeight);
verticalScrollBar()->setRange(0, qMax(0, totalLines - visibleLines));
verticalScrollBar()->setPageStep(visibleLines);
verticalScrollBar()->setSingleStep(1);
// Horizontal: offset(10) + hex(3*n) + separator(2) + ascii(n)
int contentWidth = 10 * mCharWidth + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth + mBytesPerLine * mCharWidth;
int viewportWidth = viewport()->width();
horizontalScrollBar()->setRange(0, qMax(0, contentWidth - viewportWidth));
horizontalScrollBar()->setPageStep(viewportWidth);
}
void HexView::recalculateBytesPerLine()
{
if (mData.isEmpty()) return;
int viewportWidth = viewport()->width();
if (viewportWidth < 100) return;
// Calculate available space for hex bytes
// Format: XXXXXXXX HH HH HH HH HH HH ... | AAAA...
// Offset = 10 chars (8 hex + 2 spaces)
// Each byte = 3 chars hex + 1 char ascii
// Separator = 2 chars
int fixedWidth = 10 * mCharWidth + 2 * mCharWidth; // offset + separator
int remaining = viewportWidth - fixedWidth;
// Each byte needs: 3 chars hex + 1 char ascii = 4 chars
int bytesPerLine = remaining / (4 * mCharWidth);
// Round down to multiple of 8 for clean display
bytesPerLine = qMax(8, (bytesPerLine / 8) * 8);
bytesPerLine = qMin(32, bytesPerLine);
if (bytesPerLine != mBytesPerLine) {
mBytesPerLine = bytesPerLine;
updateScrollBars();
}
}
void HexView::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(viewport());
painter.setFont(mMonoFont);
painter.setRenderHint(QPainter::TextAntialiasing);
// Fill background
painter.fillRect(viewport()->rect(), mBgColor);
if (mData.isEmpty()) {
painter.setPen(mTextColor);
painter.drawText(viewport()->rect(), Qt::AlignCenter, "No data");
return;
}
int xOffset = -horizontalScrollBar()->value();
int firstLine = verticalScrollBar()->value();
int visibleLines = viewport()->height() / mLineHeight + 2;
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
// Calculate column positions (no extra gaps - uniform spacing)
int offsetX = xOffset;
int hexX = offsetX + 10 * mCharWidth;
int asciiX = hexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
// Draw header line
int y = mLineHeight;
painter.setPen(mOffsetColor);
painter.drawText(offsetX, y - 4, "Offset");
// Draw hex column headers (uniform spacing)
painter.setPen(mTextColor);
for (int i = 0; i < mBytesPerLine; i++) {
int x = hexX + i * 3 * mCharWidth;
painter.drawText(x, y - 4, QString("%1").arg(i, 2, 16, QChar('0')).toUpper());
}
// Draw "Decoded" header
painter.setPen(mOffsetColor);
painter.drawText(asciiX, y - 4, "Decoded");
// Draw separator line
painter.setPen(mBorderColor);
painter.drawLine(0, y, viewport()->width(), y);
y += 4; // Small gap after header
// Get selection range (normalized so start <= end)
int selStart = qMin(mSelectionStart, mSelectionEnd);
int selEnd = qMax(mSelectionStart, mSelectionEnd);
bool hasSelection = mSelectionStart >= 0 && mSelectionEnd >= 0;
// Draw data rows (only visible ones)
for (int line = 0; line < visibleLines && (firstLine + line) < totalLines; line++) {
int dataLine = firstLine + line;
int offset = dataLine * mBytesPerLine;
int bytesInLine = qMin(mBytesPerLine, mData.size() - offset);
y += mLineHeight;
// Draw offset
painter.setPen(mOffsetColor);
QString offsetStr = QString("%1").arg(offset, 8, 16, QChar('0')).toUpper();
painter.drawText(offsetX, y, offsetStr);
// Draw hex bytes (uniform spacing)
for (int i = 0; i < mBytesPerLine; i++) {
int x = hexX + i * 3 * mCharWidth;
if (i < bytesInLine) {
int byteIndex = offset + i;
quint8 byte = static_cast<quint8>(mData[byteIndex]);
// Draw selection background for hex
if (hasSelection && byteIndex >= selStart && byteIndex <= selEnd) {
QRect selRect(x - 1, y - mLineHeight + 4, 2 * mCharWidth + 2, mLineHeight);
painter.fillRect(selRect, mSelectionColor);
}
painter.setPen(getByteColor(byte));
QString byteStr = QString("%1").arg(byte, 2, 16, QChar('0')).toUpper();
painter.drawText(x, y, byteStr);
}
}
// Draw vertical separator
int sepX = asciiX - mCharWidth;
painter.setPen(mBorderColor);
painter.drawLine(sepX, y - mLineHeight + 4, sepX, y + 2);
// Draw ASCII/Decoded - show extended ASCII like HxD
for (int i = 0; i < mBytesPerLine; i++) {
int x = asciiX + i * mCharWidth;
if (i < bytesInLine) {
int byteIndex = offset + i;
quint8 byte = static_cast<quint8>(mData[byteIndex]);
// Draw selection background for decoded
if (hasSelection && byteIndex >= selStart && byteIndex <= selEnd) {
QRect selRect(x, y - mLineHeight + 4, mCharWidth, mLineHeight);
painter.fillRect(selRect, mSelectionColor);
}
painter.setPen(getAsciiColor(byte));
// Show actual character for printable ASCII and extended ASCII (Latin-1)
// Control chars (0x00-0x1F) and DEL (0x7F) show as '.'
QChar c;
if (byte < 0x20 || byte == 0x7F) {
c = '.';
} else {
c = QChar::fromLatin1(static_cast<char>(byte));
}
painter.drawText(x, y, QString(c));
}
}
}
}
void HexView::resizeEvent(QResizeEvent *event)
{
QAbstractScrollArea::resizeEvent(event);
recalculateBytesPerLine();
updateScrollBars();
}
void HexView::scrollContentsBy(int dx, int dy)
{
Q_UNUSED(dx);
Q_UNUSED(dy);
viewport()->update();
}
QColor HexView::getByteColor(quint8 byte) const
{
if (byte == 0x00) {
return mNullColor;
} else if (byte == 0xFF) {
return mHighColor;
} else if (byte >= 0x20 && byte < 0x7F) {
return mPrintableColor;
} else if (byte < 0x20) {
return mControlColor;
} else {
return mNonPrintableColor;
}
}
QColor HexView::getAsciiColor(quint8 byte) const
{
if (byte < 0x20 || byte == 0x7F) {
return mNullColor; // Control characters shown as '.'
} else if (byte >= 0x20 && byte < 0x7F) {
return mPrintableColor; // Standard ASCII
} else {
return mNonPrintableColor; // Extended ASCII (0x80-0xFF)
}
}
void HexView::updateColumnPositions()
{
mOffsetX = -horizontalScrollBar()->value();
mHexX = mOffsetX + 10 * mCharWidth;
mAsciiX = mHexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
mHeaderHeight = mLineHeight + 4;
}
int HexView::byteIndexAtPos(const QPoint &pos, SelectionSource *source) const
{
// Check if in header area
if (pos.y() < mHeaderHeight) {
return -1;
}
// Calculate which line
int lineY = pos.y() - mHeaderHeight;
int line = lineY / mLineHeight + verticalScrollBar()->value();
int col = -1;
// Check if in hex area
int hexEndX = mHexX + mBytesPerLine * 3 * mCharWidth;
if (pos.x() >= mHexX && pos.x() < hexEndX) {
int relX = pos.x() - mHexX;
col = relX / (3 * mCharWidth);
if (source) *source = SelectionSource::Hex;
}
// Check if in decoded/ASCII area
else if (pos.x() >= mAsciiX) {
int relX = pos.x() - mAsciiX;
col = relX / mCharWidth;
if (source) *source = SelectionSource::Decoded;
}
if (col < 0 || col >= mBytesPerLine) {
return -1;
}
int byteIndex = line * mBytesPerLine + col;
if (byteIndex < 0 || byteIndex >= mData.size()) {
return -1;
}
return byteIndex;
}
void HexView::clearSelection()
{
mSelectionStart = -1;
mSelectionEnd = -1;
mSelectionSource = SelectionSource::None;
viewport()->update();
}
bool HexView::hasSelection() const
{
return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
}
QString HexView::getSelectedHex() const
{
if (!hasSelection()) return QString();
int start = qMin(mSelectionStart, mSelectionEnd);
int end = qMax(mSelectionStart, mSelectionEnd);
QString result;
for (int i = start; i <= end && i < mData.size(); i++) {
result += QString("%1").arg(static_cast<quint8>(mData[i]), 2, 16, QChar('0')).toUpper();
}
return result;
}
QString HexView::getSelectedDecoded() const
{
if (!hasSelection()) return QString();
int start = qMin(mSelectionStart, mSelectionEnd);
int end = qMax(mSelectionStart, mSelectionEnd);
QString result;
for (int i = start; i <= end && i < mData.size(); i++) {
quint8 byte = static_cast<quint8>(mData[i]);
if (byte < 0x20 || byte == 0x7F) {
result += '.';
} else {
result += QChar::fromLatin1(static_cast<char>(byte));
}
}
return result;
}
void HexView::copyToClipboard()
{
if (!hasSelection()) return;
QString text;
if (mSelectionSource == SelectionSource::Hex) {
text = getSelectedHex();
} else {
text = getSelectedDecoded();
}
QApplication::clipboard()->setText(text);
}
void HexView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
updateColumnPositions();
SelectionSource source = SelectionSource::None;
int idx = byteIndexAtPos(event->pos(), &source);
if (idx >= 0) {
mSelectionStart = idx;
mSelectionEnd = idx;
mSelectionSource = source;
mSelecting = true;
setFocus();
viewport()->update();
} else {
clearSelection();
}
}
QAbstractScrollArea::mousePressEvent(event);
}
void HexView::mouseMoveEvent(QMouseEvent *event)
{
if (mSelecting && (event->buttons() & Qt::LeftButton)) {
updateColumnPositions();
int idx = byteIndexAtPos(event->pos());
if (idx >= 0) {
mSelectionEnd = idx;
viewport()->update();
}
}
QAbstractScrollArea::mouseMoveEvent(event);
}
void HexView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
mSelecting = false;
}
QAbstractScrollArea::mouseReleaseEvent(event);
}
void HexView::keyPressEvent(QKeyEvent *event)
{
if (event->matches(QKeySequence::Copy)) {
copyToClipboard();
event->accept();
return;
}
// Ctrl+A to select all
if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_A) {
if (!mData.isEmpty()) {
mSelectionStart = 0;
mSelectionEnd = mData.size() - 1;
mSelectionSource = SelectionSource::Hex;
viewport()->update();
}
event->accept();
return;
}
QAbstractScrollArea::keyPressEvent(event);
}
// ============================================================================
// HexViewerWidget
// ============================================================================
HexViewerWidget::HexViewerWidget(QWidget *parent)
: QWidget(parent)
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Splitter for hex view and metadata
mSplitter = new QSplitter(Qt::Horizontal, this);
// Left side - hex view container
auto *hexContainer = new QWidget(mSplitter);
auto *hexLayout = new QVBoxLayout(hexContainer);
hexLayout->setContentsMargins(0, 0, 0, 0);
hexLayout->setSpacing(0);
// Info label
mInfoLabel = new QLabel(hexContainer);
hexLayout->addWidget(mInfoLabel);
// Hex view
mHexView = new HexView(hexContainer);
hexLayout->addWidget(mHexView, 1);
mSplitter->addWidget(hexContainer);
// Metadata tree
mMetadataTree = new QTreeWidget(mSplitter);
mMetadataTree->setHeaderLabels({"Property", "Value"});
mMetadataTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
mMetadataTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
mMetadataTree->setAlternatingRowColors(true);
mSplitter->addWidget(mMetadataTree);
mSplitter->setSizes({700, 250});
mainLayout->addWidget(mSplitter);
// Connect to theme changes
connect(&Settings::instance(), &Settings::themeChanged, this, &HexViewerWidget::applyTheme);
// Apply current theme
applyTheme(Settings::instance().theme());
}
void HexViewerWidget::applyTheme(const Theme &theme)
{
mCurrentTheme = theme;
// Update info label
mInfoLabel->setStyleSheet(QString(
"QLabel { background-color: %1; color: %2; padding: 4px 8px; font-size: 11px; }"
).arg(theme.panelColor, theme.textColorMuted));
// Update hex view
mHexView->setTheme(theme);
// Update metadata tree
mMetadataTree->setStyleSheet(QString(
"QTreeWidget { background-color: %1; color: %2; border: none; }"
"QTreeWidget::item:selected { background-color: %3; color: white; }"
"QTreeWidget::item:alternate { background-color: %4; }"
"QHeaderView::section { background-color: %4; color: %5; padding: 4px; border: none; }"
).arg(theme.backgroundColor, theme.textColor, theme.accentColor, theme.panelColor, theme.textColorMuted));
}
void HexViewerWidget::setData(const QByteArray &data, const QString &filename)
{
mData = data;
mFilename = filename;
// Update info label
QString info = QString("%1 | %2 bytes").arg(filename).arg(data.size());
if (data.size() >= 4) {
QString magic;
for (int i = 0; i < qMin(4, data.size()); i++) {
char c = data[i];
magic += (c >= 32 && c < 127) ? c : '.';
}
info += QString(" | Magic: %1").arg(magic);
}
mInfoLabel->setText(info);
// Update hex view
mHexView->setData(data);
// Update metadata tree
mMetadataTree->clear();
auto *sizeItem = new QTreeWidgetItem(mMetadataTree);
sizeItem->setText(0, "File Size");
sizeItem->setText(1, QString("%1 bytes").arg(data.size()));
auto *filenameItem = new QTreeWidgetItem(mMetadataTree);
filenameItem->setText(0, "Filename");
filenameItem->setText(1, filename);
if (data.size() >= 4) {
auto *magicItem = new QTreeWidgetItem(mMetadataTree);
magicItem->setText(0, "Magic Bytes");
QString hexMagic;
for (int i = 0; i < qMin(16, data.size()); i++) {
hexMagic += QString("%1 ").arg(static_cast<quint8>(data[i]), 2, 16, QChar('0')).toUpper();
}
magicItem->setText(1, hexMagic.trimmed());
}
}
void HexViewerWidget::setMetadata(const QVariantMap &metadata)
{
// Add metadata from parsed fields (caller provides only visible fields)
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
auto *item = new QTreeWidgetItem(mMetadataTree);
item->setText(0, it.key());
QVariant val = it.value();
if (val.typeId() == QMetaType::QByteArray) {
QByteArray ba = val.toByteArray();
item->setText(1, QString("<%1 bytes>").arg(ba.size()));
} else {
item->setText(1, val.toString());
}
}
}