XPlor/app/settings.cpp
njohnson d7488c5fa9 Add comprehensive export system with format-specific dialogs
Implement a unified export system for extracting data from parsed files:

ExportManager (singleton):
- Centralized export handling for all content types
- Content type detection (image, audio, video, text, binary)
- Batch export support with progress tracking

Format-specific export dialogs:
- ImageExportDialog: PNG, JPEG, BMP, TGA with quality options
- AudioExportDialog: WAV, MP3, OGG with FFmpeg integration
- BinaryExportDialog: Raw data export with optional decompression
- BatchExportDialog: Recursive export with filtering options
- Base ExportDialog class for common functionality

Settings additions:
- FFmpeg path configuration with auto-detection
- Search common install locations and PATH

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 20:54:38 -05:00

832 lines
21 KiB
C++

#include "settings.h"
#include "logmanager.h"
#include <QCoreApplication>
#include <QStandardPaths>
#include <QDir>
#include <QFileInfo>
// 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();
});
// Set up log-to-file checker for LogManager
LogManager::instance().setLogToFileChecker([this]() {
return logToFileEnabled();
});
}
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();
}
// Tools
QString Settings::quickBmsPath() const
{
QString path = m_settings.value("Tools/QuickBmsPath").toString();
if (path.isEmpty() || !QFileInfo::exists(path)) {
path = findQuickBms();
}
return path;
}
void Settings::setQuickBmsPath(const QString& path)
{
m_settings.setValue("Tools/QuickBmsPath", path);
emit settingsChanged();
}
QString Settings::findQuickBms()
{
// Common locations to search for QuickBMS
QStringList searchPaths = {
QCoreApplication::applicationDirPath() + "/quickbms.exe",
QCoreApplication::applicationDirPath() + "/tools/quickbms.exe",
QCoreApplication::applicationDirPath() + "/quickbms/quickbms.exe",
"C:/Program Files/QuickBMS/quickbms.exe",
"C:/Program Files (x86)/QuickBMS/quickbms.exe",
"C:/QuickBMS/quickbms.exe",
QDir::homePath() + "/QuickBMS/quickbms.exe",
"E:/Software/QuickBMS/quickbms.exe", // Legacy path
};
// Also check PATH environment
QString pathEnv = qEnvironmentVariable("PATH");
QStringList pathDirs = pathEnv.split(';', Qt::SkipEmptyParts);
for (const QString& dir : pathDirs) {
searchPaths.append(dir + "/quickbms.exe");
}
for (const QString& path : searchPaths) {
if (QFileInfo::exists(path)) {
return QDir::cleanPath(path);
}
}
return QString(); // Not found
}
QString Settings::pythonPath() const
{
QString path = m_settings.value("Tools/PythonPath").toString();
if (path.isEmpty() || !QFileInfo::exists(path)) {
path = findPython();
}
return path;
}
void Settings::setPythonPath(const QString& path)
{
m_settings.setValue("Tools/PythonPath", path);
emit settingsChanged();
}
QString Settings::findPython()
{
// Common locations to search for Python
QStringList searchPaths = {
"python",
"python3",
"C:/Python312/python.exe",
"C:/Python311/python.exe",
"C:/Python310/python.exe",
"C:/Program Files/Python312/python.exe",
"C:/Program Files/Python311/python.exe",
QDir::homePath() + "/AppData/Local/Programs/Python/Python312/python.exe",
QDir::homePath() + "/AppData/Local/Programs/Python/Python311/python.exe",
"/usr/bin/python3",
"/usr/bin/python",
};
QString pathEnv = qEnvironmentVariable("PATH");
#ifdef Q_OS_WIN
QStringList pathDirs = pathEnv.split(';', Qt::SkipEmptyParts);
#else
QStringList pathDirs = pathEnv.split(':', Qt::SkipEmptyParts);
#endif
for (const QString& dir : pathDirs) {
searchPaths.append(dir + "/python.exe");
searchPaths.append(dir + "/python3.exe");
}
for (const QString& path : searchPaths) {
if (QFileInfo::exists(path)) {
return QDir::cleanPath(path);
}
}
return QString();
}
QString Settings::ffmpegPath() const
{
QString path = m_settings.value("Tools/FFmpegPath").toString();
if (path.isEmpty() || !QFileInfo::exists(path)) {
path = findFFmpeg();
}
return path;
}
void Settings::setFFmpegPath(const QString& path)
{
m_settings.setValue("Tools/FFmpegPath", path);
emit settingsChanged();
}
QString Settings::findFFmpeg()
{
// Common locations to search for FFmpeg
QStringList searchPaths = {
"ffmpeg",
"ffmpeg.exe",
"C:/ffmpeg/bin/ffmpeg.exe",
"C:/Program Files/ffmpeg/bin/ffmpeg.exe",
"C:/Program Files (x86)/ffmpeg/bin/ffmpeg.exe",
QDir::homePath() + "/ffmpeg/bin/ffmpeg.exe",
"/usr/bin/ffmpeg",
"/usr/local/bin/ffmpeg",
};
QString pathEnv = qEnvironmentVariable("PATH");
#ifdef Q_OS_WIN
QStringList pathDirs = pathEnv.split(';', Qt::SkipEmptyParts);
#else
QStringList pathDirs = pathEnv.split(':', Qt::SkipEmptyParts);
#endif
for (const QString& dir : pathDirs) {
searchPaths.append(dir + "/ffmpeg.exe");
searchPaths.append(dir + "/ffmpeg");
}
for (const QString& path : searchPaths) {
if (QFileInfo::exists(path)) {
return QDir::cleanPath(path);
}
}
return QString();
}
QString Settings::scriptsDirectory() const
{
QString defaultPath = QCoreApplication::applicationDirPath() + "/scripts";
return m_settings.value("Tools/ScriptsDirectory", defaultPath).toString();
}
void Settings::setScriptsDirectory(const QString& path)
{
m_settings.setValue("Tools/ScriptsDirectory", path);
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);
m_settings.sync(); // Ensure immediate persistence
emit debugLoggingChanged(enable);
emit settingsChanged();
// Provide immediate feedback in log panel
if (enable) {
LogManager::instance().addEntry("[SETTINGS] Debug logging ENABLED - parse a file to see debug output");
} else {
LogManager::instance().addEntry("[SETTINGS] Debug logging DISABLED");
}
}
bool Settings::verboseParsingEnabled() const
{
return m_settings.value("Debug/VerboseParsing", false).toBool();
}
void Settings::setVerboseParsingEnabled(bool enable)
{
m_settings.setValue("Debug/VerboseParsing", enable);
m_settings.sync(); // Ensure immediate persistence
emit settingsChanged();
// Provide immediate feedback in log panel
if (enable) {
LogManager::instance().addEntry("[SETTINGS] Verbose parsing ENABLED");
} else {
LogManager::instance().addEntry("[SETTINGS] Verbose parsing DISABLED");
}
}
bool Settings::logToFileEnabled() const
{
return m_settings.value("Debug/LogToFile", false).toBool();
}
void Settings::setLogToFileEnabled(bool enable)
{
m_settings.setValue("Debug/LogToFile", enable);
m_settings.sync(); // Ensure immediate persistence
emit settingsChanged();
// Provide immediate feedback in log panel
if (enable) {
LogManager::instance().addEntry("[SETTINGS] Log to file ENABLED");
} else {
LogManager::instance().addEntry("[SETTINGS] Log to file DISABLED");
}
}
// 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();
}
// File Type Associations
static const QStringList s_defaultTextExtensions = {
"txt", "xml", "json", "csv", "cfg", "ini", "log",
"html", "htm", "css", "js", "lua", "py", "sh", "bat",
"md", "yaml", "yml", "gsc", "csc", "arena", "vision"
};
static const QStringList s_defaultImageExtensions = {
"tga", "dds", "png", "jpg", "jpeg", "bmp", "xbtex", "iwi"
};
static const QStringList s_defaultAudioExtensions = {
"wav", "wave", "mp3", "ogg", "flac", "raw"
};
static const QStringList s_defaultListExtensions = {
"str"
};
QStringList Settings::textFileExtensions() const
{
QStringList exts = m_settings.value("FileTypes/Text").toStringList();
if (exts.isEmpty()) {
return s_defaultTextExtensions;
}
return exts;
}
void Settings::setTextFileExtensions(const QStringList& extensions)
{
m_settings.setValue("FileTypes/Text", extensions);
emit settingsChanged();
}
QStringList Settings::imageFileExtensions() const
{
QStringList exts = m_settings.value("FileTypes/Image").toStringList();
if (exts.isEmpty()) {
return s_defaultImageExtensions;
}
return exts;
}
void Settings::setImageFileExtensions(const QStringList& extensions)
{
m_settings.setValue("FileTypes/Image", extensions);
emit settingsChanged();
}
QStringList Settings::audioFileExtensions() const
{
QStringList exts = m_settings.value("FileTypes/Audio").toStringList();
if (exts.isEmpty()) {
return s_defaultAudioExtensions;
}
return exts;
}
void Settings::setAudioFileExtensions(const QStringList& extensions)
{
m_settings.setValue("FileTypes/Audio", extensions);
emit settingsChanged();
}
QStringList Settings::listFileExtensions() const
{
QStringList exts = m_settings.value("FileTypes/List").toStringList();
if (exts.isEmpty()) {
return s_defaultListExtensions;
}
return exts;
}
void Settings::setListFileExtensions(const QStringList& extensions)
{
m_settings.setValue("FileTypes/List", extensions);
emit settingsChanged();
}
QString Settings::viewerForExtension(const QString& extension) const
{
QString ext = extension.toLower();
if (ext.startsWith('.')) {
ext = ext.mid(1);
}
if (textFileExtensions().contains(ext, Qt::CaseInsensitive)) {
return "text";
}
if (imageFileExtensions().contains(ext, Qt::CaseInsensitive)) {
return "image";
}
if (audioFileExtensions().contains(ext, Qt::CaseInsensitive)) {
return "audio";
}
if (listFileExtensions().contains(ext, Qt::CaseInsensitive)) {
return "list";
}
return "hex"; // Default to hex viewer
}
void Settings::setViewerForExtension(const QString& extension, const QString& viewer)
{
QString ext = extension.toLower();
if (ext.startsWith('.')) {
ext = ext.mid(1);
}
// Remove from all lists first
QStringList textExts = textFileExtensions();
QStringList imageExts = imageFileExtensions();
QStringList audioExts = audioFileExtensions();
QStringList listExts = listFileExtensions();
textExts.removeAll(ext);
imageExts.removeAll(ext);
audioExts.removeAll(ext);
listExts.removeAll(ext);
// Add to appropriate list
if (viewer == "text") {
textExts.append(ext);
} else if (viewer == "image") {
imageExts.append(ext);
} else if (viewer == "audio") {
audioExts.append(ext);
} else if (viewer == "list") {
listExts.append(ext);
}
// "hex" means don't add to any list
setTextFileExtensions(textExts);
setImageFileExtensions(imageExts);
setAudioFileExtensions(audioExts);
setListFileExtensions(listExts);
}
// Export Settings
QString Settings::defaultImageExportFormat() const
{
return m_settings.value("Export/ImageFormat", "png").toString();
}
void Settings::setDefaultImageExportFormat(const QString& format)
{
m_settings.setValue("Export/ImageFormat", format);
emit settingsChanged();
}
QString Settings::defaultAudioExportFormat() const
{
return m_settings.value("Export/AudioFormat", "wav").toString();
}
void Settings::setDefaultAudioExportFormat(const QString& format)
{
m_settings.setValue("Export/AudioFormat", format);
emit settingsChanged();
}
int Settings::imageJpegQuality() const
{
return m_settings.value("Export/JpegQuality", 90).toInt();
}
void Settings::setImageJpegQuality(int quality)
{
m_settings.setValue("Export/JpegQuality", qBound(1, quality, 100));
emit settingsChanged();
}
int Settings::imagePngCompression() const
{
return m_settings.value("Export/PngCompression", 6).toInt();
}
void Settings::setImagePngCompression(int level)
{
m_settings.setValue("Export/PngCompression", qBound(0, level, 9));
emit settingsChanged();
}
int Settings::audioMp3Bitrate() const
{
return m_settings.value("Export/Mp3Bitrate", 256).toInt();
}
void Settings::setAudioMp3Bitrate(int bitrate)
{
m_settings.setValue("Export/Mp3Bitrate", bitrate);
emit settingsChanged();
}
int Settings::audioOggQuality() const
{
return m_settings.value("Export/OggQuality", 5).toInt();
}
void Settings::setAudioOggQuality(int quality)
{
m_settings.setValue("Export/OggQuality", qBound(-1, quality, 10));
emit settingsChanged();
}
int Settings::audioFlacCompression() const
{
return m_settings.value("Export/FlacCompression", 5).toInt();
}
void Settings::setAudioFlacCompression(int level)
{
m_settings.setValue("Export/FlacCompression", qBound(0, level, 8));
emit settingsChanged();
}
bool Settings::exportRememberSettings() const
{
return m_settings.value("Export/RememberSettings", true).toBool();
}
void Settings::setExportRememberSettings(bool remember)
{
m_settings.setValue("Export/RememberSettings", remember);
emit settingsChanged();
}
QString Settings::batchExportDirectory() const
{
return m_settings.value("Export/BatchDirectory",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).toString();
}
void Settings::setBatchExportDirectory(const QString& path)
{
m_settings.setValue("Export/BatchDirectory", path);
emit settingsChanged();
}
bool Settings::batchExportPreserveStructure() const
{
return m_settings.value("Export/PreserveStructure", true).toBool();
}
void Settings::setBatchExportPreserveStructure(bool preserve)
{
m_settings.setValue("Export/PreserveStructure", preserve);
emit settingsChanged();
}
QString Settings::batchExportConflictResolution() const
{
return m_settings.value("Export/ConflictResolution", "number").toString();
}
void Settings::setBatchExportConflictResolution(const QString& resolution)
{
m_settings.setValue("Export/ConflictResolution", resolution);
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());
}
void Settings::resetToDefaults()
{
// Clear all settings
m_settings.clear();
// Apply default theme immediately
emit themeChanged(s_themes["XPlor Dark"]);
emit settingsChanged();
sync();
}