XPlor/app/mainwindow.cpp

1617 lines
63 KiB
C++
Raw Normal View History

#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"
#include "textviewerwidget.h"
#include "listpreviewwidget.h"
// Debug logging is controlled via Settings -> LogManager::debug()
#include <QFileDialog>
#include <QStandardPaths>
#include <QMessageBox>
#include <QFrame>
#include <QDebug>
#include <QTableWidgetItem>
#include <QTreeWidgetItem>
#include <QDockWidget>
#include <QPlainTextEdit>
#include <QMimeData>
#include <QProgressBar>
#include <QProgressDialog>
#include <QApplication>
#include <QtMath>
#include <QtEndian>
#include <QDirIterator>
#include <QInputDialog>
#include <QClipboard>
#include <QDesktopServices>
#include <QUrl>
#include <QRegularExpression>
#include <QImage>
#include <QTimer>
#include <algorithm>
// 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<QRgb*>(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<float>(r) / 255.0f;
QColor newColor = accentColor;
newColor.setRed(static_cast<int>(accentColor.red() * intensity));
newColor.setGreen(static_cast<int>(accentColor.green() * intensity));
newColor.setBlue(static_cast<int>(accentColor.blue() * intensity));
newColor.setAlpha(a);
line[x] = newColor.rgba();
}
}
}
return QIcon(QPixmap::fromImage(image));
}
// Check if data looks like RCB pixel format (Avatar g4rc DXT1 texture)
static bool looksLikeRCBPixel(const QByteArray &data) {
if (data.size() < 0x28) return false;
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
// RCB pixel has format byte 0x52 at offset 0x09
// and data size at offset 0x10 (big-endian)
uchar formatByte = d[0x09];
if (formatByte != 0x52) return false;
quint32 rcbDataSize = (d[0x10] << 24) | (d[0x11] << 16) | (d[0x12] << 8) | d[0x13];
// Check if header + data size fits in the buffer
return rcbDataSize > 0 && rcbDataSize + 0x24 <= (quint32)data.size();
}
// Check if data looks like a known image format
static bool looksLikeImage(const QByteArray &data) {
if (data.size() < 8) return false;
// PNG
if (data.startsWith("\x89PNG")) return true;
// JPEG
if (data.startsWith("\xFF\xD8\xFF")) return true;
// BMP
if (data.startsWith("BM")) return true;
// DDS
if (data.startsWith("DDS ")) return true;
// TGA - check for valid image type
if (data.size() >= 18) {
uchar imageType = static_cast<uchar>(data[2]);
if (imageType == 2 || imageType == 10) return true; // Uncompressed/RLE true-color
}
// RCB pixel format (Avatar textures)
if (looksLikeRCBPixel(data)) return true;
return false;
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, mTreeBuilder(nullptr, mTypeRegistry) // Placeholder, will reset after mTreeWidget created
{
ui->setupUi(this);
setAcceptDrops(true);
// 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);
// Initialize tree builder with the actual tree widget
mTreeBuilder = TreeBuilder(mTreeWidget, mTypeRegistry);
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);
// Flush any buffered log entries from before the signal was connected
LogManager::instance().flushBufferedEntries();
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 QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
// Use _type from parsed data (set by parse_here delegation) over tree item's stored type
const QString storedType = item->data(0, Qt::UserRole + 2).toString();
const QString typeName = DslKeys::contains(vars, DslKey::Type) ? DslKeys::getString(vars, DslKey::Type) : storedType;
// Prevent dup tabs
const QString tabTitle = item->text(0);
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)
LogManager::instance().addEntry(QString("[VIEWER] Checking item '%1', has _preview: %2, has data: %3")
.arg(tabTitle)
.arg(DslKeys::contains(vars, DslKey::Preview))
.arg(vars.contains("data")));
if (DslKeys::contains(vars, DslKey::Preview)) {
const QVariantMap preview = DslKeys::get(vars, DslKey::Preview).toMap();
const QString filename = preview.value("filename").toString();
const QByteArray data = preview.value("data").toByteArray();
const QString lowerFilename = filename.toLower();
LogManager::instance().addEntry(QString("[VIEWER] Preview filename='%1', data size=%2")
.arg(filename)
.arg(data.size()));
if (!data.isEmpty()) {
// Get file extension
QFileInfo fileInfo(filename);
QString extension = fileInfo.suffix();
// Check if script specified a viewer type via set_viewer()
QString viewerType;
if (DslKeys::contains(vars, DslKey::Viewer)) {
viewerType = DslKeys::getString(vars, DslKey::Viewer).toLower();
LogManager::instance().addEntry(QString("[VIEWER] Using script-defined viewer: '%1'")
.arg(viewerType));
} else if (preview.contains("viewer")) {
viewerType = preview.value("viewer").toString().toLower();
LogManager::instance().addEntry(QString("[VIEWER] Using preview-defined viewer: '%1'")
.arg(viewerType));
} else {
// Fall back to settings-based viewer type
viewerType = Settings::instance().viewerForExtension(extension);
LogManager::instance().addEntry(QString("[VIEWER] Extension='%1', viewerType='%2'")
.arg(extension)
.arg(viewerType));
}
// Collect visible metadata based on UI schema
QVariantMap metadata = mTypeRegistry.filterVisibleFields(vars, typeName);
if (viewerType == "audio") {
// Audio preview widget
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
audioWidget->setProperty("PARENT_NAME", tabTitle);
audioWidget->loadFromData(data, filename);
audioWidget->setMetadata(metadata);
ui->tabWidget->addTab(audioWidget, tabTitle);
ui->tabWidget->setCurrentWidget(audioWidget);
return;
} else if (viewerType == "image") {
// 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);
imageWidget->setMetadata(metadata);
ui->tabWidget->addTab(imageWidget, tabTitle);
ui->tabWidget->setCurrentWidget(imageWidget);
return;
} else if (viewerType == "text") {
// Text viewer widget
auto* textWidget = new TextViewerWidget(ui->tabWidget);
textWidget->setProperty("PARENT_NAME", tabTitle);
textWidget->setData(data, filename);
textWidget->setMetadata(metadata);
ui->tabWidget->addTab(textWidget, tabTitle);
ui->tabWidget->setCurrentWidget(textWidget);
return;
} else if (viewerType == "list") {
// List preview widget - parse as string table
auto* listWidget = new ListPreviewWidget(ui->tabWidget);
listWidget->setProperty("PARENT_NAME", tabTitle);
// Parse data as null-terminated strings
QVariantList items;
int start = 0;
for (int i = 0; i < data.size(); ++i) {
if (data[i] == '\0' && i > start) {
QString str = QString::fromUtf8(data.mid(start, i - start));
if (!str.isEmpty()) {
QVariantMap item;
item["string"] = str;
item["offset"] = start;
items.append(item);
}
start = i + 1;
}
}
listWidget->setListData(items, filename);
listWidget->setMetadata(metadata);
ui->tabWidget->addTab(listWidget, tabTitle);
ui->tabWidget->setCurrentWidget(listWidget);
return;
} else {
// Hex viewer for unknown file types (default)
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
hexWidget->setProperty("PARENT_NAME", tabTitle);
hexWidget->setData(data, filename);
hexWidget->setMetadata(metadata);
ui->tabWidget->addTab(hexWidget, tabTitle);
ui->tabWidget->setCurrentWidget(hexWidget);
return;
}
}
}
// Check if this is a chunk with binary data
if (vars.contains("data") && vars.value("data").typeId() == QMetaType::QByteArray) {
QByteArray chunkData = vars.value("data").toByteArray();
if (!chunkData.isEmpty()) {
// Get extension from tab title (often includes filename)
QFileInfo fileInfo(tabTitle);
QString extension = fileInfo.suffix();
// Check if script specified a viewer type via set_viewer()
QString viewerType;
if (DslKeys::contains(vars, DslKey::Viewer)) {
viewerType = DslKeys::getString(vars, DslKey::Viewer).toLower();
LogManager::instance().addEntry(QString("[VIEWER] Using script-defined viewer: '%1'")
.arg(viewerType));
} else {
viewerType = Settings::instance().viewerForExtension(extension);
}
// Collect visible metadata based on UI schema
QVariantMap metadata = mTypeRegistry.filterVisibleFields(vars, typeName);
metadata.remove("data"); // Exclude raw binary data from display
if (viewerType == "text") {
auto* textWidget = new TextViewerWidget(ui->tabWidget);
textWidget->setProperty("PARENT_NAME", tabTitle);
textWidget->setData(chunkData, tabTitle);
textWidget->setMetadata(metadata);
ui->tabWidget->addTab(textWidget, tabTitle);
ui->tabWidget->setCurrentWidget(textWidget);
return;
} else if (viewerType == "image") {
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
imageWidget->setProperty("PARENT_NAME", tabTitle);
imageWidget->setFilename(tabTitle);
imageWidget->loadFromData(chunkData);
imageWidget->setMetadata(metadata);
ui->tabWidget->addTab(imageWidget, tabTitle);
ui->tabWidget->setCurrentWidget(imageWidget);
return;
} else if (viewerType == "audio") {
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
audioWidget->setProperty("PARENT_NAME", tabTitle);
audioWidget->loadFromData(chunkData, tabTitle);
audioWidget->setMetadata(metadata);
ui->tabWidget->addTab(audioWidget, tabTitle);
ui->tabWidget->setCurrentWidget(audioWidget);
return;
} else if (viewerType == "list") {
// List preview widget - parse as string table
auto* listWidget = new ListPreviewWidget(ui->tabWidget);
listWidget->setProperty("PARENT_NAME", tabTitle);
// Parse chunkData as null-terminated strings
QVariantList items;
int start = 0;
for (int i = 0; i < chunkData.size(); ++i) {
if (chunkData[i] == '\0' && i > start) {
QString str = QString::fromUtf8(chunkData.mid(start, i - start));
if (!str.isEmpty()) {
QVariantMap item;
item["string"] = str;
item["offset"] = start;
items.append(item);
}
start = i + 1;
}
}
listWidget->setListData(items, tabTitle);
listWidget->setMetadata(metadata);
ui->tabWidget->addTab(listWidget, tabTitle);
ui->tabWidget->setCurrentWidget(listWidget);
return;
} else {
// Try image detection by content for unknown extensions
if (looksLikeImage(chunkData)) {
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
imageWidget->setProperty("PARENT_NAME", tabTitle);
imageWidget->setFilename(tabTitle);
imageWidget->loadFromData(chunkData);
imageWidget->setMetadata(metadata);
ui->tabWidget->addTab(imageWidget, tabTitle);
ui->tabWidget->setCurrentWidget(imageWidget);
return;
}
// Default to hex viewer
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
hexWidget->setProperty("PARENT_NAME", tabTitle);
hexWidget->setData(chunkData, tabTitle);
hexWidget->setMetadata(metadata);
ui->tabWidget->addTab(hexWidget, tabTitle);
ui->tabWidget->setCurrentWidget(hexWidget);
return;
}
}
}
const TypeDef& td = mTypeRegistry.module().types[typeName];
const auto schema = buildUiSchemaForType(td, &mTypeRegistry.module());
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;
}
}
});
ui->toolBar->setWindowTitle("Tool Bar");
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<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.setWindowTitle("XPlor - Parsing");
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
bool cancelled = false;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
[this, &progress, &cancelled](qint64 pos, qint64 size) {
// Only update progress if position is moving forward (avoid jumps from seek)
if (pos > progress.value()) {
progress.setValue(static_cast<int>(pos));
}
QApplication::processEvents();
if (progress.wasCanceled()) {
cancelled = true;
throw std::runtime_error("Parsing cancelled by user");
}
},
[this, &progress, &cancelled](const QString& status) {
progress.setLabelText(status);
QApplication::processEvents();
if (progress.wasCanceled()) {
cancelled = true;
throw std::runtime_error("Parsing cancelled by user");
}
});
} catch (const std::exception& e) {
if (cancelled) {
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
continue;
}
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
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 = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
progress.setLabelText(QString("Organizing %1...").arg(fileName));
QApplication::processEvents();
mTreeBuilder.organizeChildrenByExtension(rootInst);
mTreeBuilder.updateNodeCounts(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<QString, QString> 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 = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
mTreeBuilder.organizeChildrenByExtension(rootInst);
mTreeBuilder.updateNodeCounts(cat);
cat->setExpanded(false);
}
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<XTreeWidgetItem*>(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();
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
// Build a simple text representation with only visible fields
QStringList lines;
lines.append(QString("Name: %1").arg(text));
lines.append(QString("Type: %1").arg(typeName));
const QVariantMap visible = mTypeRegistry.filterVisibleFields(vars, typeName);
for (auto it = visible.begin(); it != visible.end(); ++it) {
lines.append(QString("%1: %2").arg(it.key(), it.value().toString()));
}
text = lines.join("\n");
}
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<QTreeWidgetItem*> 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<int>(fileSize), this);
progress.setWindowTitle("XPlor - Reparsing");
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
bool cancelled = false;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
[this, &progress, &cancelled](qint64 pos, qint64 size) {
if (pos > progress.value()) {
progress.setValue(static_cast<int>(pos));
}
QApplication::processEvents();
if (progress.wasCanceled()) {
cancelled = true;
throw std::runtime_error("Parsing cancelled by user");
}
},
[this, &progress, &cancelled](const QString& status) {
progress.setLabelText(status);
QApplication::processEvents();
if (progress.wasCanceled()) {
cancelled = true;
throw std::runtime_error("Parsing cancelled by user");
}
});
} catch (const std::exception& e) {
if (cancelled) {
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
continue;
}
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
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 = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
progress.setLabelText(QString("Organizing %1...").arg(fileName));
QApplication::processEvents();
mTreeBuilder.organizeChildrenByExtension(rootInst);
mTreeBuilder.updateNodeCounts(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;
}
void MainWindow::setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& 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);
cat->setData(0, Qt::UserRole, typeName);
mTreeWidget->addTopLevelItem(cat);
}
}
void MainWindow::Reset() {
// Clear the tree widget
mTreeWidget->clear();
// Clear category roots in tree builder
mTreeBuilder.reset();
// 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.setWindowTitle("XPlor - Parsing");
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(500);
QVariantMap rootVars;
bool cancelled = false;
try {
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
[this, &progress, &cancelled](qint64 pos, qint64 size) {
if (pos > progress.value()) {
progress.setValue(static_cast<int>(pos));
}
QApplication::processEvents();
if (progress.wasCanceled()) {
cancelled = true;
throw std::runtime_error("Parsing cancelled by user");
}
},
[this, &progress, &cancelled](const QString& status) {
progress.setLabelText(status);
QApplication::processEvents();
if (progress.wasCanceled()) {
cancelled = true;
throw std::runtime_error("Parsing cancelled by user");
}
});
} catch (const std::exception& e) {
if (cancelled) {
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
continue;
}
LogManager::instance().addLine();
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
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 = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
// Add instance under category (FastFiles -> test.ff)
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
// Route nested objects as subcategories under the instance (test.ff -> ZoneFiles -> ...)
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
progress.setLabelText(QString("Organizing %1...").arg(fileName));
QApplication::processEvents();
mTreeBuilder.organizeChildrenByExtension(rootInst);
mTreeBuilder.updateNodeCounts(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);
}
}