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)
|
||||
{
|
||||
// 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) {
|
||||
if (it.key().startsWith("_")) continue; // Skip internal fields
|
||||
|
||||
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||
item->setText(0, it.key());
|
||||
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
#include <QScrollBar>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
|
||||
// ============================================================================
|
||||
// HexView - Virtualized hex viewer with direct painting
|
||||
@ -33,10 +37,13 @@ HexView::HexView(QWidget *parent)
|
||||
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)
|
||||
@ -92,14 +99,17 @@ void HexView::updateScrollBars()
|
||||
}
|
||||
|
||||
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()->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;
|
||||
// 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));
|
||||
@ -113,22 +123,19 @@ void HexView::recalculateBytesPerLine()
|
||||
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
|
||||
// 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 offsetWidth = 10 * mCharWidth;
|
||||
int separatorWidth = 3 * mCharWidth;
|
||||
int remaining = viewportWidth - offsetWidth - separatorWidth - 20;
|
||||
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 to multiple of 8
|
||||
// Round down to multiple of 8 for clean display
|
||||
bytesPerLine = qMax(8, (bytesPerLine / 8) * 8);
|
||||
bytesPerLine = qMin(32, bytesPerLine);
|
||||
|
||||
@ -160,7 +167,7 @@ void HexView::paintEvent(QPaintEvent *event)
|
||||
int visibleLines = viewport()->height() / mLineHeight + 2;
|
||||
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
|
||||
|
||||
// Calculate column positions
|
||||
// Calculate column positions (no extra gaps - uniform spacing)
|
||||
int offsetX = xOffset;
|
||||
int hexX = offsetX + 10 * mCharWidth;
|
||||
int asciiX = hexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
|
||||
@ -170,11 +177,10 @@ void HexView::paintEvent(QPaintEvent *event)
|
||||
painter.setPen(mOffsetColor);
|
||||
painter.drawText(offsetX, y - 4, "Offset");
|
||||
|
||||
// Draw hex column headers
|
||||
// Draw hex column headers (uniform spacing)
|
||||
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());
|
||||
}
|
||||
|
||||
@ -188,6 +194,11 @@ void HexView::paintEvent(QPaintEvent *event)
|
||||
|
||||
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;
|
||||
@ -201,13 +212,20 @@ void HexView::paintEvent(QPaintEvent *event)
|
||||
QString offsetStr = QString("%1").arg(offset, 8, 16, QChar('0')).toUpper();
|
||||
painter.drawText(offsetX, y, offsetStr);
|
||||
|
||||
// Draw hex bytes
|
||||
// Draw hex bytes (uniform spacing)
|
||||
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<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));
|
||||
QString byteStr = QString("%1").arg(byte, 2, 16, QChar('0')).toUpper();
|
||||
painter.drawText(x, y, byteStr);
|
||||
@ -219,14 +237,29 @@ void HexView::paintEvent(QPaintEvent *event)
|
||||
painter.setPen(mBorderColor);
|
||||
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++) {
|
||||
int x = asciiX + i * mCharWidth;
|
||||
|
||||
if (i < bytesInLine) {
|
||||
quint8 byte = static_cast<quint8>(mData[offset + i]);
|
||||
char c = (byte >= 0x20 && byte < 0x7F) ? static_cast<char>(byte) : '.';
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -264,10 +297,183 @@ QColor HexView::getByteColor(quint8 byte) const
|
||||
|
||||
QColor HexView::getAsciiColor(quint8 byte) const
|
||||
{
|
||||
if (byte >= 0x20 && byte < 0x7F) {
|
||||
return mPrintableColor;
|
||||
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)
|
||||
}
|
||||
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)
|
||||
{
|
||||
// Add metadata from parsed fields (caller provides only visible fields)
|
||||
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
|
||||
if (it.key().startsWith("_")) continue;
|
||||
|
||||
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||
item->setText(0, it.key());
|
||||
|
||||
|
||||
@ -13,12 +13,14 @@
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
// Custom hex view widget with virtualized rendering
|
||||
// Custom hex view widget with virtualized rendering and selection
|
||||
class HexView : public QAbstractScrollArea
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class SelectionSource { None, Hex, Decoded };
|
||||
|
||||
explicit HexView(QWidget *parent = nullptr);
|
||||
|
||||
void setData(const QByteArray &data);
|
||||
@ -26,16 +28,29 @@ public:
|
||||
void setBytesPerLine(int bytes);
|
||||
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:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) 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:
|
||||
void updateScrollBars();
|
||||
void recalculateBytesPerLine();
|
||||
QColor getByteColor(quint8 byte) const;
|
||||
QColor getAsciiColor(quint8 byte) const;
|
||||
int byteIndexAtPos(const QPoint &pos, SelectionSource *source = nullptr) const;
|
||||
void updateColumnPositions();
|
||||
|
||||
QByteArray mData;
|
||||
QFont mMonoFont;
|
||||
@ -43,6 +58,18 @@ private:
|
||||
int mCharWidth = 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
|
||||
QColor mBgColor;
|
||||
QColor mTextColor;
|
||||
@ -53,6 +80,7 @@ private:
|
||||
QColor mControlColor;
|
||||
QColor mNonPrintableColor;
|
||||
QColor mBorderColor;
|
||||
QColor mSelectionColor;
|
||||
};
|
||||
|
||||
class HexViewerWidget : public QWidget
|
||||
|
||||
@ -67,10 +67,8 @@ void ImagePreviewWidget::setFilename(const QString &filename)
|
||||
|
||||
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) {
|
||||
if (it.key().startsWith("_")) continue; // Skip internal fields
|
||||
|
||||
auto *item = new QTreeWidgetItem(mMetadataTree);
|
||||
item->setText(0, it.key());
|
||||
|
||||
@ -273,6 +271,27 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
|
||||
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;
|
||||
|
||||
// Try standard Qt loading first
|
||||
@ -782,3 +801,146 @@ QImage ImagePreviewWidget::loadXBTX2D(const QByteArray &data)
|
||||
// Return the untiled version
|
||||
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
|
||||
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
|
||||
QString mDetectedFormat;
|
||||
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