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
|
||||
|
||||
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) {
|
||||
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/*
|
||||
INSTALLS += install_it
|
||||
defs_install.files = $$PWD/../definitions/*
|
||||
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 "settings.h"
|
||||
#include "compression.h"
|
||||
#include "logmanager.h"
|
||||
|
||||
// Application metadata
|
||||
#define APP_NAME "XPlor"
|
||||
@ -85,8 +86,6 @@ static QJsonValue variantToJson(const QVariant& v) {
|
||||
QJsonObject obj;
|
||||
const QVariantMap map = v.toMap();
|
||||
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());
|
||||
}
|
||||
return obj;
|
||||
@ -404,6 +403,8 @@ int main(int argc, char *argv[])
|
||||
int loaded = 0;
|
||||
int total = defFiles.size();
|
||||
|
||||
LogManager::instance().addEntry(QString("[INIT] Loading %1 definition files...").arg(total));
|
||||
|
||||
for (const QString& path : defFiles) {
|
||||
QString fileName = QFileInfo(path).fileName();
|
||||
splash.setStatus(QString("Loading: %1").arg(fileName));
|
||||
@ -412,18 +413,26 @@ int main(int argc, char *argv[])
|
||||
|
||||
QFile f(path);
|
||||
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"});
|
||||
} else {
|
||||
try {
|
||||
registry.ingestScript(QString::fromUtf8(f.readAll()), path);
|
||||
LogManager::instance().addEntry(QString("[DEF] Loaded: %1").arg(fileName));
|
||||
defResults.append({path, fileName, true, QString()});
|
||||
} 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())});
|
||||
}
|
||||
}
|
||||
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.setProgress(85, 100);
|
||||
a.processEvents();
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
#include "dsluischema.h"
|
||||
#include "utils.h"
|
||||
#include "imagepreviewwidget.h"
|
||||
#include "textviewerwidget.h"
|
||||
#include "listpreviewwidget.h"
|
||||
|
||||
// Debug logging is controlled via Settings -> LogManager::debug()
|
||||
|
||||
@ -41,7 +43,6 @@
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
#include <QRegularExpression>
|
||||
#include <QCollator>
|
||||
#include <QImage>
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
@ -81,38 +82,44 @@ static QIcon generateThemedIcon(const QColor &accentColor) {
|
||||
return QIcon(QPixmap::fromImage(image));
|
||||
}
|
||||
|
||||
// Natural sort comparator for strings with numbers (e.g., "chunk1" < "chunk2" < "chunk10")
|
||||
static bool naturalLessThan(const QString& a, const QString& b) {
|
||||
static QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
collator.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
return collator.compare(a, b) < 0;
|
||||
// Check if data looks like RCB pixel format (Avatar g4rc DXT1 texture)
|
||||
static bool looksLikeRCBPixel(const QByteArray &data) {
|
||||
if (data.size() < 0x28) return false;
|
||||
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||||
// RCB pixel has format byte 0x52 at offset 0x09
|
||||
// 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
|
||||
static void naturalSortChildren(QTreeWidgetItem* parent) {
|
||||
if (!parent || parent->childCount() == 0) return;
|
||||
|
||||
// Take all children
|
||||
QList<QTreeWidgetItem*> children;
|
||||
while (parent->childCount() > 0) {
|
||||
children.append(parent->takeChild(0));
|
||||
}
|
||||
|
||||
// Sort using natural order
|
||||
std::sort(children.begin(), children.end(), [](QTreeWidgetItem* a, QTreeWidgetItem* b) {
|
||||
return naturalLessThan(a->text(0), b->text(0));
|
||||
});
|
||||
|
||||
// Re-add in sorted order
|
||||
for (auto* child : children) {
|
||||
parent->addChild(child);
|
||||
// Check if data looks like a known image format
|
||||
static bool looksLikeImage(const QByteArray &data) {
|
||||
if (data.size() < 8) return false;
|
||||
// PNG
|
||||
if (data.startsWith("\x89PNG")) return true;
|
||||
// JPEG
|
||||
if (data.startsWith("\xFF\xD8\xFF")) return true;
|
||||
// BMP
|
||||
if (data.startsWith("BM")) return true;
|
||||
// DDS
|
||||
if (data.startsWith("DDS ")) return true;
|
||||
// TGA - check for valid image type
|
||||
if (data.size() >= 18) {
|
||||
uchar imageType = static_cast<uchar>(data[2]);
|
||||
if (imageType == 2 || imageType == 10) return true; // Uncompressed/RLE true-color
|
||||
}
|
||||
// RCB pixel format (Avatar textures)
|
||||
if (looksLikeRCBPixel(data)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, ui(new Ui::MainWindow)
|
||||
, mTreeBuilder(nullptr, mTypeRegistry) // Placeholder, will reset after mTreeWidget created
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setAcceptDrops(true);
|
||||
@ -139,6 +146,9 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
applyTheme(Settings::instance().theme());
|
||||
|
||||
mTreeWidget = new XTreeWidget(this);
|
||||
|
||||
// Initialize tree builder with the actual tree widget
|
||||
mTreeBuilder = TreeBuilder(mTreeWidget, mTypeRegistry);
|
||||
mTreeWidget->setColumnCount(1);
|
||||
|
||||
mLogWidget = new QPlainTextEdit(this);
|
||||
@ -156,6 +166,9 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
connect(&LogManager::instance(), &LogManager::entryAdded,
|
||||
this, &MainWindow::HandleLogEntry);
|
||||
|
||||
// Flush any buffered log entries from before the signal was connected
|
||||
LogManager::instance().flushBufferedEntries();
|
||||
|
||||
statusBar()->addPermanentWidget(mProgressBar);
|
||||
|
||||
ui->tabWidget->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
@ -244,8 +257,10 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
return;
|
||||
}
|
||||
|
||||
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
|
||||
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
|
||||
const QString tabTitle = item->text(0);
|
||||
@ -257,44 +272,55 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
}
|
||||
|
||||
// Check if this item has a preview (resource data)
|
||||
if (vars.contains("_preview")) {
|
||||
const QVariantMap preview = vars.value("_preview").toMap();
|
||||
LogManager::instance().addEntry(QString("[VIEWER] Checking item '%1', has _preview: %2, has data: %3")
|
||||
.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 QByteArray data = preview.value("data").toByteArray();
|
||||
const QString lowerFilename = filename.toLower();
|
||||
LogManager::instance().addEntry(QString("[VIEWER] Preview filename='%1', data size=%2")
|
||||
.arg(filename)
|
||||
.arg(data.size()));
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
// Check file type and use appropriate widget
|
||||
bool isImage = lowerFilename.endsWith(".tga") ||
|
||||
lowerFilename.endsWith(".dds") ||
|
||||
lowerFilename.endsWith(".png") ||
|
||||
lowerFilename.endsWith(".jpg") ||
|
||||
lowerFilename.endsWith(".jpeg") ||
|
||||
lowerFilename.endsWith(".bmp") ||
|
||||
lowerFilename.endsWith(".xbtex");
|
||||
// Get file extension
|
||||
QFileInfo fileInfo(filename);
|
||||
QString extension = fileInfo.suffix();
|
||||
|
||||
bool isAudio = lowerFilename.endsWith(".wav") ||
|
||||
lowerFilename.endsWith(".wave");
|
||||
// Check if script specified a viewer type via set_viewer()
|
||||
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
|
||||
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
|
||||
audioWidget->setProperty("PARENT_NAME", tabTitle);
|
||||
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);
|
||||
|
||||
ui->tabWidget->addTab(audioWidget, tabTitle);
|
||||
ui->tabWidget->setCurrentWidget(audioWidget);
|
||||
return;
|
||||
} else if (isImage) {
|
||||
} else if (viewerType == "image") {
|
||||
// Image preview widget
|
||||
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
|
||||
imageWidget->setProperty("PARENT_NAME", tabTitle);
|
||||
@ -308,34 +334,51 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
else if (lowerFilename.endsWith(".jpg") || lowerFilename.endsWith(".jpeg")) format = "JPG";
|
||||
|
||||
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);
|
||||
|
||||
ui->tabWidget->addTab(imageWidget, tabTitle);
|
||||
ui->tabWidget->setCurrentWidget(imageWidget);
|
||||
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 {
|
||||
// Hex viewer for unknown file types
|
||||
// Hex viewer for unknown file types (default)
|
||||
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
||||
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
||||
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);
|
||||
|
||||
ui->tabWidget->addTab(hexWidget, tabTitle);
|
||||
ui->tabWidget->setCurrentWidget(hexWidget);
|
||||
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) {
|
||||
QByteArray chunkData = vars.value("data").toByteArray();
|
||||
if (!chunkData.isEmpty()) {
|
||||
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
||||
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
||||
hexWidget->setData(chunkData, tabTitle);
|
||||
// Get extension from tab title (often includes filename)
|
||||
QFileInfo fileInfo(tabTitle);
|
||||
QString extension = fileInfo.suffix();
|
||||
|
||||
// Add other parsed fields as metadata (excluding the binary data)
|
||||
QVariantMap metadata;
|
||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
||||
if (!it.key().startsWith("_") && it.key() != "data") {
|
||||
metadata[it.key()] = it.value();
|
||||
}
|
||||
// Check if script specified a viewer type via set_viewer()
|
||||
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 {
|
||||
viewerType = Settings::instance().viewerForExtension(extension);
|
||||
}
|
||||
hexWidget->setMetadata(metadata);
|
||||
|
||||
ui->tabWidget->addTab(hexWidget, tabTitle);
|
||||
ui->tabWidget->setCurrentWidget(hexWidget);
|
||||
return;
|
||||
// Collect visible metadata based on UI schema
|
||||
QVariantMap metadata = mTypeRegistry.filterVisibleFields(vars, typeName);
|
||||
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 auto schema = buildUiSchemaForType(td);
|
||||
const auto schema = buildUiSchemaForType(td, &mTypeRegistry.module());
|
||||
|
||||
auto* w = new ScriptTypeEditorWidget(typeName, schema, ui->tabWidget);
|
||||
w->setProperty("PARENT_NAME", tabTitle);
|
||||
@ -387,6 +504,8 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
}
|
||||
});
|
||||
|
||||
ui->toolBar->setWindowTitle("Tool Bar");
|
||||
|
||||
QDockWidget *treeDockWidget = new QDockWidget(this);
|
||||
treeDockWidget->setWidget(mTreeWidget);
|
||||
treeDockWidget->setWindowTitle("Tree Browser");
|
||||
@ -589,17 +708,33 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
progress.setMinimumDuration(500);
|
||||
|
||||
QVariantMap rootVars;
|
||||
bool cancelled = false;
|
||||
try {
|
||||
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
||||
[this, &progress](qint64 pos, qint64 size) {
|
||||
progress.setValue(static_cast<int>(pos));
|
||||
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
||||
// Only update progress if position is moving forward (avoid jumps from seek)
|
||||
if (pos > progress.value()) {
|
||||
progress.setValue(static_cast<int>(pos));
|
||||
}
|
||||
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);
|
||||
QApplication::processEvents();
|
||||
if (progress.wasCanceled()) {
|
||||
cancelled = true;
|
||||
throw std::runtime_error("Parsing cancelled by user");
|
||||
}
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
if (cancelled) {
|
||||
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
||||
continue;
|
||||
}
|
||||
LogManager::instance().addLine();
|
||||
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
||||
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
||||
@ -623,15 +758,16 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
progress.setRange(0, 0); // Indeterminate mode
|
||||
QApplication::processEvents();
|
||||
|
||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
||||
routeNestedObjects(rootInst, rootVars);
|
||||
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||
|
||||
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
||||
QApplication::processEvents();
|
||||
|
||||
organizeChildrenByExtension(rootInst);
|
||||
updateTreeNodeCounts(cat);
|
||||
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||
mTreeBuilder.updateNodeCounts(cat);
|
||||
|
||||
cat->setExpanded(false);
|
||||
mTreeWidget->setCurrentItem(rootInst);
|
||||
@ -695,11 +831,12 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
mOpenedFilePaths.append(path);
|
||||
}
|
||||
|
||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
||||
routeNestedObjects(rootInst, rootVars);
|
||||
organizeChildrenByExtension(rootInst);
|
||||
updateTreeNodeCounts(cat);
|
||||
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||
mTreeBuilder.updateNodeCounts(cat);
|
||||
cat->setExpanded(false);
|
||||
}
|
||||
|
||||
@ -752,14 +889,14 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
if (kind == "INSTANCE") {
|
||||
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;
|
||||
lines.append(QString("Name: %1").arg(text));
|
||||
lines.append(QString("Type: %1").arg(item->data(0, Qt::UserRole + 2).toString()));
|
||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
||||
if (!it.key().startsWith("_")) {
|
||||
lines.append(QString("%1: %2").arg(it.key(), it.value().toString()));
|
||||
}
|
||||
lines.append(QString("Type: %1").arg(typeName));
|
||||
const QVariantMap visible = mTypeRegistry.filterVisibleFields(vars, typeName);
|
||||
for (auto it = visible.begin(); it != visible.end(); ++it) {
|
||||
lines.append(QString("%1: %2").arg(it.key(), it.value().toString()));
|
||||
}
|
||||
text = lines.join("\n");
|
||||
}
|
||||
@ -919,17 +1056,32 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
progress.setMinimumDuration(500);
|
||||
|
||||
QVariantMap rootVars;
|
||||
bool cancelled = false;
|
||||
try {
|
||||
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
||||
[this, &progress](qint64 pos, qint64 size) {
|
||||
progress.setValue(static_cast<int>(pos));
|
||||
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
||||
if (pos > progress.value()) {
|
||||
progress.setValue(static_cast<int>(pos));
|
||||
}
|
||||
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);
|
||||
QApplication::processEvents();
|
||||
if (progress.wasCanceled()) {
|
||||
cancelled = true;
|
||||
throw std::runtime_error("Parsing cancelled by user");
|
||||
}
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
if (cancelled) {
|
||||
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
||||
continue;
|
||||
}
|
||||
LogManager::instance().addLine();
|
||||
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
||||
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
||||
@ -951,15 +1103,16 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
QApplication::processEvents();
|
||||
|
||||
// Rebuild tree
|
||||
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
||||
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
||||
routeNestedObjects(rootInst, rootVars);
|
||||
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
||||
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
||||
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
||||
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||
|
||||
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
||||
QApplication::processEvents();
|
||||
|
||||
organizeChildrenByExtension(rootInst);
|
||||
updateTreeNodeCounts(cat);
|
||||
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||
mTreeBuilder.updateNodeCounts(cat);
|
||||
cat->setExpanded(false);
|
||||
}
|
||||
|
||||
@ -1011,31 +1164,6 @@ MainWindow::~MainWindow()
|
||||
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)
|
||||
{
|
||||
mTypeRegistry = std::move(registry);
|
||||
@ -1097,312 +1225,18 @@ void MainWindow::LoadTreeCategories()
|
||||
if (!td.isRoot) continue;
|
||||
|
||||
auto* cat = new XTreeWidgetItem(mTreeWidget);
|
||||
cat->setText(0, typeName); // or a nicer display name later
|
||||
cat->setData(0, Qt::UserRole, typeName); // store type name
|
||||
cat->setText(0, typeName);
|
||||
cat->setData(0, Qt::UserRole, typeName);
|
||||
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() {
|
||||
// Clear the tree widget
|
||||
mTreeWidget->clear();
|
||||
|
||||
// Clear category roots hash
|
||||
mTypeCategoryRoots.clear();
|
||||
// Clear category roots in tree builder
|
||||
mTreeBuilder.reset();
|
||||
|
||||
// Clear tabs
|
||||
ui->tabWidget->clear();
|
||||
@ -1485,17 +1319,32 @@ void MainWindow::dropEvent(QDropEvent *event) {
|
||||
progress.setMinimumDuration(500);
|
||||
|
||||
QVariantMap rootVars;
|
||||
bool cancelled = false;
|
||||
try {
|
||||
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
||||
[this, &progress](qint64 pos, qint64 size) {
|
||||
progress.setValue(static_cast<int>(pos));
|
||||
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
||||
if (pos > progress.value()) {
|
||||
progress.setValue(static_cast<int>(pos));
|
||||
}
|
||||
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);
|
||||
QApplication::processEvents();
|
||||
if (progress.wasCanceled()) {
|
||||
cancelled = true;
|
||||
throw std::runtime_error("Parsing cancelled by user");
|
||||
}
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
if (cancelled) {
|
||||
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
||||
continue;
|
||||
}
|
||||
LogManager::instance().addLine();
|
||||
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
||||
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
||||
@ -1521,19 +1370,20 @@ void MainWindow::dropEvent(QDropEvent *event) {
|
||||
QApplication::processEvents();
|
||||
|
||||
// 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)
|
||||
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 -> ...)
|
||||
routeNestedObjects(rootInst, rootVars);
|
||||
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
||||
|
||||
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
||||
QApplication::processEvents();
|
||||
|
||||
organizeChildrenByExtension(rootInst);
|
||||
updateTreeNodeCounts(cat);
|
||||
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
||||
mTreeBuilder.updateNodeCounts(cat);
|
||||
|
||||
cat->setExpanded(false);
|
||||
mTreeWidget->setCurrentItem(rootInst);
|
||||
|
||||
208
app/mainwindow.h
208
app/mainwindow.h
@ -1,107 +1,101 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QFrame>
|
||||
|
||||
#include "typeregistry.h"
|
||||
#include "settings.h"
|
||||
|
||||
struct DefinitionLoadResult {
|
||||
QString filePath;
|
||||
QString fileName;
|
||||
bool success;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class XTreeWidget;
|
||||
class XTreeWidgetItem;
|
||||
class QPlainTextEdit;
|
||||
class QProgressBar;
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
void LoadDefinitions(); // For reparse
|
||||
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
|
||||
void LoadTreeCategories();
|
||||
void Reset();
|
||||
|
||||
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
|
||||
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
|
||||
|
||||
QString pluralizeType(const QString &typeName);
|
||||
XTreeWidgetItem *ensureTypeCategoryRoot(const QString &typeName);
|
||||
XTreeWidgetItem *ensureSubcategory(XTreeWidgetItem *instanceNode, const QString &childTypeName);
|
||||
void routeNestedObjects(XTreeWidgetItem *ownerInstanceNode, const QVariantMap &vars);
|
||||
XTreeWidgetItem *addInstanceNode(XTreeWidgetItem *parent, const QString &displayName, const QString &typeName, const QVariantMap &vars);
|
||||
void organizeChildrenByExtension(XTreeWidgetItem *parent);
|
||||
void updateTreeNodeCounts(XTreeWidgetItem *node);
|
||||
static QString instanceDisplayFor(const QVariantMap &obj, const QString &fallbackType, const QString &fallbackKey = {}, std::optional<int> index = std::nullopt);
|
||||
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);
|
||||
|
||||
protected:
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||
void dragLeaveEvent(QDragLeaveEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
XTreeWidget *mTreeWidget;
|
||||
QPlainTextEdit *mLogWidget;
|
||||
QProgressBar *mProgressBar;
|
||||
QFrame *mRibbon;
|
||||
|
||||
QHash<QString, XTreeWidgetItem*> mTypeCategoryRoots;
|
||||
TypeRegistry mTypeRegistry;
|
||||
QStringList mOpenedFilePaths; // Track opened files for reparsing
|
||||
QVector<DefinitionLoadResult> mDefinitionResults;
|
||||
|
||||
// Actions - File menu
|
||||
QAction *actionNew;
|
||||
QAction *actionOpen;
|
||||
QAction *actionOpenFolder;
|
||||
QAction *actionSave;
|
||||
QAction *actionSaveAs;
|
||||
|
||||
// Actions - Edit menu
|
||||
QAction *actionUndo;
|
||||
QAction *actionRedo;
|
||||
QAction *actionCut;
|
||||
QAction *actionCopy;
|
||||
QAction *actionPaste;
|
||||
QAction *actionRename;
|
||||
QAction *actionDelete;
|
||||
QAction *actionFind;
|
||||
QAction *actionClearUndoHistory;
|
||||
QAction *actionPreferences;
|
||||
|
||||
// Actions - Tools menu
|
||||
QAction *actionRunTests;
|
||||
QAction *actionViewDefinitions;
|
||||
|
||||
// Actions - Help menu
|
||||
QAction *actionAbout;
|
||||
QAction *actionCheckForUpdates;
|
||||
QAction *actionReportIssue;
|
||||
|
||||
// Actions - Toolbar
|
||||
QAction *actionReparse;
|
||||
};
|
||||
#endif // MAINWINDOW_H
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QFrame>
|
||||
|
||||
#include "typeregistry.h"
|
||||
#include "treebuilder.h"
|
||||
#include "settings.h"
|
||||
|
||||
struct DefinitionLoadResult {
|
||||
QString filePath;
|
||||
QString fileName;
|
||||
bool success;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class XTreeWidget;
|
||||
class XTreeWidgetItem;
|
||||
class QPlainTextEdit;
|
||||
class QProgressBar;
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
void LoadDefinitions(); // For reparse
|
||||
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
|
||||
void LoadTreeCategories();
|
||||
void Reset();
|
||||
|
||||
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
|
||||
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
|
||||
|
||||
TreeBuilder& treeBuilder() { return mTreeBuilder; }
|
||||
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);
|
||||
|
||||
protected:
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||
void dragLeaveEvent(QDragLeaveEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
XTreeWidget *mTreeWidget;
|
||||
QPlainTextEdit *mLogWidget;
|
||||
QProgressBar *mProgressBar;
|
||||
QFrame *mRibbon;
|
||||
|
||||
TypeRegistry mTypeRegistry;
|
||||
TreeBuilder mTreeBuilder;
|
||||
QStringList mOpenedFilePaths; // Track opened files for reparsing
|
||||
QVector<DefinitionLoadResult> mDefinitionResults;
|
||||
|
||||
// Actions - File menu
|
||||
QAction *actionNew;
|
||||
QAction *actionOpen;
|
||||
QAction *actionOpenFolder;
|
||||
QAction *actionSave;
|
||||
QAction *actionSaveAs;
|
||||
|
||||
// Actions - Edit menu
|
||||
QAction *actionUndo;
|
||||
QAction *actionRedo;
|
||||
QAction *actionCut;
|
||||
QAction *actionCopy;
|
||||
QAction *actionPaste;
|
||||
QAction *actionRename;
|
||||
QAction *actionDelete;
|
||||
QAction *actionFind;
|
||||
QAction *actionClearUndoHistory;
|
||||
QAction *actionPreferences;
|
||||
|
||||
// Actions - Tools menu
|
||||
QAction *actionRunTests;
|
||||
QAction *actionViewDefinitions;
|
||||
|
||||
// Actions - Help menu
|
||||
QAction *actionAbout;
|
||||
QAction *actionCheckForUpdates;
|
||||
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;");
|
||||
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
|
||||
auto *audioGroup = new QGroupBox("Audio Preview", page);
|
||||
auto *audioLayout = new QVBoxLayout(audioGroup);
|
||||
@ -453,6 +483,11 @@ void PreferenceEditor::loadSettings()
|
||||
m_audioAutoPlayCheck->setChecked(s.audioAutoPlay());
|
||||
m_imageShowGridCheck->setChecked(s.imageShowGrid());
|
||||
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()
|
||||
@ -491,6 +526,21 @@ void PreferenceEditor::saveSettings()
|
||||
s.setImageShowGrid(m_imageShowGridCheck->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();
|
||||
}
|
||||
|
||||
|
||||
@ -72,6 +72,11 @@ private:
|
||||
QCheckBox *m_imageShowGridCheck;
|
||||
QCheckBox *m_imageAutoZoomCheck;
|
||||
|
||||
// File Type Associations
|
||||
QLineEdit *m_textExtensionsEdit;
|
||||
QLineEdit *m_imageExtensionsEdit;
|
||||
QLineEdit *m_audioExtensionsEdit;
|
||||
|
||||
// View Page
|
||||
QComboBox *m_fontFamilyCombo;
|
||||
QSpinBox *m_fontSizeSpin;
|
||||
|
||||
230
app/settings.cpp
230
app/settings.cpp
@ -91,6 +91,11 @@ Settings::Settings(QObject *parent)
|
||||
LogManager::instance().setDebugChecker([this]() {
|
||||
return debugLoggingEnabled();
|
||||
});
|
||||
|
||||
// Set up log-to-file checker for LogManager
|
||||
LogManager::instance().setLogToFileChecker([this]() {
|
||||
return logToFileEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
void Settings::sync()
|
||||
@ -201,6 +206,71 @@ QString Settings::findQuickBms()
|
||||
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
|
||||
bool Settings::debugLoggingEnabled() const
|
||||
{
|
||||
@ -210,8 +280,16 @@ bool Settings::debugLoggingEnabled() const
|
||||
void Settings::setDebugLoggingEnabled(bool enable)
|
||||
{
|
||||
m_settings.setValue("Debug/LoggingEnabled", enable);
|
||||
m_settings.sync(); // Ensure immediate persistence
|
||||
emit debugLoggingChanged(enable);
|
||||
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
|
||||
@ -222,7 +300,15 @@ bool Settings::verboseParsingEnabled() const
|
||||
void Settings::setVerboseParsingEnabled(bool enable)
|
||||
{
|
||||
m_settings.setValue("Debug/VerboseParsing", enable);
|
||||
m_settings.sync(); // Ensure immediate persistence
|
||||
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
|
||||
@ -233,7 +319,15 @@ bool Settings::logToFileEnabled() const
|
||||
void Settings::setLogToFileEnabled(bool enable)
|
||||
{
|
||||
m_settings.setValue("Debug/LogToFile", enable);
|
||||
m_settings.sync(); // Ensure immediate persistence
|
||||
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
|
||||
@ -373,6 +467,142 @@ void Settings::setImageAutoZoom(bool enable)
|
||||
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
|
||||
QByteArray Settings::windowGeometry() const
|
||||
{
|
||||
|
||||
@ -46,6 +46,13 @@ public:
|
||||
void setQuickBmsPath(const QString& path);
|
||||
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
|
||||
bool debugLoggingEnabled() const;
|
||||
void setDebugLoggingEnabled(bool enable);
|
||||
@ -97,6 +104,19 @@ public:
|
||||
bool imageAutoZoom() const;
|
||||
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
|
||||
QByteArray windowGeometry() const;
|
||||
void setWindowGeometry(const QByteArray& geometry);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user