#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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 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(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(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(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(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 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(""); } 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_) 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(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(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(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); } }