XPlor/app/mainwindow.cpp

867 lines
33 KiB
C++
Raw Normal View History

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "aboutdialog.h"
#include "preferenceeditor.h"
#include "reportissuedialog.h"
#include "statusbarmanager.h"
#include "logmanager.h"
#include "xtreewidget.h"
#include "xtreewidgetitem.h"
#include "compression.h"
#include "scripttypeeditorwidget.h"
#include "dsluischema.h"
#include "utils.h"
#include "imagepreviewwidget.h"
#include <QFileDialog>
#include <QStandardPaths>
#include <QMessageBox>
#include <QDebug>
#include <QTableWidgetItem>
#include <QTreeWidgetItem>
#include <QDockWidget>
#include <QPlainTextEdit>
#include <QMimeData>
#include <QProgressBar>
#include <QProgressDialog>
#include <QApplication>
#include <QtMath>
#include <QtEndian>
#include <QDirIterator>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setAcceptDrops(true);
mTreeWidget = new XTreeWidget(this);
mLogWidget = new QPlainTextEdit(this);
mProgressBar = new QProgressBar(this);
mProgressBar->setMaximum(100); // Default max value
mProgressBar->setVisible(false); // Initially hidden
connect(&StatusBarManager::instance(), &StatusBarManager::statusUpdated,
this, &MainWindow::HandleStatusUpdate);
connect(&StatusBarManager::instance(), &StatusBarManager::progressUpdated,
this, &MainWindow::HandleProgressUpdate);
connect(&LogManager::instance(), &LogManager::entryAdded,
this, &MainWindow::HandleLogEntry);
statusBar()->addPermanentWidget(mProgressBar);
ui->tabWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->tabWidget, &QTabWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
if (pos.isNull())
return;
int tabIndex = ui->tabWidget->tabBar()->tabAt(pos);
QMenu *contextMenu = new QMenu(this);
QAction *closeAction = new QAction("Close");
contextMenu->addAction(closeAction);
connect(closeAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
Q_UNUSED(checked);
ui->tabWidget->removeTab(tabIndex);
});
QMenu *closeMultipleAction = new QMenu("Close Multiple Tabs");
QAction *closeAllAction = new QAction("Close All");
closeMultipleAction->addAction(closeAllAction);
connect(closeAllAction, &QAction::triggered, this, [this](bool checked) {
Q_UNUSED(checked);
ui->tabWidget->clear();
});
QAction *closeAllButAction = new QAction("Close All BUT This");
closeMultipleAction->addAction(closeAllButAction);
connect(closeAllButAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
Q_UNUSED(checked);
for (int i = 0; i < ui->tabWidget->count(); i++) {
if (i != tabIndex) {
ui->tabWidget->removeTab(i);
}
}
});
QAction *closeLeftAction = new QAction("Close All to the Left");
closeMultipleAction->addAction(closeLeftAction);
connect(closeLeftAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
Q_UNUSED(checked);
for (int i = 0; i < tabIndex; i++) {
ui->tabWidget->removeTab(i);
}
});
QAction *closeRightAction = new QAction("Close All to the Right");
closeMultipleAction->addAction(closeRightAction);
connect(closeRightAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
Q_UNUSED(checked);
for (int i = tabIndex + 1; i < ui->tabWidget->count(); i++) {
ui->tabWidget->removeTab(i);
}
});
contextMenu->addMenu(closeMultipleAction);
QPoint pt(pos);
contextMenu->exec(ui->tabWidget->mapToGlobal(pt));
delete contextMenu;
});
connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) {
ui->tabWidget->removeTab(index);
});
connect(mTreeWidget, &XTreeWidget::Cleared, this, [this]() {
ui->tabWidget->clear();
});
connect(mTreeWidget, &XTreeWidget::ItemSelected, this, [this](const QString itemText) {
Q_UNUSED(itemText);
auto* item = static_cast<XTreeWidgetItem*>(mTreeWidget->currentItem());
if (!item) return;
const QString kind = item->data(0, Qt::UserRole + 1).toString();
if (kind != "INSTANCE") {
// Clicking categories/subcategories does nothing (or you can show a summary panel)
return;
}
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
// Prevent dup tabs
const QString tabTitle = item->text(0);
for (int i = 0; i < ui->tabWidget->count(); i++) {
if (ui->tabWidget->tabText(i) == tabTitle) {
ui->tabWidget->setCurrentIndex(i);
return;
}
}
// Check if this item has a preview (image data)
if (vars.contains("_preview")) {
const QVariantMap preview = vars.value("_preview").toMap();
const QString filename = preview.value("filename").toString();
const QByteArray data = preview.value("data").toByteArray();
if (!data.isEmpty()) {
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
imageWidget->setProperty("PARENT_NAME", tabTitle);
imageWidget->setFilename(filename);
// Determine format from filename
QString format;
if (filename.toLower().endsWith(".tga")) format = "TGA";
else if (filename.toLower().endsWith(".dds")) format = "DDS";
else if (filename.toLower().endsWith(".png")) format = "PNG";
else if (filename.toLower().endsWith(".jpg") || filename.toLower().endsWith(".jpeg")) format = "JPG";
imageWidget->loadFromData(data, format);
ui->tabWidget->addTab(imageWidget, tabTitle);
ui->tabWidget->setCurrentWidget(imageWidget);
return;
}
}
const TypeDef& td = mTypeRegistry.module().types[typeName];
const auto schema = buildUiSchemaForType(td);
auto* w = new ScriptTypeEditorWidget(typeName, schema, ui->tabWidget);
w->setProperty("PARENT_NAME", tabTitle);
w->setValues(vars); // optional, but this is "generation + populate" and still no saving
ui->tabWidget->addTab(w, tabTitle);
ui->tabWidget->setCurrentWidget(w);
});
connect(mTreeWidget, &XTreeWidget::ItemClosed, this, [this](const QString itemText) {
for (int i = 0; i < ui->tabWidget->count(); i++) {
const QString parentName = ui->tabWidget->widget(i)->property("PARENT_NAME").toString();
if (parentName == itemText) {
ui->tabWidget->removeTab(i);
break;
}
}
});
QDockWidget *treeDockWidget = new QDockWidget(this);
treeDockWidget->setWidget(mTreeWidget);
treeDockWidget->setWindowTitle("Tree Browser");
addDockWidget(Qt::LeftDockWidgetArea, treeDockWidget);
QDockWidget *logDockWidget = new QDockWidget(this);
logDockWidget->setWidget(mLogWidget);
logDockWidget->setWindowTitle("Logs");
addDockWidget(Qt::BottomDockWidgetArea, logDockWidget);
// ========== Create Actions ==========
// File menu actions
actionNew = new QAction(QIcon::fromTheme("document-new"), "New", this);
actionOpen = new QAction(QIcon::fromTheme("document-open"), "Open", this);
actionOpenFolder = new QAction(QIcon::fromTheme("folder-open"), "Open Folder", this);
actionSave = new QAction(QIcon::fromTheme("document-save"), "Save", this);
actionSaveAs = new QAction(QIcon::fromTheme("document-save-as"), "Save As", this);
// Edit menu actions
actionUndo = new QAction(QIcon::fromTheme("edit-undo"), "Undo", this);
actionRedo = new QAction(QIcon::fromTheme("edit-redo"), "Redo", this);
actionCut = new QAction(QIcon::fromTheme("edit-cut"), "Cut", this);
actionCopy = new QAction(QIcon::fromTheme("edit-copy"), "Copy", this);
actionPaste = new QAction(QIcon::fromTheme("edit-paste"), "Paste", this);
actionRename = new QAction(QIcon::fromTheme("edit-rename"), "Rename", this);
actionDelete = new QAction(QIcon::fromTheme("edit-delete"), "Delete", this);
actionFind = new QAction(QIcon::fromTheme("edit-find"), "Find", this);
actionClearUndoHistory = new QAction(QIcon::fromTheme("edit-clear"), "Clear Undo History", this);
actionPreferences = new QAction(QIcon::fromTheme("preferences-system"), "Preferences...", this);
// Tools menu actions
actionRunTests = new QAction(QIcon::fromTheme("system-run"), "Run Tests", this);
// Help menu actions
actionAbout = new QAction(QIcon::fromTheme("help-about"), "About", this);
actionCheckForUpdates = new QAction(QIcon::fromTheme("system-software-update"), "Check for Updates", this);
actionReportIssue = new QAction(QIcon::fromTheme("tools-report-bug"), "Report Issue", this);
// Toolbar actions
actionReparse = new QAction(QIcon::fromTheme("view-refresh"), "Reparse", this);
actionReparse->setToolTip("Reload definitions and reparse open files");
// ========== Add Actions to Menus ==========
ui->MenuDef->addAction(actionNew);
ui->MenuDef->addSeparator();
ui->MenuDef->addAction(actionOpen);
ui->MenuDef->addAction(actionOpenFolder);
ui->MenuDef->addSeparator();
ui->MenuDef->addAction(actionSave);
ui->MenuDef->addAction(actionSaveAs);
ui->menuEdit->addAction(actionUndo);
ui->menuEdit->addAction(actionRedo);
ui->menuEdit->addSeparator();
ui->menuEdit->addAction(actionCut);
ui->menuEdit->addAction(actionCopy);
ui->menuEdit->addAction(actionPaste);
ui->menuEdit->addAction(actionRename);
ui->menuEdit->addAction(actionDelete);
ui->menuEdit->addSeparator();
ui->menuEdit->addAction(actionFind);
ui->menuEdit->addSeparator();
ui->menuEdit->addAction(actionClearUndoHistory);
ui->menuEdit->addSeparator();
ui->menuEdit->addAction(actionPreferences);
ui->menuTools->addAction(actionRunTests);
ui->menuHelp->addAction(actionAbout);
ui->menuHelp->addAction(actionCheckForUpdates);
ui->menuHelp->addAction(actionReportIssue);
// ========== Add Actions to Toolbar ==========
ui->toolBar->addAction(actionNew);
ui->toolBar->addAction(actionOpen);
ui->toolBar->addAction(actionOpenFolder);
ui->toolBar->addAction(actionSave);
ui->toolBar->addSeparator();
ui->toolBar->addAction(actionCut);
ui->toolBar->addAction(actionCopy);
ui->toolBar->addAction(actionPaste);
ui->toolBar->addSeparator();
ui->toolBar->addAction(actionFind);
ui->toolBar->addSeparator();
ui->toolBar->addAction(actionReparse);
// ========== Connect Action Signals ==========
// File menu connections (placeholder - implement as needed)
connect(actionNew, &QAction::triggered, this, []() {
// TODO: Implement New action
});
connect(actionOpen, &QAction::triggered, this, [this]() {
// Build filter string from loaded definitions
QMap<QString, QString> exts = mTypeRegistry.supportedExtensions();
QStringList filterParts;
QStringList allExts;
for (auto it = exts.begin(); it != exts.end(); ++it) {
const QString& ext = it.key();
const QString& displayName = it.value();
filterParts.append(QString("%1 (*.%2)").arg(displayName, ext));
allExts.append(QString("*.%1").arg(ext));
}
// Sort filters alphabetically by display name
filterParts.sort(Qt::CaseInsensitive);
// Add "All Supported Files" at the beginning
QString allFilter = QString("All Supported Files (%1)").arg(allExts.join(" "));
filterParts.prepend(allFilter);
// Add "All Files" at the end
filterParts.append("All Files (*)");
QString filter = filterParts.join(";;");
QStringList filePaths = QFileDialog::getOpenFileNames(
this,
"Open File",
QString(),
filter
);
for (const QString& path : filePaths) {
const QString fileName = QFileInfo(path).fileName();
QFile inputFile(path);
if (!inputFile.open(QIODevice::ReadOnly)) {
LogManager::instance().addError(QString("Failed to open: %1").arg(path));
continue;
}
LogManager::instance().addEntry(QString("[FILE] Analyzing: %1").arg(fileName));
const QString rootType = mTypeRegistry.chooseType(&inputFile, path);
LogManager::instance().addEntry(QString("[FILE] Matched type '%1' for file: %2").arg(rootType).arg(fileName));
if (rootType.isEmpty()) {
LogManager::instance().addError(QString("No matching definition for: %1").arg(path));
continue;
}
inputFile.seek(0);
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== PARSING %1 ==========").arg(fileName));
LogManager::instance().addLine();
const qint64 fileSize = inputFile.size();
QProgressDialog progress(QString("Parsing %1...").arg(fileName), "Cancel", 0, static_cast<int>(fileSize), this);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path, [this, &progress](qint64 pos, qint64 size) {
progress.setValue(static_cast<int>(pos));
QApplication::processEvents();
});
} catch (const std::exception& e) {
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
LogManager::instance().addError(QString("File position: 0x%1").arg(inputFile.pos(), 0, 16));
LogManager::instance().addLine();
QMessageBox::critical(this, "Parse Error",
QString("Failed to parse %1:\n\n%2").arg(fileName).arg(e.what()));
continue;
}
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== FINISHED %1 ==========").arg(fileName));
LogManager::instance().addLine();
if (!mOpenedFilePaths.contains(path)) {
mOpenedFilePaths.append(path);
}
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
routeNestedObjects(rootInst, rootVars);
cat->setExpanded(true);
mTreeWidget->setCurrentItem(rootInst);
}
});
connect(actionOpenFolder, &QAction::triggered, this, []() {
// TODO: Implement Open Folder action
});
connect(actionSave, &QAction::triggered, this, []() {
// TODO: Implement Save action
});
connect(actionSaveAs, &QAction::triggered, this, []() {
// TODO: Implement Save As action
});
// Edit menu connections
connect(actionPreferences, &QAction::triggered, this, [this]() {
PreferenceEditor *prefEditor = new PreferenceEditor(this);
prefEditor->exec();
prefEditor->close();
delete prefEditor;
});
// Tools menu connections
connect(actionRunTests, &QAction::triggered, this, []() {
// TODO: Implement Run Tests action
});
// Help menu connections
connect(actionAbout, &QAction::triggered, this, [this]() {
AboutDialog *aboutDialog = new AboutDialog(this);
aboutDialog->exec();
delete aboutDialog;
});
connect(actionReportIssue, &QAction::triggered, this, [this]() {
ReportIssueDialog issueDialog("https://git.redline.llc", "njohnson", "XPlor",
"4738c4d2efd123efac1506c68c59b285c646df9f", this);
issueDialog.exec();
});
// Reparse action connection
connect(actionReparse, &QAction::triggered, this, [this]() {
if (mOpenedFilePaths.isEmpty()) {
QMessageBox::information(this, "Reparse", "No files to reparse. Drag and drop files first.");
return;
}
LogManager::instance().addLine();
LogManager::instance().addEntry("[REPARSE] Reloading definitions and reparsing files...");
// Store file paths before clearing
QStringList filesToReparse = mOpenedFilePaths;
// Clear everything
Reset();
mOpenedFilePaths.clear();
// Clear and reload type registry
mTypeRegistry = TypeRegistry();
LoadDefinitions();
LogManager::instance().addEntry(QString("[REPARSE] Definitions reloaded, reparsing %1 file(s)...").arg(filesToReparse.size()));
// Reparse each file
for (const QString& path : filesToReparse) {
QFile inputFile(path);
if (!inputFile.open(QIODevice::ReadOnly)) {
LogManager::instance().addError(QString("[REPARSE] Failed to open: %1").arg(path));
continue;
}
const QString fileName = QFileInfo(path).fileName();
LogManager::instance().addEntry(QString("[FILE] Analyzing: %1").arg(fileName));
const QString rootType = mTypeRegistry.chooseType(&inputFile, path);
LogManager::instance().addEntry(QString("[FILE] Matched type '%1' for file: %2").arg(rootType).arg(fileName));
if (rootType.isEmpty()) {
LogManager::instance().addError(QString("No matching definition for: %1").arg(path));
continue;
}
inputFile.seek(0);
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== PARSING %1 ==========").arg(fileName));
LogManager::instance().addLine();
const qint64 fileSize = inputFile.size();
QProgressDialog progress(QString("Parsing %1...").arg(fileName), "Cancel", 0, static_cast<int>(fileSize), this);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path, [this, &progress](qint64 pos, qint64 size) {
progress.setValue(static_cast<int>(pos));
QApplication::processEvents();
});
} catch (const std::exception& e) {
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
LogManager::instance().addError(QString("File position: 0x%1").arg(inputFile.pos(), 0, 16));
LogManager::instance().addLine();
continue;
}
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== FINISHED %1 ==========").arg(fileName));
LogManager::instance().addLine();
// Track this file again
mOpenedFilePaths.append(path);
// Rebuild tree
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
routeNestedObjects(rootInst, rootVars);
cat->setExpanded(true);
}
LogManager::instance().addLine();
LogManager::instance().addEntry("[REPARSE] Reparse complete!");
statusBar()->showMessage("Reparse complete!", 3000);
});
Reset();
LoadDefinitions();
//LoadTreeCategories();
}
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::LoadDefinitions()
{
const QString definitionsDir = QCoreApplication::applicationDirPath() + "/definitions/";
QDirIterator it(definitionsDir, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
const QString path = it.next();
const QString fileName = QFileInfo(path).fileName();
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
LogManager::instance().addError(QString("[DEF] Failed to open definition file: %1").arg(fileName));
continue;
}
try {
mTypeRegistry.ingestScript(QString::fromUtf8(f.readAll()), path);
LogManager::instance().addEntry(QString("[DEF] Loaded definition: %1").arg(fileName));
} catch (const std::exception& e) {
LogManager::instance().addError(QString("[DEF] ERROR loading %1: %2").arg(fileName).arg(e.what()));
}
}
// Validate all type references after loading
QStringList refErrors = mTypeRegistry.validateTypeReferences();
for (const QString& err : refErrors) {
LogManager::instance().addError(QString("[DEF] %1").arg(err));
}
if (!refErrors.isEmpty())
{
LogManager::instance().addError(QString("[DEF] Found %1 invalid type reference(s)").arg(refErrors.size()));
}
}
void MainWindow::LoadTreeCategories()
{
const Module& mod = mTypeRegistry.module();
for (const QString& typeName : mod.types.keys()) {
const TypeDef& td = mod.types[typeName];
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
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(true);
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().addEntry(QString("[TREE] Skipping array '%1' (has _skip_tree marker)").arg(key));
continue;
}
const QVariantList list = v.toList();
//LogManager::instance().addEntry(QString("[TREE] Processing array '%1' with %2 items")
// .arg(key)
// .arg(list.size()));
for (int i = 0; i < list.size(); i++) {
if (list[i].typeId() != QMetaType::QVariantMap) continue;
const QVariantMap child = list[i].toMap();
// Skip hidden objects
if (child.value("_hidden").toBool()) {
continue;
}
const QString childType = child.value("_type").toString();
if (childType.isEmpty()) continue;
const QString childName = child.value("_name").toString();
//LogManager::instance().addEntry(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(true);
return sub;
}
void MainWindow::Reset() {
// Clear the tree widget
mTreeWidget->clear();
// Clear category roots hash
mTypeCategoryRoots.clear();
// Clear tabs
ui->tabWidget->clear();
}
void MainWindow::HandleLogEntry(const QString &entry) {
QString logContents = mLogWidget->toPlainText() + "\n" + entry;
if (mLogWidget->toPlainText().isEmpty()) {
logContents = entry;
}
mLogWidget->setPlainText(logContents);
mLogWidget->moveCursor(QTextCursor::End);
}
void MainWindow::HandleStatusUpdate(const QString &message, int timeout) {
statusBar()->showMessage(message, timeout);
mProgressBar->setVisible(false); // Hide progress bar if just a message
}
void MainWindow::HandleProgressUpdate(const QString &message, int progress, int max) {
mProgressBar->setMaximum(max);
mProgressBar->setValue(progress);
mProgressBar->setVisible(true);
QString progressText = QString("%1 (%2/%3)").arg(message).arg(progress).arg(max);
statusBar()->showMessage(progressText);
}
void MainWindow::dragEnterEvent(QDragEnterEvent *event) {
const QMimeData *mimeData = event->mimeData();
if (mimeData->hasUrls()) {
event->acceptProposedAction();
}
}
void MainWindow::dragMoveEvent(QDragMoveEvent *event) {
Q_UNUSED(event);
}
void MainWindow::dragLeaveEvent(QDragLeaveEvent *event) {
Q_UNUSED(event);
}
void MainWindow::dropEvent(QDropEvent *event) {
const QMimeData *mimeData = event->mimeData();
if (!mimeData->hasUrls()) {
ui->statusBar->showMessage("Can't display dropped data!");
return;
}
for (const QUrl& url : mimeData->urls()) {
const QString path = url.toLocalFile();
const QString fileName = QFileInfo(path).fileName();
QFile inputFile(path);
if (!inputFile.open(QIODevice::ReadOnly))
continue;
LogManager::instance().addEntry(QString("[FILE] Analyzing: %1").arg(fileName));
// Type matching via XScript definitions
const QString rootType = mTypeRegistry.chooseType(&inputFile, path);
LogManager::instance().addEntry(QString("[FILE] Matched type '%1' for file: %2").arg(rootType).arg(fileName));
if (rootType.isEmpty()) {
LogManager::instance().addError(QString("No matching definition for: %1").arg(path));
continue;
}
inputFile.seek(0);
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== PARSING %1 ==========").arg(fileName));
LogManager::instance().addLine();
const qint64 fileSize = inputFile.size();
QProgressDialog progress(QString("Parsing %1...").arg(fileName), "Cancel", 0, static_cast<int>(fileSize), this);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path, [this, &progress](qint64 pos, qint64 size) {
progress.setValue(static_cast<int>(pos));
QApplication::processEvents();
});
} catch (const std::exception& e) {
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
LogManager::instance().addError(QString("File position: 0x%1").arg(inputFile.pos(), 0, 16));
LogManager::instance().addLine();
QMessageBox::critical(this, "Parse Error",
QString("Failed to parse %1:\n\n%2").arg(fileName).arg(e.what()));
continue;
}
LogManager::instance().addLine();
LogManager::instance().addEntry(QString("========== FINISHED %1 ==========").arg(fileName));
LogManager::instance().addLine();
// Track this file for reparsing
if (!mOpenedFilePaths.contains(path)) {
mOpenedFilePaths.append(path);
}
// Ensure top-level category exists (it should, but safe)
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
// Add instance under category (FastFiles -> test.ff)
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
// Route nested objects as subcategories under the instance (test.ff -> ZoneFiles -> ...)
routeNestedObjects(rootInst, rootVars);
cat->setExpanded(true);
mTreeWidget->setCurrentItem(rootInst);
}
}