714 lines
27 KiB
C++
714 lines
27 KiB
C++
#include "mainwindow.h"
|
|
#include "ui_mainwindow.h"
|
|
|
|
#include "aboutdialog.h"
|
|
#include "preferenceeditor.h"
|
|
#include "reportissuedialog.h"
|
|
#include "statusbarmanager.h"
|
|
#include "logmanager.h"
|
|
#include "xtreewidget.h"
|
|
#include "xtreewidgetitem.h"
|
|
#include "compression.h"
|
|
#include "scripttypeeditorwidget.h"
|
|
#include "dsluischema.h"
|
|
|
|
#include <QFileDialog>
|
|
#include <QStandardPaths>
|
|
#include <QMessageBox>
|
|
#include <QDebug>
|
|
#include <QTableWidgetItem>
|
|
#include <QTreeWidgetItem>
|
|
#include <QDockWidget>
|
|
#include <QPlainTextEdit>
|
|
#include <QMimeData>
|
|
#include <QProgressBar>
|
|
#include <QtMath>
|
|
#include <QDirIterator>
|
|
|
|
MainWindow::MainWindow(QWidget *parent)
|
|
: QMainWindow(parent)
|
|
, ui(new Ui::MainWindow)
|
|
{
|
|
ui->setupUi(this);
|
|
setAcceptDrops(true);
|
|
|
|
mTreeWidget = new XTreeWidget(this);
|
|
mLogWidget = new QPlainTextEdit(this);
|
|
|
|
mProgressBar = new QProgressBar(this);
|
|
mProgressBar->setMaximum(100); // Default max value
|
|
mProgressBar->setVisible(false); // Initially hidden
|
|
|
|
connect(&StatusBarManager::instance(), &StatusBarManager::statusUpdated,
|
|
this, &MainWindow::HandleStatusUpdate);
|
|
|
|
connect(&StatusBarManager::instance(), &StatusBarManager::progressUpdated,
|
|
this, &MainWindow::HandleProgressUpdate);
|
|
|
|
connect(&LogManager::instance(), &LogManager::entryAdded,
|
|
this, &MainWindow::HandleLogEntry);
|
|
|
|
statusBar()->addPermanentWidget(mProgressBar);
|
|
|
|
ui->tabWidget->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(ui->tabWidget, &QTabWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
if (pos.isNull())
|
|
return;
|
|
|
|
int tabIndex = ui->tabWidget->tabBar()->tabAt(pos);
|
|
QMenu *contextMenu = new QMenu(this);
|
|
|
|
QAction *closeAction = new QAction("Close");
|
|
contextMenu->addAction(closeAction);
|
|
connect(closeAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
|
|
Q_UNUSED(checked);
|
|
|
|
ui->tabWidget->removeTab(tabIndex);
|
|
});
|
|
|
|
QMenu *closeMultipleAction = new QMenu("Close Multiple Tabs");
|
|
|
|
QAction *closeAllAction = new QAction("Close All");
|
|
closeMultipleAction->addAction(closeAllAction);
|
|
connect(closeAllAction, &QAction::triggered, this, [this](bool checked) {
|
|
Q_UNUSED(checked);
|
|
|
|
ui->tabWidget->clear();
|
|
});
|
|
|
|
QAction *closeAllButAction = new QAction("Close All BUT This");
|
|
closeMultipleAction->addAction(closeAllButAction);
|
|
connect(closeAllButAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
|
|
Q_UNUSED(checked);
|
|
|
|
for (int i = 0; i < ui->tabWidget->count(); i++) {
|
|
if (i != tabIndex) {
|
|
ui->tabWidget->removeTab(i);
|
|
}
|
|
}
|
|
});
|
|
|
|
QAction *closeLeftAction = new QAction("Close All to the Left");
|
|
closeMultipleAction->addAction(closeLeftAction);
|
|
connect(closeLeftAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
|
|
Q_UNUSED(checked);
|
|
|
|
for (int i = 0; i < tabIndex; i++) {
|
|
ui->tabWidget->removeTab(i);
|
|
}
|
|
});
|
|
|
|
QAction *closeRightAction = new QAction("Close All to the Right");
|
|
closeMultipleAction->addAction(closeRightAction);
|
|
connect(closeRightAction, &QAction::triggered, this, [this, &tabIndex](bool checked) {
|
|
Q_UNUSED(checked);
|
|
|
|
for (int i = tabIndex + 1; i < ui->tabWidget->count(); i++) {
|
|
ui->tabWidget->removeTab(i);
|
|
}
|
|
});
|
|
|
|
contextMenu->addMenu(closeMultipleAction);
|
|
|
|
QPoint pt(pos);
|
|
contextMenu->exec(ui->tabWidget->mapToGlobal(pt));
|
|
|
|
delete contextMenu;
|
|
});
|
|
|
|
connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) {
|
|
ui->tabWidget->removeTab(index);
|
|
});
|
|
|
|
connect(mTreeWidget, &XTreeWidget::Cleared, this, [this]() {
|
|
ui->tabWidget->clear();
|
|
});
|
|
|
|
connect(mTreeWidget, &XTreeWidget::ItemSelected, this, [this](const QString itemText) {
|
|
Q_UNUSED(itemText);
|
|
|
|
auto* item = static_cast<XTreeWidgetItem*>(mTreeWidget->currentItem());
|
|
if (!item) return;
|
|
|
|
const QString kind = item->data(0, Qt::UserRole + 1).toString();
|
|
if (kind != "INSTANCE") {
|
|
// Clicking categories/subcategories does nothing (or you can show a summary panel)
|
|
return;
|
|
}
|
|
|
|
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
|
|
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
// Prevent dup tabs
|
|
const QString tabTitle = item->text(0);
|
|
for (int i = 0; i < ui->tabWidget->count(); i++) {
|
|
if (ui->tabWidget->tabText(i) == tabTitle) {
|
|
ui->tabWidget->setCurrentIndex(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const TypeDef& td = mTypeRegistry.module().types[typeName];
|
|
const auto schema = buildUiSchemaForType(td);
|
|
|
|
auto* w = new ScriptTypeEditorWidget(typeName, schema, ui->tabWidget);
|
|
w->setProperty("PARENT_NAME", tabTitle);
|
|
w->setValues(vars); // optional, but this is “generation + populate” and still no saving
|
|
|
|
ui->tabWidget->addTab(w, tabTitle);
|
|
ui->tabWidget->setCurrentWidget(w);
|
|
});
|
|
|
|
connect(mTreeWidget, &XTreeWidget::ItemClosed, this, [this](const QString itemText) {
|
|
for (int i = 0; i < ui->tabWidget->count(); i++) {
|
|
const QString parentName = ui->tabWidget->widget(i)->property("PARENT_NAME").toString();
|
|
if (parentName == itemText) {
|
|
ui->tabWidget->removeTab(i);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
QDockWidget *treeDockWidget = new QDockWidget(this);
|
|
treeDockWidget->setWidget(mTreeWidget);
|
|
treeDockWidget->setWindowTitle("Tree Browser");
|
|
addDockWidget(Qt::LeftDockWidgetArea, treeDockWidget);
|
|
|
|
QDockWidget *logDockWidget = new QDockWidget(this);
|
|
logDockWidget->setWidget(mLogWidget);
|
|
logDockWidget->setWindowTitle("Logs");
|
|
addDockWidget(Qt::BottomDockWidgetArea, logDockWidget);
|
|
|
|
// ========== Create Actions ==========
|
|
|
|
// File menu actions
|
|
actionNew = new QAction(QIcon::fromTheme("document-new"), "New", this);
|
|
actionOpen = new QAction(QIcon::fromTheme("document-open"), "Open", this);
|
|
actionOpenFolder = new QAction(QIcon::fromTheme("folder-open"), "Open Folder", this);
|
|
actionSave = new QAction(QIcon::fromTheme("document-save"), "Save", this);
|
|
actionSaveAs = new QAction(QIcon::fromTheme("document-save-as"), "Save As", this);
|
|
|
|
// Edit menu actions
|
|
actionUndo = new QAction(QIcon::fromTheme("edit-undo"), "Undo", this);
|
|
actionRedo = new QAction(QIcon::fromTheme("edit-redo"), "Redo", this);
|
|
actionCut = new QAction(QIcon::fromTheme("edit-cut"), "Cut", this);
|
|
actionCopy = new QAction(QIcon::fromTheme("edit-copy"), "Copy", this);
|
|
actionPaste = new QAction(QIcon::fromTheme("edit-paste"), "Paste", this);
|
|
actionRename = new QAction(QIcon::fromTheme("edit-rename"), "Rename", this);
|
|
actionDelete = new QAction(QIcon::fromTheme("edit-delete"), "Delete", this);
|
|
actionFind = new QAction(QIcon::fromTheme("edit-find"), "Find", this);
|
|
actionClearUndoHistory = new QAction(QIcon::fromTheme("edit-clear"), "Clear Undo History", this);
|
|
actionPreferences = new QAction(QIcon::fromTheme("preferences-system"), "Preferences...", this);
|
|
|
|
// Tools menu actions
|
|
actionRunTests = new QAction(QIcon::fromTheme("system-run"), "Run Tests", this);
|
|
|
|
// Help menu actions
|
|
actionAbout = new QAction(QIcon::fromTheme("help-about"), "About", this);
|
|
actionCheckForUpdates = new QAction(QIcon::fromTheme("system-software-update"), "Check for Updates", this);
|
|
actionReportIssue = new QAction(QIcon::fromTheme("tools-report-bug"), "Report Issue", this);
|
|
|
|
// Toolbar actions
|
|
actionReparse = new QAction(QIcon::fromTheme("view-refresh"), "Reparse", this);
|
|
actionReparse->setToolTip("Reload definitions and reparse open files");
|
|
|
|
// ========== Add Actions to Menus ==========
|
|
|
|
ui->MenuDef->addAction(actionNew);
|
|
ui->MenuDef->addSeparator();
|
|
ui->MenuDef->addAction(actionOpen);
|
|
ui->MenuDef->addAction(actionOpenFolder);
|
|
ui->MenuDef->addSeparator();
|
|
ui->MenuDef->addAction(actionSave);
|
|
ui->MenuDef->addAction(actionSaveAs);
|
|
|
|
ui->menuEdit->addAction(actionUndo);
|
|
ui->menuEdit->addAction(actionRedo);
|
|
ui->menuEdit->addSeparator();
|
|
ui->menuEdit->addAction(actionCut);
|
|
ui->menuEdit->addAction(actionCopy);
|
|
ui->menuEdit->addAction(actionPaste);
|
|
ui->menuEdit->addAction(actionRename);
|
|
ui->menuEdit->addAction(actionDelete);
|
|
ui->menuEdit->addSeparator();
|
|
ui->menuEdit->addAction(actionFind);
|
|
ui->menuEdit->addSeparator();
|
|
ui->menuEdit->addAction(actionClearUndoHistory);
|
|
ui->menuEdit->addSeparator();
|
|
ui->menuEdit->addAction(actionPreferences);
|
|
|
|
ui->menuTools->addAction(actionRunTests);
|
|
|
|
ui->menuHelp->addAction(actionAbout);
|
|
ui->menuHelp->addAction(actionCheckForUpdates);
|
|
ui->menuHelp->addAction(actionReportIssue);
|
|
|
|
// ========== Add Actions to Toolbar ==========
|
|
|
|
ui->toolBar->addAction(actionNew);
|
|
ui->toolBar->addAction(actionOpen);
|
|
ui->toolBar->addAction(actionOpenFolder);
|
|
ui->toolBar->addAction(actionSave);
|
|
ui->toolBar->addSeparator();
|
|
ui->toolBar->addAction(actionCut);
|
|
ui->toolBar->addAction(actionCopy);
|
|
ui->toolBar->addAction(actionPaste);
|
|
ui->toolBar->addSeparator();
|
|
ui->toolBar->addAction(actionFind);
|
|
ui->toolBar->addSeparator();
|
|
ui->toolBar->addAction(actionReparse);
|
|
|
|
// ========== Connect Action Signals ==========
|
|
|
|
// File menu connections (placeholder - implement as needed)
|
|
connect(actionNew, &QAction::triggered, this, []() {
|
|
// TODO: Implement New action
|
|
});
|
|
|
|
connect(actionOpen, &QAction::triggered, this, []() {
|
|
// TODO: Implement Open action
|
|
});
|
|
|
|
connect(actionOpenFolder, &QAction::triggered, this, []() {
|
|
// TODO: Implement Open Folder action
|
|
});
|
|
|
|
connect(actionSave, &QAction::triggered, this, []() {
|
|
// TODO: Implement Save action
|
|
});
|
|
|
|
connect(actionSaveAs, &QAction::triggered, this, []() {
|
|
// TODO: Implement Save As action
|
|
});
|
|
|
|
// Edit menu connections
|
|
connect(actionPreferences, &QAction::triggered, this, [this]() {
|
|
PreferenceEditor *prefEditor = new PreferenceEditor(this);
|
|
prefEditor->exec();
|
|
prefEditor->close();
|
|
delete prefEditor;
|
|
});
|
|
|
|
// Tools menu connections
|
|
connect(actionRunTests, &QAction::triggered, this, []() {
|
|
// TODO: Implement Run Tests action
|
|
});
|
|
|
|
// Help menu connections
|
|
connect(actionAbout, &QAction::triggered, this, [this]() {
|
|
AboutDialog *aboutDialog = new AboutDialog(this);
|
|
aboutDialog->exec();
|
|
delete aboutDialog;
|
|
});
|
|
|
|
connect(actionReportIssue, &QAction::triggered, this, [this]() {
|
|
ReportIssueDialog issueDialog("https://git.redline.llc", "njohnson", "XPlor",
|
|
"4738c4d2efd123efac1506c68c59b285c646df9f", this);
|
|
issueDialog.exec();
|
|
});
|
|
|
|
// Reparse action connection
|
|
connect(actionReparse, &QAction::triggered, this, [this]() {
|
|
if (mOpenedFilePaths.isEmpty()) {
|
|
QMessageBox::information(this, "Reparse", "No files to reparse. Drag and drop files first.");
|
|
return;
|
|
}
|
|
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addEntry("[REPARSE] Reloading definitions and reparsing files...");
|
|
|
|
// Store file paths before clearing
|
|
QStringList filesToReparse = mOpenedFilePaths;
|
|
|
|
// Clear everything
|
|
Reset();
|
|
mOpenedFilePaths.clear();
|
|
|
|
// Clear and reload type registry
|
|
mTypeRegistry = TypeRegistry();
|
|
LoadDefinitions();
|
|
|
|
LogManager::instance().addEntry(QString("[REPARSE] Definitions reloaded, reparsing %1 file(s)...").arg(filesToReparse.size()));
|
|
|
|
// Reparse each file
|
|
for (const QString& path : filesToReparse) {
|
|
QFile inputFile(path);
|
|
if (!inputFile.open(QIODevice::ReadOnly)) {
|
|
LogManager::instance().addError(QString("[REPARSE] Failed to open: %1").arg(path));
|
|
continue;
|
|
}
|
|
|
|
const QString fileName = QFileInfo(path).fileName();
|
|
LogManager::instance().addEntry(QString("[FILE] Analyzing: %1").arg(fileName));
|
|
|
|
const QString rootType = mTypeRegistry.chooseType(&inputFile, path);
|
|
LogManager::instance().addEntry(QString("[FILE] Matched type '%1' for file: %2").arg(rootType).arg(fileName));
|
|
|
|
if (rootType.isEmpty()) {
|
|
LogManager::instance().addError(QString("No matching definition for: %1").arg(path));
|
|
continue;
|
|
}
|
|
|
|
inputFile.seek(0);
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addEntry(QString("========== PARSING %1 ==========").arg(fileName));
|
|
LogManager::instance().addLine();
|
|
|
|
QVariantMap rootVars;
|
|
try {
|
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path);
|
|
} catch (const std::exception& e) {
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
|
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
|
LogManager::instance().addError(QString("File position: 0x%1").arg(inputFile.pos(), 0, 16));
|
|
LogManager::instance().addLine();
|
|
continue;
|
|
}
|
|
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addEntry(QString("========== FINISHED %1 ==========").arg(fileName));
|
|
LogManager::instance().addLine();
|
|
|
|
// Track this file again
|
|
mOpenedFilePaths.append(path);
|
|
|
|
// Rebuild tree
|
|
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
|
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
|
routeNestedObjects(rootInst, rootVars);
|
|
cat->setExpanded(true);
|
|
}
|
|
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addEntry("[REPARSE] Reparse complete!");
|
|
statusBar()->showMessage("Reparse complete!", 3000);
|
|
});
|
|
|
|
Reset();
|
|
|
|
LoadDefinitions();
|
|
//LoadTreeCategories();
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
delete ui;
|
|
}
|
|
|
|
QString MainWindow::pluralizeType(const QString& typeName) {
|
|
// simple: FastFile -> FastFiles, ZoneFile -> ZoneFiles
|
|
// you can replace with nicer display names later
|
|
const auto& mod = mTypeRegistry.module();
|
|
const auto it = mod.types.find(typeName);
|
|
QString groupLabel = (it != mod.types.end() && !it->display.isEmpty())
|
|
? it->display
|
|
: typeName;
|
|
return groupLabel + "s";
|
|
}
|
|
|
|
XTreeWidgetItem* MainWindow::ensureTypeCategoryRoot(const QString& typeName) {
|
|
if (mTypeCategoryRoots.contains(typeName))
|
|
return mTypeCategoryRoots[typeName];
|
|
|
|
auto* root = new XTreeWidgetItem(mTreeWidget);
|
|
root->setText(0, pluralizeType(typeName));
|
|
root->setData(0, Qt::UserRole + 1, "CATEGORY");
|
|
root->setData(0, Qt::UserRole + 2, typeName); // category's type
|
|
mTreeWidget->addTopLevelItem(root);
|
|
|
|
mTypeCategoryRoots.insert(typeName, root);
|
|
return root;
|
|
}
|
|
|
|
void MainWindow::LoadDefinitions()
|
|
{
|
|
const QString definitionsDir = QCoreApplication::applicationDirPath() + "/definitions/";
|
|
QDirIterator it(definitionsDir, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
|
|
|
|
while (it.hasNext()) {
|
|
const QString path = it.next();
|
|
const QString fileName = QFileInfo(path).fileName();
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
LogManager::instance().addError(QString("[DEF] Failed to open definition file: %1").arg(fileName));
|
|
continue;
|
|
}
|
|
try {
|
|
mTypeRegistry.ingestScript(QString::fromUtf8(f.readAll()), path);
|
|
LogManager::instance().addEntry(QString("[DEF] Loaded definition: %1").arg(fileName));
|
|
} catch (const std::exception& e) {
|
|
LogManager::instance().addError(QString("[DEF] ERROR loading %1: %2").arg(fileName).arg(e.what()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::LoadTreeCategories()
|
|
{
|
|
const Module& mod = mTypeRegistry.module();
|
|
|
|
for (const QString& typeName : mod.types.keys()) {
|
|
const TypeDef& td = mod.types[typeName];
|
|
if (!td.isRoot) continue;
|
|
|
|
auto* cat = new XTreeWidgetItem(mTreeWidget);
|
|
cat->setText(0, typeName); // or a nicer display name later
|
|
cat->setData(0, Qt::UserRole, typeName); // store type name
|
|
mTreeWidget->addTopLevelItem(cat);
|
|
}
|
|
}
|
|
|
|
XTreeWidgetItem* MainWindow::addInstanceNode(XTreeWidgetItem* parent, const QString& displayName,
|
|
const QString& typeName, const QVariantMap& vars)
|
|
{
|
|
auto* inst = new XTreeWidgetItem(parent);
|
|
inst->setText(0, displayName);
|
|
inst->setData(0, Qt::UserRole + 1, "INSTANCE");
|
|
inst->setData(0, Qt::UserRole + 2, typeName);
|
|
inst->setData(0, Qt::UserRole + 3, vars); // store the map for UI later
|
|
parent->addChild(inst);
|
|
inst->setExpanded(true);
|
|
return inst;
|
|
}
|
|
|
|
QString MainWindow::instanceDisplayFor(const QVariantMap& obj,
|
|
const QString& fallbackType,
|
|
const QString& fallbackKey,
|
|
std::optional<int> index)
|
|
{
|
|
if (obj.contains("_zone_name")) {
|
|
const QString s = obj.value("_zone_name").toString();
|
|
if (!s.isEmpty()) return s;
|
|
}
|
|
if (obj.contains("_name")) {
|
|
const QString s = obj.value("_name").toString();
|
|
if (!s.isEmpty()) return s;
|
|
}
|
|
if (!fallbackType.isEmpty()) return fallbackType;
|
|
|
|
if (!fallbackKey.isEmpty()) {
|
|
if (index.has_value()) return QString("%1[%2]").arg(fallbackKey).arg(*index);
|
|
return fallbackKey;
|
|
}
|
|
return QStringLiteral("<unnamed>");
|
|
}
|
|
|
|
void MainWindow::routeNestedObjects(XTreeWidgetItem* ownerInstanceNode, const QVariantMap& vars)
|
|
{
|
|
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
|
const QString& key = it.key();
|
|
const QVariant& v = it.value();
|
|
|
|
// Skip internal/temporary variables (underscore prefix, except special ones)
|
|
if (key.startsWith("_") && key != "_name" && key != "_type" && key != "_path" &&
|
|
key != "_basename" && key != "_ext" && key != "_zone_name") {
|
|
LogManager::instance().addEntry(QString("[TREE] Skipping internal variable: key='%1'").arg(key));
|
|
continue;
|
|
}
|
|
|
|
// child object
|
|
if (v.typeId() == QMetaType::QVariantMap) {
|
|
const QVariantMap child = v.toMap();
|
|
|
|
// Skip hidden objects
|
|
if (child.value("_hidden").toBool()) {
|
|
LogManager::instance().addEntry(QString("[TREE] Skipping hidden child object: key='%1'").arg(key));
|
|
continue;
|
|
}
|
|
|
|
const QString childType = child.value("_type").toString();
|
|
if (!childType.isEmpty()) {
|
|
const QString childName = child.value("_name").toString();
|
|
LogManager::instance().addEntry(QString("[TREE] Adding single child: key='%1', type='%2', name='%3'")
|
|
.arg(key)
|
|
.arg(childType)
|
|
.arg(childName));
|
|
|
|
auto* subcat = ensureSubcategory(ownerInstanceNode, childType);
|
|
|
|
const QString childDisplay = instanceDisplayFor(child, childType, key);
|
|
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
|
|
routeNestedObjects(childInst, child);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// array of child objects (optional, but nice)
|
|
if (v.typeId() == QMetaType::QVariantList) {
|
|
// Check for skip marker (XScript convention: _skip_tree_<arrayname>)
|
|
if (vars.contains("_skip_tree_" + key)) {
|
|
LogManager::instance().addEntry(QString("[TREE] Skipping array '%1' (has _skip_tree marker)").arg(key));
|
|
continue;
|
|
}
|
|
|
|
const QVariantList list = v.toList();
|
|
LogManager::instance().addEntry(QString("[TREE] Processing array '%1' with %2 items")
|
|
.arg(key)
|
|
.arg(list.size()));
|
|
|
|
for (int i = 0; i < list.size(); i++) {
|
|
if (list[i].typeId() != QMetaType::QVariantMap) continue;
|
|
const QVariantMap child = list[i].toMap();
|
|
|
|
// Skip hidden objects
|
|
if (child.value("_hidden").toBool()) {
|
|
continue;
|
|
}
|
|
|
|
const QString childType = child.value("_type").toString();
|
|
if (childType.isEmpty()) continue;
|
|
|
|
const QString childName = child.value("_name").toString();
|
|
LogManager::instance().addEntry(QString("[TREE] Adding array item #%1: type='%2', name='%3'")
|
|
.arg(i)
|
|
.arg(childType)
|
|
.arg(childName));
|
|
|
|
auto* subcat = ensureSubcategory(ownerInstanceNode, childType);
|
|
|
|
const QString childDisplay = instanceDisplayFor(child, childType, it.key(), i);
|
|
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
|
|
routeNestedObjects(childInst, child);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
XTreeWidgetItem* MainWindow::ensureSubcategory(XTreeWidgetItem* instanceNode, const QString& childTypeName)
|
|
{
|
|
const QString key = QString("SUBCAT:%1").arg(childTypeName);
|
|
|
|
// Look for an existing subcategory child
|
|
for (int i = 0; i < instanceNode->childCount(); i++) {
|
|
auto* c = static_cast<XTreeWidgetItem*>(instanceNode->child(i));
|
|
if (c->data(0, Qt::UserRole + 1).toString() == "SUBCATEGORY" &&
|
|
c->data(0, Qt::UserRole + 2).toString() == childTypeName)
|
|
{
|
|
return c;
|
|
}
|
|
}
|
|
|
|
auto* sub = new XTreeWidgetItem(instanceNode);
|
|
sub->setText(0, pluralizeType(childTypeName));
|
|
sub->setData(0, Qt::UserRole + 1, "SUBCATEGORY");
|
|
sub->setData(0, Qt::UserRole + 2, childTypeName);
|
|
instanceNode->addChild(sub);
|
|
sub->setExpanded(true);
|
|
return sub;
|
|
}
|
|
|
|
void MainWindow::Reset() {
|
|
// Clear the tree widget
|
|
mTreeWidget->clear();
|
|
|
|
// Clear category roots hash
|
|
mTypeCategoryRoots.clear();
|
|
|
|
// Clear tabs
|
|
ui->tabWidget->clear();
|
|
}
|
|
|
|
void MainWindow::HandleLogEntry(const QString &entry) {
|
|
QString logContents = mLogWidget->toPlainText() + "\n" + entry;
|
|
if (mLogWidget->toPlainText().isEmpty()) {
|
|
logContents = entry;
|
|
}
|
|
mLogWidget->setPlainText(logContents);
|
|
}
|
|
|
|
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));
|
|
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();
|
|
|
|
QVariantMap rootVars;
|
|
try {
|
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path);
|
|
} catch (const std::exception& e) {
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addError(QString("========== PARSE ERROR in %1 ==========").arg(fileName));
|
|
LogManager::instance().addError(QString("Error: %1").arg(e.what()));
|
|
LogManager::instance().addError(QString("File position: 0x%1").arg(inputFile.pos(), 0, 16));
|
|
LogManager::instance().addLine();
|
|
QMessageBox::critical(this, "Parse Error",
|
|
QString("Failed to parse %1:\n\n%2").arg(fileName).arg(e.what()));
|
|
continue;
|
|
}
|
|
|
|
LogManager::instance().addLine();
|
|
LogManager::instance().addEntry(QString("========== FINISHED %1 ==========").arg(fileName));
|
|
LogManager::instance().addLine();
|
|
|
|
// Track this file for reparsing
|
|
if (!mOpenedFilePaths.contains(path)) {
|
|
mOpenedFilePaths.append(path);
|
|
}
|
|
|
|
// Ensure top-level category exists (it should, but safe)
|
|
XTreeWidgetItem* cat = ensureTypeCategoryRoot(rootType);
|
|
|
|
// Add instance under category (FastFiles -> test.ff)
|
|
auto* rootInst = addInstanceNode(cat, fileName, rootType, rootVars);
|
|
|
|
// Route nested objects as subcategories under the instance (test.ff -> ZoneFiles -> ...)
|
|
routeNestedObjects(rootInst, rootVars);
|
|
|
|
cat->setExpanded(true);
|
|
mTreeWidget->setCurrentItem(rootInst);
|
|
}
|
|
}
|