#include "mainwindow.h" #include "ui_mainwindow.h" #include "aboutdialog.h" #include "audiopreviewwidget.h" #include "definitionviewer.h" #include "hexviewerwidget.h" #include "preferenceeditor.h" #include "reportissuedialog.h" #include "settings.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" // Debug logging is controlled via Settings -> LogManager::debug() #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Generate themed app icon by replacing red with accent color static QIcon generateThemedIcon(const QColor &accentColor) { QImage image(":/images/images/XPlor.png"); if (image.isNull()) { return QIcon(); } image = image.convertToFormat(QImage::Format_ARGB32); for (int y = 0; y < image.height(); y++) { QRgb *line = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { QColor pixel(line[x]); int r = pixel.red(); int g = pixel.green(); int b = pixel.blue(); int a = pixel.alpha(); // Detect red-ish pixels (high red, low green/blue) if (r > 100 && g < 80 && b < 80 && a > 0) { float intensity = static_cast(r) / 255.0f; QColor newColor = accentColor; newColor.setRed(static_cast(accentColor.red() * intensity)); newColor.setGreen(static_cast(accentColor.green() * intensity)); newColor.setBlue(static_cast(accentColor.blue() * intensity)); newColor.setAlpha(a); line[x] = newColor.rgba(); } } } 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; } // Sort tree widget children using natural sort order static void naturalSortChildren(QTreeWidgetItem* parent) { if (!parent || parent->childCount() == 0) return; // Take all children QList 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); } } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); setAcceptDrops(true); // Connect to theme changes connect(&Settings::instance(), &Settings::themeChanged, this, &MainWindow::applyTheme); // Create ribbon at absolute top of window QWidget *menuContainer = new QWidget(this); QVBoxLayout *menuLayout = new QVBoxLayout(menuContainer); menuLayout->setContentsMargins(0, 0, 0, 0); menuLayout->setSpacing(0); // Accent ribbon stripe mRibbon = new QFrame(menuContainer); mRibbon->setFixedHeight(4); menuLayout->addWidget(mRibbon); // Move the menu bar into our container menuLayout->addWidget(menuBar()); setMenuWidget(menuContainer); // Apply initial theme from settings applyTheme(Settings::instance().theme()); mTreeWidget = new XTreeWidget(this); mTreeWidget->setColumnCount(1); 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 (resource 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(); const QString lowerFilename = filename.toLower(); 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"); bool isAudio = lowerFilename.endsWith(".wav") || lowerFilename.endsWith(".wave"); if (isAudio) { // 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) { // Image preview widget auto* imageWidget = new ImagePreviewWidget(ui->tabWidget); imageWidget->setProperty("PARENT_NAME", tabTitle); imageWidget->setFilename(filename); // Determine format from filename QString format; if (lowerFilename.endsWith(".tga")) format = "TGA"; else if (lowerFilename.endsWith(".dds")) format = "DDS"; else if (lowerFilename.endsWith(".png")) format = "PNG"; 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 { // Hex viewer for unknown file types 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; } } } // Check if this is a chunk with binary data (display in hex viewer) 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); // 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(); } } 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); 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); actionViewDefinitions = new QAction(QIcon::fromTheme("document-properties"), "View Definitions...", 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"); // Disable unimplemented actions actionNew->setEnabled(false); actionOpenFolder->setEnabled(false); actionSave->setEnabled(false); actionSaveAs->setEnabled(false); actionUndo->setEnabled(false); actionRedo->setEnabled(false); actionCut->setEnabled(false); actionCopy->setEnabled(false); actionPaste->setEnabled(false); actionRename->setEnabled(false); actionDelete->setEnabled(false); actionFind->setEnabled(false); actionClearUndoHistory->setEnabled(false); actionRunTests->setEnabled(false); // ========== 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->menuTools->addAction(actionViewDefinitions); 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); // ========== Set Keyboard Shortcuts ========== actionNew->setShortcut(QKeySequence::New); actionOpen->setShortcut(QKeySequence::Open); actionSave->setShortcut(QKeySequence::Save); actionSaveAs->setShortcut(QKeySequence::SaveAs); actionUndo->setShortcut(QKeySequence::Undo); actionRedo->setShortcut(QKeySequence::Redo); actionCut->setShortcut(QKeySequence::Cut); actionCopy->setShortcut(QKeySequence::Copy); actionPaste->setShortcut(QKeySequence::Paste); actionDelete->setShortcut(QKeySequence::Delete); actionFind->setShortcut(QKeySequence::Find); actionReparse->setShortcut(QKeySequence(Qt::Key_F5)); // ========== Connect Action Signals ========== // File menu - New connect(actionNew, &QAction::triggered, this, [this]() { if (!mOpenedFilePaths.isEmpty()) { QMessageBox::StandardButton reply = QMessageBox::question( this, "New Session", "This will close all open files. Continue?", QMessageBox::Yes | QMessageBox::No); if (reply != QMessageBox::Yes) return; } Reset(); mOpenedFilePaths.clear(); statusBar()->showMessage("New session started", 3000); }); 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.setWindowTitle("XPlor - Parsing"); 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(); }, [this, &progress](const QString& status) { progress.setLabelText(status); 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); } // Update progress for tree building phase progress.setLabelText(QString("Building tree for %1...").arg(fileName)); progress.setRange(0, 0); // Indeterminate mode QApplication::processEvents(); XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType); auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars); routeNestedObjects(rootInst, rootVars); progress.setLabelText(QString("Organizing %1...").arg(fileName)); QApplication::processEvents(); organizeChildrenByExtension(rootInst); updateTreeNodeCounts(cat); cat->setExpanded(false); mTreeWidget->setCurrentItem(rootInst); } }); connect(actionOpenFolder, &QAction::triggered, this, [this]() { QString dirPath = QFileDialog::getExistingDirectory( this, "Open Folder", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); if (dirPath.isEmpty()) return; // Get supported extensions QMap exts = mTypeRegistry.supportedExtensions(); QStringList extPatterns; for (const QString& ext : exts.keys()) { extPatterns.append("*." + ext); } // Find all matching files QDirIterator it(dirPath, extPatterns, QDir::Files, QDirIterator::Subdirectories); QStringList filesToOpen; while (it.hasNext()) { filesToOpen.append(it.next()); } if (filesToOpen.isEmpty()) { QMessageBox::information(this, "Open Folder", QString("No supported files found in:\n%1").arg(dirPath)); return; } // Confirm if many files if (filesToOpen.size() > 10) { QMessageBox::StandardButton reply = QMessageBox::question( this, "Open Folder", QString("Found %1 files. Open all?").arg(filesToOpen.size()), QMessageBox::Yes | QMessageBox::No); if (reply != QMessageBox::Yes) return; } // Open each file (reuse Open logic) for (const QString& path : filesToOpen) { const QString fileName = QFileInfo(path).fileName(); QFile inputFile(path); if (!inputFile.open(QIODevice::ReadOnly)) continue; const QString rootType = mTypeRegistry.chooseType(&inputFile, path); if (rootType.isEmpty()) continue; inputFile.seek(0); QVariantMap rootVars; try { rootVars = mTypeRegistry.parse(rootType, &inputFile, path, nullptr); } catch (...) { continue; } if (!mOpenedFilePaths.contains(path)) { mOpenedFilePaths.append(path); } XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType); auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars); routeNestedObjects(rootInst, rootVars); organizeChildrenByExtension(rootInst); updateTreeNodeCounts(cat); cat->setExpanded(false); } statusBar()->showMessage(QString("Opened %1 files from folder").arg(filesToOpen.size()), 3000); }); connect(actionSave, &QAction::triggered, this, [this]() { // Save is not yet implemented - would need to serialize back to binary QMessageBox::information(this, "Save", "Save functionality is not yet implemented.\n\n" "XPlor currently operates in read-only mode."); }); connect(actionSaveAs, &QAction::triggered, this, [this]() { QMessageBox::information(this, "Save As", "Save As functionality is not yet implemented.\n\n" "XPlor currently operates in read-only mode."); }); // Edit menu connections connect(actionUndo, &QAction::triggered, this, [this]() { // Check if current tab has undo capability QWidget* current = ui->tabWidget->currentWidget(); if (!current) { statusBar()->showMessage("Nothing to undo", 2000); return; } // For now, editing is not supported statusBar()->showMessage("Undo not available - read-only mode", 2000); }); connect(actionRedo, &QAction::triggered, this, [this]() { statusBar()->showMessage("Redo not available - read-only mode", 2000); }); connect(actionCut, &QAction::triggered, this, [this]() { statusBar()->showMessage("Cut not available - read-only mode", 2000); }); connect(actionCopy, &QAction::triggered, this, [this]() { // Copy selected tree item info to clipboard auto* item = static_cast(mTreeWidget->currentItem()); if (!item) { statusBar()->showMessage("Nothing selected to copy", 2000); return; } QString text = item->text(0); const QString kind = item->data(0, Qt::UserRole + 1).toString(); if (kind == "INSTANCE") { const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap(); // Build a simple text representation 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())); } } text = lines.join("\n"); } QApplication::clipboard()->setText(text); statusBar()->showMessage("Copied to clipboard", 2000); }); connect(actionPaste, &QAction::triggered, this, [this]() { statusBar()->showMessage("Paste not available - read-only mode", 2000); }); connect(actionRename, &QAction::triggered, this, [this]() { statusBar()->showMessage("Rename not available - read-only mode", 2000); }); connect(actionDelete, &QAction::triggered, this, [this]() { statusBar()->showMessage("Delete not available - read-only mode", 2000); }); connect(actionFind, &QAction::triggered, this, [this]() { bool ok; QString searchText = QInputDialog::getText(this, "Find", "Search for:", QLineEdit::Normal, QString(), &ok); if (!ok || searchText.isEmpty()) return; // Search through tree items QList matches = mTreeWidget->findItems( searchText, Qt::MatchContains | Qt::MatchRecursive, 0); if (matches.isEmpty()) { QMessageBox::information(this, "Find", QString("No matches found for '%1'").arg(searchText)); return; } // Select first match and expand parents QTreeWidgetItem* first = matches.first(); mTreeWidget->setCurrentItem(first); mTreeWidget->scrollToItem(first); // Expand all parents QTreeWidgetItem* parent = first->parent(); while (parent) { parent->setExpanded(false); parent = parent->parent(); } statusBar()->showMessage(QString("Found %1 match(es)").arg(matches.size()), 3000); }); connect(actionClearUndoHistory, &QAction::triggered, this, [this]() { statusBar()->showMessage("Undo history cleared (no history in read-only mode)", 2000); }); 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, [this]() { // Run validation on all loaded definitions QStringList errors = mTypeRegistry.validateTypeReferences(); if (errors.isEmpty()) { QMessageBox::information(this, "Run Tests", QString("All tests passed!\n\n" "Loaded types: %1\n" "Supported extensions: %2") .arg(mTypeRegistry.typeNames().size()) .arg(mTypeRegistry.supportedExtensions().size())); } else { QString errorList = errors.join("\n"); QMessageBox::warning(this, "Run Tests", QString("Found %1 error(s):\n\n%2").arg(errors.size()).arg(errorList)); } }); connect(actionViewDefinitions, &QAction::triggered, this, [this]() { DefinitionViewer viewer(mDefinitionResults, this); viewer.exec(); }); // Help menu connections connect(actionAbout, &QAction::triggered, this, [this]() { AboutDialog *aboutDialog = new AboutDialog(this); aboutDialog->exec(); delete aboutDialog; }); connect(actionCheckForUpdates, &QAction::triggered, this, [this]() { // Open releases page in browser QDesktopServices::openUrl(QUrl("https://code.redline.llc/njohnson/XPlor/releases")); statusBar()->showMessage("Opening releases page in browser...", 3000); }); connect(actionReportIssue, &QAction::triggered, this, [this]() { ReportIssueDialog dialog(this); dialog.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.setWindowTitle("XPlor - Reparsing"); 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(); }, [this, &progress](const QString& status) { progress.setLabelText(status); 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); // Update progress for tree building phase progress.setLabelText(QString("Building tree for %1...").arg(fileName)); progress.setRange(0, 0); // Indeterminate mode QApplication::processEvents(); // Rebuild tree XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType); auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars); routeNestedObjects(rootInst, rootVars); progress.setLabelText(QString("Organizing %1...").arg(fileName)); QApplication::processEvents(); organizeChildrenByExtension(rootInst); updateTreeNodeCounts(cat); cat->setExpanded(false); } LogManager::instance().addLine(); LogManager::instance().addEntry("[REPARSE] Reparse complete!"); statusBar()->showMessage("Reparse complete!", 3000); }); Reset(); // LoadDefinitions is now called from main.cpp during splash screen //LoadTreeCategories(); // Check for QuickBMS after window is shown QTimer::singleShot(500, this, [this]() { QString quickBmsPath = Settings::instance().quickBmsPath(); if (quickBmsPath.isEmpty()) { // QuickBMS not found - prompt user QMessageBox::StandardButton reply = QMessageBox::question( this, "QuickBMS Not Found", "QuickBMS is required for decompressing certain Xbox 360 formats.\n\n" "Would you like to locate quickbms.exe now?\n\n" "(You can also configure this later in Edit > Preferences > Tools)", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes ); if (reply == QMessageBox::Yes) { QString file = QFileDialog::getOpenFileName( this, "Locate QuickBMS Executable", QDir::homePath(), "QuickBMS (quickbms.exe);;All Files (*.*)" ); if (!file.isEmpty()) { Settings::instance().setQuickBmsPath(file); Compression::setQuickBmsPath(file); statusBar()->showMessage("QuickBMS configured: " + file, 5000); } } } }); } 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& results) { mTypeRegistry = std::move(registry); mDefinitionResults = results; // 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::LoadDefinitions() { // Used for reparse - reloads all definitions mDefinitionResults.clear(); mTypeRegistry = TypeRegistry(); 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)); mDefinitionResults.append({path, fileName, false, "Failed to open file"}); continue; } try { mTypeRegistry.ingestScript(QString::fromUtf8(f.readAll()), path); LogManager::instance().addEntry(QString("[DEF] Loaded definition: %1").arg(fileName)); mDefinitionResults.append({path, fileName, true, QString()}); } catch (const std::exception& e) { LogManager::instance().addError(QString("[DEF] ERROR loading %1: %2").arg(fileName).arg(e.what())); mDefinitionResults.append({path, fileName, false, QString::fromUtf8(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(false); 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().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(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(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> byExtension; QList noExtension; QList nonInstanceChildren; // Take all children out first QList children; while (parent->childCount() > 0) { auto* child = static_cast(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 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& 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(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 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.setWindowTitle("XPlor - Parsing"); 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(); }, [this, &progress](const QString& status) { progress.setLabelText(status); 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); } // Update progress for tree building phase progress.setLabelText(QString("Building tree for %1...").arg(fileName)); progress.setRange(0, 0); // Indeterminate mode QApplication::processEvents(); // 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); progress.setLabelText(QString("Organizing %1...").arg(fileName)); QApplication::processEvents(); organizeChildrenByExtension(rootInst); updateTreeNodeCounts(cat); cat->setExpanded(false); mTreeWidget->setCurrentItem(rootInst); } } void MainWindow::applyTheme(const Theme &theme) { // Update ribbon color mRibbon->setStyleSheet(QString("background-color: %1;").arg(theme.accentColor)); // Apply global stylesheet with theme colors setStyleSheet(QString(R"( QMainWindow, QWidget { background-color: %1; } QMenuBar { background-color: %2; color: %3; border: none; } QMenuBar::item { background: transparent; padding: 4px 10px; } QMenuBar::item:selected { background-color: %4; color: white; } QMenu { background-color: %2; color: %3; border: 1px solid %4; } QMenu::item:selected { background-color: %5; color: white; } QToolBar { background-color: %2; border: none; border-bottom: 1px solid %4; spacing: 3px; padding: 2px; } QToolButton { background-color: transparent; border: none; padding: 4px; border-radius: 3px; } QToolButton:hover { background-color: %4; } QToolButton:pressed { background-color: %6; } QStatusBar { background-color: %2; color: %3; } QTabWidget::pane { border: 1px solid %4; background-color: %1; } QTabBar::tab { background-color: %2; color: %3; padding: 6px 12px; border: 1px solid %4; border-bottom: none; margin-right: 2px; } QTabBar::tab:selected { background-color: %1; border-top: 2px solid %5; } QTabBar::tab:hover:!selected { background-color: %4; } QTreeWidget, QTreeView { background-color: %1; color: %3; border: 1px solid %4; selection-background-color: %5; } QTreeWidget::item:selected, QTreeView::item:selected { background-color: %5; color: white; } QTreeWidget::item:hover, QTreeView::item:hover { background-color: %2; } QScrollBar:vertical { background-color: %4; width: 8px; border-radius: 4px; margin: 2px; } QScrollBar::handle:vertical { background-color: %5; min-height: 20px; border-radius: 4px; } QScrollBar::handle:vertical:hover { background-color: %7; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical, QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; height: 0; border: none; } QScrollBar:horizontal { background-color: %4; height: 8px; border-radius: 4px; margin: 2px; } QScrollBar::handle:horizontal { background-color: %5; min-width: 20px; border-radius: 4px; } QScrollBar::handle:horizontal:hover { background-color: %7; } QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; width: 0; border: none; } QProgressBar { background-color: %4; border: none; border-radius: 3px; text-align: center; color: white; } QProgressBar::chunk { background-color: %5; border-radius: 3px; } QDockWidget { titlebar-close-icon: url(close.png); titlebar-normal-icon: url(undock.png); } QDockWidget::title { background-color: %2; padding: 4px; border: none; } QPlainTextEdit, QTextEdit { background-color: %1; color: %3; border: 1px solid %4; selection-background-color: %5; } QSplitter::handle { background-color: %4; } QSplitter::handle:hover { background-color: %5; } QHeaderView::section { background-color: %2; color: %3; padding: 4px; border: 1px solid %4; } QLabel { background-color: transparent; color: %3; } QPushButton { background-color: %2; color: %3; border: 1px solid %4; padding: 4px 12px; border-radius: 3px; } QPushButton:hover { background-color: %4; } QPushButton:pressed { background-color: %6; } QPushButton:disabled { background-color: %4; color: %8; } QSlider::groove:horizontal { background-color: %4; height: 6px; border-radius: 3px; } QSlider::handle:horizontal { background-color: %5; width: 12px; margin: -3px 0; border-radius: 6px; } QSlider::handle:horizontal:hover { background-color: %7; } QSlider::sub-page:horizontal { background-color: %5; border-radius: 3px; } QFrame { background-color: transparent; } )") .arg(theme.backgroundColor) // %1 .arg(theme.panelColor) // %2 .arg(theme.textColor) // %3 .arg(theme.borderColor) // %4 .arg(theme.accentColor) // %5 .arg(theme.accentColorDark) // %6 .arg(theme.accentColorLight) // %7 .arg(theme.textColorMuted) // %8 ); // Update app icon with new accent color QIcon themedIcon = generateThemedIcon(QColor(theme.accentColor)); if (!themedIcon.isNull()) { qApp->setWindowIcon(themedIcon); } }