Enhance app UI with export integration and tab management
Major UI improvements across the application: MainWindow: - Integrate undo stack for field editing - Dirty state tracking with tab title indicators (*) - Save/SaveAs prompts on tab close with unsaved changes - Export menu integration in tab context menu - Tab management: close all, close left/right of current tab - Connect export system to tree widget signals XTreeWidget: - Add context menus for tree items - Quick export action for immediate saves - Export dialog action for format options - Raw data export for any item - Batch export for containers XTreeWidgetItem: - Add modified state tracking with visual indicator - Support for marking items as dirty ImagePreviewWidget: - Enhanced image display and navigation - Improved zoom and pan controls TreeBuilder: - Better handling of nested data structures - Improved child node generation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
37bde14174
commit
57c7ee7de5
@ -261,10 +261,23 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
|
||||
}
|
||||
|
||||
// Check for DDS format (starts with "DDS ")
|
||||
if (data.size() >= 4 && data.left(4) == "DDS ") {
|
||||
if (data.size() >= 128 && data.left(4) == "DDS ") {
|
||||
QImage image = loadDDS(data);
|
||||
if (!image.isNull()) {
|
||||
mImageSize = image.size();
|
||||
mOriginalPixmap = QPixmap::fromImage(image);
|
||||
mZoomFactor = 1.0;
|
||||
mImageLabel->setPixmap(mOriginalPixmap);
|
||||
mImageLabel->adjustSize();
|
||||
mInfoLabel->setText(QString("%1 - %2x%3 (%4)")
|
||||
.arg(mFilename).arg(image.width()).arg(image.height()).arg(mDetectedFormat));
|
||||
updateMetadataDisplay();
|
||||
return true;
|
||||
}
|
||||
// Fall through if DDS loading failed
|
||||
mDetectedFormat = "DDS";
|
||||
mCompression = "DXT";
|
||||
mImageLabel->setText(QString("DDS Texture Format\n\nSize: %1 bytes\n\nDDS preview not yet implemented.")
|
||||
mCompression = "Unknown";
|
||||
mImageLabel->setText(QString("DDS Texture Format\n\nSize: %1 bytes\n\nUnsupported DDS format.")
|
||||
.arg(data.size()));
|
||||
mInfoLabel->setText(QString("%1 - DDS Texture (%2 bytes)").arg(mFilename).arg(data.size()));
|
||||
updateMetadataDisplay();
|
||||
@ -588,6 +601,154 @@ static void decodeDXT5BlockStatic(const uchar *src, QRgb *dest, int destPitch)
|
||||
}
|
||||
}
|
||||
|
||||
// DXT1 block decoder - read colors as little-endian (PC/DDS native)
|
||||
static void decodeDXT1BlockStaticLE(const uchar *src, QRgb *dest, int destPitch)
|
||||
{
|
||||
// Read colors as little-endian (PC native format)
|
||||
quint16 c0 = src[0] | (src[1] << 8);
|
||||
quint16 c1 = src[2] | (src[3] << 8);
|
||||
|
||||
// Extract RGB565 components: bits 15-11=R, 10-5=G, 4-0=B
|
||||
int r0 = ((c0 >> 11) & 0x1F) * 255 / 31;
|
||||
int g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
|
||||
int b0 = (c0 & 0x1F) * 255 / 31;
|
||||
|
||||
int r1 = ((c1 >> 11) & 0x1F) * 255 / 31;
|
||||
int g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
|
||||
int b1 = (c1 & 0x1F) * 255 / 31;
|
||||
|
||||
// Build color table
|
||||
QRgb colors[4];
|
||||
colors[0] = qRgb(r0, g0, b0);
|
||||
colors[1] = qRgb(r1, g1, b1);
|
||||
|
||||
if (c0 > c1) {
|
||||
colors[2] = qRgb((2 * r0 + r1) / 3, (2 * g0 + g1) / 3, (2 * b0 + b1) / 3);
|
||||
colors[3] = qRgb((r0 + 2 * r1) / 3, (g0 + 2 * g1) / 3, (b0 + 2 * b1) / 3);
|
||||
} else {
|
||||
colors[2] = qRgb((r0 + r1) / 2, (g0 + g1) / 2, (b0 + b1) / 2);
|
||||
colors[3] = qRgba(0, 0, 0, 0); // Transparent
|
||||
}
|
||||
|
||||
// Read indices - standard order
|
||||
for (int y = 0; y < 4; y++) {
|
||||
uchar rowByte = src[4 + y];
|
||||
for (int x = 0; x < 4; x++) {
|
||||
int idx = (rowByte >> (x * 2)) & 0x03;
|
||||
dest[y * destPitch + x] = colors[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DXT5 block decoder - read as little-endian (PC/DDS native)
|
||||
static void decodeDXT5BlockStaticLE(const uchar *src, QRgb *dest, int destPitch)
|
||||
{
|
||||
// Alpha block (first 8 bytes) - standard order
|
||||
uchar alpha0 = src[0];
|
||||
uchar alpha1 = src[1];
|
||||
|
||||
// Build alpha table
|
||||
uchar alphas[8];
|
||||
alphas[0] = alpha0;
|
||||
alphas[1] = alpha1;
|
||||
|
||||
if (alpha0 > alpha1) {
|
||||
alphas[2] = (6 * alpha0 + 1 * alpha1) / 7;
|
||||
alphas[3] = (5 * alpha0 + 2 * alpha1) / 7;
|
||||
alphas[4] = (4 * alpha0 + 3 * alpha1) / 7;
|
||||
alphas[5] = (3 * alpha0 + 4 * alpha1) / 7;
|
||||
alphas[6] = (2 * alpha0 + 5 * alpha1) / 7;
|
||||
alphas[7] = (1 * alpha0 + 6 * alpha1) / 7;
|
||||
} else {
|
||||
alphas[2] = (4 * alpha0 + 1 * alpha1) / 5;
|
||||
alphas[3] = (3 * alpha0 + 2 * alpha1) / 5;
|
||||
alphas[4] = (2 * alpha0 + 3 * alpha1) / 5;
|
||||
alphas[5] = (1 * alpha0 + 4 * alpha1) / 5;
|
||||
alphas[6] = 0;
|
||||
alphas[7] = 255;
|
||||
}
|
||||
|
||||
// Alpha indices (6 bytes, 48 bits = 16 pixels * 3 bits each)
|
||||
quint64 alphaIndices = src[2] | ((quint64)src[3] << 8) | ((quint64)src[4] << 16) |
|
||||
((quint64)src[5] << 24) | ((quint64)src[6] << 32) | ((quint64)src[7] << 40);
|
||||
|
||||
// Color block (bytes 8-15, same as DXT1) - little-endian
|
||||
const uchar *colorSrc = src + 8;
|
||||
quint16 c0 = colorSrc[0] | (colorSrc[1] << 8); // Little-endian (PC)
|
||||
quint16 c1 = colorSrc[2] | (colorSrc[3] << 8); // Little-endian (PC)
|
||||
|
||||
// RGB565: bits 15-11=R, 10-5=G, 4-0=B
|
||||
int r0 = ((c0 >> 11) & 0x1F) * 255 / 31;
|
||||
int g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
|
||||
int b0 = (c0 & 0x1F) * 255 / 31;
|
||||
|
||||
int r1 = ((c1 >> 11) & 0x1F) * 255 / 31;
|
||||
int g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
|
||||
int b1 = (c1 & 0x1F) * 255 / 31;
|
||||
|
||||
QRgb colors[4];
|
||||
colors[0] = qRgb(r0, g0, b0);
|
||||
colors[1] = qRgb(r1, g1, b1);
|
||||
colors[2] = qRgb((2 * r0 + r1) / 3, (2 * g0 + g1) / 3, (2 * b0 + b1) / 3);
|
||||
colors[3] = qRgb((r0 + 2 * r1) / 3, (g0 + 2 * g1) / 3, (b0 + 2 * b1) / 3);
|
||||
|
||||
// Decode 4x4 block
|
||||
for (int y = 0; y < 4; y++) {
|
||||
uchar colorRowByte = colorSrc[4 + y];
|
||||
for (int x = 0; x < 4; x++) {
|
||||
int pixelIdx = y * 4 + x;
|
||||
int alphaIdx = (alphaIndices >> (pixelIdx * 3)) & 0x07;
|
||||
int colorIdx = (colorRowByte >> (x * 2)) & 0x03;
|
||||
|
||||
QRgb color = colors[colorIdx];
|
||||
dest[y * destPitch + x] = qRgba(qRed(color), qGreen(color), qBlue(color), alphas[alphaIdx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to decode DXT data into an image (little-endian/PC format)
|
||||
static QImage decodeDXTToImageLE(const uchar *srcData, int width, int height, int blockSize, bool isDXT5, int textureDataSize)
|
||||
{
|
||||
int blocksWide = (width + 3) / 4;
|
||||
int blocksHigh = (height + 3) / 4;
|
||||
|
||||
QImage image(width, height, QImage::Format_ARGB32);
|
||||
if (image.isNull()) return QImage();
|
||||
image.fill(Qt::black);
|
||||
|
||||
for (int by = 0; by < blocksHigh; by++) {
|
||||
for (int bx = 0; bx < blocksWide; bx++) {
|
||||
int blockIdx = by * blocksWide + bx;
|
||||
int blockOffset = blockIdx * blockSize;
|
||||
|
||||
if (blockOffset + blockSize > textureDataSize) break;
|
||||
|
||||
const uchar *blockData = srcData + blockOffset;
|
||||
|
||||
// Decode into temporary 4x4 block
|
||||
QRgb block[16];
|
||||
|
||||
if (isDXT5) {
|
||||
decodeDXT5BlockStaticLE(blockData, block, 4);
|
||||
} else {
|
||||
decodeDXT1BlockStaticLE(blockData, block, 4);
|
||||
}
|
||||
|
||||
// Copy block to image
|
||||
int px = bx * 4;
|
||||
int py = by * 4;
|
||||
for (int y = 0; y < 4 && py + y < height; y++) {
|
||||
QRgb *destLine = reinterpret_cast<QRgb*>(image.scanLine(py + y));
|
||||
for (int x = 0; x < 4 && px + x < width; x++) {
|
||||
destLine[px + x] = block[y * 4 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
// Swap endianness in 16-bit chunks (required for Xbox 360)
|
||||
static void swapEndian16(uchar *data, int size)
|
||||
{
|
||||
@ -944,3 +1105,135 @@ QImage ImagePreviewWidget::loadRawDXT(const QByteArray &data, bool tryDXT5First)
|
||||
|
||||
return untiledResult.isNull() ? linearResult : untiledResult;
|
||||
}
|
||||
|
||||
QImage ImagePreviewWidget::loadDDS(const QByteArray &data)
|
||||
{
|
||||
// DDS header structure:
|
||||
// 0x00: "DDS " magic (4 bytes)
|
||||
// 0x04: DDS_HEADER (124 bytes)
|
||||
// 0x04: dwSize = 124
|
||||
// 0x08: dwFlags
|
||||
// 0x0C: dwHeight
|
||||
// 0x10: dwWidth
|
||||
// 0x14: dwPitchOrLinearSize
|
||||
// 0x18: dwDepth
|
||||
// 0x1C: dwMipMapCount
|
||||
// 0x20-0x4B: dwReserved1[11] (44 bytes)
|
||||
// 0x4C: DDS_PIXELFORMAT (32 bytes)
|
||||
// 0x4C: dwSize = 32
|
||||
// 0x50: dwFlags (0x4 = DDPF_FOURCC)
|
||||
// 0x54: dwFourCC ("DXT1", "DXT3", "DXT5")
|
||||
// 0x58-0x6B: RGB masks and bit count
|
||||
// 0x6C-0x7B: Caps and reserved
|
||||
// 0x80: Texture data starts
|
||||
|
||||
if (data.size() < 128) {
|
||||
return QImage();
|
||||
}
|
||||
|
||||
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||||
|
||||
// Verify magic
|
||||
if (data.left(4) != "DDS ") {
|
||||
return QImage();
|
||||
}
|
||||
|
||||
// Read header fields (little-endian)
|
||||
auto readU32 = [d](int offset) -> quint32 {
|
||||
return d[offset] | (d[offset+1] << 8) | (d[offset+2] << 16) | (d[offset+3] << 24);
|
||||
};
|
||||
|
||||
quint32 headerSize = readU32(0x04);
|
||||
if (headerSize != 124) {
|
||||
return QImage(); // Invalid header size
|
||||
}
|
||||
|
||||
quint32 height = readU32(0x0C);
|
||||
quint32 width = readU32(0x10);
|
||||
|
||||
// Read pixel format
|
||||
quint32 pfFlags = readU32(0x50);
|
||||
quint32 fourCC = readU32(0x54);
|
||||
|
||||
// Determine format from FourCC
|
||||
int blockSize = 0;
|
||||
bool isDXT5 = false;
|
||||
QString formatName;
|
||||
|
||||
// FourCC codes (little-endian in file)
|
||||
const quint32 DXT1 = 0x31545844; // "DXT1"
|
||||
const quint32 DXT3 = 0x33545844; // "DXT3"
|
||||
const quint32 DXT5 = 0x35545844; // "DXT5"
|
||||
|
||||
if ((pfFlags & 0x4) == 0) {
|
||||
// Not FourCC format - check for uncompressed RGB
|
||||
quint32 rgbBitCount = readU32(0x58);
|
||||
if (rgbBitCount == 32) {
|
||||
// A8R8G8B8 or similar uncompressed format
|
||||
mDetectedFormat = "DDS (ARGB32)";
|
||||
mCompression = "None";
|
||||
mBitsPerPixel = 32;
|
||||
|
||||
int dataOffset = 128;
|
||||
int dataSize = width * height * 4;
|
||||
if (dataOffset + dataSize > data.size()) {
|
||||
return QImage();
|
||||
}
|
||||
|
||||
QImage image(width, height, QImage::Format_ARGB32);
|
||||
const uchar *src = d + dataOffset;
|
||||
for (quint32 y = 0; y < height; y++) {
|
||||
QRgb *dest = reinterpret_cast<QRgb*>(image.scanLine(y));
|
||||
for (quint32 x = 0; x < width; x++) {
|
||||
dest[x] = qRgba(src[2], src[1], src[0], src[3]);
|
||||
src += 4;
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
return QImage(); // Unsupported uncompressed format
|
||||
}
|
||||
|
||||
if (fourCC == DXT1) {
|
||||
blockSize = 8;
|
||||
isDXT5 = false;
|
||||
formatName = "DXT1";
|
||||
} else if (fourCC == DXT3) {
|
||||
blockSize = 16;
|
||||
isDXT5 = true; // DXT3 uses same 16-byte block structure
|
||||
formatName = "DXT3";
|
||||
} else if (fourCC == DXT5) {
|
||||
blockSize = 16;
|
||||
isDXT5 = true;
|
||||
formatName = "DXT5";
|
||||
} else {
|
||||
// Unsupported format
|
||||
char fourCCStr[5] = {0};
|
||||
memcpy(fourCCStr, &fourCC, 4);
|
||||
mDetectedFormat = QString("DDS (%1)").arg(fourCCStr);
|
||||
mCompression = "Unknown";
|
||||
return QImage();
|
||||
}
|
||||
|
||||
mDetectedFormat = QString("DDS (%1)").arg(formatName);
|
||||
mCompression = formatName;
|
||||
mBitsPerPixel = (blockSize == 8) ? 4 : 8;
|
||||
|
||||
// Calculate texture data size
|
||||
int blocksWide = (width + 3) / 4;
|
||||
int blocksHigh = (height + 3) / 4;
|
||||
int textureDataSize = blocksWide * blocksHigh * blockSize;
|
||||
|
||||
int dataOffset = 128; // Header is 128 bytes
|
||||
if (dataOffset + textureDataSize > data.size()) {
|
||||
return QImage();
|
||||
}
|
||||
|
||||
const uchar *srcData = d + dataOffset;
|
||||
|
||||
// DDS files are PC format (linear, little-endian)
|
||||
// Use LE decoder - no Xbox 360 untiling needed
|
||||
QImage image = decodeDXTToImageLE(srcData, width, height, blockSize, isDXT5, textureDataSize);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@ -72,6 +72,9 @@ private:
|
||||
// Load raw DXT1/DXT5 data with auto-detected dimensions
|
||||
QImage loadRawDXT(const QByteArray &data, bool tryDXT5First = false);
|
||||
|
||||
// Load DDS (DirectDraw Surface) texture
|
||||
QImage loadDDS(const QByteArray &data);
|
||||
|
||||
// Detected image info
|
||||
QString mDetectedFormat;
|
||||
int mBitsPerPixel;
|
||||
|
||||
19
app/main.cpp
19
app/main.cpp
@ -23,6 +23,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QTextStream>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QImage>
|
||||
#include <QIcon>
|
||||
#include <iostream>
|
||||
@ -342,7 +343,8 @@ int main(int argc, char *argv[])
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = runCli(args.first(), parser.value(gameOption), parser.value(platformOption), parser.isSet(jsonOption));
|
||||
int result = runCli(args.first(), parser.value(gameOption), parser.value(platformOption),
|
||||
parser.isSet(jsonOption));
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// If we allocated our own console window, wait for user input before closing
|
||||
@ -451,5 +453,20 @@ int main(int argc, char *argv[])
|
||||
// finish() will show the main window and keep splash on top if waitForInteraction is enabled
|
||||
splash.finish(&w);
|
||||
|
||||
// Check for file argument to open (GUI mode)
|
||||
QStringList args = QCoreApplication::arguments();
|
||||
for (int i = 1; i < args.size(); ++i) {
|
||||
const QString& arg = args[i];
|
||||
// Skip options (start with -)
|
||||
if (arg.startsWith("-")) continue;
|
||||
// Try to open as file
|
||||
if (QFileInfo::exists(arg)) {
|
||||
QTimer::singleShot(100, &w, [&w, arg]() {
|
||||
w.openFile(arg);
|
||||
});
|
||||
break; // Only open first file
|
||||
}
|
||||
}
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@ class XTreeWidget;
|
||||
class XTreeWidgetItem;
|
||||
class QPlainTextEdit;
|
||||
class QProgressBar;
|
||||
class QUndoStack;
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
@ -38,17 +39,28 @@ public:
|
||||
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
|
||||
void LoadTreeCategories();
|
||||
void Reset();
|
||||
bool openFile(const QString& filePath); // Open and parse a file
|
||||
|
||||
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
|
||||
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
|
||||
|
||||
TreeBuilder& treeBuilder() { return mTreeBuilder; }
|
||||
|
||||
// Undo/Redo support
|
||||
QUndoStack* undoStack() const { return mUndoStack; }
|
||||
void pushFieldEdit(int journalId, const QString& fieldName,
|
||||
const QVariant& oldValue, const QVariant& newValue);
|
||||
private slots:
|
||||
void HandleLogEntry(const QString &entry);
|
||||
void HandleStatusUpdate(const QString &message, int timeout);
|
||||
void HandleProgressUpdate(const QString &message, int progress, int max);
|
||||
void applyTheme(const Theme &theme);
|
||||
|
||||
public:
|
||||
// Save functionality for write-back
|
||||
bool saveTab(QWidget* tab);
|
||||
bool saveTabAs(QWidget* tab);
|
||||
|
||||
protected:
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||
@ -67,6 +79,9 @@ private:
|
||||
QStringList mOpenedFilePaths; // Track opened files for reparsing
|
||||
QVector<DefinitionLoadResult> mDefinitionResults;
|
||||
|
||||
// Undo/Redo support
|
||||
QUndoStack *mUndoStack;
|
||||
|
||||
// Actions - File menu
|
||||
QAction *actionNew;
|
||||
QAction *actionOpen;
|
||||
|
||||
@ -33,6 +33,9 @@ QString TreeBuilder::pluralizeType(const QString& typeName) const
|
||||
QString groupLabel = (it != mod.types.end() && !it->display.isEmpty())
|
||||
? it->display
|
||||
: typeName;
|
||||
// Don't double the 's' if already ends with 's'
|
||||
if (groupLabel.endsWith('s') || groupLabel.endsWith('S'))
|
||||
return groupLabel;
|
||||
return groupLabel + "s";
|
||||
}
|
||||
|
||||
@ -44,9 +47,14 @@ XTreeWidgetItem* TreeBuilder::ensureTypeCategoryRoot(const QString& typeName, co
|
||||
return m_categoryRoots[categoryKey];
|
||||
|
||||
auto* root = new XTreeWidgetItem(m_tree);
|
||||
const QString categoryLabel = displayOverride.isEmpty()
|
||||
? pluralizeType(typeName)
|
||||
: displayOverride + "s";
|
||||
QString categoryLabel;
|
||||
if (displayOverride.isEmpty()) {
|
||||
categoryLabel = pluralizeType(typeName);
|
||||
} else if (displayOverride.endsWith('s') || displayOverride.endsWith('S')) {
|
||||
categoryLabel = displayOverride; // Don't double the 's'
|
||||
} else {
|
||||
categoryLabel = displayOverride + "s";
|
||||
}
|
||||
root->setText(0, categoryLabel);
|
||||
root->setData(0, Qt::UserRole + 1, "CATEGORY");
|
||||
root->setData(0, Qt::UserRole + 2, typeName);
|
||||
@ -98,15 +106,23 @@ QString TreeBuilder::instanceDisplayFor(const QVariantMap& obj, const QString& f
|
||||
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;
|
||||
}
|
||||
|
||||
// Index-based fallback for array items - ensures unique names
|
||||
// This comes BEFORE type fallback to avoid all items showing same type name
|
||||
if (!fallbackKey.isEmpty() && index.has_value()) {
|
||||
return QString("%1[%2]").arg(fallbackKey).arg(*index);
|
||||
}
|
||||
|
||||
// Type fallback (for non-array items)
|
||||
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>");
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
#include "xtreewidget.h"
|
||||
#include "exportmanager.h"
|
||||
#include "qheaderview.h"
|
||||
#include "qmenu.h"
|
||||
#include "logmanager.h"
|
||||
#include "xtreewidgetitem.h"
|
||||
#include "../libs/dsl/dslkeys.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
|
||||
@ -23,21 +25,183 @@ XTreeWidget::XTreeWidget(QWidget *parent)
|
||||
connect(this, &XTreeWidget::customContextMenuRequested, this, &XTreeWidget::PrepareContextMenu);
|
||||
}
|
||||
|
||||
int XTreeWidget::countExportableChildren(QTreeWidgetItem* parent) const {
|
||||
int count = 0;
|
||||
std::function<void(QTreeWidgetItem*)> countRecursive = [&](QTreeWidgetItem* item) {
|
||||
QString kind = item->data(0, Qt::UserRole + 1).toString();
|
||||
if (kind == "INSTANCE") {
|
||||
count++;
|
||||
}
|
||||
for (int i = 0; i < item->childCount(); ++i) {
|
||||
countRecursive(item->child(i));
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < parent->childCount(); ++i) {
|
||||
countRecursive(parent->child(i));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int XTreeWidget::countExportableChildrenByType(QTreeWidgetItem* parent, int contentType) const {
|
||||
int count = 0;
|
||||
ExportManager& exp = ExportManager::instance();
|
||||
|
||||
std::function<void(QTreeWidgetItem*)> countRecursive = [&](QTreeWidgetItem* item) {
|
||||
QString kind = item->data(0, Qt::UserRole + 1).toString();
|
||||
if (kind == "INSTANCE") {
|
||||
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
||||
if (exp.detectContentType(vars) == contentType) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < item->childCount(); ++i) {
|
||||
countRecursive(item->child(i));
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < parent->childCount(); ++i) {
|
||||
countRecursive(parent->child(i));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void XTreeWidget::PrepareContextMenu(const QPoint &pos) {
|
||||
auto activeItem = itemAt(pos);
|
||||
if (!activeItem) { return; }
|
||||
if (activeItem->text(0).isEmpty()) { return; }
|
||||
|
||||
QString activeText = activeItem->text(0);
|
||||
QString kind = activeItem->data(0, Qt::UserRole + 1).toString();
|
||||
|
||||
QMenu *contextMenu = new QMenu(this);
|
||||
|
||||
if (kind == "INSTANCE") {
|
||||
prepareInstanceContextMenu(contextMenu, activeItem);
|
||||
} else if (kind == "CATEGORY" || kind == "SUBCATEGORY" || kind == "EXTENSION_GROUP") {
|
||||
prepareContainerContextMenu(contextMenu, activeItem);
|
||||
} else {
|
||||
// No context menu for other item types
|
||||
delete contextMenu;
|
||||
return;
|
||||
}
|
||||
|
||||
QPoint pt(pos);
|
||||
contextMenu->exec(mapToGlobal(pt));
|
||||
|
||||
contextMenu->exec(mapToGlobal(pos));
|
||||
delete contextMenu;
|
||||
}
|
||||
|
||||
void XTreeWidget::prepareInstanceContextMenu(QMenu* menu, QTreeWidgetItem* item) {
|
||||
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
||||
ExportManager& exp = ExportManager::instance();
|
||||
auto contentType = exp.detectContentType(vars);
|
||||
|
||||
// Quick Export - uses saved defaults
|
||||
QAction* quickAction = menu->addAction("Quick Export");
|
||||
quickAction->setShortcut(QKeySequence("Ctrl+Shift+E"));
|
||||
connect(quickAction, &QAction::triggered, this, [this, item]() {
|
||||
emit quickExportRequested(item);
|
||||
});
|
||||
|
||||
// Export with dialog - full options
|
||||
QAction* dialogAction = menu->addAction("Export...");
|
||||
dialogAction->setShortcut(QKeySequence("Ctrl+E"));
|
||||
connect(dialogAction, &QAction::triggered, this, [this, item]() {
|
||||
emit exportDialogRequested(item);
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
// Raw export - always available
|
||||
QAction* rawAction = menu->addAction("Export Raw Data...");
|
||||
connect(rawAction, &QAction::triggered, this, [this, item]() {
|
||||
emit exportRequested("raw", item);
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
// Format-specific exports based on content type
|
||||
if (contentType == ExportManager::Image) {
|
||||
QMenu* imgMenu = menu->addMenu("Export Image As");
|
||||
for (const QString& fmt : exp.supportedImageFormats()) {
|
||||
QAction* a = imgMenu->addAction(fmt.toUpper());
|
||||
connect(a, &QAction::triggered, this, [this, item, fmt]() {
|
||||
emit exportRequested(fmt, item);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (contentType == ExportManager::Audio) {
|
||||
QMenu* audMenu = menu->addMenu("Export Audio As");
|
||||
for (const QString& fmt : exp.supportedAudioFormats()) {
|
||||
QAction* a = audMenu->addAction(fmt.toUpper());
|
||||
// Disable non-WAV formats if FFmpeg not available
|
||||
a->setEnabled(fmt == "wav" || exp.hasFFmpeg());
|
||||
if (!a->isEnabled()) {
|
||||
a->setText(a->text() + " (requires FFmpeg)");
|
||||
}
|
||||
connect(a, &QAction::triggered, this, [this, item, fmt]() {
|
||||
emit exportRequested(fmt, item);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (contentType == ExportManager::Text) {
|
||||
QAction* txtAction = menu->addAction("Export as Text...");
|
||||
connect(txtAction, &QAction::triggered, this, [this, item]() {
|
||||
emit exportRequested("txt", item);
|
||||
});
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
// Clipboard operations
|
||||
QAction* copyAction = menu->addAction("Copy to Clipboard");
|
||||
connect(copyAction, &QAction::triggered, this, [this, item]() {
|
||||
emit exportRequested("clipboard", item);
|
||||
});
|
||||
}
|
||||
|
||||
void XTreeWidget::prepareContainerContextMenu(QMenu* menu, QTreeWidgetItem* item) {
|
||||
int totalCount = countExportableChildren(item);
|
||||
|
||||
if (totalCount == 0) {
|
||||
QAction* emptyAction = menu->addAction("No exportable items");
|
||||
emptyAction->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Export all children
|
||||
QAction* exportAllAction = menu->addAction(QString("Export All Children... (%1 items)").arg(totalCount));
|
||||
connect(exportAllAction, &QAction::triggered, this, [this, item]() {
|
||||
emit batchExportRequested(item);
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
// Export by type
|
||||
int imageCount = countExportableChildrenByType(item, ExportManager::Image);
|
||||
int audioCount = countExportableChildrenByType(item, ExportManager::Audio);
|
||||
int textCount = countExportableChildrenByType(item, ExportManager::Text);
|
||||
|
||||
if (imageCount > 0) {
|
||||
QAction* imgAction = menu->addAction(QString("Export All Images... (%1)").arg(imageCount));
|
||||
connect(imgAction, &QAction::triggered, this, [this, item]() {
|
||||
emit batchExportByTypeRequested(item, ExportManager::Image);
|
||||
});
|
||||
}
|
||||
|
||||
if (audioCount > 0) {
|
||||
QAction* audAction = menu->addAction(QString("Export All Audio... (%1)").arg(audioCount));
|
||||
connect(audAction, &QAction::triggered, this, [this, item]() {
|
||||
emit batchExportByTypeRequested(item, ExportManager::Audio);
|
||||
});
|
||||
}
|
||||
|
||||
if (textCount > 0) {
|
||||
QAction* txtAction = menu->addAction(QString("Export All Text... (%1)").arg(textCount));
|
||||
connect(txtAction, &QAction::triggered, this, [this, item]() {
|
||||
emit batchExportByTypeRequested(item, ExportManager::Text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void XTreeWidget::ItemSelectionChanged() {
|
||||
if (selectedItems().isEmpty()) { return; }
|
||||
|
||||
@ -46,5 +210,5 @@ void XTreeWidget::ItemSelectionChanged() {
|
||||
if (selectedItem->text(0).isEmpty()) { return; }
|
||||
QString selectedText = selectedItem->text(0);
|
||||
|
||||
emit ItemSelected(selectedText);
|
||||
emit ItemSelected(selectedText, selectedItem);
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
|
||||
#include <QTreeWidget>
|
||||
|
||||
class QTreeWidgetItem;
|
||||
|
||||
class XTreeWidget : public QTreeWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -10,16 +12,29 @@ class XTreeWidget : public QTreeWidget
|
||||
public:
|
||||
explicit XTreeWidget(QWidget *parent = nullptr);
|
||||
|
||||
// Helper methods for batch export
|
||||
int countExportableChildren(QTreeWidgetItem* parent) const;
|
||||
int countExportableChildrenByType(QTreeWidgetItem* parent, int contentType) const;
|
||||
|
||||
signals:
|
||||
void ItemSelected(const QString itemText);
|
||||
void ItemSelected(const QString itemText, QTreeWidgetItem* item);
|
||||
void ItemClosed(const QString itemText);
|
||||
void Cleared();
|
||||
|
||||
// Export signals
|
||||
void exportRequested(const QString& format, QTreeWidgetItem* item);
|
||||
void quickExportRequested(QTreeWidgetItem* item);
|
||||
void exportDialogRequested(QTreeWidgetItem* item);
|
||||
void batchExportRequested(QTreeWidgetItem* parentItem);
|
||||
void batchExportByTypeRequested(QTreeWidgetItem* parentItem, int contentType);
|
||||
|
||||
protected:
|
||||
void ItemSelectionChanged();
|
||||
void PrepareContextMenu(const QPoint &pos);
|
||||
|
||||
private:
|
||||
void prepareInstanceContextMenu(QMenu* menu, QTreeWidgetItem* item);
|
||||
void prepareContainerContextMenu(QMenu* menu, QTreeWidgetItem* item);
|
||||
};
|
||||
|
||||
#endif // XTREEWIDGET_H
|
||||
|
||||
@ -52,3 +52,32 @@ void XTreeWidgetItem::SetIsGroup(bool aIsGroup)
|
||||
{
|
||||
isGroup = aIsGroup;
|
||||
}
|
||||
|
||||
void XTreeWidgetItem::setModified(bool modified)
|
||||
{
|
||||
if (m_modified == modified) return;
|
||||
|
||||
if (modified && m_originalText.isEmpty()) {
|
||||
// Store original text before adding indicator
|
||||
m_originalText = text(0);
|
||||
}
|
||||
|
||||
m_modified = modified;
|
||||
|
||||
if (modified) {
|
||||
// Add asterisk indicator
|
||||
if (!text(0).endsWith(" *")) {
|
||||
setText(0, m_originalText + " *");
|
||||
}
|
||||
} else {
|
||||
// Remove asterisk indicator
|
||||
if (!m_originalText.isEmpty()) {
|
||||
setText(0, m_originalText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool XTreeWidgetItem::isModified() const
|
||||
{
|
||||
return m_modified;
|
||||
}
|
||||
|
||||
@ -20,8 +20,14 @@ public:
|
||||
bool GetIsGroup() const;
|
||||
void SetIsGroup(bool aIsGroup);
|
||||
|
||||
// Modified state for edit indicator
|
||||
void setModified(bool modified);
|
||||
bool isModified() const;
|
||||
|
||||
private:
|
||||
bool isGroup;
|
||||
bool m_modified = false;
|
||||
QString m_originalText;
|
||||
};
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user