#include "hexviewerwidget.h" #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"); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); viewport()->setAutoFillBackground(false); } 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; int visibleLines = viewport()->height() / mLineHeight; verticalScrollBar()->setRange(0, qMax(0, totalLines - visibleLines)); verticalScrollBar()->setPageStep(visibleLines); verticalScrollBar()->setSingleStep(1); // Horizontal: offset(8) + space + hex(3*n + gap) + separator + ascii(n) + margin int contentWidth = 10 * mCharWidth + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth + mBytesPerLine * mCharWidth + 20; 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 // Format: XXXXXXXX HH HH HH HH ... | AAAA... // Offset = 10 chars (8 + 2 spaces) // Each byte = 3 chars (2 hex + space) // Gap after 8 bytes = 1 char // Separator = 3 chars // ASCII = 1 char per byte int offsetWidth = 10 * mCharWidth; int separatorWidth = 3 * mCharWidth; int remaining = viewportWidth - offsetWidth - separatorWidth - 20; // Each byte needs: 3 chars hex + 1 char ascii = 4 chars int bytesPerLine = remaining / (4 * mCharWidth); // Round to multiple of 8 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 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 painter.setPen(mTextColor); for (int i = 0; i < mBytesPerLine; i++) { int x = hexX + i * 3 * mCharWidth; if (i == 8) x += mCharWidth; // Gap after 8 bytes 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 // 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 for (int i = 0; i < mBytesPerLine; i++) { int x = hexX + i * 3 * mCharWidth; if (i >= 8) x += mCharWidth; // Gap after 8 bytes if (i < bytesInLine) { quint8 byte = static_cast(mData[offset + i]); 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 for (int i = 0; i < mBytesPerLine; i++) { int x = asciiX + i * mCharWidth; if (i < bytesInLine) { quint8 byte = static_cast(mData[offset + i]); char c = (byte >= 0x20 && byte < 0x7F) ? static_cast(byte) : '.'; painter.setPen(getAsciiColor(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 mPrintableColor; } return mNullColor; } // ============================================================================ // 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) { for (auto it = metadata.begin(); it != metadata.end(); ++it) { if (it.key().startsWith("_")) continue; 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()); } } }