Add settings manager and splash screen

- Settings singleton for app-wide preferences (theme, recent files)
- Theme support with accent colors and dark/light modes
- Splash screen with custom painting, progress bar, theme colors
- Wait-for-interaction option for splash screen

🤖 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:35:31 -05:00
parent 68d749ee63
commit c041f25448
4 changed files with 763 additions and 0 deletions

370
app/settings.cpp Normal file
View File

@ -0,0 +1,370 @@
#include "settings.h"
#include "logmanager.h"
#include <QCoreApplication>
#include <QStandardPaths>
#include <QDir>
// Built-in themes
static const QMap<QString, Theme> s_themes = {
{"XPlor Dark", {
"XPlor Dark",
"#ad0c0c", // accentColor (red)
"#8a0a0a", // accentColorDark
"#c41010", // accentColorLight
"#1e1e1e", // backgroundColor
"#2d2d30", // panelColor
"#3c3c3c", // borderColor
"#d4d4d4", // textColor
"#888888" // textColorMuted
}},
{"Midnight Blue", {
"Midnight Blue",
"#0078d4", // accentColor (blue)
"#005a9e", // accentColorDark
"#1890ff", // accentColorLight
"#1a1a2e", // backgroundColor
"#16213e", // panelColor
"#0f3460", // borderColor
"#e0e0e0", // textColor
"#7f8c8d" // textColorMuted
}},
{"Forest Green", {
"Forest Green",
"#2e7d32", // accentColor (green)
"#1b5e20", // accentColorDark
"#43a047", // accentColorLight
"#1a1f1a", // backgroundColor
"#2d332d", // panelColor
"#3c4a3c", // borderColor
"#d4d8d4", // textColor
"#88918a" // textColorMuted
}},
{"Purple Haze", {
"Purple Haze",
"#7b1fa2", // accentColor (purple)
"#4a148c", // accentColorDark
"#9c27b0", // accentColorLight
"#1a1a1e", // backgroundColor
"#2d2d35", // panelColor
"#3c3c4a", // borderColor
"#d4d4dc", // textColor
"#8888a0" // textColorMuted
}},
{"Orange Sunset", {
"Orange Sunset",
"#e65100", // accentColor (orange)
"#bf360c", // accentColorDark
"#ff6d00", // accentColorLight
"#1e1a18", // backgroundColor
"#302820", // panelColor
"#4a3c30", // borderColor
"#d8d4d0", // textColor
"#908880" // textColorMuted
}},
{"Classic Dark", {
"Classic Dark",
"#505050", // accentColor (gray)
"#404040", // accentColorDark
"#606060", // accentColorLight
"#1e1e1e", // backgroundColor
"#252526", // panelColor
"#3c3c3c", // borderColor
"#d4d4d4", // textColor
"#888888" // textColorMuted
}}
};
Settings& Settings::instance()
{
static Settings instance;
return instance;
}
Settings::Settings(QObject *parent)
: QObject(parent)
, m_settings(QSettings::NativeFormat, QSettings::UserScope,
QCoreApplication::organizationName(),
QCoreApplication::applicationName())
{
// Set up debug checker for LogManager
LogManager::instance().setDebugChecker([this]() {
return debugLoggingEnabled();
});
}
void Settings::sync()
{
m_settings.sync();
}
// Theme
QString Settings::currentTheme() const
{
return m_settings.value("Appearance/Theme", "XPlor Dark").toString();
}
void Settings::setCurrentTheme(const QString& themeName)
{
if (s_themes.contains(themeName)) {
m_settings.setValue("Appearance/Theme", themeName);
emit themeChanged(s_themes[themeName]);
emit settingsChanged();
}
}
Theme Settings::theme() const
{
return getTheme(currentTheme());
}
QStringList Settings::availableThemes() const
{
return s_themes.keys();
}
Theme Settings::getTheme(const QString& name)
{
if (s_themes.contains(name)) {
return s_themes[name];
}
return s_themes["XPlor Dark"];
}
// General
QString Settings::exportDirectory() const
{
QString defaultPath = QCoreApplication::applicationDirPath() + "/exports";
return m_settings.value("General/ExportDirectory", defaultPath).toString();
}
void Settings::setExportDirectory(const QString& path)
{
m_settings.setValue("General/ExportDirectory", path);
emit settingsChanged();
}
bool Settings::autoExportOnParse() const
{
return m_settings.value("General/AutoExportOnParse", true).toBool();
}
void Settings::setAutoExportOnParse(bool enable)
{
m_settings.setValue("General/AutoExportOnParse", enable);
emit settingsChanged();
}
// Debug/Logging
bool Settings::debugLoggingEnabled() const
{
return m_settings.value("Debug/LoggingEnabled", false).toBool();
}
void Settings::setDebugLoggingEnabled(bool enable)
{
m_settings.setValue("Debug/LoggingEnabled", enable);
emit debugLoggingChanged(enable);
emit settingsChanged();
}
bool Settings::verboseParsingEnabled() const
{
return m_settings.value("Debug/VerboseParsing", false).toBool();
}
void Settings::setVerboseParsingEnabled(bool enable)
{
m_settings.setValue("Debug/VerboseParsing", enable);
emit settingsChanged();
}
bool Settings::logToFileEnabled() const
{
return m_settings.value("Debug/LogToFile", false).toBool();
}
void Settings::setLogToFileEnabled(bool enable)
{
m_settings.setValue("Debug/LogToFile", enable);
emit settingsChanged();
}
// View
QString Settings::fontFamily() const
{
return m_settings.value("View/FontFamily", "Segoe UI").toString();
}
void Settings::setFontFamily(const QString& family)
{
m_settings.setValue("View/FontFamily", family);
emit settingsChanged();
}
int Settings::fontSize() const
{
return m_settings.value("View/FontSize", 10).toInt();
}
void Settings::setFontSize(int size)
{
m_settings.setValue("View/FontSize", size);
emit settingsChanged();
}
int Settings::viewZoom() const
{
return m_settings.value("View/Zoom", 100).toInt();
}
void Settings::setViewZoom(int zoom)
{
m_settings.setValue("View/Zoom", zoom);
emit settingsChanged();
}
// Tree Widget
bool Settings::showItemCounts() const
{
return m_settings.value("Tree/ShowItemCounts", true).toBool();
}
void Settings::setShowItemCounts(bool show)
{
m_settings.setValue("Tree/ShowItemCounts", show);
emit settingsChanged();
}
bool Settings::collapseByDefault() const
{
return m_settings.value("Tree/CollapseByDefault", true).toBool();
}
void Settings::setCollapseByDefault(bool collapse)
{
m_settings.setValue("Tree/CollapseByDefault", collapse);
emit settingsChanged();
}
bool Settings::groupByExtension() const
{
return m_settings.value("Tree/GroupByExtension", false).toBool();
}
void Settings::setGroupByExtension(bool group)
{
m_settings.setValue("Tree/GroupByExtension", group);
emit settingsChanged();
}
bool Settings::naturalSorting() const
{
return m_settings.value("Tree/NaturalSorting", true).toBool();
}
void Settings::setNaturalSorting(bool enable)
{
m_settings.setValue("Tree/NaturalSorting", enable);
emit settingsChanged();
}
// Hex Viewer
int Settings::hexBytesPerLine() const
{
return m_settings.value("HexViewer/BytesPerLine", 16).toInt();
}
void Settings::setHexBytesPerLine(int bytes)
{
m_settings.setValue("HexViewer/BytesPerLine", bytes);
emit settingsChanged();
}
bool Settings::hexShowAscii() const
{
return m_settings.value("HexViewer/ShowAscii", true).toBool();
}
void Settings::setHexShowAscii(bool show)
{
m_settings.setValue("HexViewer/ShowAscii", show);
emit settingsChanged();
}
// Audio Preview
bool Settings::audioAutoPlay() const
{
return m_settings.value("Audio/AutoPlay", false).toBool();
}
void Settings::setAudioAutoPlay(bool enable)
{
m_settings.setValue("Audio/AutoPlay", enable);
emit settingsChanged();
}
// Image Preview
bool Settings::imageShowGrid() const
{
return m_settings.value("Image/ShowGrid", false).toBool();
}
void Settings::setImageShowGrid(bool show)
{
m_settings.setValue("Image/ShowGrid", show);
emit settingsChanged();
}
bool Settings::imageAutoZoom() const
{
return m_settings.value("Image/AutoZoom", true).toBool();
}
void Settings::setImageAutoZoom(bool enable)
{
m_settings.setValue("Image/AutoZoom", enable);
emit settingsChanged();
}
// Window State
QByteArray Settings::windowGeometry() const
{
return m_settings.value("Window/Geometry").toByteArray();
}
void Settings::setWindowGeometry(const QByteArray& geometry)
{
m_settings.setValue("Window/Geometry", geometry);
}
QByteArray Settings::windowState() const
{
return m_settings.value("Window/State").toByteArray();
}
void Settings::setWindowState(const QByteArray& state)
{
m_settings.setValue("Window/State", state);
}
// Recent Files
QStringList Settings::recentFiles() const
{
return m_settings.value("RecentFiles/List").toStringList();
}
void Settings::addRecentFile(const QString& path)
{
QStringList files = recentFiles();
files.removeAll(path);
files.prepend(path);
while (files.size() > 10) {
files.removeLast();
}
m_settings.setValue("RecentFiles/List", files);
}
void Settings::clearRecentFiles()
{
m_settings.setValue("RecentFiles/List", QStringList());
}

