#include "hexviewerwidget.h" #include #include #include #include #include #include #include #include #include // ============================================================================ // 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(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(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(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(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(mData[i]); if (byte < 0x20 || byte == 0x7F) { result += '.'; } else { result += QChar::fromLatin1(static_cast(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(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()); } } }