Update main app with theming and UI improvements

- App metadata defines at top of main.cpp
- Dynamic themed app icon (accent color replaces red)
- Icon refreshes on theme change
- Disabled unimplemented menu actions
- Image preview with theme support
- Updated CLI help text with feature list
- Splash screen integration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
njohnson 2026-01-08 00:38:04 -05:00
parent 42373120b0
commit 5f43466057
6 changed files with 1195 additions and 76 deletions

View File

@ -6,6 +6,7 @@
#include <QScrollBar>
#include <QWheelEvent>
#include <QEvent>
#include <QHeaderView>
#include "statusbarmanager.h"
ImagePreviewWidget::ImagePreviewWidget(QWidget *parent)
@ -13,8 +14,10 @@ ImagePreviewWidget::ImagePreviewWidget(QWidget *parent)
, mImageLabel(new QLabel(this))
, mInfoLabel(new QLabel(this))
, mScrollArea(new QScrollArea(this))
, mMetadataTree(new QTreeWidget(this))
, mDragging(false)
, mZoomFactor(1.0)
, mBitsPerPixel(0)
{
mImageLabel->setAlignment(Qt::AlignCenter);
mImageLabel->setBackgroundRole(QPalette::Base);
@ -31,11 +34,23 @@ ImagePreviewWidget::ImagePreviewWidget(QWidget *parent)
mInfoLabel->setAlignment(Qt::AlignCenter);
mInfoLabel->setStyleSheet("QLabel { background-color: #333; color: #fff; padding: 4px; }");
// Setup metadata tree
mMetadataTree->setHeaderLabels({"Property", "Value"});
mMetadataTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
mMetadataTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
mMetadataTree->setAlternatingRowColors(true);
// Create splitter for image and metadata
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(mScrollArea);
splitter->addWidget(mMetadataTree);
splitter->setSizes({500, 200});
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(mInfoLabel);
layout->addWidget(mScrollArea, 1);
layout->addWidget(splitter, 1);
setLayout(layout);
// Enable mouse tracking for drag-to-pan
@ -50,6 +65,53 @@ void ImagePreviewWidget::setFilename(const QString &filename)
mFilename = filename;
}
void ImagePreviewWidget::setMetadata(const QVariantMap &metadata)
{
// Add parsed metadata fields to tree
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
if (it.key().startsWith("_")) continue; // Skip internal fields
auto *item = new QTreeWidgetItem(mMetadataTree);
item->setText(0, it.key());
QVariant val = it.value();
if (val.typeId() == QMetaType::QByteArray) {
QByteArray ba = val.toByteArray();
item->setText(1, QString("<%1 bytes>").arg(ba.size()));
} else {
item->setText(1, val.toString());
}
}
}
void ImagePreviewWidget::updateMetadataDisplay()
{
mMetadataTree->clear();
// Add image info
auto *dimItem = new QTreeWidgetItem(mMetadataTree);
dimItem->setText(0, "Dimensions");
dimItem->setText(1, QString("%1 x %2").arg(mImageSize.width()).arg(mImageSize.height()));
if (!mDetectedFormat.isEmpty()) {
auto *formatItem = new QTreeWidgetItem(mMetadataTree);
formatItem->setText(0, "Format");
formatItem->setText(1, mDetectedFormat);
}
if (mBitsPerPixel > 0) {
auto *bppItem = new QTreeWidgetItem(mMetadataTree);
bppItem->setText(0, "Bit Depth");
bppItem->setText(1, QString("%1-bit").arg(mBitsPerPixel));
}
if (!mCompression.isEmpty()) {
auto *compItem = new QTreeWidgetItem(mMetadataTree);
compItem->setText(0, "Compression");
compItem->setText(1, mCompression);
}
}
QSize ImagePreviewWidget::imageSize() const
{
return mImageSize;
@ -166,19 +228,28 @@ void ImagePreviewWidget::updateZoom()
bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &format)
{
// Reset format info
mDetectedFormat.clear();
mBitsPerPixel = 0;
mCompression.clear();
// Check for Xbox texture formats (XBTX2D, TX2D, etc.)
if (data.size() >= 8) {
QString magic = QString::fromLatin1(data.left(8));
if (magic.startsWith("XBTX2D") || magic.startsWith("TX2D") || magic.startsWith("TX1D") || magic.startsWith("TX3D")) {
mDetectedFormat = "Xbox 360 Texture";
mCompression = "DXT";
// Try to decode Xbox 360 texture
QImage image = loadXBTX2D(data);
if (!image.isNull()) {
mImageSize = image.size();
mBitsPerPixel = image.depth();
mOriginalPixmap = QPixmap::fromImage(image);
mZoomFactor = 1.0;
mImageLabel->setPixmap(mOriginalPixmap);
mImageLabel->adjustSize();
mInfoLabel->setText(QString("%1 - %2x%3 (Xbox 360)").arg(mFilename).arg(image.width()).arg(image.height()));
updateMetadataDisplay();
return true;
}
// Fallback to info display if decode fails
@ -186,15 +257,19 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
.arg(magic.left(6).trimmed())
.arg(data.size()));
mInfoLabel->setText(QString("%1 - Xbox 360 Texture (%2 bytes)").arg(mFilename).arg(data.size()));
updateMetadataDisplay();
return true;
}
}
// Check for DDS format (starts with "DDS ")
if (data.size() >= 4 && data.left(4) == "DDS ") {
mDetectedFormat = "DDS";
mCompression = "DXT";
mImageLabel->setText(QString("DDS Texture Format\n\nSize: %1 bytes\n\nDDS preview not yet implemented.")
.arg(data.size()));
mInfoLabel->setText(QString("%1 - DDS Texture (%2 bytes)").arg(mFilename).arg(data.size()));
updateMetadataDisplay();
return true;
}
@ -203,6 +278,7 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
// Try standard Qt loading first
if (!format.isEmpty()) {
image.loadFromData(data, format.toUtf8().constData());
mDetectedFormat = format.toUpper();
}
if (image.isNull()) {
@ -213,6 +289,7 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
if (image.isNull() && (format.toLower() == "tga" ||
(data.size() > 18 && mFilename.toLower().endsWith(".tga")))) {
image = loadTGA(data);
mDetectedFormat = "TGA";
}
if (image.isNull()) {
@ -229,10 +306,12 @@ bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &for
}
mImageSize = image.size();
mBitsPerPixel = image.depth();
mOriginalPixmap = QPixmap::fromImage(image);
mZoomFactor = 1.0;
mImageLabel->setPixmap(mOriginalPixmap);
mImageLabel->adjustSize();
updateMetadataDisplay();
QString info = QString("%1 - %2x%3 - %4 bytes")
.arg(mFilename)

View File

@ -8,6 +8,8 @@
#include <QPixmap>
#include <QImage>
#include <QMouseEvent>
#include <QTreeWidget>
#include <QSplitter>
class ImagePreviewWidget : public QWidget
{
@ -26,6 +28,9 @@ public:
// Set the filename for display
void setFilename(const QString &filename);
// Set metadata to display in the properties panel
void setMetadata(const QVariantMap &metadata);
// Get current image size
QSize imageSize() const;
@ -41,6 +46,7 @@ private:
QLabel *mImageLabel;
QLabel *mInfoLabel;
QScrollArea *mScrollArea;
QTreeWidget *mMetadataTree;
QString mFilename;
QSize mImageSize;
@ -52,12 +58,18 @@ private:
double mZoomFactor;
QPixmap mOriginalPixmap;
void updateZoom();
void updateMetadataDisplay();
// Try to load TGA manually if Qt can't handle it
QImage loadTGA(const QByteArray &data);
// Load Xbox 360 XBTX2D texture format
QImage loadXBTX2D(const QByteArray &data);
// Detected image info
QString mDetectedFormat;
int mBitsPerPixel;
QString mCompression;
};
#endif // IMAGEPREVIEWWIDGET_H

View File

@ -1,5 +1,13 @@
#include "mainwindow.h"
#include "splashscreen.h"
#include "typeregistry.h"
#include "settings.h"
// Application metadata
#define APP_NAME "XPlor"
#define APP_VERSION "1.0.8"
#define APP_ORG_NAME "RedLine Solutions LLC."
#define APP_ORG_DOMAIN "redline.llc"
#include <QApplication>
#include <QCoreApplication>
@ -12,6 +20,9 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QTextStream>
#include <QThread>
#include <QImage>
#include <QIcon>
#include <iostream>
#ifdef Q_OS_WIN
@ -102,6 +113,55 @@ static QJsonValue variantToJson(const QVariant& v) {
return v.toString();
}
// Generate themed app icon by replacing red with accent color
static QIcon generateThemedIcon(const QColor &accentColor) {
// Try loading from Qt resource first (XPlor.png)
QImage image(":/images/images/XPlor.png");
if (image.isNull()) {
// Fallback: try app.ico in app directory
QString iconPath = QCoreApplication::applicationDirPath() + "/app.ico";
image = QImage(iconPath);
}
if (image.isNull()) {
return QIcon();
}
// Convert to ARGB32 for pixel manipulation
image = image.convertToFormat(QImage::Format_ARGB32);
// Replace red-ish pixels with the accent color
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)
// The icon uses #ad0c0c (173, 12, 12) as the red color
if (r > 100 && g < 80 && b < 80 && a > 0) {
// Calculate how "red" this pixel is (0-1 scale)
float intensity = static_cast<float>(r) / 255.0f;
// Apply the accent color with the same intensity
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));
}
// CLI output - writes to both console and log file for reliability
static QFile* g_logFile = nullptr;
@ -238,30 +298,41 @@ int main(int argc, char *argv[])
attachWindowsConsole();
#endif
QCoreApplication app(argc, argv);
app.setApplicationName("XPlor");
app.setApplicationVersion("1.0");
app.setOrganizationDomain(APP_ORG_DOMAIN);
app.setOrganizationName(APP_ORG_NAME);
app.setApplicationName(APP_NAME);
app.setApplicationVersion(APP_VERSION);
// Initialize log file for CLI output
initCliLog();
QCommandLineParser parser;
parser.setApplicationDescription("XPlor - Call of Duty FastFile Explorer");
parser.setApplicationDescription(
"XPlor - Binary File Format Explorer\n\n"
"Parse and explore binary file formats using XScript definitions.\n"
"Supports Call of Duty FastFiles, Asura archives, and custom formats.\n\n"
"Features:\n"
" - XScript DSL for defining binary structures\n"
" - Hex viewer with highlighting\n"
" - Audio/image preview for embedded assets\n"
" - Theme support with customizable colors"
);
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption cliOption(QStringList() << "cli" << "parse" << "p", "Run in CLI mode (no GUI)");
parser.addOption(cliOption);
QCommandLineOption jsonOption(QStringList() << "json" << "j", "Output results as JSON");
QCommandLineOption jsonOption(QStringList() << "json" << "j", "Output parsed data as JSON");
parser.addOption(jsonOption);
QCommandLineOption gameOption(QStringList() << "game" << "g", "Game identifier", "game", "COD4");
QCommandLineOption gameOption(QStringList() << "game" << "g", "Game identifier (e.g., COD4, COD5, MW2)", "game", "COD4");
parser.addOption(gameOption);
QCommandLineOption platformOption(QStringList() << "platform" << "t", "Platform (PC, Xbox360, PS3)", "platform", "PC");
QCommandLineOption platformOption(QStringList() << "platform" << "t", "Target platform (PC, Xbox360, PS3)", "platform", "PC");
parser.addOption(platformOption);
parser.addPositionalArgument("file", "FastFile to parse");
parser.addPositionalArgument("file", "Binary file to parse (e.g., FastFile, archive)");
parser.process(app);
const QStringList args = parser.positionalArguments();
@ -284,7 +355,85 @@ int main(int argc, char *argv[])
}
QApplication a(argc, argv);
a.setOrganizationDomain(APP_ORG_DOMAIN);
a.setOrganizationName(APP_ORG_NAME);
a.setApplicationName(APP_NAME);
a.setApplicationVersion(APP_VERSION);
// Set themed window icon
Theme currentTheme = Settings::instance().theme();
QIcon themedIcon = generateThemedIcon(QColor(currentTheme.accentColor));
if (!themedIcon.isNull()) {
a.setWindowIcon(themedIcon);
}
// Show splash screen
SplashScreen splash;
splash.setWaitForInteraction(false); // Normal behavior - close when finished
splash.show();
a.processEvents();
splash.setStatus("Initializing...");
splash.setProgress(0, 100);
a.processEvents();
// Load definitions with progress updates
splash.setStatus("Loading definitions...");
splash.setProgress(10, 100);
a.processEvents();
const QString definitionsDir = QCoreApplication::applicationDirPath() + "/definitions/";
// First pass: count files
QStringList defFiles;
QDirIterator countIt(definitionsDir, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
while (countIt.hasNext()) {
defFiles.append(countIt.next());
}
// Second pass: load definitions
TypeRegistry registry;
QVector<DefinitionLoadResult> defResults;
int loaded = 0;
int total = defFiles.size();
for (const QString& path : defFiles) {
QString fileName = QFileInfo(path).fileName();
splash.setStatus(QString("Loading: %1").arg(fileName));
splash.setProgress(10 + (loaded * 70 / qMax(1, total)), 100);
a.processEvents();
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
defResults.append({path, fileName, false, "Failed to open file"});
} else {
try {
registry.ingestScript(QString::fromUtf8(f.readAll()), path);
defResults.append({path, fileName, true, QString()});
} catch (const std::exception& e) {
defResults.append({path, fileName, false, QString::fromUtf8(e.what())});
}
}
loaded++;
}
splash.setStatus("Creating main window...");
splash.setProgress(85, 100);
a.processEvents();
MainWindow w;
w.show();
// Pass loaded definitions to MainWindow
w.setTypeRegistry(std::move(registry), defResults);
splash.setStatus("Ready");
splash.setProgress(100, 100);
a.processEvents();
QThread::msleep(200);
// finish() will show the main window and keep splash on top if waitForInteraction is enabled
splash.finish(&w);
return a.exec();
}

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,17 @@
#define MAINWINDOW_H
#include <QMainWindow>
#include <QFrame>
#include "typeregistry.h"
#include "settings.h"
struct DefinitionLoadResult {
QString filePath;
QString fileName;
bool success;
QString errorMessage;
};
class XTreeWidget;
class XTreeWidgetItem;
@ -24,20 +33,27 @@ public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void LoadDefinitions();
void LoadDefinitions(); // For reparse
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
void LoadTreeCategories();
void Reset();
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
QString pluralizeType(const QString &typeName);
XTreeWidgetItem *ensureTypeCategoryRoot(const QString &typeName);
XTreeWidgetItem *ensureSubcategory(XTreeWidgetItem *instanceNode, const QString &childTypeName);
void routeNestedObjects(XTreeWidgetItem *ownerInstanceNode, const QVariantMap &vars);
XTreeWidgetItem *addInstanceNode(XTreeWidgetItem *parent, const QString &displayName, const QString &typeName, const QVariantMap &vars);
void organizeChildrenByExtension(XTreeWidgetItem *parent);
void updateTreeNodeCounts(XTreeWidgetItem *node);
static QString instanceDisplayFor(const QVariantMap &obj, const QString &fallbackType, const QString &fallbackKey = {}, std::optional<int> index = std::nullopt);
private slots:
void HandleLogEntry(const QString &entry);
void HandleStatusUpdate(const QString &message, int timeout);
void HandleProgressUpdate(const QString &message, int progress, int max);
void applyTheme(const Theme &theme);
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
@ -50,10 +66,12 @@ private:
XTreeWidget *mTreeWidget;
QPlainTextEdit *mLogWidget;
QProgressBar *mProgressBar;
QFrame *mRibbon;
QHash<QString, XTreeWidgetItem*> mTypeCategoryRoots;
TypeRegistry mTypeRegistry;
QStringList mOpenedFilePaths; // Track opened files for reparsing
QVector<DefinitionLoadResult> mDefinitionResults;
// Actions - File menu
QAction *actionNew;
@ -76,6 +94,7 @@ private:
// Actions - Tools menu
QAction *actionRunTests;
QAction *actionViewDefinitions;
// Actions - Help menu
QAction *actionAbout;

View File

@ -54,25 +54,21 @@
<property name="title">
<string>File</string>
</property>
<!-- Actions added in mainwindow.cpp -->
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
<!-- Actions added in mainwindow.cpp -->
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<!-- Actions added in mainwindow.cpp -->
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
<string>Tools</string>
</property>
<!-- Actions added in mainwindow.cpp -->
</widget>
<addaction name="MenuDef"/>
<addaction name="menuEdit"/>
@ -91,10 +87,7 @@
</attribute>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<!-- All actions are now created in mainwindow.cpp -->
</widget>
<resources>
<include location="../data/data.qrc"/>
</resources>
<resources/>
<connections/>
</ui>