Refactor main window and extend settings functionality
- Refactor MainWindow to use TreeBuilder for parsed data organization - Integrate new ListPreviewWidget and TextViewerWidget - Add Python path auto-detection and scripts directory settings - Add log-to-file option and improve debug logging feedback - Improve image format detection (PNG, JPEG, BMP, DDS, TGA, RCB) - Update app.pro with new source files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
57ad7c4111
commit
e4c913bb06
11
app/app.pro
11
app/app.pro
@ -53,10 +53,13 @@ DEPENDPATH += \
|
|||||||
$$PWD/../libs/encryption
|
$$PWD/../libs/encryption
|
||||||
|
|
||||||
CONFIG(debug, debug|release) {
|
CONFIG(debug, debug|release) {
|
||||||
install_it.path = $$OUT_PWD/debug/definitions
|
defs_install.path = $$OUT_PWD/debug/definitions
|
||||||
|
scripts_install.path = $$OUT_PWD/debug/scripts
|
||||||
} CONFIG(release, debug|release) {
|
} CONFIG(release, debug|release) {
|
||||||
install_it.path = $$OUT_PWD/release/definitions
|
defs_install.path = $$OUT_PWD/release/definitions
|
||||||
|
scripts_install.path = $$OUT_PWD/release/scripts
|
||||||
}
|
}
|
||||||
|
|
||||||
install_it.files = $$PWD/../definitions/*
|
defs_install.files = $$PWD/../definitions/*
|
||||||
INSTALLS += install_it
|
scripts_install.files = $$PWD/../scripts/*
|
||||||
|
INSTALLS += defs_install scripts_install
|
||||||
|
|||||||
13
app/main.cpp
13
app/main.cpp
@ -3,6 +3,7 @@
|
|||||||
#include "typeregistry.h"
|
#include "typeregistry.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "compression.h"
|
#include "compression.h"
|
||||||
|
#include "logmanager.h"
|
||||||
|
|
||||||
// Application metadata
|
// Application metadata
|
||||||
#define APP_NAME "XPlor"
|
#define APP_NAME "XPlor"
|
||||||
@ -85,8 +86,6 @@ static QJsonValue variantToJson(const QVariant& v) {
|
|||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
const QVariantMap map = v.toMap();
|
const QVariantMap map = v.toMap();
|
||||||
for (auto it = map.begin(); it != map.end(); ++it) {
|
for (auto it = map.begin(); it != map.end(); ++it) {
|
||||||
if (it.key().startsWith("_") && it.key() != "_name" && it.key() != "_type")
|
|
||||||
continue;
|
|
||||||
obj[it.key()] = variantToJson(it.value());
|
obj[it.key()] = variantToJson(it.value());
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
@ -404,6 +403,8 @@ int main(int argc, char *argv[])
|
|||||||
int loaded = 0;
|
int loaded = 0;
|
||||||
int total = defFiles.size();
|
int total = defFiles.size();
|
||||||
|
|
||||||
|
LogManager::instance().addEntry(QString("[INIT] Loading %1 definition files...").arg(total));
|
||||||
|
|
||||||
for (const QString& path : defFiles) {
|
for (const QString& path : defFiles) {
|
||||||
QString fileName = QFileInfo(path).fileName();
|
QString fileName = QFileInfo(path).fileName();
|
||||||
splash.setStatus(QString("Loading: %1").arg(fileName));
|
splash.setStatus(QString("Loading: %1").arg(fileName));
|
||||||
@ -412,18 +413,26 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QFile f(path);
|
QFile f(path);
|
||||||
if (!f.open(QIODevice::ReadOnly)) {
|
if (!f.open(QIODevice::ReadOnly)) {
|
||||||
|
LogManager::instance().addError(QString("[DEF] Failed to open: %1").arg(fileName));
|
||||||
defResults.append({path, fileName, false, "Failed to open file"});
|
defResults.append({path, fileName, false, "Failed to open file"});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
registry.ingestScript(QString::fromUtf8(f.readAll()), path);
|
registry.ingestScript(QString::fromUtf8(f.readAll()), path);
|
||||||
|
LogManager::instance().addEntry(QString("[DEF] Loaded: %1").arg(fileName));
|
||||||
defResults.append({path, fileName, true, QString()});
|
defResults.append({path, fileName, true, QString()});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
LogManager::instance().addError(QString("[DEF] Error in %1: %2").arg(fileName).arg(e.what()));
|
||||||
defResults.append({path, fileName, false, QString::fromUtf8(e.what())});
|
defResults.append({path, fileName, false, QString::fromUtf8(e.what())});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loaded++;
|
loaded++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int successCount = std::count_if(defResults.begin(), defResults.end(),
|
||||||
|
[](const DefinitionLoadResult& r) { return r.success; });
|
||||||
|
LogManager::instance().addEntry(QString("[INIT] Loaded %1/%2 definitions successfully").arg(successCount).arg(total));
|
||||||
|
LogManager::instance().addLine();
|
||||||
|
|
||||||
splash.setStatus("Creating main window...");
|
splash.setStatus("Creating main window...");
|
||||||
splash.setProgress(85, 100);
|
splash.setProgress(85, 100);
|
||||||
a.processEvents();
|
a.processEvents();
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
#include "dsluischema.h"
|
#include "dsluischema.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "imagepreviewwidget.h"
|
#include "imagepreviewwidget.h"
|
||||||
|
#include "textviewerwidget.h"
|
||||||
|
#include "listpreviewwidget.h"
|
||||||
|
|
||||||
// Debug logging is controlled via Settings -> LogManager::debug()
|
// Debug logging is controlled via Settings -> LogManager::debug()
|
||||||
|
|
||||||
@ -41,7 +43,6 @@
|
|||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QCollator>
|
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -81,38 +82,44 @@ static QIcon generateThemedIcon(const QColor &accentColor) {
|
|||||||
return QIcon(QPixmap::fromImage(image));
|
return QIcon(QPixmap::fromImage(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Natural sort comparator for strings with numbers (e.g., "chunk1" < "chunk2" < "chunk10")
|
// Check if data looks like RCB pixel format (Avatar g4rc DXT1 texture)
|
||||||
static bool naturalLessThan(const QString& a, const QString& b) {
|
static bool looksLikeRCBPixel(const QByteArray &data) {
|
||||||
static QCollator collator;
|
if (data.size() < 0x28) return false;
|
||||||
collator.setNumericMode(true);
|
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||||||
collator.setCaseSensitivity(Qt::CaseInsensitive);
|
// RCB pixel has format byte 0x52 at offset 0x09
|
||||||
return collator.compare(a, b) < 0;
|
// and data size at offset 0x10 (big-endian)
|
||||||
|
uchar formatByte = d[0x09];
|
||||||
|
if (formatByte != 0x52) return false;
|
||||||
|
quint32 rcbDataSize = (d[0x10] << 24) | (d[0x11] << 16) | (d[0x12] << 8) | d[0x13];
|
||||||
|
// Check if header + data size fits in the buffer
|
||||||
|
return rcbDataSize > 0 && rcbDataSize + 0x24 <= (quint32)data.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort tree widget children using natural sort order
|
// Check if data looks like a known image format
|
||||||
static void naturalSortChildren(QTreeWidgetItem* parent) {
|
static bool looksLikeImage(const QByteArray &data) {
|
||||||
if (!parent || parent->childCount() == 0) return;
|
if (data.size() < 8) return false;
|
||||||
|
// PNG
|
||||||
// Take all children
|
if (data.startsWith("\x89PNG")) return true;
|
||||||
QList<QTreeWidgetItem*> children;
|
// JPEG
|
||||||
while (parent->childCount() > 0) {
|
if (data.startsWith("\xFF\xD8\xFF")) return true;
|
||||||
children.append(parent->takeChild(0));
|
// BMP
|
||||||
}
|
if (data.startsWith("BM")) return true;
|
||||||
|
// DDS
|
||||||
// Sort using natural order
|
if (data.startsWith("DDS ")) return true;
|
||||||
std::sort(children.begin(), children.end(), [](QTreeWidgetItem* a, QTreeWidgetItem* b) {
|
// TGA - check for valid image type
|
||||||
return naturalLessThan(a->text(0), b->text(0));
|
if (data.size() >= 18) {
|
||||||
});
|
uchar imageType = static_cast<uchar>(data[2]);
|
||||||
|
if (imageType == 2 || imageType == 10) return true; // Uncompressed/RLE true-color
|
||||||
// Re-add in sorted order
|
|
||||||
for (auto* child : children) {
|
|
||||||
parent->addChild(child);
|
|
||||||
}
|
}
|
||||||
|
// RCB pixel format (Avatar textures)
|
||||||
|
if (looksLikeRCBPixel(data)) return true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget *parent)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent)
|
||||||
, ui(new Ui::MainWindow)
|
, ui(new Ui::MainWindow)
|
||||||
|
, mTreeBuilder(nullptr, mTypeRegistry) // Placeholder, will reset after mTreeWidget created
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
setAcceptDrops(true);
|
setAcceptDrops(true);
|
||||||
@ -139,6 +146,9 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
applyTheme(Settings::instance().theme());
|
applyTheme(Settings::instance().theme());
|
||||||
|
|
||||||
mTreeWidget = new XTreeWidget(this);
|
mTreeWidget = new XTreeWidget(this);
|
||||||
|
|
||||||
|
// Initialize tree builder with the actual tree widget
|
||||||
|
mTreeBuilder = TreeBuilder(mTreeWidget, mTypeRegistry);
|
||||||
mTreeWidget->setColumnCount(1);
|
mTreeWidget->setColumnCount(1);
|
||||||
|
|
||||||
mLogWidget = new QPlainTextEdit(this);
|
mLogWidget = new QPlainTextEdit(this);
|
||||||
@ -156,6 +166,9 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
connect(&LogManager::instance(), &LogManager::entryAdded,
|
connect(&LogManager::instance(), &LogManager::entryAdded,
|
||||||
this, &MainWindow::HandleLogEntry);
|
this, &MainWindow::HandleLogEntry);
|
||||||
|
|
||||||
|
// Flush any buffered log entries from before the signal was connected
|
||||||
|
LogManager::instance().flushBufferedEntries();
|
||||||
|
|
||||||
statusBar()->addPermanentWidget(mProgressBar);
|
statusBar()->addPermanentWidget(mProgressBar);
|
||||||
|
|
||||||
ui->tabWidget->setContextMenuPolicy(Qt::CustomContextMenu);
|
ui->tabWidget->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
@ -244,8 +257,10 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
|
|
||||||
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
||||||
|
// Use _type from parsed data (set by parse_here delegation) over tree item's stored type
|
||||||
|
const QString storedType = item->data(0, Qt::UserRole + 2).toString();
|
||||||
|
const QString typeName = DslKeys::contains(vars, DslKey::Type) ? DslKeys::getString(vars, DslKey::Type) : storedType;
|
||||||
|
|
||||||
// Prevent dup tabs
|
// Prevent dup tabs
|
||||||
const QString tabTitle = item->text(0);
|
const QString tabTitle = item->text(0);
|
||||||
@ -257,44 +272,55 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this item has a preview (resource data)
|
// Check if this item has a preview (resource data)
|
||||||
if (vars.contains("_preview")) {
|
LogManager::instance().addEntry(QString("[VIEWER] Checking item '%1', has _preview: %2, has data: %3")
|
||||||
const QVariantMap preview = vars.value("_preview").toMap();
|
.arg(tabTitle)
|
||||||
|
.arg(DslKeys::contains(vars, DslKey::Preview))
|
||||||
|
.arg(vars.contains("data")));
|
||||||
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
||||||
|
const QVariantMap preview = DslKeys::get(vars, DslKey::Preview).toMap();
|
||||||
const QString filename = preview.value("filename").toString();
|
const QString filename = preview.value("filename").toString();
|
||||||
const QByteArray data = preview.value("data").toByteArray();
|
const QByteArray data = preview.value("data").toByteArray();
|
||||||
const QString lowerFilename = filename.toLower();
|
const QString lowerFilename = filename.toLower();
|
||||||
|
LogManager::instance().addEntry(QString("[VIEWER] Preview filename='%1', data size=%2")
|
||||||
|
.arg(filename)
|
||||||
|
.arg(data.size()));
|
||||||
|
|
||||||
if (!data.isEmpty()) {
|
if (!data.isEmpty()) {
|
||||||
// Check file type and use appropriate widget
|
// Get file extension
|
||||||
bool isImage = lowerFilename.endsWith(".tga") ||
|
QFileInfo fileInfo(filename);
|
||||||
lowerFilename.endsWith(".dds") ||
|
QString extension = fileInfo.suffix();
|
||||||
lowerFilename.endsWith(".png") ||
|
|
||||||
lowerFilename.endsWith(".jpg") ||
|
|
||||||
lowerFilename.endsWith(".jpeg") ||
|
|
||||||
lowerFilename.endsWith(".bmp") ||
|
|
||||||
lowerFilename.endsWith(".xbtex");
|
|
||||||
|
|
||||||
bool isAudio = lowerFilename.endsWith(".wav") ||
|
// Check if script specified a viewer type via set_viewer()
|
||||||
lowerFilename.endsWith(".wave");
|
QString viewerType;
|
||||||
|
if (DslKeys::contains(vars, DslKey::Viewer)) {
|
||||||
|
viewerType = DslKeys::getString(vars, DslKey::Viewer).toLower();
|
||||||
|
LogManager::instance().addEntry(QString("[VIEWER] Using script-defined viewer: '%1'")
|
||||||
|
.arg(viewerType));
|
||||||
|
} else if (preview.contains("viewer")) {
|
||||||
|
viewerType = preview.value("viewer").toString().toLower();
|
||||||
|
LogManager::instance().addEntry(QString("[VIEWER] Using preview-defined viewer: '%1'")
|
||||||
|
.arg(viewerType));
|
||||||
|
} else {
|
||||||
|
// Fall back to settings-based viewer type
|
||||||
|
viewerType = Settings::instance().viewerForExtension(extension);
|
||||||
|
LogManager::instance().addEntry(QString("[VIEWER] Extension='%1', viewerType='%2'")
|
||||||
|
.arg(extension)
|
||||||
|
.arg(viewerType));
|
||||||
|
}
|
||||||
|
|
||||||
if (isAudio) {
|
// Collect visible metadata based on UI schema
|
||||||
|
QVariantMap metadata = mTypeRegistry.filterVisibleFields(vars, typeName);
|
||||||
|
|
||||||
|
if (viewerType == "audio") {
|
||||||
// Audio preview widget
|
// Audio preview widget
|
||||||
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
|
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
|
||||||
audioWidget->setProperty("PARENT_NAME", tabTitle);
|
audioWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
audioWidget->loadFromData(data, filename);
|
audioWidget->loadFromData(data, filename);
|
||||||
|
|
||||||
// Add any parsed metadata from vars
|
|
||||||
QVariantMap metadata;
|
|
||||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
|
||||||
if (!it.key().startsWith("_")) {
|
|
||||||
metadata[it.key()] = it.value();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
audioWidget->setMetadata(metadata);
|
audioWidget->setMetadata(metadata);
|
||||||
|
|
||||||
ui->tabWidget->addTab(audioWidget, tabTitle);
|
ui->tabWidget->addTab(audioWidget, tabTitle);
|
||||||
ui->tabWidget->setCurrentWidget(audioWidget);
|
ui->tabWidget->setCurrentWidget(audioWidget);
|
||||||
return;
|
return;
|
||||||
} else if (isImage) {
|
} else if (viewerType == "image") {
|
||||||
// Image preview widget
|
// Image preview widget
|
||||||
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
|
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
|
||||||
imageWidget->setProperty("PARENT_NAME", tabTitle);
|
imageWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
@ -308,34 +334,51 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
else if (lowerFilename.endsWith(".jpg") || lowerFilename.endsWith(".jpeg")) format = "JPG";
|
else if (lowerFilename.endsWith(".jpg") || lowerFilename.endsWith(".jpeg")) format = "JPG";
|
||||||
|
|
||||||
imageWidget->loadFromData(data, format);
|
imageWidget->loadFromData(data, format);
|
||||||
|
|
||||||
// Add any parsed metadata from vars
|
|
||||||
QVariantMap metadata;
|
|
||||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
|
||||||
if (!it.key().startsWith("_")) {
|
|
||||||
metadata[it.key()] = it.value();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageWidget->setMetadata(metadata);
|
imageWidget->setMetadata(metadata);
|
||||||
|
|
||||||
ui->tabWidget->addTab(imageWidget, tabTitle);
|
ui->tabWidget->addTab(imageWidget, tabTitle);
|
||||||
ui->tabWidget->setCurrentWidget(imageWidget);
|
ui->tabWidget->setCurrentWidget(imageWidget);
|
||||||
return;
|
return;
|
||||||
|
} else if (viewerType == "text") {
|
||||||
|
// Text viewer widget
|
||||||
|
auto* textWidget = new TextViewerWidget(ui->tabWidget);
|
||||||
|
textWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
textWidget->setData(data, filename);
|
||||||
|
textWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(textWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(textWidget);
|
||||||
|
return;
|
||||||
|
} else if (viewerType == "list") {
|
||||||
|
// List preview widget - parse as string table
|
||||||
|
auto* listWidget = new ListPreviewWidget(ui->tabWidget);
|
||||||
|
listWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
|
||||||
|
// Parse data as null-terminated strings
|
||||||
|
QVariantList items;
|
||||||
|
int start = 0;
|
||||||
|
for (int i = 0; i < data.size(); ++i) {
|
||||||
|
if (data[i] == '\0' && i > start) {
|
||||||
|
QString str = QString::fromUtf8(data.mid(start, i - start));
|
||||||
|
if (!str.isEmpty()) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["string"] = str;
|
||||||
|
item["offset"] = start;
|
||||||
|
items.append(item);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listWidget->setListData(items, filename);
|
||||||
|
listWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(listWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(listWidget);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Hex viewer for unknown file types
|
// Hex viewer for unknown file types (default)
|
||||||
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
||||||
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
hexWidget->setData(data, filename);
|
hexWidget->setData(data, filename);
|
||||||
|
|
||||||
// Add any parsed metadata from vars
|
|
||||||
QVariantMap metadata;
|
|
||||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
|
||||||
if (!it.key().startsWith("_")) {
|
|
||||||
metadata[it.key()] = it.value();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hexWidget->setMetadata(metadata);
|
hexWidget->setMetadata(metadata);
|
||||||
|
|
||||||
ui->tabWidget->addTab(hexWidget, tabTitle);
|
ui->tabWidget->addTab(hexWidget, tabTitle);
|
||||||
ui->tabWidget->setCurrentWidget(hexWidget);
|
ui->tabWidget->setCurrentWidget(hexWidget);
|
||||||
return;
|
return;
|
||||||
@ -343,31 +386,105 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a chunk with binary data (display in hex viewer)
|
// Check if this is a chunk with binary data
|
||||||
if (vars.contains("data") && vars.value("data").typeId() == QMetaType::QByteArray) {
|
if (vars.contains("data") && vars.value("data").typeId() == QMetaType::QByteArray) {
|
||||||
QByteArray chunkData = vars.value("data").toByteArray();
|
QByteArray chunkData = vars.value("data").toByteArray();
|
||||||
if (!chunkData.isEmpty()) {
|
if (!chunkData.isEmpty()) {
|
||||||
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
// Get extension from tab title (often includes filename)
|
||||||
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
QFileInfo fileInfo(tabTitle);
|
||||||
hexWidget->setData(chunkData, tabTitle);
|
QString extension = fileInfo.suffix();
|
||||||
|
|
||||||
// Add other parsed fields as metadata (excluding the binary data)
|
// Check if script specified a viewer type via set_viewer()
|
||||||
QVariantMap metadata;
|
QString viewerType;
|
||||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
if (DslKeys::contains(vars, DslKey::Viewer)) {
|
||||||
if (!it.key().startsWith("_") && it.key() != "data") {
|
viewerType = DslKeys::getString(vars, DslKey::Viewer).toLower();
|
||||||
metadata[it.key()] = it.value();
|
LogManager::instance().addEntry(QString("[VIEWER] Using script-defined viewer: '%1'")
|
||||||
}
|
.arg(viewerType));
|
||||||
|
} else {
|
||||||
|
viewerType = Settings::instance().viewerForExtension(extension);
|
||||||
}
|
}
|
||||||
hexWidget->setMetadata(metadata);
|
|
||||||
|
|
||||||
ui->tabWidget->addTab(hexWidget, tabTitle);
|
// Collect visible metadata based on UI schema
|
||||||
ui->tabWidget->setCurrentWidget(hexWidget);
|
QVariantMap metadata = mTypeRegistry.filterVisibleFields(vars, typeName);
|
||||||
return;
|
metadata.remove("data"); // Exclude raw binary data from display
|
||||||
|
|
||||||
|
if (viewerType == "text") {
|
||||||
|
auto* textWidget = new TextViewerWidget(ui->tabWidget);
|
||||||
|
textWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
textWidget->setData(chunkData, tabTitle);
|
||||||
|
textWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(textWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(textWidget);
|
||||||
|
return;
|
||||||
|
} else if (viewerType == "image") {
|
||||||
|
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
|
||||||
|
imageWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
imageWidget->setFilename(tabTitle);
|
||||||
|
imageWidget->loadFromData(chunkData);
|
||||||
|
imageWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(imageWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(imageWidget);
|
||||||
|
return;
|
||||||
|
} else if (viewerType == "audio") {
|
||||||
|
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
|
||||||
|
audioWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
audioWidget->loadFromData(chunkData, tabTitle);
|
||||||
|
audioWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(audioWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(audioWidget);
|
||||||
|
return;
|
||||||
|
} else if (viewerType == "list") {
|
||||||
|
// List preview widget - parse as string table
|
||||||
|
auto* listWidget = new ListPreviewWidget(ui->tabWidget);
|
||||||
|
listWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
|
||||||
|
// Parse chunkData as null-terminated strings
|
||||||
|
QVariantList items;
|
||||||
|
int start = 0;
|
||||||
|
for (int i = 0; i < chunkData.size(); ++i) {
|
||||||
|
if (chunkData[i] == '\0' && i > start) {
|
||||||
|
QString str = QString::fromUtf8(chunkData.mid(start, i - start));
|
||||||
|
if (!str.isEmpty()) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["string"] = str;
|
||||||
|
item["offset"] = start;
|
||||||
|
items.append(item);
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listWidget->setListData(items, tabTitle);
|
||||||
|
listWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(listWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(listWidget);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Try image detection by content for unknown extensions
|
||||||
|
if (looksLikeImage(chunkData)) {
|
||||||
|
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
|
||||||
|
imageWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
imageWidget->setFilename(tabTitle);
|
||||||
|
imageWidget->loadFromData(chunkData);
|
||||||
|
imageWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(imageWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(imageWidget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Default to hex viewer
|
||||||
|
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
||||||
|
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
||||||
|
hexWidget->setData(chunkData, tabTitle);
|
||||||
|
hexWidget->setMetadata(metadata);
|
||||||
|
ui->tabWidget->addTab(hexWidget, tabTitle);
|
||||||
|
ui->tabWidget->setCurrentWidget(hexWidget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TypeDef& td = mTypeRegistry.module().types[typeName];
|
const TypeDef& td = mTypeRegistry.module().types[typeName];
|
||||||
const auto schema = buildUiSchemaForType(td);
|
const auto schema = buildUiSchemaForType(td, &mTypeRegistry.module());
|
||||||
|
|
||||||
auto* w = new ScriptTypeEditorWidget(typeName, schema, ui->tabWidget);
|
auto* w = new ScriptTypeEditorWidget(typeName, schema, ui->tabWidget);
|
||||||
w->setProperty("PARENT_NAME", tabTitle);
|
w->setProperty("PARENT_NAME", tabTitle);
|
||||||
@ -387,6 +504,8 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui->toolBar->setWindowTitle("Tool Bar");
|
||||||
|
|
||||||
QDockWidget *treeDockWidget = new QDockWidget(this);
|
QDockWidget *treeDockWidget = new QDockWidget(this);
|
||||||
treeDockWidget->setWidget(mTreeWidget);
|
treeDockWidget->setWidget(mTreeWidget);
|
||||||
treeDockWidget->setWindowTitle("Tree Browser");
|
treeDockWidget->setWindowTitle("Tree Browser");
|
||||||
@ -589,17 +708,33 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
progress.setMinimumDuration(500);
|
progress.setMinimumDuration(500);
|
||||||
|
|
||||||
QVariantMap rootVars;
|
QVariantMap rootVars;
|
||||||
|
bool cancelled = false;
|
||||||
try {
|
try {
|
||||||
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
||||||
[this, &progress](qint64 pos, qint64 size) {
|
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
||||||
progress.setValue(static_cast<int>(pos));
|
// Only update progress if position is moving forward (avoid jumps from seek)
|
||||||
|
if (pos > progress.value()) {
|
||||||
|
progress.setValue(static_cast<int>(pos));
|
||||||
|
}
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
if (progress.wasCanceled()) {
|
||||||
|
cancelled = true;
|
||||||
|
throw std::runtime_error("Parsing cancelled by user");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[this, &progress](const QString& status) {
|
[this, &progress, &cancelled](const QString& status) {
|
||||||
progress.setLabelText(status);
|
progress.setLabelText(status);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
if (progress.wasCanceled()) {
|
||||||
|
cancelled = true;
|
||||||
|
throw std::runtime_error("Parsing cancelled by user");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
if (cancelled) {
|
||||||
|
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
LogManager::instance().addLine();
|
LogManager::instance().addLine();
|
||||||
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
||||||
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
||||||
@ -623,15 +758,16 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
progress.setRange(0, 0); // Indeterminate mode
|
progress.setRange(0, 0); // Indeterminate mode
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||||
routeNestedObjects(rootInst, rootVars);
|
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||||
|
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||||
|
|
||||||
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
organizeChildrenByExtension(rootInst);
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||||
updateTreeNodeCounts(cat);
|
mTreeBuilder.updateNodeCounts(cat);
|
||||||
|
|
||||||
cat->setExpanded(false);
|
cat->setExpanded(false);
|
||||||
mTreeWidget->setCurrentItem(rootInst);
|
mTreeWidget->setCurrentItem(rootInst);
|
||||||
@ -695,11 +831,12 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
mOpenedFilePaths.append(path);
|
mOpenedFilePaths.append(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||||
routeNestedObjects(rootInst, rootVars);
|
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||||
organizeChildrenByExtension(rootInst);
|
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||||
updateTreeNodeCounts(cat);
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||||
|
mTreeBuilder.updateNodeCounts(cat);
|
||||||
cat->setExpanded(false);
|
cat->setExpanded(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,14 +889,14 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
|
|
||||||
if (kind == "INSTANCE") {
|
if (kind == "INSTANCE") {
|
||||||
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
||||||
// Build a simple text representation
|
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
|
||||||
|
// Build a simple text representation with only visible fields
|
||||||
QStringList lines;
|
QStringList lines;
|
||||||
lines.append(QString("Name: %1").arg(text));
|
lines.append(QString("Name: %1").arg(text));
|
||||||
lines.append(QString("Type: %1").arg(item->data(0, Qt::UserRole + 2).toString()));
|
lines.append(QString("Type: %1").arg(typeName));
|
||||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
const QVariantMap visible = mTypeRegistry.filterVisibleFields(vars, typeName);
|
||||||
if (!it.key().startsWith("_")) {
|
for (auto it = visible.begin(); it != visible.end(); ++it) {
|
||||||
lines.append(QString("%1: %2").arg(it.key(), it.value().toString()));
|
lines.append(QString("%1: %2").arg(it.key(), it.value().toString()));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
text = lines.join("\n");
|
text = lines.join("\n");
|
||||||
}
|
}
|
||||||
@ -919,17 +1056,32 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
progress.setMinimumDuration(500);
|
progress.setMinimumDuration(500);
|
||||||
|
|
||||||
QVariantMap rootVars;
|
QVariantMap rootVars;
|
||||||
|
bool cancelled = false;
|
||||||
try {
|
try {
|
||||||
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
||||||
[this, &progress](qint64 pos, qint64 size) {
|
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
||||||
progress.setValue(static_cast<int>(pos));
|
if (pos > progress.value()) {
|
||||||
|
progress.setValue(static_cast<int>(pos));
|
||||||
|
}
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
if (progress.wasCanceled()) {
|
||||||
|
cancelled = true;
|
||||||
|
throw std::runtime_error("Parsing cancelled by user");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[this, &progress](const QString& status) {
|
[this, &progress, &cancelled](const QString& status) {
|
||||||
progress.setLabelText(status);
|
progress.setLabelText(status);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
if (progress.wasCanceled()) {
|
||||||
|
cancelled = true;
|
||||||
|
throw std::runtime_error("Parsing cancelled by user");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
if (cancelled) {
|
||||||
|
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
LogManager::instance().addLine();
|
LogManager::instance().addLine();
|
||||||
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
||||||
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
||||||
@ -951,15 +1103,16 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
// Rebuild tree
|
// Rebuild tree
|
||||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||||
routeNestedObjects(rootInst, rootVars);
|
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||||
|
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||||
|
|
||||||
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
organizeChildrenByExtension(rootInst);
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||||
updateTreeNodeCounts(cat);
|
mTreeBuilder.updateNodeCounts(cat);
|
||||||
cat->setExpanded(false);
|
cat->setExpanded(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1011,31 +1164,6 @@ MainWindow::~MainWindow()
|
|||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::pluralizeType(const QString& typeName) {
|
|
||||||
// simple: FastFile -> FastFiles, ZoneFile -> ZoneFiles
|
|
||||||
// you can replace with nicer display names later
|
|
||||||
const auto& mod = mTypeRegistry.module();
|
|
||||||
const auto it = mod.types.find(typeName);
|
|
||||||
QString groupLabel = (it != mod.types.end() && !it->display.isEmpty())
|
|
||||||
? it->display
|
|
||||||
: typeName;
|
|
||||||
return groupLabel + "s";
|
|
||||||
}
|
|
||||||
|
|
||||||
XTreeWidgetItem* MainWindow::ensureTypeCategoryRoot(const QString& typeName) {
|
|
||||||
if (mTypeCategoryRoots.contains(typeName))
|
|
||||||
return mTypeCategoryRoots[typeName];
|
|
||||||
|
|
||||||
auto* root = new XTreeWidgetItem(mTreeWidget);
|
|
||||||
root->setText(0, pluralizeType(typeName));
|
|
||||||
root->setData(0, Qt::UserRole + 1, "CATEGORY");
|
|
||||||
root->setData(0, Qt::UserRole + 2, typeName); // category's type
|
|
||||||
mTreeWidget->addTopLevelItem(root);
|
|
||||||
|
|
||||||
mTypeCategoryRoots.insert(typeName, root);
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results)
|
void MainWindow::setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results)
|
||||||
{
|
{
|
||||||
mTypeRegistry = std::move(registry);
|
mTypeRegistry = std::move(registry);
|
||||||
@ -1097,312 +1225,18 @@ void MainWindow::LoadTreeCategories()
|
|||||||
if (!td.isRoot) continue;
|
if (!td.isRoot) continue;
|
||||||
|
|
||||||
auto* cat = new XTreeWidgetItem(mTreeWidget);
|
auto* cat = new XTreeWidgetItem(mTreeWidget);
|
||||||
cat->setText(0, typeName); // or a nicer display name later
|
cat->setText(0, typeName);
|
||||||
cat->setData(0, Qt::UserRole, typeName); // store type name
|
cat->setData(0, Qt::UserRole, typeName);
|
||||||
mTreeWidget->addTopLevelItem(cat);
|
mTreeWidget->addTopLevelItem(cat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
XTreeWidgetItem* MainWindow::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); // store the map for UI later
|
|
||||||
parent->addChild(inst);
|
|
||||||
inst->setExpanded(false);
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString MainWindow::instanceDisplayFor(const QVariantMap& obj,
|
|
||||||
const QString& fallbackType,
|
|
||||||
const QString& fallbackKey,
|
|
||||||
std::optional<int> index)
|
|
||||||
{
|
|
||||||
if (obj.contains("_zone_name")) {
|
|
||||||
const QString s = obj.value("_zone_name").toString();
|
|
||||||
if (!s.isEmpty()) return s;
|
|
||||||
}
|
|
||||||
if (obj.contains("_name")) {
|
|
||||||
const QString s = obj.value("_name").toString();
|
|
||||||
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 MainWindow::routeNestedObjects(XTreeWidgetItem* ownerInstanceNode, const QVariantMap& vars)
|
|
||||||
{
|
|
||||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
|
||||||
const QString& key = it.key();
|
|
||||||
const QVariant& v = it.value();
|
|
||||||
|
|
||||||
// Skip internal/temporary variables (underscore prefix, except special ones)
|
|
||||||
if (key.startsWith("_") && key != "_name" && key != "_type" && key != "_path" &&
|
|
||||||
key != "_basename" && key != "_ext" && key != "_zone_name") {
|
|
||||||
//LogManager::instance().addEntry(QString("[TREE] Skipping internal variable: key='%1'").arg(key));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// child object
|
|
||||||
if (v.typeId() == QMetaType::QVariantMap) {
|
|
||||||
const QVariantMap child = v.toMap();
|
|
||||||
|
|
||||||
// Skip hidden objects
|
|
||||||
if (child.value("_hidden").toBool()) {
|
|
||||||
//LogManager::instance().addEntry(QString("[TREE] Skipping hidden child object: key='%1'").arg(key));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString childType = child.value("_type").toString();
|
|
||||||
if (!childType.isEmpty()) {
|
|
||||||
const QString childName = child.value("_name").toString();
|
|
||||||
//LogManager::instance().addEntry(QString("[TREE] Adding single child: key='%1', type='%2', name='%3'")
|
|
||||||
// .arg(key)
|
|
||||||
// .arg(childType)
|
|
||||||
// .arg(childName));
|
|
||||||
|
|
||||||
auto* subcat = ensureSubcategory(ownerInstanceNode, childType);
|
|
||||||
|
|
||||||
const QString childDisplay = instanceDisplayFor(child, childType, key);
|
|
||||||
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
|
|
||||||
routeNestedObjects(childInst, child);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// array of child objects (optional, but nice)
|
|
||||||
if (v.typeId() == QMetaType::QVariantList) {
|
|
||||||
// Check for skip marker (XScript convention: _skip_tree_<arrayname>)
|
|
||||||
if (vars.contains("_skip_tree_" + 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) {
|
|
||||||
LogManager::instance().debug(QString("[TREE] Item %1 in '%2' is not a map, skipping").arg(i).arg(key));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QVariantMap child = list[i].toMap();
|
|
||||||
|
|
||||||
// Skip hidden objects
|
|
||||||
if (child.value("_hidden").toBool()) {
|
|
||||||
LogManager::instance().debug(QString("[TREE] Item %1 in '%2' is hidden, skipping").arg(i).arg(key));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString childType = child.value("_type").toString();
|
|
||||||
if (childType.isEmpty()) {
|
|
||||||
LogManager::instance().debug(QString("[TREE] Item %1 in '%2' has no _type, skipping (name='%3')")
|
|
||||||
.arg(i).arg(key).arg(child.value("_name").toString()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString childName = child.value("_name").toString();
|
|
||||||
LogManager::instance().debug(QString("[TREE] Adding array item #%1: type='%2', name='%3'")
|
|
||||||
.arg(i)
|
|
||||||
.arg(childType)
|
|
||||||
.arg(childName));
|
|
||||||
|
|
||||||
auto* subcat = ensureSubcategory(ownerInstanceNode, childType);
|
|
||||||
|
|
||||||
const QString childDisplay = instanceDisplayFor(child, childType, it.key(), i);
|
|
||||||
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
|
|
||||||
routeNestedObjects(childInst, child);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
XTreeWidgetItem* MainWindow::ensureSubcategory(XTreeWidgetItem* instanceNode, const QString& childTypeName)
|
|
||||||
{
|
|
||||||
const QString key = QString("SUBCAT:%1").arg(childTypeName);
|
|
||||||
|
|
||||||
// Look for an existing subcategory child
|
|
||||||
for (int i = 0; i < instanceNode->childCount(); i++) {
|
|
||||||
auto* c = static_cast<XTreeWidgetItem*>(instanceNode->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(instanceNode);
|
|
||||||
sub->setText(0, pluralizeType(childTypeName));
|
|
||||||
sub->setData(0, Qt::UserRole + 1, "SUBCATEGORY");
|
|
||||||
sub->setData(0, Qt::UserRole + 2, childTypeName);
|
|
||||||
instanceNode->addChild(sub);
|
|
||||||
sub->setExpanded(false);
|
|
||||||
return sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::organizeChildrenByExtension(XTreeWidgetItem* parent)
|
|
||||||
{
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
// First, recursively process all children (depth-first)
|
|
||||||
for (int i = 0; i < parent->childCount(); i++) {
|
|
||||||
organizeChildrenByExtension(static_cast<XTreeWidgetItem*>(parent->child(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process this node if it's a SUBCATEGORY
|
|
||||||
QString nodeType = parent->data(0, Qt::UserRole + 1).toString();
|
|
||||||
if (nodeType != "SUBCATEGORY") return;
|
|
||||||
|
|
||||||
// Collect all instance children and group by extension
|
|
||||||
QMap<QString, QList<XTreeWidgetItem*>> byExtension;
|
|
||||||
QList<XTreeWidgetItem*> noExtension;
|
|
||||||
QList<XTreeWidgetItem*> nonInstanceChildren;
|
|
||||||
|
|
||||||
// Take all children out first
|
|
||||||
QList<XTreeWidgetItem*> children;
|
|
||||||
while (parent->childCount() > 0) {
|
|
||||||
auto* child = static_cast<XTreeWidgetItem*>(parent->takeChild(0));
|
|
||||||
children.append(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group by extension
|
|
||||||
for (auto* child : children) {
|
|
||||||
QString childNodeType = child->data(0, Qt::UserRole + 1).toString();
|
|
||||||
|
|
||||||
// Keep non-instance children (like nested SUBCATEGORYs or EXTENSION_GROUPs) as-is
|
|
||||||
if (childNodeType == "SUBCATEGORY" || childNodeType == "EXTENSION_GROUP") {
|
|
||||||
nonInstanceChildren.append(child);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantMap vars = child->data(0, Qt::UserRole + 3).toMap();
|
|
||||||
QString name = vars.value("_name").toString();
|
|
||||||
|
|
||||||
// Skip names that start with a dot (indexed chunk names like .tsxt0, .fcsr0)
|
|
||||||
// These are already indexed and shouldn't be grouped by extension
|
|
||||||
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 organize if there are multiple extensions
|
|
||||||
int uniqueExtensions = byExtension.size() + (noExtension.isEmpty() ? 0 : 1);
|
|
||||||
if (uniqueExtensions <= 1) {
|
|
||||||
// Put them all 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);
|
|
||||||
|
|
||||||
// Sort items within the group
|
|
||||||
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 MainWindow::updateTreeNodeCounts(XTreeWidgetItem* node)
|
|
||||||
{
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
// Recursively update children first
|
|
||||||
for (int i = 0; i < node->childCount(); i++) {
|
|
||||||
updateTreeNodeCounts(static_cast<XTreeWidgetItem*>(node->child(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update count for grouping nodes (SUBCATEGORY, CATEGORY, EXTENSION_GROUP)
|
|
||||||
QString nodeType = node->data(0, Qt::UserRole + 1).toString();
|
|
||||||
if (nodeType == "SUBCATEGORY" || nodeType == "CATEGORY" || nodeType == "EXTENSION_GROUP") {
|
|
||||||
// Count direct children only
|
|
||||||
int count = node->childCount();
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
QString currentText = node->text(0);
|
|
||||||
// Remove any existing count suffix
|
|
||||||
int parenPos = currentText.lastIndexOf(" (");
|
|
||||||
if (parenPos > 0) {
|
|
||||||
currentText = currentText.left(parenPos);
|
|
||||||
}
|
|
||||||
node->setText(0, QString("%1 (%2)").arg(currentText).arg(count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::Reset() {
|
void MainWindow::Reset() {
|
||||||
// Clear the tree widget
|
// Clear the tree widget
|
||||||
mTreeWidget->clear();
|
mTreeWidget->clear();
|
||||||
|
|
||||||
// Clear category roots hash
|
// Clear category roots in tree builder
|
||||||
mTypeCategoryRoots.clear();
|
mTreeBuilder.reset();
|
||||||
|
|
||||||
// Clear tabs
|
// Clear tabs
|
||||||
ui->tabWidget->clear();
|
ui->tabWidget->clear();
|
||||||
@ -1485,17 +1319,32 @@ void MainWindow::dropEvent(QDropEvent *event) {
|
|||||||
progress.setMinimumDuration(500);
|
progress.setMinimumDuration(500);
|
||||||
|
|
||||||
QVariantMap rootVars;
|
QVariantMap rootVars;
|
||||||
|
bool cancelled = false;
|
||||||
try {
|
try {
|
||||||
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
||||||
[this, &progress](qint64 pos, qint64 size) {
|
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
||||||
progress.setValue(static_cast<int>(pos));
|
if (pos > progress.value()) {
|
||||||
|
progress.setValue(static_cast<int>(pos));
|
||||||
|
}
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
if (progress.wasCanceled()) {
|
||||||
|
cancelled = true;
|
||||||
|
throw std::runtime_error("Parsing cancelled by user");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[this, &progress](const QString& status) {
|
[this, &progress, &cancelled](const QString& status) {
|
||||||
progress.setLabelText(status);
|
progress.setLabelText(status);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
if (progress.wasCanceled()) {
|
||||||
|
cancelled = true;
|
||||||
|
throw std::runtime_error("Parsing cancelled by user");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
if (cancelled) {
|
||||||
|
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
LogManager::instance().addLine();
|
LogManager::instance().addLine();
|
||||||
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
||||||
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
||||||
@ -1521,19 +1370,20 @@ void MainWindow::dropEvent(QDropEvent *event) {
|
|||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
// Ensure top-level category exists (it should, but safe)
|
// Ensure top-level category exists (it should, but safe)
|
||||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||||
|
|
||||||
// Add instance under category (FastFiles -> test.ff)
|
// Add instance under category (FastFiles -> test.ff)
|
||||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||||
|
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||||
|
|
||||||
// Route nested objects as subcategories under the instance (test.ff -> ZoneFiles -> ...)
|
// Route nested objects as subcategories under the instance (test.ff -> ZoneFiles -> ...)
|
||||||
routeNestedObjects(rootInst, rootVars);
|
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||||
|
|
||||||
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
organizeChildrenByExtension(rootInst);
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||||
updateTreeNodeCounts(cat);
|
mTreeBuilder.updateNodeCounts(cat);
|
||||||
|
|
||||||
cat->setExpanded(false);
|
cat->setExpanded(false);
|
||||||
mTreeWidget->setCurrentItem(rootInst);
|
mTreeWidget->setCurrentItem(rootInst);
|
||||||
|
|||||||
208
app/mainwindow.h
208
app/mainwindow.h
@ -1,107 +1,101 @@
|
|||||||
#ifndef MAINWINDOW_H
|
#ifndef MAINWINDOW_H
|
||||||
#define MAINWINDOW_H
|
#define MAINWINDOW_H
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
|
||||||
#include "typeregistry.h"
|
#include "typeregistry.h"
|
||||||
#include "settings.h"
|
#include "treebuilder.h"
|
||||||
|
#include "settings.h"
|
||||||
struct DefinitionLoadResult {
|
|
||||||
QString filePath;
|
struct DefinitionLoadResult {
|
||||||
QString fileName;
|
QString filePath;
|
||||||
bool success;
|
QString fileName;
|
||||||
QString errorMessage;
|
bool success;
|
||||||
};
|
QString errorMessage;
|
||||||
|
};
|
||||||
class XTreeWidget;
|
|
||||||
class XTreeWidgetItem;
|
class XTreeWidget;
|
||||||
class QPlainTextEdit;
|
class XTreeWidgetItem;
|
||||||
class QProgressBar;
|
class QPlainTextEdit;
|
||||||
|
class QProgressBar;
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
namespace Ui {
|
QT_BEGIN_NAMESPACE
|
||||||
class MainWindow;
|
namespace Ui {
|
||||||
}
|
class MainWindow;
|
||||||
QT_END_NAMESPACE
|
}
|
||||||
|
QT_END_NAMESPACE
|
||||||
class MainWindow : public QMainWindow
|
|
||||||
{
|
class MainWindow : public QMainWindow
|
||||||
Q_OBJECT
|
{
|
||||||
|
Q_OBJECT
|
||||||
public:
|
|
||||||
MainWindow(QWidget *parent = nullptr);
|
public:
|
||||||
~MainWindow();
|
MainWindow(QWidget *parent = nullptr);
|
||||||
|
~MainWindow();
|
||||||
void LoadDefinitions(); // For reparse
|
|
||||||
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
|
void LoadDefinitions(); // For reparse
|
||||||
void LoadTreeCategories();
|
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
|
||||||
void Reset();
|
void LoadTreeCategories();
|
||||||
|
void Reset();
|
||||||
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
|
|
||||||
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
|
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
|
||||||
|
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
|
||||||
QString pluralizeType(const QString &typeName);
|
|
||||||
XTreeWidgetItem *ensureTypeCategoryRoot(const QString &typeName);
|
TreeBuilder& treeBuilder() { return mTreeBuilder; }
|
||||||
XTreeWidgetItem *ensureSubcategory(XTreeWidgetItem *instanceNode, const QString &childTypeName);
|
private slots:
|
||||||
void routeNestedObjects(XTreeWidgetItem *ownerInstanceNode, const QVariantMap &vars);
|
void HandleLogEntry(const QString &entry);
|
||||||
XTreeWidgetItem *addInstanceNode(XTreeWidgetItem *parent, const QString &displayName, const QString &typeName, const QVariantMap &vars);
|
void HandleStatusUpdate(const QString &message, int timeout);
|
||||||
void organizeChildrenByExtension(XTreeWidgetItem *parent);
|
void HandleProgressUpdate(const QString &message, int progress, int max);
|
||||||
void updateTreeNodeCounts(XTreeWidgetItem *node);
|
void applyTheme(const Theme &theme);
|
||||||
static QString instanceDisplayFor(const QVariantMap &obj, const QString &fallbackType, const QString &fallbackKey = {}, std::optional<int> index = std::nullopt);
|
|
||||||
private slots:
|
protected:
|
||||||
void HandleLogEntry(const QString &entry);
|
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||||
void HandleStatusUpdate(const QString &message, int timeout);
|
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||||
void HandleProgressUpdate(const QString &message, int progress, int max);
|
void dragLeaveEvent(QDragLeaveEvent *event) override;
|
||||||
void applyTheme(const Theme &theme);
|
void dropEvent(QDropEvent *event) override;
|
||||||
|
|
||||||
protected:
|
private:
|
||||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
Ui::MainWindow *ui;
|
||||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
XTreeWidget *mTreeWidget;
|
||||||
void dragLeaveEvent(QDragLeaveEvent *event) override;
|
QPlainTextEdit *mLogWidget;
|
||||||
void dropEvent(QDropEvent *event) override;
|
QProgressBar *mProgressBar;
|
||||||
|
QFrame *mRibbon;
|
||||||
private:
|
|
||||||
Ui::MainWindow *ui;
|
TypeRegistry mTypeRegistry;
|
||||||
XTreeWidget *mTreeWidget;
|
TreeBuilder mTreeBuilder;
|
||||||
QPlainTextEdit *mLogWidget;
|
QStringList mOpenedFilePaths; // Track opened files for reparsing
|
||||||
QProgressBar *mProgressBar;
|
QVector<DefinitionLoadResult> mDefinitionResults;
|
||||||
QFrame *mRibbon;
|
|
||||||
|
// Actions - File menu
|
||||||
QHash<QString, XTreeWidgetItem*> mTypeCategoryRoots;
|
QAction *actionNew;
|
||||||
TypeRegistry mTypeRegistry;
|
QAction *actionOpen;
|
||||||
QStringList mOpenedFilePaths; // Track opened files for reparsing
|
QAction *actionOpenFolder;
|
||||||
QVector<DefinitionLoadResult> mDefinitionResults;
|
QAction *actionSave;
|
||||||
|
QAction *actionSaveAs;
|
||||||
// Actions - File menu
|
|
||||||
QAction *actionNew;
|
// Actions - Edit menu
|
||||||
QAction *actionOpen;
|
QAction *actionUndo;
|
||||||
QAction *actionOpenFolder;
|
QAction *actionRedo;
|
||||||
QAction *actionSave;
|
QAction *actionCut;
|
||||||
QAction *actionSaveAs;
|
QAction *actionCopy;
|
||||||
|
QAction *actionPaste;
|
||||||
// Actions - Edit menu
|
QAction *actionRename;
|
||||||
QAction *actionUndo;
|
QAction *actionDelete;
|
||||||
QAction *actionRedo;
|
QAction *actionFind;
|
||||||
QAction *actionCut;
|
QAction *actionClearUndoHistory;
|
||||||
QAction *actionCopy;
|
QAction *actionPreferences;
|
||||||
QAction *actionPaste;
|
|
||||||
QAction *actionRename;
|
// Actions - Tools menu
|
||||||
QAction *actionDelete;
|
QAction *actionRunTests;
|
||||||
QAction *actionFind;
|
QAction *actionViewDefinitions;
|
||||||
QAction *actionClearUndoHistory;
|
|
||||||
QAction *actionPreferences;
|
// Actions - Help menu
|
||||||
|
QAction *actionAbout;
|
||||||
// Actions - Tools menu
|
QAction *actionCheckForUpdates;
|
||||||
QAction *actionRunTests;
|
QAction *actionReportIssue;
|
||||||
QAction *actionViewDefinitions;
|
|
||||||
|
// Actions - Toolbar
|
||||||
// Actions - Help menu
|
QAction *actionReparse;
|
||||||
QAction *actionAbout;
|
};
|
||||||
QAction *actionCheckForUpdates;
|
#endif // MAINWINDOW_H
|
||||||
QAction *actionReportIssue;
|
|
||||||
|
|
||||||
// Actions - Toolbar
|
|
||||||
QAction *actionReparse;
|
|
||||||
};
|
|
||||||
#endif // MAINWINDOW_H
|
|
||||||
|
|||||||
@ -385,6 +385,36 @@ void PreferenceEditor::createPreviewPage()
|
|||||||
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
|
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
|
||||||
layout->addWidget(titleLabel);
|
layout->addWidget(titleLabel);
|
||||||
|
|
||||||
|
// File Type Associations group
|
||||||
|
auto *fileTypeGroup = new QGroupBox("File Type Associations", page);
|
||||||
|
auto *fileTypeLayout = new QFormLayout(fileTypeGroup);
|
||||||
|
fileTypeLayout->setSpacing(10);
|
||||||
|
|
||||||
|
m_textExtensionsEdit = new QLineEdit(fileTypeGroup);
|
||||||
|
m_textExtensionsEdit->setPlaceholderText("txt, xml, json, cfg, lua...");
|
||||||
|
m_textExtensionsEdit->setToolTip("Comma-separated list of extensions to open in text viewer");
|
||||||
|
fileTypeLayout->addRow("Text Viewer:", m_textExtensionsEdit);
|
||||||
|
|
||||||
|
m_imageExtensionsEdit = new QLineEdit(fileTypeGroup);
|
||||||
|
m_imageExtensionsEdit->setPlaceholderText("tga, dds, png, jpg...");
|
||||||
|
m_imageExtensionsEdit->setToolTip("Comma-separated list of extensions to open in image viewer");
|
||||||
|
fileTypeLayout->addRow("Image Viewer:", m_imageExtensionsEdit);
|
||||||
|
|
||||||
|
m_audioExtensionsEdit = new QLineEdit(fileTypeGroup);
|
||||||
|
m_audioExtensionsEdit->setPlaceholderText("wav, mp3, ogg...");
|
||||||
|
m_audioExtensionsEdit->setToolTip("Comma-separated list of extensions to open in audio player");
|
||||||
|
fileTypeLayout->addRow("Audio Player:", m_audioExtensionsEdit);
|
||||||
|
|
||||||
|
auto *fileTypeDesc = new QLabel(
|
||||||
|
"File types not listed above will open in the hex viewer. "
|
||||||
|
"Enter extensions without dots, separated by commas.",
|
||||||
|
fileTypeGroup);
|
||||||
|
fileTypeDesc->setWordWrap(true);
|
||||||
|
fileTypeDesc->setStyleSheet("color: #888; font-size: 11px;");
|
||||||
|
fileTypeLayout->addRow("", fileTypeDesc);
|
||||||
|
|
||||||
|
layout->addWidget(fileTypeGroup);
|
||||||
|
|
||||||
// Audio group
|
// Audio group
|
||||||
auto *audioGroup = new QGroupBox("Audio Preview", page);
|
auto *audioGroup = new QGroupBox("Audio Preview", page);
|
||||||
auto *audioLayout = new QVBoxLayout(audioGroup);
|
auto *audioLayout = new QVBoxLayout(audioGroup);
|
||||||
@ -453,6 +483,11 @@ void PreferenceEditor::loadSettings()
|
|||||||
m_audioAutoPlayCheck->setChecked(s.audioAutoPlay());
|
m_audioAutoPlayCheck->setChecked(s.audioAutoPlay());
|
||||||
m_imageShowGridCheck->setChecked(s.imageShowGrid());
|
m_imageShowGridCheck->setChecked(s.imageShowGrid());
|
||||||
m_imageAutoZoomCheck->setChecked(s.imageAutoZoom());
|
m_imageAutoZoomCheck->setChecked(s.imageAutoZoom());
|
||||||
|
|
||||||
|
// File Type Associations
|
||||||
|
m_textExtensionsEdit->setText(s.textFileExtensions().join(", "));
|
||||||
|
m_imageExtensionsEdit->setText(s.imageFileExtensions().join(", "));
|
||||||
|
m_audioExtensionsEdit->setText(s.audioFileExtensions().join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PreferenceEditor::saveSettings()
|
void PreferenceEditor::saveSettings()
|
||||||
@ -491,6 +526,21 @@ void PreferenceEditor::saveSettings()
|
|||||||
s.setImageShowGrid(m_imageShowGridCheck->isChecked());
|
s.setImageShowGrid(m_imageShowGridCheck->isChecked());
|
||||||
s.setImageAutoZoom(m_imageAutoZoomCheck->isChecked());
|
s.setImageAutoZoom(m_imageAutoZoomCheck->isChecked());
|
||||||
|
|
||||||
|
// File Type Associations - parse comma-separated lists
|
||||||
|
auto parseExtList = [](const QString& text) -> QStringList {
|
||||||
|
QStringList result;
|
||||||
|
for (QString ext : text.split(',', Qt::SkipEmptyParts)) {
|
||||||
|
ext = ext.trimmed().toLower();
|
||||||
|
if (ext.startsWith('.')) ext = ext.mid(1);
|
||||||
|
if (!ext.isEmpty()) result.append(ext);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
s.setTextFileExtensions(parseExtList(m_textExtensionsEdit->text()));
|
||||||
|
s.setImageFileExtensions(parseExtList(m_imageExtensionsEdit->text()));
|
||||||
|
s.setAudioFileExtensions(parseExtList(m_audioExtensionsEdit->text()));
|
||||||
|
|
||||||
s.sync();
|
s.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,11 @@ private:
|
|||||||
QCheckBox *m_imageShowGridCheck;
|
QCheckBox *m_imageShowGridCheck;
|
||||||
QCheckBox *m_imageAutoZoomCheck;
|
QCheckBox *m_imageAutoZoomCheck;
|
||||||
|
|
||||||
|
// File Type Associations
|
||||||
|
QLineEdit *m_textExtensionsEdit;
|
||||||
|
QLineEdit *m_imageExtensionsEdit;
|
||||||
|
QLineEdit *m_audioExtensionsEdit;
|
||||||
|
|
||||||
// View Page
|
// View Page
|
||||||
QComboBox *m_fontFamilyCombo;
|
QComboBox *m_fontFamilyCombo;
|
||||||
QSpinBox *m_fontSizeSpin;
|
QSpinBox *m_fontSizeSpin;
|
||||||
|
|||||||
230
app/settings.cpp
230
app/settings.cpp
@ -91,6 +91,11 @@ Settings::Settings(QObject *parent)
|
|||||||
LogManager::instance().setDebugChecker([this]() {
|
LogManager::instance().setDebugChecker([this]() {
|
||||||
return debugLoggingEnabled();
|
return debugLoggingEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up log-to-file checker for LogManager
|
||||||
|
LogManager::instance().setLogToFileChecker([this]() {
|
||||||
|
return logToFileEnabled();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::sync()
|
void Settings::sync()
|
||||||
@ -201,6 +206,71 @@ QString Settings::findQuickBms()
|
|||||||
return QString(); // Not found
|
return QString(); // Not found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
QString Settings::pythonPath() const
|
||||||
|
{
|
||||||
|
QString path = m_settings.value("Tools/PythonPath").toString();
|
||||||
|
if (path.isEmpty() || !QFileInfo::exists(path)) {
|
||||||
|
path = findPython();
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setPythonPath(const QString& path)
|
||||||
|
{
|
||||||
|
m_settings.setValue("Tools/PythonPath", path);
|
||||||
|
emit settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Settings::findPython()
|
||||||
|
{
|
||||||
|
// Common locations to search for Python
|
||||||
|
QStringList searchPaths = {
|
||||||
|
"python",
|
||||||
|
"python3",
|
||||||
|
"C:/Python312/python.exe",
|
||||||
|
"C:/Python311/python.exe",
|
||||||
|
"C:/Python310/python.exe",
|
||||||
|
"C:/Program Files/Python312/python.exe",
|
||||||
|
"C:/Program Files/Python311/python.exe",
|
||||||
|
QDir::homePath() + "/AppData/Local/Programs/Python/Python312/python.exe",
|
||||||
|
QDir::homePath() + "/AppData/Local/Programs/Python/Python311/python.exe",
|
||||||
|
"/usr/bin/python3",
|
||||||
|
"/usr/bin/python",
|
||||||
|
};
|
||||||
|
|
||||||
|
QString pathEnv = qEnvironmentVariable("PATH");
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
QStringList pathDirs = pathEnv.split(';', Qt::SkipEmptyParts);
|
||||||
|
#else
|
||||||
|
QStringList pathDirs = pathEnv.split(':', Qt::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
|
for (const QString& dir : pathDirs) {
|
||||||
|
searchPaths.append(dir + "/python.exe");
|
||||||
|
searchPaths.append(dir + "/python3.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QString& path : searchPaths) {
|
||||||
|
if (QFileInfo::exists(path)) {
|
||||||
|
return QDir::cleanPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Settings::scriptsDirectory() const
|
||||||
|
{
|
||||||
|
QString defaultPath = QCoreApplication::applicationDirPath() + "/scripts";
|
||||||
|
return m_settings.value("Tools/ScriptsDirectory", defaultPath).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setScriptsDirectory(const QString& path)
|
||||||
|
{
|
||||||
|
m_settings.setValue("Tools/ScriptsDirectory", path);
|
||||||
|
emit settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
// Debug/Logging
|
// Debug/Logging
|
||||||
bool Settings::debugLoggingEnabled() const
|
bool Settings::debugLoggingEnabled() const
|
||||||
{
|
{
|
||||||
@ -210,8 +280,16 @@ bool Settings::debugLoggingEnabled() const
|
|||||||
void Settings::setDebugLoggingEnabled(bool enable)
|
void Settings::setDebugLoggingEnabled(bool enable)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Debug/LoggingEnabled", enable);
|
m_settings.setValue("Debug/LoggingEnabled", enable);
|
||||||
|
m_settings.sync(); // Ensure immediate persistence
|
||||||
emit debugLoggingChanged(enable);
|
emit debugLoggingChanged(enable);
|
||||||
emit settingsChanged();
|
emit settingsChanged();
|
||||||
|
|
||||||
|
// Provide immediate feedback in log panel
|
||||||
|
if (enable) {
|
||||||
|
LogManager::instance().addEntry("[SETTINGS] Debug logging ENABLED - parse a file to see debug output");
|
||||||
|
} else {
|
||||||
|
LogManager::instance().addEntry("[SETTINGS] Debug logging DISABLED");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::verboseParsingEnabled() const
|
bool Settings::verboseParsingEnabled() const
|
||||||
@ -222,7 +300,15 @@ bool Settings::verboseParsingEnabled() const
|
|||||||
void Settings::setVerboseParsingEnabled(bool enable)
|
void Settings::setVerboseParsingEnabled(bool enable)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Debug/VerboseParsing", enable);
|
m_settings.setValue("Debug/VerboseParsing", enable);
|
||||||
|
m_settings.sync(); // Ensure immediate persistence
|
||||||
emit settingsChanged();
|
emit settingsChanged();
|
||||||
|
|
||||||
|
// Provide immediate feedback in log panel
|
||||||
|
if (enable) {
|
||||||
|
LogManager::instance().addEntry("[SETTINGS] Verbose parsing ENABLED");
|
||||||
|
} else {
|
||||||
|
LogManager::instance().addEntry("[SETTINGS] Verbose parsing DISABLED");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::logToFileEnabled() const
|
bool Settings::logToFileEnabled() const
|
||||||
@ -233,7 +319,15 @@ bool Settings::logToFileEnabled() const
|
|||||||
void Settings::setLogToFileEnabled(bool enable)
|
void Settings::setLogToFileEnabled(bool enable)
|
||||||
{
|
{
|
||||||
m_settings.setValue("Debug/LogToFile", enable);
|
m_settings.setValue("Debug/LogToFile", enable);
|
||||||
|
m_settings.sync(); // Ensure immediate persistence
|
||||||
emit settingsChanged();
|
emit settingsChanged();
|
||||||
|
|
||||||
|
// Provide immediate feedback in log panel
|
||||||
|
if (enable) {
|
||||||
|
LogManager::instance().addEntry("[SETTINGS] Log to file ENABLED");
|
||||||
|
} else {
|
||||||
|
LogManager::instance().addEntry("[SETTINGS] Log to file DISABLED");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// View
|
// View
|
||||||
@ -373,6 +467,142 @@ void Settings::setImageAutoZoom(bool enable)
|
|||||||
emit settingsChanged();
|
emit settingsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File Type Associations
|
||||||
|
static const QStringList s_defaultTextExtensions = {
|
||||||
|
"txt", "xml", "json", "csv", "cfg", "ini", "log",
|
||||||
|
"html", "htm", "css", "js", "lua", "py", "sh", "bat",
|
||||||
|
"md", "yaml", "yml", "gsc", "csc", "arena", "vision"
|
||||||
|
};
|
||||||
|
|
||||||
|
static const QStringList s_defaultImageExtensions = {
|
||||||
|
"tga", "dds", "png", "jpg", "jpeg", "bmp", "xbtex", "iwi"
|
||||||
|
};
|
||||||
|
|
||||||
|
static const QStringList s_defaultAudioExtensions = {
|
||||||
|
"wav", "wave", "mp3", "ogg", "flac", "raw"
|
||||||
|
};
|
||||||
|
static const QStringList s_defaultListExtensions = {
|
||||||
|
"str"
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList Settings::textFileExtensions() const
|
||||||
|
{
|
||||||
|
QStringList exts = m_settings.value("FileTypes/Text").toStringList();
|
||||||
|
if (exts.isEmpty()) {
|
||||||
|
return s_defaultTextExtensions;
|
||||||
|
}
|
||||||
|
return exts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setTextFileExtensions(const QStringList& extensions)
|
||||||
|
{
|
||||||
|
m_settings.setValue("FileTypes/Text", extensions);
|
||||||
|
emit settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Settings::imageFileExtensions() const
|
||||||
|
{
|
||||||
|
QStringList exts = m_settings.value("FileTypes/Image").toStringList();
|
||||||
|
if (exts.isEmpty()) {
|
||||||
|
return s_defaultImageExtensions;
|
||||||
|
}
|
||||||
|
return exts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setImageFileExtensions(const QStringList& extensions)
|
||||||
|
{
|
||||||
|
m_settings.setValue("FileTypes/Image", extensions);
|
||||||
|
emit settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Settings::audioFileExtensions() const
|
||||||
|
{
|
||||||
|
QStringList exts = m_settings.value("FileTypes/Audio").toStringList();
|
||||||
|
if (exts.isEmpty()) {
|
||||||
|
return s_defaultAudioExtensions;
|
||||||
|
}
|
||||||
|
return exts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setAudioFileExtensions(const QStringList& extensions)
|
||||||
|
{
|
||||||
|
m_settings.setValue("FileTypes/Audio", extensions);
|
||||||
|
emit settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Settings::listFileExtensions() const
|
||||||
|
{
|
||||||
|
QStringList exts = m_settings.value("FileTypes/List").toStringList();
|
||||||
|
if (exts.isEmpty()) {
|
||||||
|
return s_defaultListExtensions;
|
||||||
|
}
|
||||||
|
return exts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setListFileExtensions(const QStringList& extensions)
|
||||||
|
{
|
||||||
|
m_settings.setValue("FileTypes/List", extensions);
|
||||||
|
emit settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Settings::viewerForExtension(const QString& extension) const
|
||||||
|
{
|
||||||
|
QString ext = extension.toLower();
|
||||||
|
if (ext.startsWith('.')) {
|
||||||
|
ext = ext.mid(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textFileExtensions().contains(ext, Qt::CaseInsensitive)) {
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
if (imageFileExtensions().contains(ext, Qt::CaseInsensitive)) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (audioFileExtensions().contains(ext, Qt::CaseInsensitive)) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
if (listFileExtensions().contains(ext, Qt::CaseInsensitive)) {
|
||||||
|
return "list";
|
||||||
|
}
|
||||||
|
return "hex"; // Default to hex viewer
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setViewerForExtension(const QString& extension, const QString& viewer)
|
||||||
|
{
|
||||||
|
QString ext = extension.toLower();
|
||||||
|
if (ext.startsWith('.')) {
|
||||||
|
ext = ext.mid(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from all lists first
|
||||||
|
QStringList textExts = textFileExtensions();
|
||||||
|
QStringList imageExts = imageFileExtensions();
|
||||||
|
QStringList audioExts = audioFileExtensions();
|
||||||
|
QStringList listExts = listFileExtensions();
|
||||||
|
|
||||||
|
textExts.removeAll(ext);
|
||||||
|
imageExts.removeAll(ext);
|
||||||
|
audioExts.removeAll(ext);
|
||||||
|
listExts.removeAll(ext);
|
||||||
|
|
||||||
|
// Add to appropriate list
|
||||||
|
if (viewer == "text") {
|
||||||
|
textExts.append(ext);
|
||||||
|
} else if (viewer == "image") {
|
||||||
|
imageExts.append(ext);
|
||||||
|
} else if (viewer == "audio") {
|
||||||
|
audioExts.append(ext);
|
||||||
|
} else if (viewer == "list") {
|
||||||
|
listExts.append(ext);
|
||||||
|
}
|
||||||
|
// "hex" means don't add to any list
|
||||||
|
|
||||||
|
setTextFileExtensions(textExts);
|
||||||
|
setImageFileExtensions(imageExts);
|
||||||
|
setAudioFileExtensions(audioExts);
|
||||||
|
setListFileExtensions(listExts);
|
||||||
|
}
|
||||||
|
|
||||||
// Window State
|
// Window State
|
||||||
QByteArray Settings::windowGeometry() const
|
QByteArray Settings::windowGeometry() const
|
||||||
{
|
{
|
||||||
|
|||||||
@ -46,6 +46,13 @@ public:
|
|||||||
void setQuickBmsPath(const QString& path);
|
void setQuickBmsPath(const QString& path);
|
||||||
static QString findQuickBms(); // Auto-detect QuickBMS
|
static QString findQuickBms(); // Auto-detect QuickBMS
|
||||||
|
|
||||||
|
QString pythonPath() const;
|
||||||
|
void setPythonPath(const QString& path);
|
||||||
|
static QString findPython(); // Auto-detect Python
|
||||||
|
|
||||||
|
QString scriptsDirectory() const;
|
||||||
|
void setScriptsDirectory(const QString& path);
|
||||||
|
|
||||||
// Debug/Logging
|
// Debug/Logging
|
||||||
bool debugLoggingEnabled() const;
|
bool debugLoggingEnabled() const;
|
||||||
void setDebugLoggingEnabled(bool enable);
|
void setDebugLoggingEnabled(bool enable);
|
||||||
@ -97,6 +104,19 @@ public:
|
|||||||
bool imageAutoZoom() const;
|
bool imageAutoZoom() const;
|
||||||
void setImageAutoZoom(bool enable);
|
void setImageAutoZoom(bool enable);
|
||||||
|
|
||||||
|
// File Type Associations
|
||||||
|
// Returns the viewer type for a given extension: "hex", "text", "image", "audio", "list"
|
||||||
|
QString viewerForExtension(const QString& extension) const;
|
||||||
|
void setViewerForExtension(const QString& extension, const QString& viewer);
|
||||||
|
QStringList textFileExtensions() const;
|
||||||
|
void setTextFileExtensions(const QStringList& extensions);
|
||||||
|
QStringList imageFileExtensions() const;
|
||||||
|
void setImageFileExtensions(const QStringList& extensions);
|
||||||
|
QStringList audioFileExtensions() const;
|
||||||
|
void setAudioFileExtensions(const QStringList& extensions);
|
||||||
|
QStringList listFileExtensions() const;
|
||||||
|
void setListFileExtensions(const QStringList& extensions);
|
||||||
|
|
||||||
// Window State
|
// Window State
|
||||||
QByteArray windowGeometry() const;
|
QByteArray windowGeometry() const;
|
||||||
void setWindowGeometry(const QByteArray& geometry);
|
void setWindowGeometry(const QByteArray& geometry);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user