Add new preview widgets and enhance existing viewers
- Add ListPreviewWidget for displaying parsed list data with table view - Add TextViewerWidget for text file preview with syntax highlighting - Add TreeBuilder class to organize parsed data into tree structure - Enhance HexView with selection support, copy functionality, keyboard navigation - Enhance ImagePreviewWidget with additional format support and metadata display - Minor audio preview widget adjustments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c45d5cba86
commit
57ad7c4111
@ -430,10 +430,8 @@ void AudioPreviewWidget::updateWaveformPosition()
|
|||||||
|
|
||||||
void AudioPreviewWidget::setMetadata(const QVariantMap &metadata)
|
void AudioPreviewWidget::setMetadata(const QVariantMap &metadata)
|
||||||
{
|
{
|
||||||
// Add custom metadata from parsed fields
|
// Add custom metadata from parsed fields (caller provides only visible fields)
|
||||||
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
||||||
if (it.key().startsWith("_")) continue; // Skip internal fields
|
|
||||||
|
|
||||||
auto *item = new QTreeWidgetItem(mMetadataTree);
|
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||||
item->setText(0, it.key());
|
item->setText(0, it.key());
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// HexView - Virtualized hex viewer with direct painting
|
// HexView - Virtualized hex viewer with direct painting
|
||||||
@ -33,10 +37,13 @@ HexView::HexView(QWidget *parent)
|
|||||||
mControlColor = QColor("#707070");
|
mControlColor = QColor("#707070");
|
||||||
mNonPrintableColor = QColor("#909090");
|
mNonPrintableColor = QColor("#909090");
|
||||||
mBorderColor = QColor("#333333");
|
mBorderColor = QColor("#333333");
|
||||||
|
mSelectionColor = QColor("#264f78"); // Blue selection highlight
|
||||||
|
|
||||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
||||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
viewport()->setAutoFillBackground(false);
|
viewport()->setAutoFillBackground(false);
|
||||||
|
viewport()->setMouseTracking(true);
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HexView::setData(const QByteArray &data)
|
void HexView::setData(const QByteArray &data)
|
||||||
@ -92,14 +99,17 @@ void HexView::updateScrollBars()
|
|||||||
}
|
}
|
||||||
|
|
||||||
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
|
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
|
||||||
int visibleLines = viewport()->height() / mLineHeight;
|
// 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()->setRange(0, qMax(0, totalLines - visibleLines));
|
||||||
verticalScrollBar()->setPageStep(visibleLines);
|
verticalScrollBar()->setPageStep(visibleLines);
|
||||||
verticalScrollBar()->setSingleStep(1);
|
verticalScrollBar()->setSingleStep(1);
|
||||||
|
|
||||||
// Horizontal: offset(8) + space + hex(3*n + gap) + separator + ascii(n) + margin
|
// Horizontal: offset(10) + hex(3*n) + separator(2) + ascii(n)
|
||||||
int contentWidth = 10 * mCharWidth + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth + mBytesPerLine * mCharWidth + 20;
|
int contentWidth = 10 * mCharWidth + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth + mBytesPerLine * mCharWidth;
|
||||||
int viewportWidth = viewport()->width();
|
int viewportWidth = viewport()->width();
|
||||||
|
|
||||||
horizontalScrollBar()->setRange(0, qMax(0, contentWidth - viewportWidth));
|
horizontalScrollBar()->setRange(0, qMax(0, contentWidth - viewportWidth));
|
||||||
@ -113,22 +123,19 @@ void HexView::recalculateBytesPerLine()
|
|||||||
int viewportWidth = viewport()->width();
|
int viewportWidth = viewport()->width();
|
||||||
if (viewportWidth < 100) return;
|
if (viewportWidth < 100) return;
|
||||||
|
|
||||||
// Calculate available space
|
// Calculate available space for hex bytes
|
||||||
// Format: XXXXXXXX HH HH HH HH ... | AAAA...
|
// Format: XXXXXXXX HH HH HH HH HH HH ... | AAAA...
|
||||||
// Offset = 10 chars (8 + 2 spaces)
|
// Offset = 10 chars (8 hex + 2 spaces)
|
||||||
// Each byte = 3 chars (2 hex + space)
|
// Each byte = 3 chars hex + 1 char ascii
|
||||||
// Gap after 8 bytes = 1 char
|
// Separator = 2 chars
|
||||||
// Separator = 3 chars
|
|
||||||
// ASCII = 1 char per byte
|
|
||||||
|
|
||||||
int offsetWidth = 10 * mCharWidth;
|
int fixedWidth = 10 * mCharWidth + 2 * mCharWidth; // offset + separator
|
||||||
int separatorWidth = 3 * mCharWidth;
|
int remaining = viewportWidth - fixedWidth;
|
||||||
int remaining = viewportWidth - offsetWidth - separatorWidth - 20;
|
|
||||||
|
|
||||||
// Each byte needs: 3 chars hex + 1 char ascii = 4 chars
|
// Each byte needs: 3 chars hex + 1 char ascii = 4 chars
|
||||||
int bytesPerLine = remaining / (4 * mCharWidth);
|
int bytesPerLine = remaining / (4 * mCharWidth);
|
||||||
|
|
||||||
// Round to multiple of 8
|
// Round down to multiple of 8 for clean display
|
||||||
bytesPerLine = qMax(8, (bytesPerLine / 8) * 8);
|
bytesPerLine = qMax(8, (bytesPerLine / 8) * 8);
|
||||||
bytesPerLine = qMin(32, bytesPerLine);
|
bytesPerLine = qMin(32, bytesPerLine);
|
||||||
|
|
||||||
@ -160,7 +167,7 @@ void HexView::paintEvent(QPaintEvent *event)
|
|||||||
int visibleLines = viewport()->height() / mLineHeight + 2;
|
int visibleLines = viewport()->height() / mLineHeight + 2;
|
||||||
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
|
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
|
||||||
|
|
||||||
// Calculate column positions
|
// Calculate column positions (no extra gaps - uniform spacing)
|
||||||
int offsetX = xOffset;
|
int offsetX = xOffset;
|
||||||
int hexX = offsetX + 10 * mCharWidth;
|
int hexX = offsetX + 10 * mCharWidth;
|
||||||
int asciiX = hexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
|
int asciiX = hexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
|
||||||
@ -170,11 +177,10 @@ void HexView::paintEvent(QPaintEvent *event)
|
|||||||
painter.setPen(mOffsetColor);
|
painter.setPen(mOffsetColor);
|
||||||
painter.drawText(offsetX, y - 4, "Offset");
|
painter.drawText(offsetX, y - 4, "Offset");
|
||||||
|
|
||||||
// Draw hex column headers
|
// Draw hex column headers (uniform spacing)
|
||||||
painter.setPen(mTextColor);
|
painter.setPen(mTextColor);
|
||||||
for (int i = 0; i < mBytesPerLine; i++) {
|
for (int i = 0; i < mBytesPerLine; i++) {
|
||||||
int x = hexX + i * 3 * mCharWidth;
|
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());
|
painter.drawText(x, y - 4, QString("%1").arg(i, 2, 16, QChar('0')).toUpper());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +194,11 @@ void HexView::paintEvent(QPaintEvent *event)
|
|||||||
|
|
||||||
y += 4; // Small gap after header
|
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)
|
// Draw data rows (only visible ones)
|
||||||
for (int line = 0; line < visibleLines && (firstLine + line) < totalLines; line++) {
|
for (int line = 0; line < visibleLines && (firstLine + line) < totalLines; line++) {
|
||||||
int dataLine = firstLine + line;
|
int dataLine = firstLine + line;
|
||||||
@ -201,13 +212,20 @@ void HexView::paintEvent(QPaintEvent *event)
|
|||||||
QString offsetStr = QString("%1").arg(offset, 8, 16, QChar('0')).toUpper();
|
QString offsetStr = QString("%1").arg(offset, 8, 16, QChar('0')).toUpper();
|
||||||
painter.drawText(offsetX, y, offsetStr);
|
painter.drawText(offsetX, y, offsetStr);
|
||||||
|
|
||||||
// Draw hex bytes
|
// Draw hex bytes (uniform spacing)
|
||||||
for (int i = 0; i < mBytesPerLine; i++) {
|
for (int i = 0; i < mBytesPerLine; i++) {
|
||||||
int x = hexX + i * 3 * mCharWidth;
|
int x = hexX + i * 3 * mCharWidth;
|
||||||
if (i >= 8) x += mCharWidth; // Gap after 8 bytes
|
|
||||||
|
|
||||||
if (i < bytesInLine) {
|
if (i < bytesInLine) {
|
||||||
quint8 byte = static_cast<quint8>(mData[offset + i]);
|
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));
|
painter.setPen(getByteColor(byte));
|
||||||
QString byteStr = QString("%1").arg(byte, 2, 16, QChar('0')).toUpper();
|
QString byteStr = QString("%1").arg(byte, 2, 16, QChar('0')).toUpper();
|
||||||
painter.drawText(x, y, byteStr);
|
painter.drawText(x, y, byteStr);
|
||||||
@ -219,14 +237,29 @@ void HexView::paintEvent(QPaintEvent *event)
|
|||||||
painter.setPen(mBorderColor);
|
painter.setPen(mBorderColor);
|
||||||
painter.drawLine(sepX, y - mLineHeight + 4, sepX, y + 2);
|
painter.drawLine(sepX, y - mLineHeight + 4, sepX, y + 2);
|
||||||
|
|
||||||
// Draw ASCII
|
// Draw ASCII/Decoded - show extended ASCII like HxD
|
||||||
for (int i = 0; i < mBytesPerLine; i++) {
|
for (int i = 0; i < mBytesPerLine; i++) {
|
||||||
int x = asciiX + i * mCharWidth;
|
int x = asciiX + i * mCharWidth;
|
||||||
|
|
||||||
if (i < bytesInLine) {
|
if (i < bytesInLine) {
|
||||||
quint8 byte = static_cast<quint8>(mData[offset + i]);
|
int byteIndex = offset + i;
|
||||||
char c = (byte >= 0x20 && byte < 0x7F) ? static_cast<char>(byte) : '.';
|
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));
|
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));
|
painter.drawText(x, y, QString(c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,10 +297,183 @@ QColor HexView::getByteColor(quint8 byte) const
|
|||||||
|
|
||||||
QColor HexView::getAsciiColor(quint8 byte) const
|
QColor HexView::getAsciiColor(quint8 byte) const
|
||||||
{
|
{
|
||||||
if (byte >= 0x20 && byte < 0x7F) {
|
if (byte < 0x20 || byte == 0x7F) {
|
||||||
return mPrintableColor;
|
return mNullColor; // Control characters shown as '.'
|
||||||
|
} else if (byte >= 0x20 && byte < 0x7F) {
|
||||||
|
return mPrintableColor; // Standard ASCII
|
||||||
|
} else {
|
||||||
|
return mNonPrintableColor; // Extended ASCII (0x80-0xFF)
|
||||||
}
|
}
|
||||||
return mNullColor;
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -383,9 +589,8 @@ void HexViewerWidget::setData(const QByteArray &data, const QString &filename)
|
|||||||
|
|
||||||
void HexViewerWidget::setMetadata(const QVariantMap &metadata)
|
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) {
|
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
||||||
if (it.key().startsWith("_")) continue;
|
|
||||||
|
|
||||||
auto *item = new QTreeWidgetItem(mMetadataTree);
|
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||||
item->setText(0, it.key());
|
item->setText(0, it.key());
|
||||||
|
|
||||||
|
|||||||
@ -13,12 +13,14 @@
|
|||||||
|
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
|
||||||
// Custom hex view widget with virtualized rendering
|
// Custom hex view widget with virtualized rendering and selection
|
||||||
class HexView : public QAbstractScrollArea
|
class HexView : public QAbstractScrollArea
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum class SelectionSource { None, Hex, Decoded };
|
||||||
|
|
||||||
explicit HexView(QWidget *parent = nullptr);
|
explicit HexView(QWidget *parent = nullptr);
|
||||||
|
|
||||||
void setData(const QByteArray &data);
|
void setData(const QByteArray &data);
|
||||||
@ -26,16 +28,29 @@ public:
|
|||||||
void setBytesPerLine(int bytes);
|
void setBytesPerLine(int bytes);
|
||||||
int bytesPerLine() const { return mBytesPerLine; }
|
int bytesPerLine() const { return mBytesPerLine; }
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
void clearSelection();
|
||||||
|
bool hasSelection() const;
|
||||||
|
QString getSelectedHex() const; // Returns hex without spaces
|
||||||
|
QString getSelectedDecoded() const; // Returns decoded ASCII/Latin-1
|
||||||
|
void copyToClipboard();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
void scrollContentsBy(int dx, int dy) override;
|
void scrollContentsBy(int dx, int dy) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateScrollBars();
|
void updateScrollBars();
|
||||||
void recalculateBytesPerLine();
|
void recalculateBytesPerLine();
|
||||||
QColor getByteColor(quint8 byte) const;
|
QColor getByteColor(quint8 byte) const;
|
||||||
QColor getAsciiColor(quint8 byte) const;
|
QColor getAsciiColor(quint8 byte) const;
|
||||||
|
int byteIndexAtPos(const QPoint &pos, SelectionSource *source = nullptr) const;
|
||||||
|
void updateColumnPositions();
|
||||||
|
|
||||||
QByteArray mData;
|
QByteArray mData;
|
||||||
QFont mMonoFont;
|
QFont mMonoFont;
|
||||||
@ -43,6 +58,18 @@ private:
|
|||||||
int mCharWidth = 0;
|
int mCharWidth = 0;
|
||||||
int mLineHeight = 0;
|
int mLineHeight = 0;
|
||||||
|
|
||||||
|
// Column positions (calculated once, updated on resize)
|
||||||
|
int mOffsetX = 0;
|
||||||
|
int mHexX = 0;
|
||||||
|
int mAsciiX = 0;
|
||||||
|
int mHeaderHeight = 0;
|
||||||
|
|
||||||
|
// Selection state
|
||||||
|
int mSelectionStart = -1;
|
||||||
|
int mSelectionEnd = -1;
|
||||||
|
SelectionSource mSelectionSource = SelectionSource::None;
|
||||||
|
bool mSelecting = false;
|
||||||
|
|
||||||
// Theme colors
|
// Theme colors
|
||||||
QColor mBgColor;
|
QColor mBgColor;
|
||||||
QColor mTextColor;
|
QColor mTextColor;
|
||||||
@ -53,6 +80,7 @@ private:
|
|||||||
QColor mControlColor;
|
QColor mControlColor;
|
||||||
QColor mNonPrintableColor;
|
QColor mNonPrintableColor;
|
||||||
QColor mBorderColor;
|
QColor mBorderColor;
|
||||||
|
QColor mSelectionColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HexViewerWidget : public QWidget
|
class HexViewerWidget : public QWidget
|
||||||
|
|||||||
@ -67,10 +67,8 @@ void ImagePreviewWidget::setFilename(const QString &filename)
|
|||||||
|
|
||||||
void ImagePreviewWidget::setMetadata(const QVariantMap &metadata)
|
void ImagePreviewWidget::setMetadata(const QVariantMap &metadata)
|
||||||
{
|
{
|
||||||
// Add parsed metadata fields to tree
|
// Add parsed metadata fields to tree (caller provides only visible fields)
|
||||||
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
||||||
if (it.key().startsWith("_")) continue; // Skip internal fields
|
|
||||||
|
|
||||||
auto *item = new QTreeWidgetItem(mMetadataTree);
|
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||||
item->setText(0, it.key());
|
item->setText(0, it.key());
|
||||||
|
|
||||||
@ -273,6 +271,27 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for RCB pixel block format (Avatar g4rc textures)
|
||||||
|
if (data.size() >= 0x28) {
|
||||||
|
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
quint32 rcbDataSize = (d[0x10] << 24) | (d[0x11] << 16) | (d[0x12] << 8) | d[0x13];
|
||||||
|
uchar formatByte = d[0x09];
|
||||||
|
if (rcbDataSize > 0 && rcbDataSize + 0x24 <= (quint32)data.size() && formatByte == 0x52) {
|
||||||
|
QImage image = loadRCBPixel(data);
|
||||||
|
if (!image.isNull()) {
|
||||||
|
mImageSize = image.size();
|
||||||
|
mBitsPerPixel = image.depth();
|
||||||
|
mOriginalPixmap = QPixmap::fromImage(image);
|
||||||
|
mZoomFactor = 1.0;
|
||||||
|
mImageLabel->setPixmap(mOriginalPixmap);
|
||||||
|
mImageLabel->adjustSize();
|
||||||
|
mInfoLabel->setText(QString("%1 - %2x%3 (RCB DXT1)").arg(mFilename).arg(image.width()).arg(image.height()));
|
||||||
|
updateMetadataDisplay();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QImage image;
|
QImage image;
|
||||||
|
|
||||||
// Try standard Qt loading first
|
// Try standard Qt loading first
|
||||||
@ -782,3 +801,146 @@ QImage ImagePreviewWidget::loadXBTX2D(const QByteArray &data)
|
|||||||
// Return the untiled version
|
// Return the untiled version
|
||||||
return untiledResult;
|
return untiledResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to find dimensions from DXT1 data size
|
||||||
|
static bool findDXT1Dimensions(int dataSize, int &outWidth, int &outHeight)
|
||||||
|
{
|
||||||
|
int blockCount = dataSize / 8; // DXT1 = 8 bytes per 4x4 block
|
||||||
|
|
||||||
|
// Try common power-of-2 dimensions
|
||||||
|
static const int sizes[] = {2048, 1024, 512, 256, 128, 64, 32, 16};
|
||||||
|
for (int w : sizes) {
|
||||||
|
for (int h : sizes) {
|
||||||
|
if ((w / 4) * (h / 4) == blockCount) {
|
||||||
|
outWidth = w;
|
||||||
|
outHeight = h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try non-square common sizes
|
||||||
|
static const int heights[] = {1736, 868, 768, 384, 192, 96, 48};
|
||||||
|
for (int w : sizes) {
|
||||||
|
for (int h : heights) {
|
||||||
|
if ((w / 4) * (h / 4) == blockCount) {
|
||||||
|
outWidth = w;
|
||||||
|
outHeight = h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage ImagePreviewWidget::loadRCBPixel(const QByteArray &data)
|
||||||
|
{
|
||||||
|
// RCB pixel block header (0x24 bytes):
|
||||||
|
// 0x00: Unknown (4 bytes)
|
||||||
|
// 0x08: Format info (byte at +9 = 0x52 for DXT1)
|
||||||
|
// 0x10: Pixel data size (4 bytes, BE)
|
||||||
|
// 0x24+: Raw DXT1 data
|
||||||
|
|
||||||
|
if (data.size() < 0x28) return QImage();
|
||||||
|
|
||||||
|
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
|
||||||
|
// Read data size (big-endian)
|
||||||
|
quint32 pixelDataSize = (d[0x10] << 24) | (d[0x11] << 16) | (d[0x12] << 8) | d[0x13];
|
||||||
|
|
||||||
|
// Verify it matches
|
||||||
|
if (pixelDataSize == 0 || (int)(pixelDataSize + 0x24) > data.size()) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check format byte (0x52 = DXT1)
|
||||||
|
uchar formatByte = d[0x09];
|
||||||
|
bool isDXT1 = (formatByte == 0x52);
|
||||||
|
|
||||||
|
if (!isDXT1) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find dimensions from data size
|
||||||
|
int width = 0, height = 0;
|
||||||
|
if (!findDXT1Dimensions(pixelDataSize, width, height)) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
mDetectedFormat = "RCB Pixel (DXT1)";
|
||||||
|
mCompression = "DXT1";
|
||||||
|
mBitsPerPixel = 4;
|
||||||
|
|
||||||
|
const uchar *pixelData = d + 0x24;
|
||||||
|
int blockSize = 8;
|
||||||
|
|
||||||
|
// Try untiled first (Xbox 360 textures are often tiled)
|
||||||
|
QByteArray untiled(pixelDataSize, 0);
|
||||||
|
untileXbox360(pixelData, reinterpret_cast<uchar*>(untiled.data()), width, height, blockSize);
|
||||||
|
|
||||||
|
QImage untiledResult = decodeDXTToImage(reinterpret_cast<const uchar*>(untiled.constData()),
|
||||||
|
width, height, blockSize, false, pixelDataSize);
|
||||||
|
QImage linearResult = decodeDXTToImage(pixelData, width, height, blockSize, false, pixelDataSize);
|
||||||
|
|
||||||
|
// Export for debugging
|
||||||
|
QDir().mkdir("exports");
|
||||||
|
QString debugName = mFilename.isEmpty() ? "rcb_debug" : mFilename;
|
||||||
|
int dot = debugName.lastIndexOf('.');
|
||||||
|
if (dot > 0) debugName = debugName.left(dot);
|
||||||
|
if (!untiledResult.isNull()) {
|
||||||
|
untiledResult.save("exports/" + debugName + "_UNTILED.png", "PNG");
|
||||||
|
}
|
||||||
|
if (!linearResult.isNull()) {
|
||||||
|
linearResult.save("exports/" + debugName + "_LINEAR.png", "PNG");
|
||||||
|
}
|
||||||
|
|
||||||
|
return untiledResult.isNull() ? linearResult : untiledResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage ImagePreviewWidget::loadRawDXT(const QByteArray &data, bool tryDXT5First)
|
||||||
|
{
|
||||||
|
int dataSize = data.size();
|
||||||
|
int width = 0, height = 0;
|
||||||
|
bool isDXT5 = false;
|
||||||
|
int blockSize = 8;
|
||||||
|
|
||||||
|
if (tryDXT5First) {
|
||||||
|
int blockCount = dataSize / 16;
|
||||||
|
for (int w = 2048; w >= 16; w /= 2) {
|
||||||
|
for (int h = 2048; h >= 16; h /= 2) {
|
||||||
|
if ((w / 4) * (h / 4) == blockCount) {
|
||||||
|
width = w;
|
||||||
|
height = h;
|
||||||
|
isDXT5 = true;
|
||||||
|
blockSize = 16;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (width > 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width == 0) {
|
||||||
|
if (!findDXT1Dimensions(dataSize, width, height)) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
isDXT5 = false;
|
||||||
|
blockSize = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
mDetectedFormat = isDXT5 ? "Raw DXT5" : "Raw DXT1";
|
||||||
|
mCompression = isDXT5 ? "DXT5" : "DXT1";
|
||||||
|
mBitsPerPixel = isDXT5 ? 8 : 4;
|
||||||
|
|
||||||
|
const uchar *srcData = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
|
||||||
|
QByteArray untiled(dataSize, 0);
|
||||||
|
untileXbox360(srcData, reinterpret_cast<uchar*>(untiled.data()), width, height, blockSize);
|
||||||
|
|
||||||
|
QImage untiledResult = decodeDXTToImage(reinterpret_cast<const uchar*>(untiled.constData()),
|
||||||
|
width, height, blockSize, isDXT5, dataSize);
|
||||||
|
QImage linearResult = decodeDXTToImage(srcData, width, height, blockSize, isDXT5, dataSize);
|
||||||
|
|
||||||
|
return untiledResult.isNull() ? linearResult : untiledResult;
|
||||||
|
}
|
||||||
|
|||||||
@ -66,6 +66,12 @@ private:
|
|||||||
// Load Xbox 360 XBTX2D texture format
|
// Load Xbox 360 XBTX2D texture format
|
||||||
QImage loadXBTX2D(const QByteArray &data);
|
QImage loadXBTX2D(const QByteArray &data);
|
||||||
|
|
||||||
|
// Load RCB pixel block (Avatar g4rc texture format)
|
||||||
|
QImage loadRCBPixel(const QByteArray &data);
|
||||||
|
|
||||||
|
// Load raw DXT1/DXT5 data with auto-detected dimensions
|
||||||
|
QImage loadRawDXT(const QByteArray &data, bool tryDXT5First = false);
|
||||||
|
|
||||||
// Detected image info
|
// Detected image info
|
||||||
QString mDetectedFormat;
|
QString mDetectedFormat;
|
||||||
int mBitsPerPixel;
|
int mBitsPerPixel;
|
||||||
|
|||||||
271
app/listpreviewwidget.cpp
Normal file
271
app/listpreviewwidget.cpp
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
#include "listpreviewwidget.h"
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
ListPreviewWidget::ListPreviewWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setupUi();
|
||||||
|
|
||||||
|
// Connect to theme changes
|
||||||
|
connect(&Settings::instance(), &Settings::themeChanged,
|
||||||
|
this, &ListPreviewWidget::applyTheme);
|
||||||
|
applyTheme(Settings::instance().theme());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::setupUi()
|
||||||
|
{
|
||||||
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
mainLayout->setSpacing(0);
|
||||||
|
|
||||||
|
// Info label at top
|
||||||
|
mInfoLabel = new QLabel(this);
|
||||||
|
mInfoLabel->setContentsMargins(8, 4, 8, 4);
|
||||||
|
mainLayout->addWidget(mInfoLabel);
|
||||||
|
|
||||||
|
// Splitter for table and metadata
|
||||||
|
mSplitter = new QSplitter(Qt::Horizontal, this);
|
||||||
|
mainLayout->addWidget(mSplitter, 1);
|
||||||
|
|
||||||
|
// Table widget for list items
|
||||||
|
mTableWidget = new QTableWidget(this);
|
||||||
|
mTableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||||
|
mTableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
mTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
|
mTableWidget->horizontalHeader()->setStretchLastSection(true);
|
||||||
|
mTableWidget->verticalHeader()->setVisible(false);
|
||||||
|
mTableWidget->setAlternatingRowColors(true);
|
||||||
|
mSplitter->addWidget(mTableWidget);
|
||||||
|
|
||||||
|
// Metadata tree on the right
|
||||||
|
mMetadataTree = new QTreeWidget(this);
|
||||||
|
mMetadataTree->setHeaderLabels({"Property", "Value"});
|
||||||
|
mMetadataTree->setColumnCount(2);
|
||||||
|
mMetadataTree->header()->setStretchLastSection(true);
|
||||||
|
mMetadataTree->setMinimumWidth(200);
|
||||||
|
mSplitter->addWidget(mMetadataTree);
|
||||||
|
|
||||||
|
// Set initial splitter sizes (70% table, 30% metadata)
|
||||||
|
mSplitter->setSizes({700, 300});
|
||||||
|
|
||||||
|
// Connect selection change
|
||||||
|
connect(mTableWidget, &QTableWidget::itemSelectionChanged,
|
||||||
|
this, &ListPreviewWidget::onItemSelectionChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::setListData(const QVariantList &items, const QString &listName)
|
||||||
|
{
|
||||||
|
mItems = items;
|
||||||
|
mListName = listName;
|
||||||
|
|
||||||
|
// Update info label
|
||||||
|
mInfoLabel->setText(QString("%1 | %2 items")
|
||||||
|
.arg(listName.isEmpty() ? "List" : listName)
|
||||||
|
.arg(items.size()));
|
||||||
|
|
||||||
|
populateTable(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::populateTable(const QVariantList &items)
|
||||||
|
{
|
||||||
|
mTableWidget->clear();
|
||||||
|
mTableWidget->setRowCount(0);
|
||||||
|
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
mTableWidget->setColumnCount(1);
|
||||||
|
mTableWidget->setHorizontalHeaderLabels({"(empty)"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all unique keys from all items to determine columns
|
||||||
|
QStringList columns;
|
||||||
|
for (const QVariant &item : items) {
|
||||||
|
if (item.typeId() == QMetaType::QVariantMap) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
|
||||||
|
const QString &key = it.key();
|
||||||
|
// Skip internal fields and binary data
|
||||||
|
if (key.startsWith('_') && key != "_name")
|
||||||
|
continue;
|
||||||
|
if (it.value().typeId() == QMetaType::QByteArray)
|
||||||
|
continue;
|
||||||
|
if (!columns.contains(key))
|
||||||
|
columns.append(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If items are simple values (not maps), use a single column
|
||||||
|
if (columns.isEmpty()) {
|
||||||
|
columns.append("Value");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup columns
|
||||||
|
mTableWidget->setColumnCount(columns.size());
|
||||||
|
mTableWidget->setHorizontalHeaderLabels(columns);
|
||||||
|
|
||||||
|
// Add rows
|
||||||
|
mTableWidget->setRowCount(items.size());
|
||||||
|
for (int row = 0; row < items.size(); ++row) {
|
||||||
|
const QVariant &item = items[row];
|
||||||
|
|
||||||
|
if (item.typeId() == QMetaType::QVariantMap) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
for (int col = 0; col < columns.size(); ++col) {
|
||||||
|
const QString &key = columns[col];
|
||||||
|
QVariant val = map.value(key);
|
||||||
|
QString displayText;
|
||||||
|
|
||||||
|
if (val.typeId() == QMetaType::QVariantMap) {
|
||||||
|
displayText = QString("{%1 fields}").arg(val.toMap().size());
|
||||||
|
} else if (val.typeId() == QMetaType::QVariantList) {
|
||||||
|
displayText = QString("[%1 items]").arg(val.toList().size());
|
||||||
|
} else if (val.typeId() == QMetaType::QByteArray) {
|
||||||
|
displayText = QString("<%1 bytes>").arg(val.toByteArray().size());
|
||||||
|
} else {
|
||||||
|
displayText = val.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *tableItem = new QTableWidgetItem(displayText);
|
||||||
|
mTableWidget->setItem(row, col, tableItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Simple value
|
||||||
|
auto *tableItem = new QTableWidgetItem(item.toString());
|
||||||
|
mTableWidget->setItem(row, 0, tableItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize columns to content
|
||||||
|
mTableWidget->resizeColumnsToContents();
|
||||||
|
|
||||||
|
// Clear metadata tree initially
|
||||||
|
mMetadataTree->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::setMetadata(const QVariantMap &metadata)
|
||||||
|
{
|
||||||
|
// This can be used to set overall list metadata
|
||||||
|
updateMetadataTree(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::clear()
|
||||||
|
{
|
||||||
|
mItems.clear();
|
||||||
|
mListName.clear();
|
||||||
|
mTableWidget->clear();
|
||||||
|
mTableWidget->setRowCount(0);
|
||||||
|
mTableWidget->setColumnCount(0);
|
||||||
|
mMetadataTree->clear();
|
||||||
|
mInfoLabel->setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::onItemSelectionChanged()
|
||||||
|
{
|
||||||
|
int row = mTableWidget->currentRow();
|
||||||
|
if (row >= 0 && row < mItems.size()) {
|
||||||
|
const QVariant &item = mItems[row];
|
||||||
|
if (item.typeId() == QMetaType::QVariantMap) {
|
||||||
|
updateMetadataTree(item.toMap());
|
||||||
|
} else {
|
||||||
|
QVariantMap simple;
|
||||||
|
simple["value"] = item;
|
||||||
|
simple["index"] = row;
|
||||||
|
updateMetadataTree(simple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::updateMetadataTree(const QVariantMap &item)
|
||||||
|
{
|
||||||
|
mMetadataTree->clear();
|
||||||
|
|
||||||
|
for (auto it = item.constBegin(); it != item.constEnd(); ++it) {
|
||||||
|
const QString &key = it.key();
|
||||||
|
const QVariant &val = it.value();
|
||||||
|
|
||||||
|
auto *treeItem = new QTreeWidgetItem(mMetadataTree);
|
||||||
|
treeItem->setText(0, key);
|
||||||
|
|
||||||
|
if (val.typeId() == QMetaType::QVariantMap) {
|
||||||
|
treeItem->setText(1, QString("{%1 fields}").arg(val.toMap().size()));
|
||||||
|
// Add nested items
|
||||||
|
const QVariantMap nested = val.toMap();
|
||||||
|
for (auto nit = nested.constBegin(); nit != nested.constEnd(); ++nit) {
|
||||||
|
auto *child = new QTreeWidgetItem(treeItem);
|
||||||
|
child->setText(0, nit.key());
|
||||||
|
if (nit.value().typeId() == QMetaType::QByteArray) {
|
||||||
|
child->setText(1, QString("<%1 bytes>").arg(nit.value().toByteArray().size()));
|
||||||
|
} else {
|
||||||
|
child->setText(1, nit.value().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (val.typeId() == QMetaType::QVariantList) {
|
||||||
|
treeItem->setText(1, QString("[%1 items]").arg(val.toList().size()));
|
||||||
|
} else if (val.typeId() == QMetaType::QByteArray) {
|
||||||
|
treeItem->setText(1, QString("<%1 bytes>").arg(val.toByteArray().size()));
|
||||||
|
} else {
|
||||||
|
treeItem->setText(1, val.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mMetadataTree->expandAll();
|
||||||
|
mMetadataTree->resizeColumnToContents(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListPreviewWidget::applyTheme(const Theme &theme)
|
||||||
|
{
|
||||||
|
// Style info label
|
||||||
|
mInfoLabel->setStyleSheet(QString(
|
||||||
|
"QLabel {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border-bottom: 1px solid %3;"
|
||||||
|
"}"
|
||||||
|
).arg(theme.panelColor, theme.textColorMuted, theme.borderColor));
|
||||||
|
|
||||||
|
// Style table widget
|
||||||
|
mTableWidget->setStyleSheet(QString(
|
||||||
|
"QTableWidget {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
" gridline-color: %3;"
|
||||||
|
"}"
|
||||||
|
"QTableWidget::item {"
|
||||||
|
" padding: 4px;"
|
||||||
|
"}"
|
||||||
|
"QTableWidget::item:selected {"
|
||||||
|
" background-color: %4;"
|
||||||
|
"}"
|
||||||
|
"QTableWidget::item:alternate {"
|
||||||
|
" background-color: %5;"
|
||||||
|
"}"
|
||||||
|
"QHeaderView::section {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
" padding: 4px;"
|
||||||
|
" font-weight: bold;"
|
||||||
|
"}"
|
||||||
|
).arg(theme.panelColor, theme.textColor, theme.borderColor,
|
||||||
|
theme.accentColor, theme.backgroundColor));
|
||||||
|
|
||||||
|
// Style metadata tree
|
||||||
|
mMetadataTree->setStyleSheet(QString(
|
||||||
|
"QTreeWidget {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
"}"
|
||||||
|
"QTreeWidget::item:selected {"
|
||||||
|
" background-color: %4;"
|
||||||
|
"}"
|
||||||
|
"QHeaderView::section {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
" padding: 4px;"
|
||||||
|
"}"
|
||||||
|
).arg(theme.panelColor, theme.textColor, theme.borderColor, theme.accentColor));
|
||||||
|
}
|
||||||
48
app/listpreviewwidget.h
Normal file
48
app/listpreviewwidget.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#ifndef LISTPREVIEWWIDGET_H
|
||||||
|
#define LISTPREVIEWWIDGET_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
class ListPreviewWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ListPreviewWidget(QWidget *parent = nullptr);
|
||||||
|
~ListPreviewWidget() = default;
|
||||||
|
|
||||||
|
// Set list data from QVariantList (each item is a QVariantMap with fields)
|
||||||
|
void setListData(const QVariantList &items, const QString &listName = QString());
|
||||||
|
|
||||||
|
// Set metadata for the sidebar
|
||||||
|
void setMetadata(const QVariantMap &metadata);
|
||||||
|
|
||||||
|
// Clear the widget
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void applyTheme(const Theme &theme);
|
||||||
|
void onItemSelectionChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupUi();
|
||||||
|
void populateTable(const QVariantList &items);
|
||||||
|
void updateMetadataTree(const QVariantMap &item);
|
||||||
|
|
||||||
|
QLabel *mInfoLabel;
|
||||||
|
QTableWidget *mTableWidget;
|
||||||
|
QTreeWidget *mMetadataTree;
|
||||||
|
QSplitter *mSplitter;
|
||||||
|
|
||||||
|
QVariantList mItems;
|
||||||
|
QString mListName;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // LISTPREVIEWWIDGET_H
|
||||||
149
app/textviewerwidget.cpp
Normal file
149
app/textviewerwidget.cpp
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
#include "textviewerwidget.h"
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QHeaderView>
|
||||||
|
|
||||||
|
TextViewerWidget::TextViewerWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
mainLayout->setSpacing(0);
|
||||||
|
|
||||||
|
// Info label at top
|
||||||
|
mInfoLabel = new QLabel(this);
|
||||||
|
mInfoLabel->setContentsMargins(8, 4, 8, 4);
|
||||||
|
mainLayout->addWidget(mInfoLabel);
|
||||||
|
|
||||||
|
// Splitter for text view and metadata
|
||||||
|
mSplitter = new QSplitter(Qt::Horizontal, this);
|
||||||
|
mainLayout->addWidget(mSplitter, 1);
|
||||||
|
|
||||||
|
// Text editor (read-only)
|
||||||
|
mTextEdit = new QPlainTextEdit(this);
|
||||||
|
mTextEdit->setReadOnly(true);
|
||||||
|
mTextEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||||
|
|
||||||
|
// Use monospace font
|
||||||
|
QFont monoFont("Consolas", 10);
|
||||||
|
monoFont.setStyleHint(QFont::Monospace);
|
||||||
|
mTextEdit->setFont(monoFont);
|
||||||
|
|
||||||
|
mSplitter->addWidget(mTextEdit);
|
||||||
|
|
||||||
|
// Metadata tree on the right
|
||||||
|
mMetadataTree = new QTreeWidget(this);
|
||||||
|
mMetadataTree->setHeaderLabels({"Property", "Value"});
|
||||||
|
mMetadataTree->setColumnCount(2);
|
||||||
|
mMetadataTree->header()->setStretchLastSection(true);
|
||||||
|
mMetadataTree->setMinimumWidth(200);
|
||||||
|
mSplitter->addWidget(mMetadataTree);
|
||||||
|
|
||||||
|
// Set initial splitter sizes (80% text, 20% metadata)
|
||||||
|
mSplitter->setSizes({800, 200});
|
||||||
|
|
||||||
|
// Connect to theme changes
|
||||||
|
connect(&Settings::instance(), &Settings::themeChanged,
|
||||||
|
this, &TextViewerWidget::applyTheme);
|
||||||
|
applyTheme(Settings::instance().theme());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextViewerWidget::setData(const QByteArray &data, const QString &filename)
|
||||||
|
{
|
||||||
|
mData = data;
|
||||||
|
mFilename = filename;
|
||||||
|
|
||||||
|
// Set text content
|
||||||
|
QString text = QString::fromUtf8(data);
|
||||||
|
mTextEdit->setPlainText(text);
|
||||||
|
|
||||||
|
// Update info label
|
||||||
|
QFileInfo fi(filename);
|
||||||
|
int lineCount = text.count('\n') + 1;
|
||||||
|
mInfoLabel->setText(QString("%1 | %2 bytes | %3 lines")
|
||||||
|
.arg(filename)
|
||||||
|
.arg(data.size())
|
||||||
|
.arg(lineCount));
|
||||||
|
|
||||||
|
// Setup syntax highlighting based on extension
|
||||||
|
setupSyntaxHighlighting(fi.suffix().toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextViewerWidget::setMetadata(const QVariantMap &metadata)
|
||||||
|
{
|
||||||
|
mMetadataTree->clear();
|
||||||
|
|
||||||
|
for (auto it = metadata.constBegin(); it != metadata.constEnd(); ++it) {
|
||||||
|
const QString &key = it.key();
|
||||||
|
const QVariant &val = it.value();
|
||||||
|
|
||||||
|
// Skip internal fields and large binary data
|
||||||
|
if (key.startsWith('_') && key != "_name" && key != "_type")
|
||||||
|
continue;
|
||||||
|
if (val.typeId() == QMetaType::QByteArray)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||||
|
item->setText(0, key);
|
||||||
|
|
||||||
|
if (val.typeId() == QMetaType::QVariantMap) {
|
||||||
|
item->setText(1, QString("{%1 fields}").arg(val.toMap().size()));
|
||||||
|
} else if (val.typeId() == QMetaType::QVariantList) {
|
||||||
|
item->setText(1, QString("[%1 items]").arg(val.toList().size()));
|
||||||
|
} else {
|
||||||
|
item->setText(1, val.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mMetadataTree->resizeColumnToContents(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextViewerWidget::applyTheme(const Theme &theme)
|
||||||
|
{
|
||||||
|
mCurrentTheme = theme;
|
||||||
|
|
||||||
|
// Style the text editor
|
||||||
|
QString textStyle = QString(
|
||||||
|
"QPlainTextEdit {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
" selection-background-color: %4;"
|
||||||
|
"}"
|
||||||
|
).arg(theme.panelColor, theme.textColor, theme.borderColor, theme.accentColor);
|
||||||
|
|
||||||
|
mTextEdit->setStyleSheet(textStyle);
|
||||||
|
|
||||||
|
// Style info label
|
||||||
|
mInfoLabel->setStyleSheet(QString(
|
||||||
|
"QLabel {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border-bottom: 1px solid %3;"
|
||||||
|
"}"
|
||||||
|
).arg(theme.panelColor, theme.textColorMuted, theme.borderColor));
|
||||||
|
|
||||||
|
// Style metadata tree
|
||||||
|
mMetadataTree->setStyleSheet(QString(
|
||||||
|
"QTreeWidget {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
"}"
|
||||||
|
"QTreeWidget::item:selected {"
|
||||||
|
" background-color: %4;"
|
||||||
|
"}"
|
||||||
|
"QHeaderView::section {"
|
||||||
|
" background-color: %1;"
|
||||||
|
" color: %2;"
|
||||||
|
" border: 1px solid %3;"
|
||||||
|
" padding: 4px;"
|
||||||
|
"}"
|
||||||
|
).arg(theme.panelColor, theme.textColor, theme.borderColor, theme.accentColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextViewerWidget::setupSyntaxHighlighting(const QString &extension)
|
||||||
|
{
|
||||||
|
// Basic syntax highlighting could be added here in the future
|
||||||
|
// For now, just adjust display based on file type
|
||||||
|
Q_UNUSED(extension)
|
||||||
|
}
|
||||||
42
app/textviewerwidget.h
Normal file
42
app/textviewerwidget.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#ifndef TEXTVIEWERWIDGET_H
|
||||||
|
#define TEXTVIEWERWIDGET_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QPlainTextEdit>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
|
||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
class TextViewerWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TextViewerWidget(QWidget *parent = nullptr);
|
||||||
|
~TextViewerWidget() = default;
|
||||||
|
|
||||||
|
void setData(const QByteArray &data, const QString &filename);
|
||||||
|
void setMetadata(const QVariantMap &metadata);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void applyTheme(const Theme &theme);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupSyntaxHighlighting(const QString &extension);
|
||||||
|
|
||||||
|
QByteArray mData;
|
||||||
|
QString mFilename;
|
||||||
|
|
||||||
|
QSplitter *mSplitter;
|
||||||
|
QLabel *mInfoLabel;
|
||||||
|
QPlainTextEdit *mTextEdit;
|
||||||
|
QTreeWidget *mMetadataTree;
|
||||||
|
|
||||||
|
Theme mCurrentTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TEXTVIEWERWIDGET_H
|
||||||
324
app/treebuilder.cpp
Normal file
324
app/treebuilder.cpp
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
#include "treebuilder.h"
|
||||||
|
#include "xtreewidget.h"
|
||||||
|
#include "xtreewidgetitem.h"
|
||||||
|
#include "typeregistry.h"
|
||||||
|
#include "logmanager.h"
|
||||||
|
|
||||||
|
#include <QCollator>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// Natural sort comparator for strings with numbers (e.g., "chunk1" < "chunk2" < "chunk10")
|
||||||
|
static bool naturalLessThan(const QString& a, const QString& b) {
|
||||||
|
static QCollator collator;
|
||||||
|
collator.setNumericMode(true);
|
||||||
|
collator.setCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
return collator.compare(a, b) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeBuilder::TreeBuilder(XTreeWidget* tree, const TypeRegistry& registry)
|
||||||
|
: m_tree(tree)
|
||||||
|
, m_registry(registry)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TreeBuilder::reset()
|
||||||
|
{
|
||||||
|
m_categoryRoots.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TreeBuilder::pluralizeType(const QString& typeName) const
|
||||||
|
{
|
||||||
|
const auto& mod = m_registry.module();
|
||||||
|
const auto it = mod.types.find(typeName);
|
||||||
|
QString groupLabel = (it != mod.types.end() && !it->display.isEmpty())
|
||||||
|
? it->display
|
||||||
|
: typeName;
|
||||||
|
return groupLabel + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
XTreeWidgetItem* TreeBuilder::ensureTypeCategoryRoot(const QString& typeName, const QString& displayOverride)
|
||||||
|
{
|
||||||
|
const QString categoryKey = displayOverride.isEmpty() ? typeName : displayOverride;
|
||||||
|
|
||||||
|
if (m_categoryRoots.contains(categoryKey))
|
||||||
|
return m_categoryRoots[categoryKey];
|
||||||
|
|
||||||
|
auto* root = new XTreeWidgetItem(m_tree);
|
||||||
|
const QString categoryLabel = displayOverride.isEmpty()
|
||||||
|
? pluralizeType(typeName)
|
||||||
|
: displayOverride + "s";
|
||||||
|
root->setText(0, categoryLabel);
|
||||||
|
root->setData(0, Qt::UserRole + 1, "CATEGORY");
|
||||||
|
root->setData(0, Qt::UserRole + 2, typeName);
|
||||||
|
m_tree->addTopLevelItem(root);
|
||||||
|
|
||||||
|
m_categoryRoots.insert(categoryKey, root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
XTreeWidgetItem* TreeBuilder::ensureSubcategory(XTreeWidgetItem* parent, const QString& childTypeName)
|
||||||
|
{
|
||||||
|
// Look for existing subcategory
|
||||||
|
for (int i = 0; i < parent->childCount(); i++) {
|
||||||
|
auto* c = static_cast<XTreeWidgetItem*>(parent->child(i));
|
||||||
|
if (c->data(0, Qt::UserRole + 1).toString() == "SUBCATEGORY" &&
|
||||||
|
c->data(0, Qt::UserRole + 2).toString() == childTypeName)
|
||||||
|
{
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* sub = new XTreeWidgetItem(parent);
|
||||||
|
sub->setText(0, pluralizeType(childTypeName));
|
||||||
|
sub->setData(0, Qt::UserRole + 1, "SUBCATEGORY");
|
||||||
|
sub->setData(0, Qt::UserRole + 2, childTypeName);
|
||||||
|
parent->addChild(sub);
|
||||||
|
sub->setExpanded(false);
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
XTreeWidgetItem* TreeBuilder::addInstanceNode(XTreeWidgetItem* parent, const QString& displayName,
|
||||||
|
const QString& typeName, const QVariantMap& vars)
|
||||||
|
{
|
||||||
|
auto* inst = new XTreeWidgetItem(parent);
|
||||||
|
inst->setText(0, displayName);
|
||||||
|
inst->setData(0, Qt::UserRole + 1, "INSTANCE");
|
||||||
|
inst->setData(0, Qt::UserRole + 2, typeName);
|
||||||
|
inst->setData(0, Qt::UserRole + 3, vars);
|
||||||
|
parent->addChild(inst);
|
||||||
|
inst->setExpanded(false);
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TreeBuilder::instanceDisplayFor(const QVariantMap& obj, const QString& fallbackType,
|
||||||
|
const QString& fallbackKey, std::optional<int> index)
|
||||||
|
{
|
||||||
|
// _name takes priority (explicit name set by script via set_name())
|
||||||
|
if (DslKeys::contains(obj, DslKey::Name)) {
|
||||||
|
const QString s = DslKeys::getString(obj, DslKey::Name);
|
||||||
|
if (!s.isEmpty()) return s;
|
||||||
|
}
|
||||||
|
// _display is secondary (set via set_display())
|
||||||
|
if (DslKeys::contains(obj, DslKey::Display)) {
|
||||||
|
const QString s = DslKeys::getString(obj, DslKey::Display);
|
||||||
|
if (!s.isEmpty()) return s;
|
||||||
|
}
|
||||||
|
if (!fallbackType.isEmpty()) return fallbackType;
|
||||||
|
|
||||||
|
if (!fallbackKey.isEmpty()) {
|
||||||
|
if (index.has_value()) return QString("%1[%2]").arg(fallbackKey).arg(*index);
|
||||||
|
return fallbackKey;
|
||||||
|
}
|
||||||
|
return QStringLiteral("<unnamed>");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TreeBuilder::addParsedFile(const QString& typeName, const QVariantMap& vars, const QString& fileName)
|
||||||
|
{
|
||||||
|
XTreeWidgetItem* cat = ensureTypeCategoryRoot(typeName, DslKeys::getString(vars, DslKey::Display));
|
||||||
|
const QString displayName = DslKeys::contains(vars, DslKey::Name)
|
||||||
|
? DslKeys::getString(vars, DslKey::Name)
|
||||||
|
: fileName;
|
||||||
|
XTreeWidgetItem* inst = addInstanceNode(cat, displayName, typeName, vars);
|
||||||
|
routeNestedObjects(inst, vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TreeBuilder::routeNestedObjects(XTreeWidgetItem* parent, const QVariantMap& vars)
|
||||||
|
{
|
||||||
|
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
||||||
|
const QString& key = it.key();
|
||||||
|
const QVariant& v = it.value();
|
||||||
|
|
||||||
|
// Child object (QVariantMap with _type)
|
||||||
|
if (v.typeId() == QMetaType::QVariantMap) {
|
||||||
|
const QVariantMap child = v.toMap();
|
||||||
|
|
||||||
|
// Skip hidden objects
|
||||||
|
if (DslKeys::get(child, DslKey::Hidden).toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString childType = DslKeys::getString(child, DslKey::Type);
|
||||||
|
if (!childType.isEmpty()) {
|
||||||
|
auto* subcat = ensureSubcategory(parent, childType);
|
||||||
|
const QString childDisplay = instanceDisplayFor(child, childType, key);
|
||||||
|
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
|
||||||
|
routeNestedObjects(childInst, child);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array of child objects
|
||||||
|
if (v.typeId() == QMetaType::QVariantList) {
|
||||||
|
// Check for skip marker
|
||||||
|
if (DslKeys::hasSkipTree(vars, key)) {
|
||||||
|
LogManager::instance().debug(QString("[TREE] Skipping array '%1' (has skip_tree marker)").arg(key));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantList list = v.toList();
|
||||||
|
LogManager::instance().debug(QString("[TREE] Processing array '%1' with %2 items")
|
||||||
|
.arg(key).arg(list.size()));
|
||||||
|
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
if (list[i].typeId() != QMetaType::QVariantMap) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QVariantMap child = list[i].toMap();
|
||||||
|
|
||||||
|
// Skip hidden objects
|
||||||
|
if (DslKeys::get(child, DslKey::Hidden).toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString childType = DslKeys::getString(child, DslKey::Type);
|
||||||
|
if (childType.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* subcat = ensureSubcategory(parent, childType);
|
||||||
|
const QString childDisplay = instanceDisplayFor(child, childType, key, i);
|
||||||
|
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
|
||||||
|
routeNestedObjects(childInst, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TreeBuilder::organizeChildrenByExtension(XTreeWidgetItem* parent)
|
||||||
|
{
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
// Recursively process children first (depth-first)
|
||||||
|
for (int i = 0; i < parent->childCount(); i++) {
|
||||||
|
organizeChildrenByExtension(static_cast<XTreeWidgetItem*>(parent->child(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only organize SUBCATEGORY nodes
|
||||||
|
QString nodeType = parent->data(0, Qt::UserRole + 1).toString();
|
||||||
|
if (nodeType != "SUBCATEGORY") return;
|
||||||
|
|
||||||
|
// Group children by extension
|
||||||
|
QMap<QString, QList<XTreeWidgetItem*>> byExtension;
|
||||||
|
QList<XTreeWidgetItem*> noExtension;
|
||||||
|
QList<XTreeWidgetItem*> nonInstanceChildren;
|
||||||
|
|
||||||
|
// Take all children
|
||||||
|
QList<XTreeWidgetItem*> children;
|
||||||
|
while (parent->childCount() > 0) {
|
||||||
|
children.append(static_cast<XTreeWidgetItem*>(parent->takeChild(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* child : children) {
|
||||||
|
QString childNodeType = child->data(0, Qt::UserRole + 1).toString();
|
||||||
|
|
||||||
|
// Keep non-instance children as-is
|
||||||
|
if (childNodeType == "SUBCATEGORY" || childNodeType == "EXTENSION_GROUP") {
|
||||||
|
nonInstanceChildren.append(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap vars = child->data(0, Qt::UserRole + 3).toMap();
|
||||||
|
QString name = DslKeys::getString(vars, DslKey::Name);
|
||||||
|
|
||||||
|
// Skip names starting with dot (indexed chunk names)
|
||||||
|
if (name.startsWith('.')) {
|
||||||
|
noExtension.append(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dotPos = name.lastIndexOf('.');
|
||||||
|
if (dotPos > 0) {
|
||||||
|
QString ext = name.mid(dotPos + 1).toLower();
|
||||||
|
byExtension[ext].append(child);
|
||||||
|
} else {
|
||||||
|
noExtension.append(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add non-instance children first
|
||||||
|
for (auto* child : nonInstanceChildren) {
|
||||||
|
parent->addChild(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only create groups if multiple extensions exist
|
||||||
|
int uniqueExtensions = byExtension.size() + (noExtension.isEmpty() ? 0 : 1);
|
||||||
|
if (uniqueExtensions <= 1) {
|
||||||
|
// Put all items back directly (sorted)
|
||||||
|
QList<XTreeWidgetItem*> allItems;
|
||||||
|
for (auto& list : byExtension) {
|
||||||
|
allItems.append(list);
|
||||||
|
}
|
||||||
|
allItems.append(noExtension);
|
||||||
|
|
||||||
|
std::sort(allItems.begin(), allItems.end(), [](XTreeWidgetItem* a, XTreeWidgetItem* b) {
|
||||||
|
return naturalLessThan(a->text(0), b->text(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto* child : allItems) {
|
||||||
|
parent->addChild(child);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create extension groups
|
||||||
|
QStringList sortedExts = byExtension.keys();
|
||||||
|
std::sort(sortedExts.begin(), sortedExts.end());
|
||||||
|
|
||||||
|
for (const QString& ext : sortedExts) {
|
||||||
|
auto* extGroup = new XTreeWidgetItem(parent);
|
||||||
|
extGroup->setText(0, QString(".%1").arg(ext));
|
||||||
|
extGroup->setData(0, Qt::UserRole + 1, "EXTENSION_GROUP");
|
||||||
|
extGroup->setData(0, Qt::UserRole + 2, ext);
|
||||||
|
|
||||||
|
QList<XTreeWidgetItem*>& items = byExtension[ext];
|
||||||
|
std::sort(items.begin(), items.end(), [](XTreeWidgetItem* a, XTreeWidgetItem* b) {
|
||||||
|
return naturalLessThan(a->text(0), b->text(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto* item : items) {
|
||||||
|
extGroup->addChild(item);
|
||||||
|
}
|
||||||
|
extGroup->setExpanded(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add items with no extension at the end
|
||||||
|
if (!noExtension.isEmpty()) {
|
||||||
|
auto* otherGroup = new XTreeWidgetItem(parent);
|
||||||
|
otherGroup->setText(0, "(other)");
|
||||||
|
otherGroup->setData(0, Qt::UserRole + 1, "EXTENSION_GROUP");
|
||||||
|
otherGroup->setData(0, Qt::UserRole + 2, "");
|
||||||
|
|
||||||
|
std::sort(noExtension.begin(), noExtension.end(), [](XTreeWidgetItem* a, XTreeWidgetItem* b) {
|
||||||
|
return naturalLessThan(a->text(0), b->text(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto* item : noExtension) {
|
||||||
|
otherGroup->addChild(item);
|
||||||
|
}
|
||||||
|
otherGroup->setExpanded(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TreeBuilder::updateNodeCounts(XTreeWidgetItem* node)
|
||||||
|
{
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
// Recursively update children first
|
||||||
|
for (int i = 0; i < node->childCount(); i++) {
|
||||||
|
updateNodeCounts(static_cast<XTreeWidgetItem*>(node->child(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update count for grouping nodes
|
||||||
|
QString nodeType = node->data(0, Qt::UserRole + 1).toString();
|
||||||
|
if (nodeType == "SUBCATEGORY" || nodeType == "CATEGORY" || nodeType == "EXTENSION_GROUP") {
|
||||||
|
int count = node->childCount();
|
||||||
|
if (count > 0) {
|
||||||
|
QString currentText = node->text(0);
|
||||||
|
int parenPos = currentText.lastIndexOf(" (");
|
||||||
|
if (parenPos > 0) {
|
||||||
|
currentText = currentText.left(parenPos);
|
||||||
|
}
|
||||||
|
node->setText(0, QString("%1 (%2)").arg(currentText).arg(count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/treebuilder.h
Normal file
52
app/treebuilder.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#ifndef TREEBUILDER_H
|
||||||
|
#define TREEBUILDER_H
|
||||||
|
|
||||||
|
#include "dslkeys.h"
|
||||||
|
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QHash>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
class XTreeWidget;
|
||||||
|
class XTreeWidgetItem;
|
||||||
|
class TypeRegistry;
|
||||||
|
|
||||||
|
class TreeBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TreeBuilder(XTreeWidget* tree, const TypeRegistry& registry);
|
||||||
|
|
||||||
|
// Build tree from parsed data
|
||||||
|
void addParsedFile(const QString& typeName, const QVariantMap& vars, const QString& fileName);
|
||||||
|
|
||||||
|
// Category management
|
||||||
|
XTreeWidgetItem* ensureTypeCategoryRoot(const QString& typeName, const QString& displayOverride = {});
|
||||||
|
XTreeWidgetItem* ensureSubcategory(XTreeWidgetItem* parent, const QString& childTypeName);
|
||||||
|
|
||||||
|
// Instance management
|
||||||
|
XTreeWidgetItem* addInstanceNode(XTreeWidgetItem* parent, const QString& displayName,
|
||||||
|
const QString& typeName, const QVariantMap& vars);
|
||||||
|
|
||||||
|
// Recursively route nested objects into tree
|
||||||
|
void routeNestedObjects(XTreeWidgetItem* parent, const QVariantMap& vars);
|
||||||
|
|
||||||
|
// Post-processing
|
||||||
|
void organizeChildrenByExtension(XTreeWidgetItem* parent);
|
||||||
|
void updateNodeCounts(XTreeWidgetItem* node);
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
QString pluralizeType(const QString& typeName) const;
|
||||||
|
static QString instanceDisplayFor(const QVariantMap& obj, const QString& fallbackType,
|
||||||
|
const QString& fallbackKey = {}, std::optional<int> index = std::nullopt);
|
||||||
|
|
||||||
|
// Clear all category roots
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
XTreeWidget* m_tree;
|
||||||
|
const TypeRegistry& m_registry;
|
||||||
|
QHash<QString, XTreeWidgetItem*> m_categoryRoots;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TREEBUILDER_H
|
||||||
Loading…
x
Reference in New Issue
Block a user