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:
njohnson 2026-01-11 12:09:31 -05:00
parent 57ad7c4111
commit e4c913bb06
8 changed files with 715 additions and 554 deletions

View File

@ -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

View File

@ -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();

View File

@ -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);

View File

@ -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

View File

@ -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();
}

View File

@ -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;

View File

@ -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
{

View File

@ -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);