2026-01-07 16:36:53 -05:00
|
|
|
#include "mainwindow.h"
|
|
|
|
|
#include "ui_mainwindow.h"
|
|
|
|
|
|
|
|
|
|
#include "aboutdialog.h"
|
2026-01-08 00:38:04 -05:00
|
|
|
#include "audiopreviewwidget.h"
|
2026-01-12 20:55:41 -05:00
|
|
|
#include "batchexportdialog.h"
|
2026-01-08 00:38:04 -05:00
|
|
|
#include "definitionviewer.h"
|
2026-01-12 20:55:41 -05:00
|
|
|
#include "exportmanager.h"
|
2026-01-08 00:38:04 -05:00
|
|
|
#include "hexviewerwidget.h"
|
2026-01-07 16:36:53 -05:00
|
|
|
#include "preferenceeditor.h"
|
|
|
|
|
#include "reportissuedialog.h"
|
2026-01-08 00:38:04 -05:00
|
|
|
#include "settings.h"
|
2026-01-07 16:36:53 -05:00
|
|
|
#include "statusbarmanager.h"
|
|
|
|
|
#include "logmanager.h"
|
|
|
|
|
#include "xtreewidget.h"
|
|
|
|
|
#include "xtreewidgetitem.h"
|
|
|
|
|
#include "compression.h"
|
|
|
|
|
#include "scripttypeeditorwidget.h"
|
|
|
|
|
#include "dsluischema.h"
|
2026-01-12 20:55:41 -05:00
|
|
|
#include "recompiler.h"
|
|
|
|
|
#include "operationjournal.h"
|
|
|
|
|
#include "dirtystatemanager.h"
|
2026-01-07 16:36:53 -05:00
|
|
|
#include "utils.h"
|
|
|
|
|
#include "imagepreviewwidget.h"
|
2026-01-11 12:09:31 -05:00
|
|
|
#include "textviewerwidget.h"
|
|
|
|
|
#include "listpreviewwidget.h"
|
2026-01-07 16:36:53 -05:00
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// Debug logging is controlled via Settings -> LogManager::debug()
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
#include <QFileDialog>
|
|
|
|
|
#include <QStandardPaths>
|
|
|
|
|
#include <QMessageBox>
|
2026-01-08 00:38:04 -05:00
|
|
|
#include <QFrame>
|
2026-01-07 16:36:53 -05:00
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QTableWidgetItem>
|
|
|
|
|
#include <QTreeWidgetItem>
|
|
|
|
|
#include <QDockWidget>
|
|
|
|
|
#include <QPlainTextEdit>
|
2026-01-12 20:55:41 -05:00
|
|
|
#include <QTabWidget>
|
|
|
|
|
#include <QVBoxLayout>
|
2026-01-07 16:36:53 -05:00
|
|
|
#include <QMimeData>
|
|
|
|
|
#include <QProgressBar>
|
|
|
|
|
#include <QProgressDialog>
|
|
|
|
|
#include <QApplication>
|
|
|
|
|
#include <QtMath>
|
|
|
|
|
#include <QtEndian>
|
|
|
|
|
#include <QDirIterator>
|
2026-01-08 00:38:04 -05:00
|
|
|
#include <QInputDialog>
|
|
|
|
|
#include <QClipboard>
|
2026-01-12 20:55:41 -05:00
|
|
|
#include <QDataStream>
|
2026-01-08 00:38:04 -05:00
|
|
|
#include <QDesktopServices>
|
2026-01-12 20:55:41 -05:00
|
|
|
#include <QUndoStack>
|
|
|
|
|
|
|
|
|
|
#include "fieldeditcommand.h"
|
2026-01-08 00:38:04 -05:00
|
|
|
#include <QUrl>
|
|
|
|
|
#include <QRegularExpression>
|
|
|
|
|
#include <QImage>
|
2026-01-08 00:54:57 -05:00
|
|
|
#include <QTimer>
|
2026-01-08 00:38:04 -05:00
|
|
|
#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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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();
|
2026-01-08 00:38:04 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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
|
2026-01-08 00:38:04 -05:00
|
|
|
}
|
2026-01-11 12:09:31 -05:00
|
|
|
// RCB pixel format (Avatar textures)
|
|
|
|
|
if (looksLikeRCBPixel(data)) return true;
|
|
|
|
|
return false;
|
2026-01-08 00:38:04 -05:00
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
MainWindow::MainWindow(QWidget *parent)
|
|
|
|
|
: QMainWindow(parent)
|
|
|
|
|
, ui(new Ui::MainWindow)
|
2026-01-11 12:09:31 -05:00
|
|
|
, mTreeBuilder(nullptr, mTypeRegistry) // Placeholder, will reset after mTreeWidget created
|
2026-01-07 16:36:53 -05:00
|
|
|
{
|
|
|
|
|
ui->setupUi(this);
|
|
|
|
|
setAcceptDrops(true);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Initialize global undo stack
|
|
|
|
|
mUndoStack = new QUndoStack(this);
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// 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());
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
mTreeWidget = new XTreeWidget(this);
|
2026-01-11 12:09:31 -05:00
|
|
|
|
|
|
|
|
// Initialize tree builder with the actual tree widget
|
|
|
|
|
mTreeBuilder = TreeBuilder(mTreeWidget, mTypeRegistry);
|
2026-01-08 00:38:04 -05:00
|
|
|
mTreeWidget->setColumnCount(1);
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// Flush any buffered log entries from before the signal was connected
|
|
|
|
|
LogManager::instance().flushBufferedEntries();
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
statusBar()->addPermanentWidget(mProgressBar);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Connect dirty state manager to update tab titles and tree items
|
|
|
|
|
connect(&DirtyStateManager::instance(), &DirtyStateManager::dirtyStateChanged,
|
|
|
|
|
this, [this](QWidget* tab, bool isDirty) {
|
|
|
|
|
int index = ui->tabWidget->indexOf(tab);
|
|
|
|
|
if (index < 0) return;
|
|
|
|
|
|
|
|
|
|
QString title = ui->tabWidget->tabText(index);
|
|
|
|
|
if (isDirty && !title.endsWith(" *")) {
|
|
|
|
|
ui->tabWidget->setTabText(index, title + " *");
|
|
|
|
|
} else if (!isDirty && title.endsWith(" *")) {
|
|
|
|
|
ui->tabWidget->setTabText(index, title.chopped(2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Also update tree item indicator
|
|
|
|
|
QVariant treeItemVar = tab->property("treeItem");
|
|
|
|
|
if (treeItemVar.isValid()) {
|
|
|
|
|
auto* treeItem = treeItemVar.value<XTreeWidgetItem*>();
|
|
|
|
|
if (treeItem) {
|
|
|
|
|
treeItem->setModified(isDirty);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update save actions when tab changes
|
|
|
|
|
connect(ui->tabWidget, &QTabWidget::currentChanged, this, [this](int index) {
|
|
|
|
|
QWidget* tab = ui->tabWidget->widget(index);
|
|
|
|
|
bool canSave = tab && tab->property("formEditor").isValid();
|
|
|
|
|
actionSave->setEnabled(canSave);
|
|
|
|
|
actionSaveAs->setEnabled(canSave);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle tab close requests (tabCloseRequested signal)
|
|
|
|
|
ui->tabWidget->setTabsClosable(true);
|
|
|
|
|
connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) {
|
|
|
|
|
QWidget* tab = ui->tabWidget->widget(index);
|
|
|
|
|
if (!tab) return;
|
|
|
|
|
|
|
|
|
|
if (DirtyStateManager::instance().isDirty(tab)) {
|
|
|
|
|
auto result = QMessageBox::question(this, "Unsaved Changes",
|
|
|
|
|
"This tab has unsaved changes.\n\nSave changes before closing?",
|
|
|
|
|
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
|
|
|
|
QMessageBox::Save);
|
|
|
|
|
|
|
|
|
|
if (result == QMessageBox::Cancel) {
|
|
|
|
|
return; // Don't close
|
|
|
|
|
} else if (result == QMessageBox::Save) {
|
|
|
|
|
// Try to save - if save fails, don't close
|
|
|
|
|
if (!saveTab(tab)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Discard falls through to close
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DirtyStateManager::instance().removeTab(tab);
|
|
|
|
|
ui->tabWidget->removeTab(index);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
if (tabIndex < 0) return;
|
|
|
|
|
|
|
|
|
|
QWidget* tabWidget = ui->tabWidget->widget(tabIndex);
|
|
|
|
|
if (!tabWidget) return;
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
QMenu *contextMenu = new QMenu(this);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Get data associated with this tab for export
|
|
|
|
|
QByteArray tabData = tabWidget->property("TAB_DATA").toByteArray();
|
|
|
|
|
QString tabName = ui->tabWidget->tabText(tabIndex);
|
|
|
|
|
QVariantMap tabVars = tabWidget->property("TAB_VARS").toMap();
|
|
|
|
|
|
|
|
|
|
// Export options (only if tab has data)
|
|
|
|
|
if (!tabData.isEmpty()) {
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
auto contentType = exp.detectContentType(tabVars);
|
|
|
|
|
if (contentType == ExportManager::Unknown) {
|
|
|
|
|
contentType = exp.detectContentType(tabData, tabName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Quick Export
|
|
|
|
|
QAction *quickExportAction = new QAction("Quick Export");
|
|
|
|
|
quickExportAction->setShortcut(QKeySequence("Ctrl+Shift+E"));
|
|
|
|
|
contextMenu->addAction(quickExportAction);
|
|
|
|
|
connect(quickExportAction, &QAction::triggered, this, [this, tabData, tabName, contentType]() {
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
if (exp.quickExport(tabData, tabName, contentType, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Export with dialog
|
|
|
|
|
QAction *exportDialogAction = new QAction("Export...");
|
|
|
|
|
exportDialogAction->setShortcut(QKeySequence("Ctrl+E"));
|
|
|
|
|
contextMenu->addAction(exportDialogAction);
|
|
|
|
|
connect(exportDialogAction, &QAction::triggered, this, [this, tabData, tabName, contentType]() {
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
if (exp.exportWithDialog(tabData, tabName, contentType, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Raw data export
|
|
|
|
|
QAction *rawExportAction = new QAction("Export Raw Data...");
|
|
|
|
|
contextMenu->addAction(rawExportAction);
|
|
|
|
|
connect(rawExportAction, &QAction::triggered, this, [this, tabData, tabName]() {
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
if (exp.exportRawData(tabData, tabName, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
contextMenu->addSeparator();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
QAction *closeAction = new QAction("Close");
|
|
|
|
|
contextMenu->addAction(closeAction);
|
2026-01-12 20:55:41 -05:00
|
|
|
connect(closeAction, &QAction::triggered, this, [this, tabIndex](bool checked) {
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
connect(closeAllButAction, &QAction::triggered, this, [this, tabIndex](bool checked) {
|
2026-01-07 16:36:53 -05:00
|
|
|
Q_UNUSED(checked);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
for (int i = ui->tabWidget->count() - 1; i >= 0; i--) {
|
2026-01-07 16:36:53 -05:00
|
|
|
if (i != tabIndex) {
|
|
|
|
|
ui->tabWidget->removeTab(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
QAction *closeLeftAction = new QAction("Close All to the Left");
|
|
|
|
|
closeMultipleAction->addAction(closeLeftAction);
|
2026-01-12 20:55:41 -05:00
|
|
|
connect(closeLeftAction, &QAction::triggered, this, [this, tabIndex](bool checked) {
|
2026-01-07 16:36:53 -05:00
|
|
|
Q_UNUSED(checked);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
for (int i = tabIndex - 1; i >= 0; i--) {
|
2026-01-07 16:36:53 -05:00
|
|
|
ui->tabWidget->removeTab(i);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
QAction *closeRightAction = new QAction("Close All to the Right");
|
|
|
|
|
closeMultipleAction->addAction(closeRightAction);
|
2026-01-12 20:55:41 -05:00
|
|
|
connect(closeRightAction, &QAction::triggered, this, [this, tabIndex](bool checked) {
|
2026-01-07 16:36:53 -05:00
|
|
|
Q_UNUSED(checked);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
for (int i = ui->tabWidget->count() - 1; i > tabIndex; i--) {
|
2026-01-07 16:36:53 -05:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Handle export requests from tree context menu
|
|
|
|
|
connect(mTreeWidget, &XTreeWidget::exportRequested, this, [this](const QString& format, QTreeWidgetItem* item) {
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
|
QString name = item->text(0);
|
|
|
|
|
|
|
|
|
|
// Get data from vars (check _preview first, then data field)
|
|
|
|
|
QByteArray data;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
data = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
data = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
data = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("No data available to export", 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
|
|
|
|
|
if (format == "raw") {
|
|
|
|
|
if (exp.exportRawData(data, name, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (format == "clipboard") {
|
|
|
|
|
QString typeName = item->data(0, Qt::UserRole + 2).toString();
|
|
|
|
|
|
|
|
|
|
QMimeData* mimeData = new QMimeData();
|
|
|
|
|
|
|
|
|
|
// 1. Internal format - full metadata for paste inside app
|
|
|
|
|
QByteArray internalData;
|
|
|
|
|
QDataStream stream(&internalData, QIODevice::WriteOnly);
|
|
|
|
|
stream << name << typeName << vars << data;
|
|
|
|
|
mimeData->setData("application/x-xplor-item", internalData);
|
|
|
|
|
|
|
|
|
|
// 2. External format - raw file bytes
|
|
|
|
|
if (!data.isEmpty()) {
|
|
|
|
|
mimeData->setData("application/octet-stream", data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Text representation
|
|
|
|
|
QStringList lines;
|
|
|
|
|
lines.append(QString("Name: %1").arg(name));
|
|
|
|
|
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()));
|
|
|
|
|
}
|
|
|
|
|
mimeData->setText(lines.join("\n"));
|
|
|
|
|
|
|
|
|
|
// 4. Native format if content is image/audio
|
|
|
|
|
auto contentType = exp.detectContentType(vars);
|
|
|
|
|
if (contentType == ExportManager::Unknown && !data.isEmpty()) {
|
|
|
|
|
contentType = exp.detectContentType(data, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentType == ExportManager::Image && !data.isEmpty()) {
|
|
|
|
|
QImage img;
|
|
|
|
|
if (img.loadFromData(data)) {
|
|
|
|
|
mimeData->setImageData(img);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QApplication::clipboard()->setMimeData(mimeData);
|
|
|
|
|
StatusBarManager::instance().updateStatus(QString("Copied '%1' to clipboard").arg(name), 3000);
|
|
|
|
|
}
|
|
|
|
|
else if (exp.supportedImageFormats().contains(format)) {
|
|
|
|
|
QImage img;
|
|
|
|
|
img.loadFromData(data);
|
|
|
|
|
if (img.isNull()) {
|
|
|
|
|
// Try ImagePreviewWidget's loaders for game formats
|
|
|
|
|
auto* tempWidget = new ImagePreviewWidget();
|
|
|
|
|
tempWidget->loadFromData(data, name);
|
|
|
|
|
// Get the pixmap and convert to image
|
|
|
|
|
QPixmap pix = tempWidget->grab();
|
|
|
|
|
img = pix.toImage();
|
|
|
|
|
delete tempWidget;
|
|
|
|
|
}
|
|
|
|
|
if (!img.isNull()) {
|
|
|
|
|
if (exp.exportImage(img, format, name, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Image exported successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Failed to decode image data", 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (exp.supportedAudioFormats().contains(format)) {
|
|
|
|
|
if (exp.exportAudio(data, format, name, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Audio exported successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (format == "txt") {
|
|
|
|
|
if (exp.exportText(data, name, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Text exported successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle quick export requests
|
|
|
|
|
connect(mTreeWidget, &XTreeWidget::quickExportRequested, this, [this](QTreeWidgetItem* item) {
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
|
QString name = item->text(0);
|
|
|
|
|
|
|
|
|
|
QByteArray data;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
data = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
data = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
data = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("No data available to export", 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
auto contentType = exp.detectContentType(vars);
|
|
|
|
|
if (contentType == ExportManager::Unknown) {
|
|
|
|
|
contentType = exp.detectContentType(data, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exp.quickExport(data, name, contentType, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle export with dialog requests
|
|
|
|
|
connect(mTreeWidget, &XTreeWidget::exportDialogRequested, this, [this](QTreeWidgetItem* item) {
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
|
QString name = item->text(0);
|
|
|
|
|
|
|
|
|
|
QByteArray data;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
data = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
data = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
data = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("No data available to export", 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
auto contentType = exp.detectContentType(vars);
|
|
|
|
|
if (contentType == ExportManager::Unknown) {
|
|
|
|
|
contentType = exp.detectContentType(data, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exp.exportWithDialog(data, name, contentType, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle batch export requests
|
|
|
|
|
connect(mTreeWidget, &XTreeWidget::batchExportRequested, this, [this](QTreeWidgetItem* parentItem) {
|
|
|
|
|
if (!parentItem) return;
|
|
|
|
|
|
|
|
|
|
QList<BatchExportItem> items;
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
|
|
|
|
|
std::function<void(QTreeWidgetItem*, const QString&)> collectItems =
|
|
|
|
|
[&](QTreeWidgetItem* item, const QString& basePath) {
|
|
|
|
|
QString kind = item->data(0, Qt::UserRole + 1).toString();
|
|
|
|
|
if (kind == "INSTANCE") {
|
|
|
|
|
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
|
QString name = item->text(0);
|
|
|
|
|
|
|
|
|
|
QByteArray data;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
data = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
data = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
data = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data.isEmpty()) {
|
|
|
|
|
BatchExportItem batchItem;
|
|
|
|
|
batchItem.name = name;
|
|
|
|
|
batchItem.path = basePath.isEmpty() ? name : basePath + "/" + name;
|
|
|
|
|
batchItem.data = data;
|
|
|
|
|
batchItem.contentType = static_cast<int>(exp.detectContentType(vars));
|
|
|
|
|
if (batchItem.contentType == ExportManager::Unknown) {
|
|
|
|
|
batchItem.contentType = static_cast<int>(exp.detectContentType(data, name));
|
|
|
|
|
}
|
|
|
|
|
batchItem.selected = true;
|
|
|
|
|
items.append(batchItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < item->childCount(); ++i) {
|
|
|
|
|
QString childPath = basePath;
|
|
|
|
|
if (kind == "CATEGORY" || kind == "SUBCATEGORY" || kind == "EXTENSION_GROUP") {
|
|
|
|
|
childPath = basePath.isEmpty() ? item->text(0) : basePath + "/" + item->text(0);
|
|
|
|
|
}
|
|
|
|
|
collectItems(item->child(i), childPath);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < parentItem->childCount(); ++i) {
|
|
|
|
|
collectItems(parentItem->child(i), "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (items.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("No exportable items found", 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exp.batchExport(items, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Batch export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle batch export by type requests
|
|
|
|
|
connect(mTreeWidget, &XTreeWidget::batchExportByTypeRequested, this,
|
|
|
|
|
[this](QTreeWidgetItem* parentItem, int contentType) {
|
|
|
|
|
if (!parentItem) return;
|
|
|
|
|
|
|
|
|
|
QList<BatchExportItem> items;
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
|
|
|
|
|
std::function<void(QTreeWidgetItem*, const QString&)> collectItems =
|
|
|
|
|
[&](QTreeWidgetItem* item, const QString& basePath) {
|
|
|
|
|
QString kind = item->data(0, Qt::UserRole + 1).toString();
|
|
|
|
|
if (kind == "INSTANCE") {
|
|
|
|
|
QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
|
QString name = item->text(0);
|
|
|
|
|
|
|
|
|
|
auto itemType = exp.detectContentType(vars);
|
|
|
|
|
if (itemType == ExportManager::Unknown) {
|
|
|
|
|
QByteArray tempData;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
tempData = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
tempData = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
tempData = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
itemType = exp.detectContentType(tempData, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (static_cast<int>(itemType) != contentType) return;
|
|
|
|
|
|
|
|
|
|
QByteArray data;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
data = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
data = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
data = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data.isEmpty()) {
|
|
|
|
|
BatchExportItem batchItem;
|
|
|
|
|
batchItem.name = name;
|
|
|
|
|
batchItem.path = basePath.isEmpty() ? name : basePath + "/" + name;
|
|
|
|
|
batchItem.data = data;
|
|
|
|
|
batchItem.contentType = contentType;
|
|
|
|
|
batchItem.selected = true;
|
|
|
|
|
items.append(batchItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < item->childCount(); ++i) {
|
|
|
|
|
QString childPath = basePath;
|
|
|
|
|
if (kind == "CATEGORY" || kind == "SUBCATEGORY" || kind == "EXTENSION_GROUP") {
|
|
|
|
|
childPath = basePath.isEmpty() ? item->text(0) : basePath + "/" + item->text(0);
|
|
|
|
|
}
|
|
|
|
|
collectItems(item->child(i), childPath);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < parentItem->childCount(); ++i) {
|
|
|
|
|
collectItems(parentItem->child(i), "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (items.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("No exportable items found", 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exp.batchExport(items, this)) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Batch export completed successfully", 3000);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(mTreeWidget, &XTreeWidget::ItemSelected, this, [this](const QString itemText, QTreeWidgetItem* treeItem) {
|
2026-01-07 16:36:53 -05:00
|
|
|
Q_UNUSED(itemText);
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
auto* item = static_cast<XTreeWidgetItem*>(treeItem);
|
2026-01-07 16:36:53 -05:00
|
|
|
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();
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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;
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// Check if this item has a preview (resource data)
|
2026-01-11 12:09:31 -05:00
|
|
|
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();
|
2026-01-07 16:36:53 -05:00
|
|
|
const QString filename = preview.value("filename").toString();
|
|
|
|
|
const QByteArray data = preview.value("data").toByteArray();
|
2026-01-08 00:38:04 -05:00
|
|
|
const QString lowerFilename = filename.toLower();
|
2026-01-11 12:09:31 -05:00
|
|
|
LogManager::instance().addEntry(QString("[VIEWER] Preview filename='%1', data size=%2")
|
|
|
|
|
.arg(filename)
|
|
|
|
|
.arg(data.size()));
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
if (!data.isEmpty()) {
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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") {
|
2026-01-08 00:38:04 -05:00
|
|
|
// Audio preview widget
|
|
|
|
|
auto* audioWidget = new AudioPreviewWidget(ui->tabWidget);
|
|
|
|
|
audioWidget->setProperty("PARENT_NAME", tabTitle);
|
2026-01-12 20:55:41 -05:00
|
|
|
audioWidget->setProperty("TAB_DATA", data);
|
|
|
|
|
audioWidget->setProperty("TAB_VARS", vars);
|
2026-01-08 00:38:04 -05:00
|
|
|
audioWidget->loadFromData(data, filename);
|
|
|
|
|
audioWidget->setMetadata(metadata);
|
|
|
|
|
ui->tabWidget->addTab(audioWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(audioWidget);
|
|
|
|
|
return;
|
2026-01-11 12:09:31 -05:00
|
|
|
} else if (viewerType == "image") {
|
2026-01-08 00:38:04 -05:00
|
|
|
// Image preview widget
|
|
|
|
|
auto* imageWidget = new ImagePreviewWidget(ui->tabWidget);
|
|
|
|
|
imageWidget->setProperty("PARENT_NAME", tabTitle);
|
2026-01-12 20:55:41 -05:00
|
|
|
imageWidget->setProperty("TAB_DATA", data);
|
|
|
|
|
imageWidget->setProperty("TAB_VARS", vars);
|
2026-01-08 00:38:04 -05:00
|
|
|
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;
|
2026-01-11 12:09:31 -05:00
|
|
|
} else if (viewerType == "text") {
|
|
|
|
|
// Text viewer widget
|
|
|
|
|
auto* textWidget = new TextViewerWidget(ui->tabWidget);
|
|
|
|
|
textWidget->setProperty("PARENT_NAME", tabTitle);
|
2026-01-12 20:55:41 -05:00
|
|
|
textWidget->setProperty("TAB_DATA", data);
|
|
|
|
|
textWidget->setProperty("TAB_VARS", vars);
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
listWidget->setProperty("TAB_DATA", data);
|
|
|
|
|
listWidget->setProperty("TAB_VARS", vars);
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 20:55:41 -05:00
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
listWidget->setListData(items, filename);
|
|
|
|
|
listWidget->setMetadata(metadata);
|
|
|
|
|
ui->tabWidget->addTab(listWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(listWidget);
|
|
|
|
|
return;
|
2026-01-08 00:38:04 -05:00
|
|
|
} else {
|
2026-01-11 12:09:31 -05:00
|
|
|
// Hex viewer for unknown file types (default)
|
2026-01-08 00:38:04 -05:00
|
|
|
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
|
|
|
|
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
2026-01-12 20:55:41 -05:00
|
|
|
hexWidget->setProperty("TAB_DATA", data);
|
|
|
|
|
hexWidget->setProperty("TAB_VARS", vars);
|
2026-01-08 00:38:04 -05:00
|
|
|
hexWidget->setData(data, filename);
|
|
|
|
|
hexWidget->setMetadata(metadata);
|
|
|
|
|
ui->tabWidget->addTab(hexWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(hexWidget);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// Check if this is a chunk with binary data
|
2026-01-08 00:38:04 -05:00
|
|
|
if (vars.contains("data") && vars.value("data").typeId() == QMetaType::QByteArray) {
|
|
|
|
|
QByteArray chunkData = vars.value("data").toByteArray();
|
|
|
|
|
if (!chunkData.isEmpty()) {
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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);
|
2026-01-08 00:38:04 -05:00
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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);
|
2026-01-12 20:55:41 -05:00
|
|
|
textWidget->setProperty("TAB_DATA", chunkData);
|
|
|
|
|
textWidget->setProperty("TAB_VARS", vars);
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
imageWidget->setProperty("TAB_DATA", chunkData);
|
|
|
|
|
imageWidget->setProperty("TAB_VARS", vars);
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
audioWidget->setProperty("TAB_DATA", chunkData);
|
|
|
|
|
audioWidget->setProperty("TAB_VARS", vars);
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
listWidget->setProperty("TAB_DATA", chunkData);
|
|
|
|
|
listWidget->setProperty("TAB_VARS", vars);
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 20:55:41 -05:00
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
imageWidget->setProperty("TAB_DATA", chunkData);
|
|
|
|
|
imageWidget->setProperty("TAB_VARS", vars);
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-12 20:55:41 -05:00
|
|
|
hexWidget->setProperty("TAB_DATA", chunkData);
|
|
|
|
|
hexWidget->setProperty("TAB_VARS", vars);
|
2026-01-11 12:09:31 -05:00
|
|
|
hexWidget->setData(chunkData, tabTitle);
|
|
|
|
|
hexWidget->setMetadata(metadata);
|
|
|
|
|
ui->tabWidget->addTab(hexWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(hexWidget);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Check if a specific viewer type was requested via set_viewer()
|
|
|
|
|
// This allows xscript to override the default form+hex view
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Viewer)) {
|
|
|
|
|
QString viewerType = DslKeys::getString(vars, DslKey::Viewer).toLower();
|
|
|
|
|
|
|
|
|
|
// Get data from wherever available
|
|
|
|
|
QByteArray viewData;
|
|
|
|
|
QString filename = tabTitle;
|
|
|
|
|
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariantMap preview = DslKeys::get(vars, DslKey::Preview).toMap();
|
|
|
|
|
viewData = preview.value("data").toByteArray();
|
|
|
|
|
if (preview.contains("filename"))
|
|
|
|
|
filename = preview.value("filename").toString();
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
viewData = vars.value("data").toByteArray();
|
|
|
|
|
} else if (vars.contains("_original_bytes")) {
|
|
|
|
|
viewData = vars.value("_original_bytes").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewerType == "text" && !viewData.isEmpty()) {
|
|
|
|
|
auto* textWidget = new TextViewerWidget(ui->tabWidget);
|
|
|
|
|
textWidget->setProperty("PARENT_NAME", tabTitle);
|
|
|
|
|
textWidget->setProperty("treeItem", QVariant::fromValue(item));
|
|
|
|
|
textWidget->setData(viewData, filename);
|
|
|
|
|
ui->tabWidget->addTab(textWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(textWidget);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// For hex viewer type with data, create simple hex view
|
|
|
|
|
if (viewerType == "hex" && !viewData.isEmpty()) {
|
|
|
|
|
auto* hexWidget = new HexViewerWidget(ui->tabWidget);
|
|
|
|
|
hexWidget->setProperty("PARENT_NAME", tabTitle);
|
|
|
|
|
hexWidget->setProperty("treeItem", QVariant::fromValue(item));
|
|
|
|
|
hexWidget->setData(viewData, filename);
|
|
|
|
|
ui->tabWidget->addTab(hexWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(hexWidget);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
const TypeDef& td = mTypeRegistry.module().types[typeName];
|
2026-01-11 12:09:31 -05:00
|
|
|
const auto schema = buildUiSchemaForType(td, &mTypeRegistry.module());
|
2026-01-07 16:36:53 -05:00
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Create container widget with tabbed layout (Form + Hex views)
|
|
|
|
|
auto* containerWidget = new QWidget(ui->tabWidget);
|
|
|
|
|
auto* containerLayout = new QVBoxLayout(containerWidget);
|
|
|
|
|
containerLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
auto* innerTabWidget = new QTabWidget(containerWidget);
|
|
|
|
|
|
|
|
|
|
// Form tab
|
|
|
|
|
auto* formEditor = new ScriptTypeEditorWidget(typeName, schema, innerTabWidget);
|
|
|
|
|
formEditor->setValues(vars);
|
|
|
|
|
innerTabWidget->addTab(formEditor, "Form");
|
|
|
|
|
|
|
|
|
|
// Hex tab - shows recompiled binary
|
|
|
|
|
auto* hexView = new HexView(innerTabWidget);
|
|
|
|
|
hexView->setTheme(Settings::instance().theme());
|
|
|
|
|
innerTabWidget->addTab(hexView, "Hex");
|
|
|
|
|
|
|
|
|
|
// Store original bytes if available from vars (for diff highlighting later)
|
|
|
|
|
QByteArray originalBytes;
|
|
|
|
|
if (vars.contains("_original_bytes")) {
|
|
|
|
|
originalBytes = vars.value("_original_bytes").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
formEditor->setOriginalBytes(originalBytes);
|
|
|
|
|
|
|
|
|
|
// Initialize hex view with original data
|
|
|
|
|
if (!originalBytes.isEmpty()) {
|
|
|
|
|
hexView->setData(originalBytes);
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
// Fall back to data field if available
|
|
|
|
|
hexView->setData(vars.value("data").toByteArray());
|
|
|
|
|
} else if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
// Try preview data as last resort
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
QVariantMap pm = preview.toMap();
|
|
|
|
|
if (pm.contains("data")) {
|
|
|
|
|
hexView->setData(pm.value("data").toByteArray());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect recompilation on edit
|
|
|
|
|
connect(formEditor, &ScriptTypeEditorWidget::requestRecompile, containerWidget, [formEditor, hexView, this]() {
|
|
|
|
|
int journalId = formEditor->journalId();
|
|
|
|
|
if (journalId < 0) {
|
|
|
|
|
// No journal - show original bytes or empty
|
|
|
|
|
hexView->setData(formEditor->originalBytes());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Recompiler recompiler;
|
|
|
|
|
if (!recompiler.canRecompile(journalId)) {
|
|
|
|
|
// Not exportable - show warning in status bar
|
|
|
|
|
StatusBarManager::instance().updateStatus(
|
|
|
|
|
"Cannot recompile: " + recompiler.whyNotRecompilable(journalId), 3000);
|
|
|
|
|
hexView->setData(formEditor->originalBytes());
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
QStringList errors;
|
|
|
|
|
QByteArray recompiled = recompiler.recompileWithErrors(journalId, formEditor->values(), errors);
|
|
|
|
|
if (!errors.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus("Recompile warnings: " + errors.join("; "), 3000);
|
|
|
|
|
}
|
|
|
|
|
hexView->setData(recompiled);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connect theme changes
|
|
|
|
|
connect(&Settings::instance(), &Settings::themeChanged, hexView, &HexView::setTheme);
|
|
|
|
|
|
|
|
|
|
containerLayout->addWidget(innerTabWidget);
|
|
|
|
|
|
|
|
|
|
containerWidget->setProperty("PARENT_NAME", tabTitle);
|
|
|
|
|
containerWidget->setProperty("formEditor", QVariant::fromValue<QWidget*>(formEditor));
|
|
|
|
|
containerWidget->setProperty("hexView", QVariant::fromValue<QWidget*>(hexView));
|
|
|
|
|
containerWidget->setProperty("treeItem", QVariant::fromValue(item));
|
|
|
|
|
|
|
|
|
|
// Connect dirty state tracking
|
|
|
|
|
connect(formEditor, &ScriptTypeEditorWidget::modifiedChanged, containerWidget, [containerWidget, this](bool modified) {
|
|
|
|
|
if (modified) {
|
|
|
|
|
DirtyStateManager::instance().markDirty(containerWidget);
|
|
|
|
|
} else {
|
|
|
|
|
DirtyStateManager::instance().markClean(containerWidget);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connect value changes for undo/redo support
|
|
|
|
|
connect(formEditor, &ScriptTypeEditorWidget::valueChanged, this,
|
|
|
|
|
[this, formEditor](const QString& fieldName, const QVariant& oldValue, const QVariant& newValue) {
|
|
|
|
|
int journalId = formEditor->journalId();
|
|
|
|
|
if (journalId >= 0) {
|
|
|
|
|
pushFieldEdit(journalId, fieldName, oldValue, newValue);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ui->tabWidget->addTab(containerWidget, tabTitle);
|
|
|
|
|
ui->tabWidget->setCurrentWidget(containerWidget);
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
ui->toolBar->setWindowTitle("Tool Bar");
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
2026-01-08 00:38:04 -05:00
|
|
|
actionViewDefinitions = new QAction(QIcon::fromTheme("document-properties"), "View Definitions...", this);
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// Disable unimplemented actions
|
|
|
|
|
actionNew->setEnabled(false);
|
|
|
|
|
actionOpenFolder->setEnabled(false);
|
2026-01-12 20:55:41 -05:00
|
|
|
// Save/SaveAs enabled based on current tab
|
2026-01-08 00:38:04 -05:00
|
|
|
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);
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
// ========== 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);
|
2026-01-08 00:38:04 -05:00
|
|
|
ui->menuTools->addAction(actionViewDefinitions);
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
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();
|
2026-01-12 20:55:41 -05:00
|
|
|
ui->toolBar->addAction(actionUndo);
|
|
|
|
|
ui->toolBar->addAction(actionRedo);
|
|
|
|
|
ui->toolBar->addSeparator();
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// ========== 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));
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
// ========== Connect Action Signals ==========
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// 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);
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
2026-01-07 16:41:46 -05:00
|
|
|
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);
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setWindowTitle("XPlor - Parsing");
|
2026-01-07 16:41:46 -05:00
|
|
|
progress.setWindowModality(Qt::WindowModal);
|
|
|
|
|
progress.setMinimumDuration(500);
|
|
|
|
|
|
|
|
|
|
QVariantMap rootVars;
|
2026-01-11 12:09:31 -05:00
|
|
|
bool cancelled = false;
|
2026-01-07 16:41:46 -05:00
|
|
|
try {
|
2026-01-08 00:38:04 -05:00
|
|
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
2026-01-11 12:09:31 -05:00
|
|
|
[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));
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
QApplication::processEvents();
|
2026-01-11 12:09:31 -05:00
|
|
|
if (progress.wasCanceled()) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
throw std::runtime_error("Parsing cancelled by user");
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
},
|
2026-01-11 12:09:31 -05:00
|
|
|
[this, &progress, &cancelled](const QString& status) {
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setLabelText(status);
|
|
|
|
|
QApplication::processEvents();
|
2026-01-11 12:09:31 -05:00
|
|
|
if (progress.wasCanceled()) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
throw std::runtime_error("Parsing cancelled by user");
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
});
|
2026-01-07 16:41:46 -05:00
|
|
|
} catch (const std::exception& e) {
|
2026-01-11 12:09:31 -05:00
|
|
|
if (cancelled) {
|
|
|
|
|
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-07 16:41:46 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// Update progress for tree building phase
|
|
|
|
|
progress.setLabelText(QString("Building tree for %1...").arg(fileName));
|
|
|
|
|
progress.setRange(0, 0); // Indeterminate mode
|
|
|
|
|
QApplication::processEvents();
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-07 16:41:46 -05:00
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
|
|
|
|
QApplication::processEvents();
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
|
|
|
|
mTreeBuilder.updateNodeCounts(cat);
|
2026-01-08 00:38:04 -05:00
|
|
|
|
|
|
|
|
cat->setExpanded(false);
|
2026-01-07 16:41:46 -05:00
|
|
|
mTreeWidget->setCurrentItem(rootInst);
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-08 00:38:04 -05:00
|
|
|
cat->setExpanded(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statusBar()->showMessage(QString("Opened %1 files from folder").arg(filesToOpen.size()), 3000);
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
connect(actionSave, &QAction::triggered, this, [this]() {
|
2026-01-12 20:55:41 -05:00
|
|
|
QWidget* currentTab = ui->tabWidget->currentWidget();
|
|
|
|
|
if (currentTab) {
|
|
|
|
|
saveTab(currentTab);
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
connect(actionSaveAs, &QAction::triggered, this, [this]() {
|
2026-01-12 20:55:41 -05:00
|
|
|
QWidget* currentTab = ui->tabWidget->currentWidget();
|
|
|
|
|
if (currentTab) {
|
|
|
|
|
saveTabAs(currentTab);
|
2026-01-08 00:38:04 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Edit menu connections - wire to undo stack
|
|
|
|
|
connect(actionUndo, &QAction::triggered, mUndoStack, &QUndoStack::undo);
|
|
|
|
|
connect(actionRedo, &QAction::triggered, mUndoStack, &QUndoStack::redo);
|
|
|
|
|
|
|
|
|
|
// Update action enabled state based on stack state
|
|
|
|
|
connect(mUndoStack, &QUndoStack::canUndoChanged, actionUndo, &QAction::setEnabled);
|
|
|
|
|
connect(mUndoStack, &QUndoStack::canRedoChanged, actionRedo, &QAction::setEnabled);
|
|
|
|
|
|
|
|
|
|
// Update action text with undo/redo description
|
|
|
|
|
connect(mUndoStack, &QUndoStack::undoTextChanged, this, [this](const QString& text) {
|
|
|
|
|
actionUndo->setText(text.isEmpty() ? "Undo" : QString("Undo %1").arg(text));
|
|
|
|
|
});
|
|
|
|
|
connect(mUndoStack, &QUndoStack::redoTextChanged, this, [this](const QString& text) {
|
|
|
|
|
actionRedo->setText(text.isEmpty() ? "Redo" : QString("Redo %1").arg(text));
|
2026-01-08 00:38:04 -05:00
|
|
|
});
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Initialize action state
|
|
|
|
|
actionUndo->setEnabled(mUndoStack->canUndo());
|
|
|
|
|
actionRedo->setEnabled(mUndoStack->canRedo());
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
connect(actionCut, &QAction::triggered, this, [this]() {
|
|
|
|
|
statusBar()->showMessage("Cut not available - read-only mode", 2000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(actionCopy, &QAction::triggered, this, [this]() {
|
2026-01-12 20:55:41 -05:00
|
|
|
// Copy selected tree item to clipboard with multiple formats
|
2026-01-08 00:38:04 -05:00
|
|
|
auto* item = static_cast<XTreeWidgetItem*>(mTreeWidget->currentItem());
|
|
|
|
|
if (!item) {
|
|
|
|
|
statusBar()->showMessage("Nothing selected to copy", 2000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
QString name = item->text(0);
|
2026-01-08 00:38:04 -05:00
|
|
|
const QString kind = item->data(0, Qt::UserRole + 1).toString();
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
// Only allow copying INSTANCE items (not groups/categories)
|
|
|
|
|
if (kind != "INSTANCE") {
|
|
|
|
|
statusBar()->showMessage("Can only copy file items", 2000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QVariantMap vars = item->data(0, Qt::UserRole + 3).toMap();
|
|
|
|
|
const QString typeName = item->data(0, Qt::UserRole + 2).toString();
|
|
|
|
|
|
|
|
|
|
// Extract file data from the item
|
|
|
|
|
QByteArray data;
|
|
|
|
|
if (DslKeys::contains(vars, DslKey::Preview)) {
|
|
|
|
|
QVariant preview = DslKeys::get(vars, DslKey::Preview);
|
|
|
|
|
if (preview.typeId() == QMetaType::QVariantMap) {
|
|
|
|
|
data = preview.toMap().value("data").toByteArray();
|
|
|
|
|
} else {
|
|
|
|
|
data = preview.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
} else if (vars.contains("data")) {
|
|
|
|
|
data = vars.value("data").toByteArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QMimeData* mimeData = new QMimeData();
|
|
|
|
|
|
|
|
|
|
// 1. Internal format - full metadata for paste inside app
|
|
|
|
|
QByteArray internalData;
|
|
|
|
|
QDataStream stream(&internalData, QIODevice::WriteOnly);
|
|
|
|
|
stream << name << typeName << vars << data;
|
|
|
|
|
mimeData->setData("application/x-xplor-item", internalData);
|
|
|
|
|
|
|
|
|
|
// 2. External format - raw file bytes
|
|
|
|
|
if (!data.isEmpty()) {
|
|
|
|
|
mimeData->setData("application/octet-stream", data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Text representation - for pasting into text editors
|
|
|
|
|
QStringList lines;
|
|
|
|
|
lines.append(QString("Name: %1").arg(name));
|
|
|
|
|
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()));
|
|
|
|
|
}
|
|
|
|
|
mimeData->setText(lines.join("\n"));
|
|
|
|
|
|
|
|
|
|
// 4. Native image format if content is an image
|
|
|
|
|
ExportManager& exp = ExportManager::instance();
|
|
|
|
|
ExportManager::ContentType contentType = exp.detectContentType(vars);
|
|
|
|
|
if (contentType == ExportManager::Unknown && !data.isEmpty()) {
|
|
|
|
|
contentType = exp.detectContentType(data, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentType == ExportManager::Image && !data.isEmpty()) {
|
|
|
|
|
QImage img;
|
|
|
|
|
if (img.loadFromData(data)) {
|
|
|
|
|
mimeData->setImageData(img);
|
2026-01-08 00:38:04 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 20:55:41 -05:00
|
|
|
QApplication::clipboard()->setMimeData(mimeData);
|
|
|
|
|
statusBar()->showMessage(QString("Copied '%1' to clipboard").arg(name), 2000);
|
2026-01-08 00:38:04 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
connect(actionPreferences, &QAction::triggered, this, [this]() {
|
|
|
|
|
PreferenceEditor *prefEditor = new PreferenceEditor(this);
|
|
|
|
|
prefEditor->exec();
|
|
|
|
|
prefEditor->close();
|
|
|
|
|
delete prefEditor;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Tools menu connections
|
2026-01-08 00:38:04 -05:00
|
|
|
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();
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Help menu connections
|
|
|
|
|
connect(actionAbout, &QAction::triggered, this, [this]() {
|
|
|
|
|
AboutDialog *aboutDialog = new AboutDialog(this);
|
|
|
|
|
aboutDialog->exec();
|
|
|
|
|
delete aboutDialog;
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
connect(actionReportIssue, &QAction::triggered, this, [this]() {
|
2026-01-08 00:38:04 -05:00
|
|
|
ReportIssueDialog dialog(this);
|
|
|
|
|
dialog.exec();
|
2026-01-07 16:36:53 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setWindowTitle("XPlor - Reparsing");
|
2026-01-07 16:36:53 -05:00
|
|
|
progress.setWindowModality(Qt::WindowModal);
|
|
|
|
|
progress.setMinimumDuration(500);
|
|
|
|
|
|
|
|
|
|
QVariantMap rootVars;
|
2026-01-11 12:09:31 -05:00
|
|
|
bool cancelled = false;
|
2026-01-07 16:36:53 -05:00
|
|
|
try {
|
2026-01-08 00:38:04 -05:00
|
|
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
2026-01-11 12:09:31 -05:00
|
|
|
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
|
|
|
|
if (pos > progress.value()) {
|
|
|
|
|
progress.setValue(static_cast<int>(pos));
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
QApplication::processEvents();
|
2026-01-11 12:09:31 -05:00
|
|
|
if (progress.wasCanceled()) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
throw std::runtime_error("Parsing cancelled by user");
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
},
|
2026-01-11 12:09:31 -05:00
|
|
|
[this, &progress, &cancelled](const QString& status) {
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setLabelText(status);
|
|
|
|
|
QApplication::processEvents();
|
2026-01-11 12:09:31 -05:00
|
|
|
if (progress.wasCanceled()) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
throw std::runtime_error("Parsing cancelled by user");
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
});
|
2026-01-07 16:36:53 -05:00
|
|
|
} catch (const std::exception& e) {
|
2026-01-11 12:09:31 -05:00
|
|
|
if (cancelled) {
|
|
|
|
|
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// Update progress for tree building phase
|
|
|
|
|
progress.setLabelText(QString("Building tree for %1...").arg(fileName));
|
|
|
|
|
progress.setRange(0, 0); // Indeterminate mode
|
|
|
|
|
QApplication::processEvents();
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
// Rebuild tree
|
2026-01-11 12:09:31 -05:00
|
|
|
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);
|
2026-01-08 00:38:04 -05:00
|
|
|
|
|
|
|
|
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
|
|
|
|
QApplication::processEvents();
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
|
|
|
|
mTreeBuilder.updateNodeCounts(cat);
|
2026-01-08 00:38:04 -05:00
|
|
|
cat->setExpanded(false);
|
2026-01-07 16:36:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogManager::instance().addLine();
|
|
|
|
|
LogManager::instance().addEntry("[REPARSE] Reparse complete!");
|
|
|
|
|
statusBar()->showMessage("Reparse complete!", 3000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Reset();
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// LoadDefinitions is now called from main.cpp during splash screen
|
2026-01-07 16:36:53 -05:00
|
|
|
//LoadTreeCategories();
|
2026-01-08 00:54:57 -05:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-07 16:36:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MainWindow::~MainWindow()
|
|
|
|
|
{
|
|
|
|
|
delete ui;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
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()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
void MainWindow::LoadDefinitions()
|
|
|
|
|
{
|
2026-01-08 00:38:04 -05:00
|
|
|
// Used for reparse - reloads all definitions
|
|
|
|
|
mDefinitionResults.clear();
|
|
|
|
|
mTypeRegistry = TypeRegistry();
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
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));
|
2026-01-08 00:38:04 -05:00
|
|
|
mDefinitionResults.append({path, fileName, false, "Failed to open file"});
|
2026-01-07 16:36:53 -05:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
mTypeRegistry.ingestScript(QString::fromUtf8(f.readAll()), path);
|
|
|
|
|
LogManager::instance().addEntry(QString("[DEF] Loaded definition: %1").arg(fileName));
|
2026-01-08 00:38:04 -05:00
|
|
|
mDefinitionResults.append({path, fileName, true, QString()});
|
2026-01-07 16:36:53 -05:00
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
LogManager::instance().addError(QString("[DEF] ERROR loading %1: %2").arg(fileName).arg(e.what()));
|
2026-01-08 00:38:04 -05:00
|
|
|
mDefinitionResults.append({path, fileName, false, QString::fromUtf8(e.what())});
|
2026-01-07 16:36:53 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Validate all type references after loading
|
|
|
|
|
QStringList refErrors = mTypeRegistry.validateTypeReferences();
|
|
|
|
|
for (const QString& err : refErrors) {
|
|
|
|
|
LogManager::instance().addError(QString("[DEF] %1").arg(err));
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
if (!refErrors.isEmpty()) {
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
2026-01-11 12:09:31 -05:00
|
|
|
cat->setText(0, typeName);
|
|
|
|
|
cat->setData(0, Qt::UserRole, typeName);
|
2026-01-07 16:36:53 -05:00
|
|
|
mTreeWidget->addTopLevelItem(cat);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainWindow::Reset() {
|
|
|
|
|
// Clear the tree widget
|
|
|
|
|
mTreeWidget->clear();
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
// Clear category roots in tree builder
|
|
|
|
|
mTreeBuilder.reset();
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
// 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);
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setWindowTitle("XPlor - Parsing");
|
2026-01-07 16:36:53 -05:00
|
|
|
progress.setWindowModality(Qt::WindowModal);
|
|
|
|
|
progress.setMinimumDuration(500);
|
|
|
|
|
|
|
|
|
|
QVariantMap rootVars;
|
2026-01-11 12:09:31 -05:00
|
|
|
bool cancelled = false;
|
2026-01-07 16:36:53 -05:00
|
|
|
try {
|
2026-01-08 00:38:04 -05:00
|
|
|
rootVars = mTypeRegistry.parse(rootType, &inputFile, path,
|
2026-01-11 12:09:31 -05:00
|
|
|
[this, &progress, &cancelled](qint64 pos, qint64 size) {
|
|
|
|
|
if (pos > progress.value()) {
|
|
|
|
|
progress.setValue(static_cast<int>(pos));
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
QApplication::processEvents();
|
2026-01-11 12:09:31 -05:00
|
|
|
if (progress.wasCanceled()) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
throw std::runtime_error("Parsing cancelled by user");
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
},
|
2026-01-11 12:09:31 -05:00
|
|
|
[this, &progress, &cancelled](const QString& status) {
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setLabelText(status);
|
|
|
|
|
QApplication::processEvents();
|
2026-01-11 12:09:31 -05:00
|
|
|
if (progress.wasCanceled()) {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
throw std::runtime_error("Parsing cancelled by user");
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
});
|
2026-01-07 16:36:53 -05:00
|
|
|
} catch (const std::exception& e) {
|
2026-01-11 12:09:31 -05:00
|
|
|
if (cancelled) {
|
|
|
|
|
LogManager::instance().addEntry(QString("[FILE] Parsing cancelled: %1").arg(fileName));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-07 16:36:53 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
// Update progress for tree building phase
|
|
|
|
|
progress.setLabelText(QString("Building tree for %1...").arg(fileName));
|
|
|
|
|
progress.setRange(0, 0); // Indeterminate mode
|
|
|
|
|
QApplication::processEvents();
|
|
|
|
|
|
2026-01-07 16:36:53 -05:00
|
|
|
// Ensure top-level category exists (it should, but safe)
|
2026-01-11 12:09:31 -05:00
|
|
|
XTreeWidgetItem* cat = mTreeBuilder.ensureTypeCategoryRoot(rootType, DslKeys::getString(rootVars, DslKey::Display));
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
// Add instance under category (FastFiles -> test.ff)
|
2026-01-11 12:09:31 -05:00
|
|
|
const QString itemDisplayName = DslKeys::contains(rootVars, DslKey::Name) ? DslKeys::getString(rootVars, DslKey::Name) : fileName;
|
|
|
|
|
auto* rootInst = mTreeBuilder.addInstanceNode(cat, itemDisplayName, rootType, rootVars);
|
2026-01-07 16:36:53 -05:00
|
|
|
|
|
|
|
|
// Route nested objects as subcategories under the instance (test.ff -> ZoneFiles -> ...)
|
2026-01-11 12:09:31 -05:00
|
|
|
mTreeBuilder.routeNestedObjects(rootInst, rootVars);
|
2026-01-07 16:36:53 -05:00
|
|
|
|
2026-01-08 00:38:04 -05:00
|
|
|
progress.setLabelText(QString("Organizing %1...").arg(fileName));
|
|
|
|
|
QApplication::processEvents();
|
|
|
|
|
|
2026-01-11 12:09:31 -05:00
|
|
|
mTreeBuilder.organizeChildrenByExtension(rootInst);
|
|
|
|
|
mTreeBuilder.updateNodeCounts(cat);
|
2026-01-08 00:38:04 -05:00
|
|
|
|
|
|
|
|
cat->setExpanded(false);
|
2026-01-07 16:36:53 -05:00
|
|
|
mTreeWidget->setCurrentItem(rootInst);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-08 00:38:04 -05:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 20:55:41 -05:00
|
|
|
|
|
|
|
|
bool MainWindow::openFile(const QString& path) {
|
|
|
|
|
const QString fileName = QFileInfo(path).fileName();
|
|
|
|
|
|
|
|
|
|
QFile inputFile(path);
|
|
|
|
|
if (!inputFile.open(QIODevice::ReadOnly)) {
|
|
|
|
|
LogManager::instance().addError(QString("Failed to open: %1").arg(path));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
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()));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
statusBar()->showMessage(QString("Opened: %1").arg(fileName), 3000);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool MainWindow::saveTab(QWidget* tab) {
|
|
|
|
|
if (!tab) return false;
|
|
|
|
|
|
|
|
|
|
// Get the form editor from the container
|
|
|
|
|
QWidget* formEditorWidget = tab->property("formEditor").value<QWidget*>();
|
|
|
|
|
auto* formEditor = qobject_cast<ScriptTypeEditorWidget*>(formEditorWidget);
|
|
|
|
|
if (!formEditor) {
|
|
|
|
|
QMessageBox::warning(this, "Save Error", "Cannot save: not a form editor tab.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we have a file path
|
|
|
|
|
QString filePath = DirtyStateManager::instance().filePath(tab);
|
|
|
|
|
if (filePath.isEmpty()) {
|
|
|
|
|
// No path - need to do Save As
|
|
|
|
|
return saveTabAs(tab);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if exportable
|
|
|
|
|
int journalId = formEditor->journalId();
|
|
|
|
|
if (journalId < 0) {
|
|
|
|
|
QMessageBox::warning(this, "Save Error",
|
|
|
|
|
"Cannot save: no journal data available.\n\n"
|
|
|
|
|
"This file was not parsed with write-back tracking enabled.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Recompiler recompiler;
|
|
|
|
|
if (!recompiler.canRecompile(journalId)) {
|
|
|
|
|
QMessageBox::warning(this, "Save Error",
|
|
|
|
|
QString("Cannot save: %1\n\n"
|
|
|
|
|
"Files that use random access reads or unsupported compression cannot be saved.")
|
|
|
|
|
.arg(recompiler.whyNotRecompilable(journalId)));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recompile
|
|
|
|
|
QStringList errors;
|
|
|
|
|
QByteArray recompiled = recompiler.recompileWithErrors(journalId, formEditor->values(), errors);
|
|
|
|
|
if (recompiled.isEmpty() && !errors.isEmpty()) {
|
|
|
|
|
QMessageBox::critical(this, "Save Error",
|
|
|
|
|
QString("Failed to recompile:\n\n%1").arg(errors.join("\n")));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write to file
|
|
|
|
|
QFile file(filePath);
|
|
|
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
|
|
|
QMessageBox::critical(this, "Save Error",
|
|
|
|
|
QString("Cannot write to file:\n%1\n\n%2").arg(filePath).arg(file.errorString()));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qint64 written = file.write(recompiled);
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
if (written != recompiled.size()) {
|
|
|
|
|
QMessageBox::critical(this, "Save Error",
|
|
|
|
|
QString("Failed to write all data to file.\nExpected %1 bytes, wrote %2 bytes.")
|
|
|
|
|
.arg(recompiled.size()).arg(written));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark as clean
|
|
|
|
|
formEditor->setModified(false);
|
|
|
|
|
DirtyStateManager::instance().markClean(tab);
|
|
|
|
|
|
|
|
|
|
if (!errors.isEmpty()) {
|
|
|
|
|
StatusBarManager::instance().updateStatus(QString("Saved with warnings: %1").arg(errors.join("; ")), 5000);
|
|
|
|
|
} else {
|
|
|
|
|
statusBar()->showMessage(QString("Saved: %1").arg(filePath), 3000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool MainWindow::saveTabAs(QWidget* tab) {
|
|
|
|
|
if (!tab) return false;
|
|
|
|
|
|
|
|
|
|
// Get the form editor from the container
|
|
|
|
|
QWidget* formEditorWidget = tab->property("formEditor").value<QWidget*>();
|
|
|
|
|
auto* formEditor = qobject_cast<ScriptTypeEditorWidget*>(formEditorWidget);
|
|
|
|
|
if (!formEditor) {
|
|
|
|
|
QMessageBox::warning(this, "Save Error", "Cannot save: not a form editor tab.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if exportable
|
|
|
|
|
int journalId = formEditor->journalId();
|
|
|
|
|
if (journalId >= 0) {
|
|
|
|
|
Recompiler recompiler;
|
|
|
|
|
if (!recompiler.canRecompile(journalId)) {
|
|
|
|
|
QMessageBox::warning(this, "Save Error",
|
|
|
|
|
QString("Cannot save: %1\n\n"
|
|
|
|
|
"Files that use random access reads or unsupported compression cannot be saved.")
|
|
|
|
|
.arg(recompiler.whyNotRecompilable(journalId)));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current file path or use parent name as default
|
|
|
|
|
QString currentPath = DirtyStateManager::instance().filePath(tab);
|
|
|
|
|
if (currentPath.isEmpty()) {
|
|
|
|
|
currentPath = tab->property("PARENT_NAME").toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show save dialog
|
|
|
|
|
QString filePath = QFileDialog::getSaveFileName(this, "Save As", currentPath, "All Files (*)");
|
|
|
|
|
if (filePath.isEmpty()) {
|
|
|
|
|
return false; // User cancelled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store the path and do regular save
|
|
|
|
|
DirtyStateManager::instance().setFilePath(tab, filePath);
|
|
|
|
|
return saveTab(tab);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainWindow::pushFieldEdit(int journalId, const QString& fieldName,
|
|
|
|
|
const QVariant& oldValue, const QVariant& newValue)
|
|
|
|
|
{
|
|
|
|
|
// Create callback to apply value changes
|
|
|
|
|
auto applyCallback = [this, journalId](const QString& field, const QVariant& value) {
|
|
|
|
|
// Find the tab widget that owns this journal
|
|
|
|
|
for (int i = 0; i < ui->tabWidget->count(); ++i) {
|
|
|
|
|
QWidget* tab = ui->tabWidget->widget(i);
|
|
|
|
|
ScriptTypeEditorWidget* editor = tab->findChild<ScriptTypeEditorWidget*>();
|
|
|
|
|
if (editor && editor->journalId() == journalId) {
|
|
|
|
|
// Apply the value without triggering a new undo command
|
|
|
|
|
editor->blockSignals(true);
|
|
|
|
|
editor->setFieldValue(field, value);
|
|
|
|
|
editor->blockSignals(false);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create and push the command
|
|
|
|
|
auto* cmd = new FieldEditCommand(journalId, fieldName, oldValue, newValue, applyCallback);
|
|
|
|
|
mUndoStack->push(cmd);
|
|
|
|
|
}
|