124
app/settings.h Normal file
View File

@ -0,0 +1,124 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <QObject>
#include <QSettings>
#include <QString>
#include <QFont>
#include <QColor>
// Theme definition
struct Theme {
QString name;
QString accentColor;
QString accentColorDark;
QString accentColorLight;
QString backgroundColor;
QString panelColor;
QString borderColor;
QString textColor;
QString textColorMuted;
};
class Settings : public QObject
{
Q_OBJECT
public:
static Settings& instance();
// Theme
QString currentTheme() const;
void setCurrentTheme(const QString& themeName);
Theme theme() const;
QStringList availableThemes() const;
static Theme getTheme(const QString& name);
// General
QString exportDirectory() const;
void setExportDirectory(const QString& path);
bool autoExportOnParse() const;
void setAutoExportOnParse(bool enable);
// Debug/Logging
bool debugLoggingEnabled() const;
void setDebugLoggingEnabled(bool enable);
bool verboseParsingEnabled() const;
void setVerboseParsingEnabled(bool enable);
bool logToFileEnabled() const;
void setLogToFileEnabled(bool enable);
// View
QString fontFamily() const;
void setFontFamily(const QString& family);
int fontSize() const;
void setFontSize(int size);
int viewZoom() const;
void setViewZoom(int zoom);
// Tree Widget
bool showItemCounts() const;
void setShowItemCounts(bool show);
bool collapseByDefault() const;
void setCollapseByDefault(bool collapse);
bool groupByExtension() const;
void setGroupByExtension(bool group);
bool naturalSorting() const;
void setNaturalSorting(bool enable);
// Hex Viewer
int hexBytesPerLine() const;
void setHexBytesPerLine(int bytes);
bool hexShowAscii() const;
void setHexShowAscii(bool show);
// Audio Preview
bool audioAutoPlay() const;
void setAudioAutoPlay(bool enable);
// Image Preview
bool imageShowGrid() const;
void setImageShowGrid(bool show);
bool imageAutoZoom() const;
void setImageAutoZoom(bool enable);
// Window State
QByteArray windowGeometry() const;
void setWindowGeometry(const QByteArray& geometry);
QByteArray windowState() const;
void setWindowState(const QByteArray& state);
// Recent Files
QStringList recentFiles() const;
void addRecentFile(const QString& path);
void clearRecentFiles();
// Sync to disk
void sync();
signals:
void debugLoggingChanged(bool enabled);
void themeChanged(const Theme& theme);
void settingsChanged();
private:
explicit Settings(QObject *parent = nullptr);
~Settings() = default;
Settings(const Settings&) = delete;
Settings& operator=(const Settings&) = delete;
QSettings m_settings;
};
#endif // SETTINGS_H

219
app/splashscreen.cpp Normal file
View File

@ -0,0 +1,219 @@
#include "splashscreen.h"
#include "settings.h"
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QScreen>
#include <QCoreApplication>
#include <QDate>
SplashScreen::SplashScreen(QWidget *parent)
: QSplashScreen()
{
Q_UNUSED(parent);
// Load theme colors
loadThemeColors();
// Create transparent pixmap
QPixmap pixmap(WIDTH, HEIGHT);
pixmap.fill(Qt::transparent);
setPixmap(pixmap);
setWindowFlags(Qt::SplashScreen | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_TranslucentBackground);
// Center on screen
if (QScreen *screen = QApplication::primaryScreen()) {
QRect screenGeometry = screen->geometry();
int x = (screenGeometry.width() - WIDTH) / 2;
int y = (screenGeometry.height() - HEIGHT) / 2;
move(x, y);
}
}
void SplashScreen::loadThemeColors()
{
Theme theme = Settings::instance().theme();
mPrimaryColor = QColor(theme.accentColor);
mBgColor = QColor(theme.backgroundColor);
mPanelColor = QColor(theme.panelColor);
mTextColor = QColor(theme.textColor);
mTextColorMuted = QColor(theme.textColorMuted);
mBorderColor = QColor(theme.borderColor);
}
void SplashScreen::setStatus(const QString &message)
{
mStatus = message;
repaint();
QApplication::processEvents();
}
void SplashScreen::setProgress(int value, int max)
{
mProgress = value;
mProgressMax = max;
repaint();
QApplication::processEvents();
}
void SplashScreen::setWaitForInteraction(bool wait)
{
mWaitForInteraction = wait;
}
void SplashScreen::finish(QWidget *mainWindow)
{
// Always show the main window
if (mainWindow) {
mainWindow->show();
}
if (mWaitForInteraction && !mInteractionReceived) {
// Keep splash visible on top until user interacts
mPendingMainWindow = mainWindow;
raise();
activateWindow();
mStatus = "Click or press any key to continue...";
repaint();
return;
}
// Normal behavior - close splash
close();
}
void SplashScreen::mousePressEvent(QMouseEvent *event)
{
Q_UNUSED(event);
mInteractionReceived = true;
if (mWaitForInteraction && mPendingMainWindow) {
close(); // Just close the splash
}
}
void SplashScreen::keyPressEvent(QKeyEvent *event)
{
Q_UNUSED(event);
mInteractionReceived = true;
if (mWaitForInteraction && mPendingMainWindow) {
close(); // Just close the splash
}
}
void SplashScreen::drawContents(QPainter *painter)
{
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setRenderHint(QPainter::TextAntialiasing, true);
// Create clipping path for rounded corners
QPainterPath clipPath;
clipPath.addRoundedRect(0, 0, WIDTH, HEIGHT, 12, 12);
painter->setClipPath(clipPath);
// Draw background
painter->setPen(Qt::NoPen);
painter->setBrush(mBgColor);
painter->drawRect(0, 0, WIDTH, HEIGHT);
// Draw accent stripe at top (clipped to rounded corners)
painter->setBrush(mPrimaryColor);
painter->drawRect(0, 0, WIDTH, 8);
// Draw logo/title area
QFont titleFont("Segoe UI", 36, QFont::Bold);
painter->setFont(titleFont);
painter->setPen(mTextColor);
// App name with primary color accent
QString appName = "XPlor";
QFontMetrics titleMetrics(titleFont);
int titleWidth = titleMetrics.horizontalAdvance(appName);
int titleX = (WIDTH - titleWidth) / 2;
int titleY = 80;
// Draw "X" in primary color
painter->setPen(mPrimaryColor);
painter->drawText(titleX, titleY, "X");
// Draw "Plor" in text color
painter->setPen(mTextColor);
int xWidth = titleMetrics.horizontalAdvance("X");
painter->drawText(titleX + xWidth, titleY, "Plor");
// Tagline
QFont taglineFont("Segoe UI", 11);
painter->setFont(taglineFont);
painter->setPen(mTextColorMuted);
QString tagline = "Binary File Format Explorer";
QFontMetrics taglineMetrics(taglineFont);
int taglineWidth = taglineMetrics.horizontalAdvance(tagline);
painter->drawText((WIDTH - taglineWidth) / 2, titleY + 25, tagline);
// Version info
QFont versionFont("Segoe UI", 10);
painter->setFont(versionFont);
painter->setPen(mTextColorMuted);
QString version = QString("Version %1").arg(QCoreApplication::applicationVersion());
QFontMetrics versionMetrics(versionFont);
int versionWidth = versionMetrics.horizontalAdvance(version);
painter->drawText((WIDTH - versionWidth) / 2, titleY + 50, version);
// Company/copyright - use QCoreApplication values
QFont copyrightFont("Segoe UI", 9);
painter->setFont(copyrightFont);
painter->setPen(mTextColorMuted);
QString orgName = QCoreApplication::organizationName();
QFontMetrics copyrightMetrics(copyrightFont);
int copyrightWidth = copyrightMetrics.horizontalAdvance(orgName);
painter->drawText((WIDTH - copyrightWidth) / 2, HEIGHT - 45, orgName);
QString year = QString::number(QDate::currentDate().year());
int yearWidth = copyrightMetrics.horizontalAdvance(year);
painter->drawText((WIDTH - yearWidth) / 2, HEIGHT - 30, year);
// Progress bar background
int progressX = 40;
int progressY = HEIGHT - 80;
int progressWidth = WIDTH - 80;
int progressHeight = 6;
painter->setPen(Qt::NoPen);
painter->setBrush(mPanelColor);
painter->drawRoundedRect(progressX, progressY, progressWidth, progressHeight, 3, 3);
// Progress bar fill
if (mProgressMax > 0 && mProgress > 0) {
int fillWidth = (progressWidth * mProgress) / mProgressMax;
if (fillWidth > 0) {
// Gradient from primary to lighter
QLinearGradient gradient(progressX, 0, progressX + fillWidth, 0);
gradient.setColorAt(0, mPrimaryColor);
gradient.setColorAt(1, mPrimaryColor.lighter(120));
painter->setBrush(gradient);
painter->drawRoundedRect(progressX, progressY, fillWidth, progressHeight, 3, 3);
}
}
// Status text
if (!mStatus.isEmpty()) {
QFont statusFont("Segoe UI", 9);
painter->setFont(statusFont);
painter->setPen(mTextColorMuted);
QFontMetrics statusMetrics(statusFont);
QString elidedStatus = statusMetrics.elidedText(mStatus, Qt::ElideMiddle, progressWidth);
painter->drawText(progressX, progressY - 8, elidedStatus);
}
// Draw subtle border (disable clipping first)
painter->setClipping(false);
painter->setPen(QPen(mBorderColor, 1));
painter->setBrush(Qt::NoBrush);
painter->drawRoundedRect(0, 0, WIDTH - 1, HEIGHT - 1, 12, 12);
}

50
app/splashscreen.h Normal file
View File

@ -0,0 +1,50 @@
#ifndef SPLASHSCREEN_H
#define SPLASHSCREEN_H
#include <QSplashScreen>
#include <QLabel>
#include <QProgressBar>
#include <QColor>
#include <QMouseEvent>
#include <QKeyEvent>
class SplashScreen : public QSplashScreen
{
Q_OBJECT
public:
explicit SplashScreen(QWidget *parent = nullptr);
void setStatus(const QString &message);
void setProgress(int value, int max = 100);
void setWaitForInteraction(bool wait);
void finish(QWidget *mainWindow);
protected:
void drawContents(QPainter *painter) override;
void mousePressEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
void loadThemeColors();
QString mStatus;
int mProgress = 0;
int mProgressMax = 100;
bool mWaitForInteraction = false;
bool mInteractionReceived = false;
QWidget *mPendingMainWindow = nullptr;
// Theme colors
QColor mPrimaryColor;
QColor mBgColor;
QColor mPanelColor;
QColor mTextColor;
QColor mTextColorMuted;
QColor mBorderColor;
static constexpr int WIDTH = 480;
static constexpr int HEIGHT = 300;
};
#endif // SPLASHSCREEN_H