Compare commits

..

No commits in common. "main" and "feature/icon_factory" have entirely different histories.

527 changed files with 185093 additions and 36655 deletions

View File

@ -1,154 +0,0 @@
name: CI Build
# Disabled for now - only release workflow (tag-triggered) is active
# on:
# push:
# branches:
# - '**'
# pull_request:
on:
workflow_dispatch: # Manual trigger only
jobs:
build-windows:
runs-on: windows
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Build
shell: cmd
run: |
set PATH=C:\Qt\6.10.1\msvc2022_64\bin;C:\Qt\Tools\QtCreator\bin\jom;%PATH%
if not exist build mkdir build
cd build
echo === Running qmake ===
qmake.exe ..\XPlor.pro -spec win32-msvc "CONFIG+=release"
if errorlevel 1 exit /b 1
echo === Building with jom ===
jom.exe -j %NUMBER_OF_PROCESSORS%
if errorlevel 1 exit /b 1
echo === Build successful ===
- name: Deploy Qt
shell: cmd
run: |
set PATH=C:\Qt\6.10.1\msvc2022_64\bin;%PATH%
cd build\app\release
windeployqt6.exe --release --no-translations app.exe
if errorlevel 1 exit /b 1
echo === windeployqt successful ===
build-macos:
runs-on: macos
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
shell: bash
run: |
# Try multiple Qt locations
for QT_PATH in ~/Qt/6.10.1/macos ~/Qt/6.9.0/macos ~/Qt/6.8.0/macos /opt/homebrew/opt/qt@6; do
if [ -f "$QT_PATH/bin/qmake" ]; then
echo "Found Qt at: $QT_PATH"
export PATH="$QT_PATH/bin:$PATH"
break
fi
done
# Verify qmake exists
which qmake || { echo "ERROR: qmake not found"; exit 1; }
mkdir -p build
cd build
echo "=== Running qmake ==="
qmake ../XPlor.pro -spec macx-clang "CONFIG+=release"
echo "=== Building with make ==="
make -j$(sysctl -n hw.ncpu)
echo "=== Build successful ==="
- name: Deploy Qt
shell: bash
run: |
# Find Qt again for this step
for QT_PATH in ~/Qt/6.10.1/macos ~/Qt/6.9.0/macos ~/Qt/6.8.0/macos /opt/homebrew/opt/qt@6; do
if [ -f "$QT_PATH/bin/macdeployqt" ]; then
export PATH="$QT_PATH/bin:$PATH"
break
fi
done
cd build/app
if [ -d "XPlor.app" ]; then
macdeployqt XPlor.app -verbose=1
fi
echo "=== macdeployqt complete ==="
build-ubuntu:
runs-on: ubuntu
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Dependencies
shell: bash
run: |
# Use sudo if available, otherwise run directly (for root/container environments)
if command -v sudo &> /dev/null; then
SUDO="sudo"
else
SUDO=""
fi
# Update package lists (ignore failures from third-party repos)
$SUDO apt-get update || true
# Install build dependencies
$SUDO apt-get install -y build-essential libgl1-mesa-dev zlib1g-dev
- name: Build
shell: bash
run: |
# Try multiple Qt locations
for QT_PATH in ~/Qt/6.10.1/gcc_64 ~/Qt/6.9.0/gcc_64 ~/Qt/6.8.0/gcc_64 /opt/Qt/6.10.1/gcc_64 /usr/lib/qt6; do
if [ -f "$QT_PATH/bin/qmake" ]; then
echo "Found Qt at: $QT_PATH"
export PATH="$QT_PATH/bin:$PATH"
export LD_LIBRARY_PATH="$QT_PATH/lib:$LD_LIBRARY_PATH"
break
fi
done
# Verify qmake exists
which qmake || { echo "ERROR: qmake not found"; exit 1; }
mkdir -p build
cd build
echo "=== Running qmake ==="
qmake ../XPlor.pro -spec linux-g++ "CONFIG+=release"
echo "=== Building with make ==="
make -j$(nproc)
echo "=== Build successful ==="

View File

@ -1,704 +0,0 @@
name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+' # stable: v1.0.0
- 'v[0-9]+.[0-9]+.[0-9]+-alpha*' # alpha: v1.0.0-alpha1
- 'v[0-9]+.[0-9]+.[0-9]+-test*' # tester: v1.0.0-test1
jobs:
# ===========================================================================
# Windows Build
# ===========================================================================
build-windows:
runs-on: windows
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Extract version info
shell: cmd
run: |
setlocal enabledelayedexpansion
set "TAG=%GITHUB_REF_NAME%"
set "VERSION=!TAG:~1!"
set "CHANNEL=stable"
echo !TAG! | findstr /C:"-alpha" >nul && set "CHANNEL=alpha"
echo !TAG! | findstr /C:"-test" >nul && set "CHANNEL=tester"
REM Extract clean version (remove -alpha* or -test* suffix)
set "CLEAN_VERSION=!VERSION!"
for /f "tokens=1 delims=-" %%a in ("!VERSION!") do set "CLEAN_VERSION=%%a"
echo VERSION=!VERSION!>> %GITHUB_ENV%
echo CLEAN_VERSION=!CLEAN_VERSION!>> %GITHUB_ENV%
echo CHANNEL=!CHANNEL!>> %GITHUB_ENV%
echo Building version: !VERSION! for channel: !CHANNEL!
- name: Build Release
shell: cmd
run: |
set PATH=C:\Qt\6.10.1\msvc2022_64\bin;C:\Qt\Tools\QtCreator\bin\jom;%PATH%
if not exist build mkdir build
cd build
echo === Running qmake ===
qmake.exe ..\XPlor.pro -spec win32-msvc "CONFIG+=release"
if errorlevel 1 exit /b 1
echo === Building with jom ===
jom.exe -j %NUMBER_OF_PROCESSORS%
if errorlevel 1 exit /b 1
- name: Deploy Qt Libraries
shell: cmd
run: |
set PATH=C:\Qt\6.10.1\msvc2022_64\bin;%PATH%
echo === windeployqt for app ===
cd build\app\release
windeployqt6.exe --release --no-translations app.exe
if errorlevel 1 exit /b 1
cd ..\..\..
echo === windeployqt for cli ===
cd build\tools\cli\release
windeployqt6.exe --release --no-translations cli.exe
- name: Package for Installer
shell: cmd
run: |
set GUI_DATA=installer\packages\com.xplor.gui\data
set CLI_DATA=installer\packages\com.xplor.cli\data
REM Clean and create directories
if exist "%GUI_DATA%" rmdir /s /q "%GUI_DATA%"
if exist "%CLI_DATA%" rmdir /s /q "%CLI_DATA%"
mkdir "%GUI_DATA%"
mkdir "%CLI_DATA%\cli"
REM Copy GUI files
xcopy /s /e /y /q "build\app\release\*" "%GUI_DATA%\" >nul
if exist "%GUI_DATA%\definitions" rmdir /s /q "%GUI_DATA%\definitions"
if exist "%GUI_DATA%\scripts" rmdir /s /q "%GUI_DATA%\scripts"
if exist "%GUI_DATA%\app.exe" ren "%GUI_DATA%\app.exe" "XPlor.exe"
REM Copy CLI files
xcopy /s /e /y /q "build\tools\cli\release\*" "%CLI_DATA%\cli\" >nul
if exist "%CLI_DATA%\cli\cli.exe" ren "%CLI_DATA%\cli\cli.exe" "xplor-cli.exe"
echo Packaging complete
- name: Package Definitions and Docs
shell: cmd
run: |
setlocal enabledelayedexpansion
REM Package each definition set
for %%D in (cod volition deadrising asura fmod thqa wii) do (
set "DEF_DATA=installer\packages\com.xplor.definitions.%%D\data"
if exist "!DEF_DATA!" rmdir /s /q "!DEF_DATA!"
mkdir "!DEF_DATA!\definitions\%%D" 2>nul
if exist "definitions\%%D" (
xcopy /s /e /y /q "definitions\%%D\*" "!DEF_DATA!\definitions\%%D\" >nul
)
)
REM Package docs
set DOCS_DATA=installer\packages\com.xplor.docs\data
if exist "%DOCS_DATA%" rmdir /s /q "%DOCS_DATA%"
mkdir "%DOCS_DATA%\docs"
if exist "docs\xscript-guide.pdf" copy /y "docs\xscript-guide.pdf" "%DOCS_DATA%\docs\" >nul
if exist "docs\xscript-guide.md" copy /y "docs\xscript-guide.md" "%DOCS_DATA%\docs\" >nul
REM Package scripts
set SCRIPTS_DATA=installer\packages\com.xplor.scripts\data
if exist "%SCRIPTS_DATA%" rmdir /s /q "%SCRIPTS_DATA%"
mkdir "%SCRIPTS_DATA%\scripts"
if exist "scripts" xcopy /s /e /y /q "scripts\*" "%SCRIPTS_DATA%\scripts\" >nul
echo Definitions and docs packaged
- name: Update Package Versions
shell: cmd
run: |
setlocal enabledelayedexpansion
set "VERSION=%CLEAN_VERSION%"
set "CHANNEL=%CHANNEL%"
REM Get today's date in YYYY-MM-DD format
for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set "DT=%%I"
set "TODAY=!DT:~0,4!-!DT:~4,2!-!DT:~6,2!"
echo Updating to version !VERSION! for channel !CHANNEL! on !TODAY!
REM Update package.xml files using PowerShell for regex
for /r installer\packages %%f in (package.xml) do (
if exist "%%f" (
powershell -Command "(Get-Content '%%f') -replace '<Version>.*</Version>', '<Version>%VERSION%</Version>' -replace '<ReleaseDate>.*</ReleaseDate>', '<ReleaseDate>%TODAY%</ReleaseDate>' | Set-Content '%%f'"
)
)
REM Update config.xml
powershell -Command "(Get-Content 'installer\config\config.xml') -replace '<Version>.*</Version>', '<Version>%VERSION%</Version>' -replace 'repository/[^<\"]*', 'repository/%CHANNEL%' | Set-Content 'installer\config\config.xml'"
echo Updated versions to !VERSION! for channel !CHANNEL!
- name: Generate Repository
shell: cmd
run: |
set PATH=C:\Qt\Tools\QtInstallerFramework\4.8\bin;%PATH%
if exist repository rmdir /s /q repository
mkdir repository
repogen.exe --update-new-components -p installer\packages repository
if errorlevel 1 exit /b 1
echo Repository generated
- name: Create Offline Installer
shell: cmd
run: |
set PATH=C:\Qt\Tools\QtInstallerFramework\4.8\bin;%PATH%
binarycreator.exe --offline-only -c installer\config\config.xml -p installer\packages "XPlor-%VERSION%-Windows-Setup.exe"
if errorlevel 1 exit /b 1
echo Created: XPlor-%VERSION%-Windows-Setup.exe
- name: Create Online Installer
shell: cmd
run: |
set PATH=C:\Qt\Tools\QtInstallerFramework\4.8\bin;%PATH%
binarycreator.exe --online-only -c installer\config\config.xml -p installer\packages "XPlor-%VERSION%-Windows-Online.exe"
if errorlevel 1 exit /b 1
echo Created: XPlor-%VERSION%-Windows-Online.exe
- name: Deploy to Repository
shell: cmd
run: |
set REPO_PATH=P:\repository\%CHANNEL%
echo Deploying to %REPO_PATH%
if not exist "%REPO_PATH%" mkdir "%REPO_PATH%"
REM Copy repository contents
xcopy /s /e /y /q "repository\*" "%REPO_PATH%\" >nul
echo Repository updated at %REPO_PATH%
- name: Upload Installers
uses: actions/upload-artifact@v4
with:
name: windows-installers
path: |
XPlor-*-Windows-Setup.exe
XPlor-*-Windows-Online.exe
retention-days: 90
# ===========================================================================
# macOS Build
# ===========================================================================
build-macos:
runs-on: macos
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract version info
shell: bash
run: |
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
if [[ "$TAG" == *"-alpha"* ]]; then
CHANNEL="alpha"
elif [[ "$TAG" == *"-test"* ]]; then
CHANNEL="tester"
else
CHANNEL="stable"
fi
CLEAN_VERSION=$(echo "$VERSION" | sed 's/-alpha.*//' | sed 's/-test.*//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "CLEAN_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV
echo "CHANNEL=$CHANNEL" >> $GITHUB_ENV
- name: Build Release
shell: bash
run: |
# Find Qt
for QT_PATH in ~/Qt/6.10.1/macos ~/Qt/6.9.0/macos ~/Qt/6.8.0/macos /opt/homebrew/opt/qt@6; do
if [ -f "$QT_PATH/bin/qmake" ]; then
echo "Found Qt at: $QT_PATH"
export PATH="$QT_PATH/bin:$PATH"
echo "QT_PATH=$QT_PATH" >> $GITHUB_ENV
break
fi
done
which qmake || { echo "ERROR: qmake not found"; exit 1; }
mkdir -p build
cd build
qmake ../XPlor.pro -spec macx-clang "CONFIG+=release"
make -j$(sysctl -n hw.ncpu)
- name: Deploy Qt Libraries
shell: bash
run: |
export PATH="$QT_PATH/bin:$PATH"
cd build/app
if [ -d "XPlor.app" ] || [ -d "app.app" ]; then
APP_NAME=$(ls -d *.app | head -1)
macdeployqt "$APP_NAME" -verbose=1
# Rename if needed
if [ "$APP_NAME" = "app.app" ]; then
mv app.app XPlor.app
fi
fi
- name: Package for Installer
shell: bash
run: |
set -x # Debug: show commands
echo "=== Starting Package for Installer ==="
echo "Current directory: $(pwd)"
echo "Build directory contents:"
ls -la build/app/ || echo "build/app not found"
GUI_DATA="installer/packages/com.xplor.gui/data"
CLI_DATA="installer/packages/com.xplor.cli/data"
rm -rf "$GUI_DATA" "$CLI_DATA"
mkdir -p "$GUI_DATA" "$CLI_DATA/cli"
# Copy app bundle
if [ -d "build/app/XPlor.app" ]; then
echo "Copying XPlor.app..."
cp -R "build/app/XPlor.app" "$GUI_DATA/"
elif [ -d "build/app/app.app" ]; then
echo "Copying app.app as XPlor.app..."
cp -R "build/app/app.app" "$GUI_DATA/XPlor.app"
else
echo "WARNING: No app bundle found"
ls -la build/app/ || true
fi
# Copy CLI
if [ -f "build/tools/cli/cli" ]; then
cp "build/tools/cli/cli" "$CLI_DATA/cli/xplor-cli"
chmod +x "$CLI_DATA/cli/xplor-cli"
else
echo "WARNING: CLI binary not found"
fi
# Package definitions (use find to avoid glob issues)
for def in cod volition deadrising asura fmod thqa wii; do
DEF_DATA="installer/packages/com.xplor.definitions.$def/data"
rm -rf "$DEF_DATA"
mkdir -p "$DEF_DATA/definitions/$def"
if [ -d "definitions/$def" ] && [ "$(ls -A definitions/$def 2>/dev/null)" ]; then
cp -R "definitions/$def/"* "$DEF_DATA/definitions/$def/" || true
fi
done
# Package docs
DOCS_DATA="installer/packages/com.xplor.docs/data"
rm -rf "$DOCS_DATA"
mkdir -p "$DOCS_DATA/docs"
[ -f "docs/xscript-guide.pdf" ] && cp "docs/xscript-guide.pdf" "$DOCS_DATA/docs/" || true
[ -f "docs/xscript-guide.md" ] && cp "docs/xscript-guide.md" "$DOCS_DATA/docs/" || true
# Package scripts (may not exist in repo)
SCRIPTS_DATA="installer/packages/com.xplor.scripts/data"
rm -rf "$SCRIPTS_DATA"
mkdir -p "$SCRIPTS_DATA/scripts"
if [ -d "scripts" ] && [ "$(ls -A scripts 2>/dev/null)" ]; then
cp -R scripts/* "$SCRIPTS_DATA/scripts/" || true
fi
echo "=== Package for Installer complete ==="
- name: Update Package Versions
shell: bash
run: |
VERSION="${{ env.CLEAN_VERSION }}"
CHANNEL="${{ env.CHANNEL }}"
TODAY=$(date +%Y-%m-%d)
# Update package.xml files
find installer/packages -name "package.xml" -exec sed -i '' \
-e "s|<Version>.*</Version>|<Version>$VERSION</Version>|" \
-e "s|<ReleaseDate>.*</ReleaseDate>|<ReleaseDate>$TODAY</ReleaseDate>|" {} \;
# Update config.xml
sed -i '' \
-e "s|<Version>.*</Version>|<Version>$VERSION</Version>|" \
-e "s|repository/[^<\"]*|repository/$CHANNEL|" \
installer/config/config.xml
- name: Generate Repository and Installers
shell: bash
run: |
# Find Qt IFW
for IFW_PATH in ~/Qt/Tools/QtInstallerFramework/4.8/bin ~/Qt/Tools/QtInstallerFramework/4.7/bin /opt/homebrew/opt/qt-installer-framework/bin; do
if [ -f "$IFW_PATH/repogen" ]; then
echo "Found Qt IFW at: $IFW_PATH"
export PATH="$IFW_PATH:$PATH"
break
fi
done
which repogen || { echo "ERROR: Qt IFW not found"; exit 1; }
# Generate repository
rm -rf repository
mkdir repository
repogen --update-new-components -p installer/packages repository
# Create offline installer
binarycreator --offline-only -c installer/config/config.xml -p installer/packages "XPlor-${VERSION}-macOS-Setup"
# Create online installer
binarycreator --online-only -c installer/config/config.xml -p installer/packages "XPlor-${VERSION}-macOS-Online"
# Create DMG for offline installer
if [ -d "XPlor-${VERSION}-macOS-Setup.app" ]; then
hdiutil create -volname "XPlor Installer" -srcfolder "XPlor-${VERSION}-macOS-Setup.app" -ov -format UDZO "XPlor-${VERSION}-macOS-Setup.dmg"
fi
if [ -d "XPlor-${VERSION}-macOS-Online.app" ]; then
hdiutil create -volname "XPlor Online Installer" -srcfolder "XPlor-${VERSION}-macOS-Online.app" -ov -format UDZO "XPlor-${VERSION}-macOS-Online.dmg"
fi
ls -la XPlor-*
- name: Upload Installers
uses: actions/upload-artifact@v4
with:
name: macos-installers
path: |
XPlor-*-macOS-Setup.dmg
XPlor-*-macOS-Online.dmg
XPlor-*-macOS-Setup
XPlor-*-macOS-Online
retention-days: 90
- name: Upload Repository
uses: actions/upload-artifact@v4
with:
name: macos-repository
path: repository/
retention-days: 90
# ===========================================================================
# Linux Build
# ===========================================================================
build-linux:
runs-on: ubuntu
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Dependencies
shell: bash
run: |
if command -v sudo &> /dev/null; then
SUDO="sudo"
else
SUDO=""
fi
$SUDO apt-get update || true
$SUDO apt-get install -y build-essential libgl1-mesa-dev zlib1g-dev
- name: Extract version info
shell: bash
run: |
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
if [[ "$TAG" == *"-alpha"* ]]; then
CHANNEL="alpha"
elif [[ "$TAG" == *"-test"* ]]; then
CHANNEL="tester"
else
CHANNEL="stable"
fi
CLEAN_VERSION=$(echo "$VERSION" | sed 's/-alpha.*//' | sed 's/-test.*//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "CLEAN_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV
echo "CHANNEL=$CHANNEL" >> $GITHUB_ENV
- name: Build Release
shell: bash
run: |
# Find Qt
for QT_PATH in ~/Qt/6.10.1/gcc_64 ~/Qt/6.9.0/gcc_64 ~/Qt/6.8.0/gcc_64 /opt/Qt/6.10.1/gcc_64 /usr/lib/qt6; do
if [ -f "$QT_PATH/bin/qmake" ]; then
echo "Found Qt at: $QT_PATH"
export PATH="$QT_PATH/bin:$PATH"
export LD_LIBRARY_PATH="$QT_PATH/lib:$LD_LIBRARY_PATH"
echo "QT_PATH=$QT_PATH" >> $GITHUB_ENV
break
fi
done
which qmake || { echo "ERROR: qmake not found"; exit 1; }
mkdir -p build
cd build
qmake ../XPlor.pro -spec linux-g++ "CONFIG+=release"
make -j$(nproc)
- name: Package for Installer
shell: bash
run: |
set -x # Debug: show commands
echo "=== Starting Package for Installer (Linux) ==="
GUI_DATA="installer/packages/com.xplor.gui/data"
CLI_DATA="installer/packages/com.xplor.cli/data"
rm -rf "$GUI_DATA" "$CLI_DATA"
mkdir -p "$GUI_DATA" "$CLI_DATA/cli"
# Copy GUI binary and create wrapper script
if [ -f "build/app/app" ]; then
cp "build/app/app" "$GUI_DATA/XPlor"
chmod +x "$GUI_DATA/XPlor"
else
echo "WARNING: GUI binary not found"
fi
# Copy CLI
if [ -f "build/tools/cli/cli" ]; then
cp "build/tools/cli/cli" "$CLI_DATA/cli/xplor-cli"
chmod +x "$CLI_DATA/cli/xplor-cli"
else
echo "WARNING: CLI binary not found"
fi
# Copy Qt libraries (basic deployment)
if [ -n "$QT_PATH" ] && [ -d "$QT_PATH/lib" ]; then
mkdir -p "$GUI_DATA/lib"
for lib in Core Gui Widgets Network Multimedia Svg; do
[ -f "$QT_PATH/lib/libQt6${lib}.so.6" ] && cp "$QT_PATH/lib/libQt6${lib}.so.6" "$GUI_DATA/lib/"
done
# Copy plugins
mkdir -p "$GUI_DATA/plugins/platforms"
[ -d "$QT_PATH/plugins/platforms" ] && cp "$QT_PATH/plugins/platforms/"*.so "$GUI_DATA/plugins/platforms/" 2>/dev/null || true
fi
# Package definitions (check for non-empty directories)
for def in cod volition deadrising asura fmod thqa wii; do
DEF_DATA="installer/packages/com.xplor.definitions.$def/data"
rm -rf "$DEF_DATA"
mkdir -p "$DEF_DATA/definitions/$def"
if [ -d "definitions/$def" ] && [ "$(ls -A definitions/$def 2>/dev/null)" ]; then
cp -R "definitions/$def/"* "$DEF_DATA/definitions/$def/" || true
fi
done
# Package docs
DOCS_DATA="installer/packages/com.xplor.docs/data"
rm -rf "$DOCS_DATA"
mkdir -p "$DOCS_DATA/docs"
[ -f "docs/xscript-guide.pdf" ] && cp "docs/xscript-guide.pdf" "$DOCS_DATA/docs/" || true
[ -f "docs/xscript-guide.md" ] && cp "docs/xscript-guide.md" "$DOCS_DATA/docs/" || true
# Package scripts (may not exist in repo)
SCRIPTS_DATA="installer/packages/com.xplor.scripts/data"
rm -rf "$SCRIPTS_DATA"
mkdir -p "$SCRIPTS_DATA/scripts"
if [ -d "scripts" ] && [ "$(ls -A scripts 2>/dev/null)" ]; then
cp -R scripts/* "$SCRIPTS_DATA/scripts/" || true
fi
echo "=== Package for Installer complete ==="
- name: Update Package Versions
shell: bash
run: |
VERSION="${{ env.CLEAN_VERSION }}"
CHANNEL="${{ env.CHANNEL }}"
TODAY=$(date +%Y-%m-%d)
# Update package.xml files
find installer/packages -name "package.xml" -exec sed -i \
-e "s|<Version>.*</Version>|<Version>$VERSION</Version>|" \
-e "s|<ReleaseDate>.*</ReleaseDate>|<ReleaseDate>$TODAY</ReleaseDate>|" {} \;
# Update config.xml
sed -i \
-e "s|<Version>.*</Version>|<Version>$VERSION</Version>|" \
-e "s|repository/[^<\"]*|repository/$CHANNEL|" \
installer/config/config.xml
- name: Generate Repository and Installers
shell: bash
run: |
# Find Qt IFW
for IFW_PATH in ~/Qt/Tools/QtInstallerFramework/4.8/bin ~/Qt/Tools/QtInstallerFramework/4.7/bin /opt/Qt/Tools/QtInstallerFramework/4.8/bin; do
if [ -f "$IFW_PATH/repogen" ]; then
echo "Found Qt IFW at: $IFW_PATH"
export PATH="$IFW_PATH:$PATH"
break
fi
done
which repogen || { echo "ERROR: Qt IFW not found"; exit 1; }
# Generate repository
rm -rf repository
mkdir repository
repogen --update-new-components -p installer/packages repository
# Create offline installer (.run)
binarycreator --offline-only -c installer/config/config.xml -p installer/packages "XPlor-${VERSION}-Linux-Setup.run"
# Create online installer (.run)
binarycreator --online-only -c installer/config/config.xml -p installer/packages "XPlor-${VERSION}-Linux-Online.run"
chmod +x XPlor-*.run
ls -la XPlor-*
- name: Upload Installers
uses: actions/upload-artifact@v4
with:
name: linux-installers
path: |
XPlor-*-Linux-Setup.run
XPlor-*-Linux-Online.run
retention-days: 90
- name: Upload Repository
uses: actions/upload-artifact@v4
with:
name: linux-repository
path: repository/
retention-days: 90
# ===========================================================================
# Deploy - Merge repositories and create release
# ===========================================================================
deploy:
runs-on: windows
needs: [build-windows, build-macos, build-linux]
steps:
- name: Extract version info
shell: cmd
run: |
setlocal enabledelayedexpansion
set "TAG=%GITHUB_REF_NAME%"
set "VERSION=!TAG:~1!"
set "CHANNEL=stable"
echo !TAG! | findstr /C:"-alpha" >nul && set "CHANNEL=alpha"
echo !TAG! | findstr /C:"-test" >nul && set "CHANNEL=tester"
echo VERSION=!VERSION!>> %GITHUB_ENV%
echo CHANNEL=!CHANNEL!>> %GITHUB_ENV%
- name: Download all installers
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Download macOS repository
uses: actions/download-artifact@v4
with:
name: macos-repository
path: repo-macos
- name: Download Linux repository
uses: actions/download-artifact@v4
with:
name: linux-repository
path: repo-linux
- name: Merge repositories
shell: cmd
run: |
setlocal enabledelayedexpansion
set "CHANNEL=%CHANNEL%"
set "REPO_PATH=P:\repository\!CHANNEL!"
echo Deploying to: !REPO_PATH!
REM Ensure directory exists
if not exist "!REPO_PATH!" mkdir "!REPO_PATH!"
REM The Windows build already deployed its repository
REM Now merge macOS and Linux platform-specific packages
REM Copy macOS repository components
if exist "repo-macos" (
xcopy /s /e /y /q "repo-macos\*" "!REPO_PATH!\" >nul
echo Merged macOS repository
)
REM Copy Linux repository components
if exist "repo-linux" (
xcopy /s /e /y /q "repo-linux\*" "!REPO_PATH!\" >nul
echo Merged Linux repository
)
echo Repository deployed to !REPO_PATH!
- name: Collect all installers
shell: cmd
run: |
REM Create release directory
if not exist release mkdir release
REM Copy all installers from artifacts
for /r artifacts %%f in (XPlor-*.exe XPlor-*.dmg XPlor-*.run) do (
if exist "%%f" (
copy /y "%%f" release\ >nul
echo Found: %%~nxf
)
)
dir release\
- name: Create Gitea Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'test') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

91
.gitignore vendored
View File

@ -1,79 +1,12 @@
/build/
/data/dlls/
/data/fastfiles/
/releases/
/exports/
/docs/
tmpcl*
*/tmpcl*
.vscode/*
.qmake.stash
installer/*
# Ignore Qt Creator user files
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.creator.user
*.creator.user.*
*.creator.*
*.ps1
version.txt
*.autosave
*.XMODEL_EXPORT
data/obj/*
libs/*/release/*
libs/*/debug/*
.git.stash
*Makefile*
.cl*/
CL*.md
# Build artifacts
*.lib
*.pdb
*.obj
ui_*.h
moc_*.cpp
qrc_*.cpp
# Exception: allow third-party libraries
!third_party/**/*.lib
!third_party/**/*.dll
# Temporary files
*.bat
stderr.txt
stdout.txt
nul
%REPORT%
test.md
scripts/
tools/steamcmd/
# Environment files (API keys)
.env
# Build and deploy scripts (local config)
build_debug.cmd
build_release.cmd
build_all_debug.sh
build_all_release.sh
deploy.sh
# Auto-generated LaTeX config
docs/appconfig.tex
# Unused third-party libraries (not referenced in any .pro file)
third_party/dx9_sdk/
third_party/xna/
third_party/lzxdhelper/
# Qt Installer Framework
repository/
installer/packages/*/data/
.deploy-temp/
/build/
/data/dlls/
/data/fastfiles/
# Ignore Qt Creator user files
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.creator.user
*.creator.user.*
*.creator.*

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "tools/hexes"]
path = tools/hexes
url = https://code.redline.llc/njohnson/Hexes.git

147
README.md
View File

@ -1,147 +0,0 @@
# XPlor
A Qt-based binary file format explorer and parser using a custom domain-specific language (XScript) for defining file structures.
## Overview
XPlor is a desktop application designed to explore, parse, and visualize binary file formats from video games. It uses XScript, a custom DSL (Domain-Specific Language), to define how binary files should be interpreted, making it easy to add support for new file formats without modifying the core application code.
## Features
- **XScript DSL**: Define binary file structures using a readable, declarative scripting language
- **Dynamic File Type Detection**: Automatically identifies file types based on extension and magic bytes
- **Tree-Based UI**: Hierarchical view of parsed file structures
- **Image Preview**: Built-in preview for texture assets (TGA, DXT, Xbox 360 formats)
- **Extensible**: Add support for new file formats by creating XScript definition files
- **Multi-Platform Support**: Parse files from PC, Xbox 360, PS3, and other platforms
## Supported Games/Formats
### Call of Duty Series
- Fast Files (`.ff`)
- Various asset types: materials, textures, models, sounds, menus, weapons, etc.
### Rebellion Asura Engine (Sniper Elite V2)
- Archive files (`.asrbe`, `.tsBE`, `.mapBE`, etc.)
- Texture archives
- Various chunk types
## Project Structure
```
XPlor/
├── app/ # Main application
│ ├── mainwindow.* # Main window UI and logic
│ ├── imagepreviewwidget.* # Texture preview widget
│ └── xtreewidget.* # Tree view components
├── libs/
│ ├── core/ # Core utilities and managers
│ ├── dsl/ # XScript DSL engine
│ │ ├── lexer.* # Tokenizer
│ │ ├── parser.* # AST parser
│ │ ├── interpreter.* # Runtime interpreter
│ │ └── typeregistry.* # Type management
│ ├── compression/ # LZO compression support
│ └── encryption/ # Salsa20, SHA1 encryption
├── definitions/ # XScript format definitions
│ ├── cod/ # Call of Duty formats
│ └── asura/ # Asura Engine formats
├── tools/ # Command-line utilities
└── third_party/ # External dependencies
```
## Building
### Prerequisites
- Qt 6.x (tested with 6.10.0)
- MSVC 2022 (Windows)
- CMake or qmake
### Build Steps
1. Open `XPlor.pro` in Qt Creator
2. Configure the project for your Qt kit
3. Build the project
Or from command line:
```bash
qmake XPlor.pro
make # or nmake/jom on Windows
```
## XScript Language
XScript is a domain-specific language for defining binary file structures. Here's an example:
```xscript
type fastfile [root, display="Fast File"] byteorder LE
{
criteria {
require _ext == "ff";
}
// Read header
magic = ascii(read(8));
version = u32;
if (version == 5) {
platform = "PC";
}
// Parse nested structures
asset_count = u32;
repeat asset_count {
asset = parse_here("asset");
}
}
```
### Key Features
- **Type definitions** with byte order specification
- **Criteria blocks** for file type detection
- **Built-in functions**: `read()`, `u32`, `ascii()`, `cstring()`, etc.
- **Control flow**: `if/else`, `while`, `repeat`, `for` loops
- **Pipeline syntax**: `data | decompress("zlib") | parse("asset")`
- **UI annotations**: `[ui, display="Name", readonly]`
## Usage
1. Launch XPlor
2. Drag and drop a supported file onto the window, or use File > Open
3. The file will be parsed and displayed in a tree structure
4. Click on tree items to view their properties
5. For texture assets, a preview will be shown
## Adding New Format Support
1. Create a new `.xscript` file in `definitions/`
2. Define your root type with `[root]` attribute
3. Add criteria for file detection
4. Define the binary structure using XScript syntax
5. Restart XPlor to load the new definition
## Libraries
- **Qt 6**: UI framework
- **miniLZO**: LZO compression
- **Salsa20**: Stream cipher encryption
- **zlib**: Deflate compression
## License
This project is for educational and research purposes.
## Contributing
Contributions are welcome! Please feel free to submit pull requests for:
- New file format definitions
- Bug fixes
- Feature improvements
- Documentation
## Acknowledgments
- Xbox 360 SDK documentation for texture untiling algorithms
- Various game modding communities for format documentation

View File

@ -3,8 +3,8 @@ TEMPLATE = subdirs
SUBDIRS += libs \
app \
tools \
#tests
tests
#tests.depends = libs
tests.depends = libs
app.depends = libs
tools.depends = libs

View File

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="Windows-1252" ?>
<NotepadPlus>
<UserLang name="XPlor DSL" ext="dsl" udlVersion="2.1">
<Settings>
<Global caseIgnored="no" allowFoldOfComments="yes" foldCompact="no" forcePureLC="0" decimalSeparator="0" />
<Prefix Keywords1="no" Keywords2="no" Keywords3="no" Keywords4="no" Keywords5="no" Keywords6="no" Keywords7="no" Keywords8="no" />
</Settings>
<KeywordLists>
<!-- Comments definition (UDL2):
00 = line comment start
03 = block comment start
04 = block comment end
This format is what Notepad++ expects for udlVersion="2.1". -->
<Keywords name="Comments">00// 01 02 03/* 04*/</Keywords>
<Keywords name="Numbers, prefix1"></Keywords>
<Keywords name="Numbers, prefix2"></Keywords>
<!-- Primary language keywords -->
<Keywords name="Keywords1">
type byteorder criteria require skip if else while align seek repeat for in EOF LE BE
</Keywords>
<!-- Types / builtins / common stage names -->
<Keywords name="Keywords2">
u8 u16 u32 u64 i8 i16 i32 i64
read pos size zlib parse parse_here
u8at u16at u32at u64at bytesat
</Keywords>
<!-- Optional: common identifiers you want highlighted (keep or delete) -->
<Keywords name="Keywords3">
company file_type marker platform_u32
</Keywords>
<Keywords name="Keywords4"></Keywords>
<Keywords name="Keywords5"></Keywords>
<Keywords name="Keywords6"></Keywords>
<Keywords name="Keywords7"></Keywords>
<Keywords name="Keywords8"></Keywords>
<!-- Operators (optional; Notepad++ UDL can colorize these) -->
<Keywords name="Operators1">= == != &lt; &lt;= &gt; &gt;= &amp;&amp; || + - * / % &lt;&lt; &gt;&gt; &amp; ^ |</Keywords>
<Keywords name="Operators2">..</Keywords>
<!-- Delimiters for strings -->
<Keywords name="Delimiters">00" 01 02\" 03' 04 05\'</Keywords>
</KeywordLists>
<Styles>
<!-- Light mode base -->
<WordsStyle name="DEFAULT" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
<WordsStyle name="FOLDEROPEN" fgColor="000000" bgColor="FFFFFF" fontStyle="1" />
<WordsStyle name="FOLDERCLOSE" fgColor="000000" bgColor="FFFFFF" fontStyle="1" />
<WordsStyle name="FOLDER" fgColor="000000" bgColor="FFFFFF" fontStyle="1" />
<WordsStyle name="COMMENT" fgColor="008000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="COMMENT LINE" fgColor="008000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="COMMENT DOC" fgColor="008000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="NUMBER" fgColor="800080" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="KEYWORD1" fgColor="0000C0" bgColor="FFFFFF" fontStyle="1" />
<WordsStyle name="KEYWORD2" fgColor="0060A0" bgColor="FFFFFF" fontStyle="1" />
<WordsStyle name="KEYWORD3" fgColor="000000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="KEYWORD4" fgColor="000000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="KEYWORD5" fgColor="000000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="KEYWORD6" fgColor="000000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="KEYWORD7" fgColor="000000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="KEYWORD8" fgColor="000000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="OPERATOR" fgColor="A00000" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="DELIMITER1" fgColor="A0522D" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="DELIMITER2" fgColor="A0522D" bgColor="FFFFFF" fontStyle="0" />
<WordsStyle name="DELIMITER3" fgColor="A0522D" bgColor="FFFFFF" fontStyle="0" />
</Styles>
</UserLang>
</NotepadPlus>

View File

@ -1,201 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,96 +1,14 @@
#include "aboutdialog.h"
#include "settings.h"
#include <QCoreApplication>
#include <QDate>
#include <QPainter>
#include <QPainterPath>
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("About XPlor"));
setFixedSize(WIDTH, HEIGHT);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// Load theme colors
Theme theme = Settings::instance().theme();
mPrimaryColor = QColor(theme.accentColor);
mBgColor = QColor(theme.backgroundColor);
mTextColor = QColor(theme.textColor);
mTextColorMuted = QColor(theme.textColorMuted);
mBorderColor = QColor(theme.borderColor);
}
void AboutDialog::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
// Draw background
painter.setPen(Qt::NoPen);
painter.setBrush(mBgColor);
painter.drawRect(rect());
// Draw accent stripe at top
painter.setBrush(mPrimaryColor);
painter.drawRect(0, 0, WIDTH, 6);
// Draw app icon
QPixmap icon(":/images/data/images/XPlor.png");
if (!icon.isNull()) {
QRect iconRect(30, 40, 64, 64);
painter.drawPixmap(iconRect, icon.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
// Draw "XPlor" title
int textX = 115;
int titleY = 60;
QFont titleFont("Segoe UI", 28, QFont::Bold);
painter.setFont(titleFont);
QFontMetrics titleMetrics(titleFont);
// Draw "X" in accent color
painter.setPen(mPrimaryColor);
painter.drawText(textX, titleY, "X");
// Draw "Plor" in text color
painter.setPen(mTextColor);
int xWidth = titleMetrics.horizontalAdvance("X");
painter.drawText(textX + xWidth, titleY, "Plor");
// Version
QFont versionFont("Segoe UI", 11);
painter.setFont(versionFont);
painter.setPen(mTextColorMuted);
QString version = QString("Version %1").arg(QCoreApplication::applicationVersion());
painter.drawText(textX, titleY + 25, version);
// Tagline
QFont taglineFont("Segoe UI", 10);
painter.setFont(taglineFont);
painter.setPen(mTextColorMuted);
painter.drawText(textX, titleY + 50, "Binary File Format Explorer");
// Copyright
QFont copyrightFont("Segoe UI", 9);
painter.setFont(copyrightFont);
painter.setPen(mTextColorMuted);
QString copyright = QString("Copyright %1 %2 RedLine Solutions LLC")
.arg(QChar(0x00A9))
.arg(QDate::currentDate().year());
painter.drawText(30, HEIGHT - 45, copyright);
// Website
painter.drawText(30, HEIGHT - 25, "redline.llc");
// Draw subtle border
painter.setPen(QPen(mBorderColor, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(0, 0, WIDTH - 1, HEIGHT - 1);
}
#include "aboutdialog.h"
#include "ui_aboutdialog.h"
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::AboutDialog)
{
ui->setupUi(this);
}
AboutDialog::~AboutDialog()
{
delete ui;
}

View File

@ -1,28 +1,22 @@
#ifndef ABOUTDIALOG_H
#define ABOUTDIALOG_H
#include <QDialog>
#include <QColor>
class AboutDialog : public QDialog
{
Q_OBJECT
public:
explicit AboutDialog(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QColor mPrimaryColor;
QColor mBgColor;
QColor mTextColor;
QColor mTextColorMuted;
QColor mBorderColor;
static constexpr int WIDTH = 380;
static constexpr int HEIGHT = 200;
};
#endif // ABOUTDIALOG_H
#ifndef ABOUTDIALOG_H
#define ABOUTDIALOG_H
#include <QDialog>
namespace Ui {
class AboutDialog;
}
class AboutDialog : public QDialog
{
Q_OBJECT
public:
explicit AboutDialog(QWidget *parent = nullptr);
~AboutDialog();
private:
Ui::AboutDialog *ui;
};
#endif // ABOUTDIALOG_H

View File

@ -1,153 +1,241 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>140</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>140</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>140</height>
</size>
</property>
<property name="windowTitle">
<string>About XPlor</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="labelIcon">
<property name="minimumSize">
<size>
<width>80</width>
<height>80</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>80</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="Data.qrc">:/images/data/images/XPlor.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="labelVersion">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>XPlor</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelCopyright">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>Copyright</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelWebsite">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>For more, check out redline.llc</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="Data.qrc"/>
</resources>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>200</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>200</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>200</height>
</size>
</property>
<property name="windowTitle">
<string>About XPlor</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>80</width>
<height>80</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>80</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="Data.qrc">:/images/data/images/XPlor.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>XPlor v1.5</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>Copyright © 2024 RedLine Solutions LLC</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>For more, check out redline.llc</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>With Help From:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>- Paging Red</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>- ISOCheated</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Roboto</family>
</font>
</property>
<property name="text">
<string>- SureShotIan</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="Data.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -1,82 +1,118 @@
QT += core widgets gui multimedia network
SUBDIRS += app
CONFIG += c++latest
RC_ICONS = app.ico
# Load API token from .env file at build time
ENV_FILE = $$PWD/../.env
exists($$ENV_FILE) {
ENV_CONTENTS = $$cat($$ENV_FILE, lines)
for(line, ENV_CONTENTS) {
# Parse GITEA_ACCESS_TOKEN=value
contains(line, "GITEA_ACCESS_TOKEN=.*") {
TOKEN = $$replace(line, "GITEA_ACCESS_TOKEN=", "")
DEFINES += GITEA_ACCESS_TOKEN=\\\"$$TOKEN\\\"
}
}
}
SOURCES += $$files($$PWD/*.cpp)
HEADERS += $$files($$PWD/*.h)
FORMS += $$files($$PWD/*.ui)
RESOURCES += ../data/Data.qrc
# Core libraries (all platforms)
LIBS += \
-L$$OUT_PWD/../libs/ -lcore \
-L$$OUT_PWD/../libs/ -lcompression \
-L$$OUT_PWD/../libs/ -ldsl \
-L$$OUT_PWD/../libs/ -lencryption
# Windows-only: DevIL, bundled zlib, Xbox SDK
win32 {
LIBS += -L$$PWD/../third_party/devil_sdk/lib/ -lDevIL -lILU -lILUT
LIBS += -L$$PWD/../third_party/zlib/lib/ -lzlib
LIBS += -L$$PWD/../third_party/xbox_sdk/lib -lxcompress64
}
# macOS/Linux: use system zlib
unix {
LIBS += -lz
}
INCLUDEPATH += \
$$PWD/../third_party/devil_sdk/include/ \
$$PWD/../third_party/zlib/include \
$$PWD/../third_party/xbox_sdk/include \
$$PWD/../libs/core \
$$PWD/../libs/compression \
$$PWD/../libs/dsl \
$$PWD/../libs/encryption
DEPENDPATH += \
$$PWD/../third_party/devil_sdk/include/ \
$$PWD/../third_party/zlib/include \
$$PWD/../third_party/xbox_sdk/include \
$$PWD/../libs/core \
$$PWD/../libs/compression \
$$PWD/../libs/dsl \
$$PWD/../libs/encryption
CONFIG(debug, debug|release) {
FULL_OUT_DIR = $$OUT_PWD/debug
} CONFIG(release, debug|release) {
FULL_OUT_DIR = $$OUT_PWD/release
}
defs_install.path = $$FULL_OUT_DIR/definitions
defs_install.files = $$PWD/../definitions/*
scripts_install.path = $$FULL_OUT_DIR/scripts
scripts_install.files = $$PWD/../scripts/*
INSTALLS += defs_install scripts_install
# Windows-only: deploy DLLs
win32 {
dll_install.path = $$FULL_OUT_DIR
dll_install.files = $$PWD/../third_party/xbox_sdk/lib/xcompress64.dll
INSTALLS += dll_install
}
QT += core widgets gui multimedia
RC_ICONS = app.ico
SUBDIRS += app
CONFIG += c++17
SOURCES += \
aboutdialog.cpp \
ddsviewer.cpp \
fastfileviewer.cpp \
imagewidget.cpp \
iwiviewer.cpp \
localstringviewer.cpp \
main.cpp \
mainwindow.cpp \
materialviewer.cpp \
preferenceeditor.cpp \
soundviewer.cpp \
stringtableviewer.cpp \
rumblegraphviewer.cpp \
rumblefileviewer.cpp \
techsetviewer.cpp \
xtreewidget.cpp \
xtreewidgetitem.cpp \
zonefileviewer.cpp
HEADERS += \
aboutdialog.h \
d3dbsp_structs.h \
ddsviewer.h \
fastfileviewer.h \
imagewidget.h \
iwiviewer.h \
localstringviewer.h \
mainwindow.h \
materialviewer.h \
preferenceeditor.h \
soundviewer.h \
stringtableviewer.h \
rumblegraphviewer.h \
rumblefileviewer.h \
techsetviewer.h \
xtreewidget.h \
xtreewidgetitem.h \
zonefileviewer.h
FORMS += \
aboutdialog.ui \
ddsviewer.ui \
fastfileviewer.ui \
imagewidget.ui \
iwiviewer.ui \
localstringviewer.ui \
mainwindow.ui \
materialviewer.ui \
modelviewer.ui \
preferenceeditor.ui \
soundviewer.ui \
stringtableviewer.ui \
rumblegraphviewer.ui \
rumblefileviewer.ui \
techsetviewer.ui \
zonefileviewer.ui
RESOURCES += ../data/data.qrc
LIBS += \
-L$$PWD/../third_party/devil_sdk/lib/ -lDevIL -lILU -lILUT \
-L$$PWD/../third_party/zlib/lib/ -lzlib \
-L$$PWD/../third_party/xbox_sdk/lib -lxcompress64 \
-L$$OUT_PWD/../libs/ -lcore \
-L$$OUT_PWD/../libs/ -lcompression \
-L$$OUT_PWD/../libs/ -lencryption \
-L$$OUT_PWD/../libs/ -lfastfile \
-L$$OUT_PWD/../libs/ -lddsfile \
-L$$OUT_PWD/../libs/ -lipakfile \
-L$$OUT_PWD/../libs/ -liwifile \
-L$$OUT_PWD/../libs/ -lzonefile
INCLUDEPATH += \
$$PWD/../third_party/devil_sdk/include/ \
$$PWD/../third_party/zlib/include \
$$PWD/../third_party/xbox_sdk/include \
$$PWD/../libs/core \
$$PWD/../libs/compression \
$$PWD/../libs/encryption \
$$PWD/../libs/fastfile \
$$PWD/../libs/ddsfile \
$$PWD/../libs/ipakfile \
$$PWD/../libs/iwifile \
$$PWD/../libs/zonefile
DEPENDPATH += \
$$PWD/../third_party/devil_sdk/include/ \
$$PWD/../third_party/zlib/include \
$$PWD/../third_party/xbox_sdk/include \
$$PWD/../libs/core \
$$PWD/../libs/compression \
$$PWD/../libs/encryption \
$$PWD/../libs/fastfile \
$$PWD/../libs/ddsfile \
$$PWD/../libs/ipakfile \
$$PWD/../libs/iwifile \
$$PWD/../libs/zonefile
# Copy DLLs to Debug folder
QMAKE_POST_LINK += xcopy /Y /E /I \"G:/Projects/Qt/XPlor/third_party/devil_sdk/lib\\*.dll\" \"$$OUT_PWD/debug/\" $$escape_expand(\\n\\t)
QMAKE_POST_LINK += xcopy /Y /E /I \"G:/Projects/Qt/XPlor/third_party/zlib/lib\\*.dll\" \"$$OUT_PWD/debug/\" $$escape_expand(\\n\\t)
QMAKE_POST_LINK += xcopy /Y /E /I \"G:/Projects/Qt/XPlor/third_party/xna/lib\\*.dll\" \"$$OUT_PWD/debug/\" $$escape_expand(\\n\\t)
QMAKE_POST_LINK += xcopy /Y /E /I \"$$PWD/../third_party/xbox_sdk/lib\\*.dll\" \"$$OUT_PWD/debug/\" $$escape_expand(\\n\\t)
# Copy DLLs to Release folder
QMAKE_POST_LINK += xcopy /Y /E /I \"G:/Projects/Qt/XPlor/third_party/devil_sdk/lib\\*.dll\" \"$$OUT_PWD/release/\" $$escape_expand(\\n\\t)
QMAKE_POST_LINK += xcopy /Y /E /I \"G:/Projects/Qt/XPlor/third_party/zlib/lib\\*.dll\" \"$$OUT_PWD/release/\" $$escape_expand(\\n\\t)
QMAKE_POST_LINK += xcopy /Y /E /I \"G:/Projects/Qt/XPlor/third_party/xna/lib\\*.dll\" \"$$OUT_PWD/release/\" $$escape_expand(\\n\\t)
QMAKE_POST_LINK += xcopy /Y /E /I \"$$PWD/../third_party/xbox_sdk/lib\\*.dll\" \"$$OUT_PWD/release/\" $$escape_expand(\\n\\t)

View File

@ -1,40 +0,0 @@
#include <windows.h>
IDI_ICON1 ICON "E:\\Projects\\Qt\\XPlor\\app\\app.ico"
VS_VERSION_INFO VERSIONINFO
FILEVERSION 0,0,0,0
PRODUCTVERSION 0,0,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "\0"
VALUE "FileDescription", "\0"
VALUE "FileVersion", "0.0.0.0\0"
VALUE "LegalCopyright", "\0"
VALUE "OriginalFilename", "app.exe\0"
VALUE "ProductName", "app\0"
VALUE "ProductVersion", "0.0.0.0\0"
VALUE "InternalName", "\0"
VALUE "Comments", "\0"
VALUE "LegalTrademarks", "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 1200
END
END
/* End of Version info */

View File

@ -1,384 +0,0 @@
#include "audioexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>
#include <QComboBox>
#include <QStackedWidget>
#include <QGroupBox>
#include <QPainter>
#include <QtEndian>
AudioExportDialog::AudioExportDialog(QWidget *parent)
: ExportDialog(ContentType::Audio, parent)
, mWaveformLabel(nullptr)
, mDurationLabel(nullptr)
, mSampleRateLabel(nullptr)
, mChannelsLabel(nullptr)
, mBitDepthLabel(nullptr)
, mSampleRate(44100)
, mChannels(2)
, mBitsPerSample(16)
, mDuration(0)
, mOptionsStack(nullptr)
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
// Set default format from settings
QString defaultFormat = Settings::instance().defaultAudioExportFormat().toUpper();
int index = formatCombo()->findText(defaultFormat);
if (index >= 0) {
formatCombo()->setCurrentIndex(index);
}
setupPreview();
}
void AudioExportDialog::setupPreview()
{
// Create waveform label inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mWaveformLabel = new QLabel(previewContainer());
mWaveformLabel->setAlignment(Qt::AlignCenter);
mWaveformLabel->setMinimumSize(280, 100);
mWaveformLabel->setText("No audio loaded");
mWaveformLabel->setStyleSheet("color: #808080;");
previewLayout->addWidget(mWaveformLabel);
// Audio info labels
QHBoxLayout* infoLayout = new QHBoxLayout();
mDurationLabel = new QLabel("Duration: --", this);
mSampleRateLabel = new QLabel("Sample Rate: --", this);
mChannelsLabel = new QLabel("Channels: --", this);
infoLayout->addWidget(mDurationLabel);
infoLayout->addWidget(new QLabel("|", this));
infoLayout->addWidget(mSampleRateLabel);
infoLayout->addWidget(new QLabel("|", this));
infoLayout->addWidget(mChannelsLabel);
infoLayout->addStretch();
previewLayout->addLayout(infoLayout);
// Create stacked widget for format-specific options
mOptionsStack = new QStackedWidget(this);
// WAV options (none)
mWavOptionsWidget = new QWidget(this);
QVBoxLayout* wavLayout = new QVBoxLayout(mWavOptionsWidget);
wavLayout->setContentsMargins(0, 0, 0, 0);
QLabel* wavLabel = new QLabel("WAV: Uncompressed PCM audio.", mWavOptionsWidget);
wavLabel->setStyleSheet("color: #808080;");
wavLayout->addWidget(wavLabel);
wavLayout->addStretch();
// MP3 options
mMp3OptionsWidget = new QWidget(this);
QVBoxLayout* mp3Layout = new QVBoxLayout(mMp3OptionsWidget);
mp3Layout->setContentsMargins(0, 0, 0, 0);
QLabel* bitrateLabel = new QLabel("Bitrate:", mMp3OptionsWidget);
mBitrateCombo = new QComboBox(mMp3OptionsWidget);
mBitrateCombo->addItem("128 kbps", 128);
mBitrateCombo->addItem("192 kbps", 192);
mBitrateCombo->addItem("256 kbps", 256);
mBitrateCombo->addItem("320 kbps", 320);
// Set default bitrate from settings
int defaultBitrate = Settings::instance().audioMp3Bitrate();
for (int i = 0; i < mBitrateCombo->count(); ++i) {
if (mBitrateCombo->itemData(i).toInt() == defaultBitrate) {
mBitrateCombo->setCurrentIndex(i);
break;
}
}
mp3Layout->addWidget(bitrateLabel);
mp3Layout->addWidget(mBitrateCombo);
// FFmpeg warning
if (!Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegOk = new QLabel("FFmpeg available", mMp3OptionsWidget);
ffmpegOk->setStyleSheet("color: #4CAF50;");
mp3Layout->addWidget(ffmpegOk);
} else {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for MP3 export", mMp3OptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
mp3Layout->addWidget(ffmpegWarn);
}
mp3Layout->addStretch();
connect(mBitrateCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &AudioExportDialog::onBitrateChanged);
// OGG options
mOggOptionsWidget = new QWidget(this);
QVBoxLayout* oggLayout = new QVBoxLayout(mOggOptionsWidget);
oggLayout->setContentsMargins(0, 0, 0, 0);
QLabel* oggQualityLabel = new QLabel("Quality (-1=lowest, 10=highest):", mOggOptionsWidget);
QHBoxLayout* oggSliderLayout = new QHBoxLayout();
mOggQualitySlider = new QSlider(Qt::Horizontal, mOggOptionsWidget);
mOggQualitySlider->setRange(-1, 10);
mOggQualitySlider->setValue(Settings::instance().audioOggQuality());
mOggQualitySpinBox = new QSpinBox(mOggOptionsWidget);
mOggQualitySpinBox->setRange(-1, 10);
mOggQualitySpinBox->setValue(Settings::instance().audioOggQuality());
oggSliderLayout->addWidget(mOggQualitySlider);
oggSliderLayout->addWidget(mOggQualitySpinBox);
oggLayout->addWidget(oggQualityLabel);
oggLayout->addLayout(oggSliderLayout);
if (Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for OGG export", mOggOptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
oggLayout->addWidget(ffmpegWarn);
}
oggLayout->addStretch();
connect(mOggQualitySlider, &QSlider::valueChanged, this, &AudioExportDialog::onOggQualityChanged);
connect(mOggQualitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mOggQualitySlider, &QSlider::setValue);
// FLAC options
mFlacOptionsWidget = new QWidget(this);
QVBoxLayout* flacLayout = new QVBoxLayout(mFlacOptionsWidget);
flacLayout->setContentsMargins(0, 0, 0, 0);
QLabel* flacCompressionLabel = new QLabel("Compression (0=fast, 8=best):", mFlacOptionsWidget);
QHBoxLayout* flacSliderLayout = new QHBoxLayout();
mFlacCompressionSlider = new QSlider(Qt::Horizontal, mFlacOptionsWidget);
mFlacCompressionSlider->setRange(0, 8);
mFlacCompressionSlider->setValue(Settings::instance().audioFlacCompression());
mFlacCompressionSpinBox = new QSpinBox(mFlacOptionsWidget);
mFlacCompressionSpinBox->setRange(0, 8);
mFlacCompressionSpinBox->setValue(Settings::instance().audioFlacCompression());
flacSliderLayout->addWidget(mFlacCompressionSlider);
flacSliderLayout->addWidget(mFlacCompressionSpinBox);
flacLayout->addWidget(flacCompressionLabel);
flacLayout->addLayout(flacSliderLayout);
if (Settings::instance().ffmpegPath().isEmpty()) {
QLabel* ffmpegWarn = new QLabel("FFmpeg required for FLAC export", mFlacOptionsWidget);
ffmpegWarn->setStyleSheet("color: #FF9800;");
flacLayout->addWidget(ffmpegWarn);
}
flacLayout->addStretch();
connect(mFlacCompressionSlider, &QSlider::valueChanged, this, &AudioExportDialog::onFlacCompressionChanged);
connect(mFlacCompressionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mFlacCompressionSlider, &QSlider::setValue);
// Add to stacked widget
mOptionsStack->addWidget(mWavOptionsWidget); // Index 0
mOptionsStack->addWidget(mMp3OptionsWidget); // Index 1
mOptionsStack->addWidget(mOggOptionsWidget); // Index 2
mOptionsStack->addWidget(mFlacOptionsWidget); // Index 3
// Add stacked widget to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
optionsLayout->addWidget(mOptionsStack);
}
// Show options for current format
showOptionsForFormat(formatCombo()->currentText());
}
void AudioExportDialog::updatePreview()
{
if (mData.isEmpty()) {
mWaveformLabel->setText("No audio loaded");
return;
}
parseWavInfo();
drawWaveform();
}
QStringList AudioExportDialog::supportedFormats() const
{
return {"wav", "mp3", "ogg", "flac"};
}
void AudioExportDialog::parseWavInfo()
{
if (mData.size() < 44) return;
const char* data = mData.constData();
// Check for RIFF header
bool isRiff = (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F');
bool isRifx = (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'X');
if (!isRiff && !isRifx) return;
bool bigEndian = isRifx;
// Parse format chunk
if (bigEndian) {
mChannels = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(data + 22));
mSampleRate = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(data + 24));
mBitsPerSample = qFromBigEndian<quint16>(reinterpret_cast<const uchar*>(data + 34));
} else {
mChannels = qFromLittleEndian<quint16>(reinterpret_cast<const uchar*>(data + 22));
mSampleRate = qFromLittleEndian<quint32>(reinterpret_cast<const uchar*>(data + 24));
mBitsPerSample = qFromLittleEndian<quint16>(reinterpret_cast<const uchar*>(data + 34));
}
// Calculate duration
int dataSize = mData.size() - 44;
int bytesPerSample = mBitsPerSample / 8;
if (bytesPerSample > 0 && mChannels > 0 && mSampleRate > 0) {
int numSamples = dataSize / (bytesPerSample * mChannels);
mDuration = static_cast<double>(numSamples) / mSampleRate;
}
// Update labels
int minutes = static_cast<int>(mDuration) / 60;
double seconds = mDuration - (minutes * 60);
mDurationLabel->setText(QString("Duration: %1:%2")
.arg(minutes)
.arg(seconds, 6, 'f', 3, '0'));
mSampleRateLabel->setText(QString("%1 Hz").arg(mSampleRate));
mChannelsLabel->setText(mChannels == 1 ? "Mono" : "Stereo");
}
void AudioExportDialog::drawWaveform()
{
if (mData.size() < 44) return;
int labelWidth = mWaveformLabel->width();
int labelHeight = mWaveformLabel->height();
if (labelWidth < 10 || labelHeight < 10) {
labelWidth = 280;
labelHeight = 100;
}
// Get theme colors
Theme theme = Settings::instance().theme();
QColor bgColor(theme.panelColor);
QColor waveColor(theme.accentColor);
QColor centerLineColor(theme.borderColor);
// Create pixmap
mWaveformPixmap = QPixmap(labelWidth, labelHeight);
mWaveformPixmap.fill(bgColor);
QPainter painter(&mWaveformPixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Draw center line
int centerY = labelHeight / 2;
painter.setPen(centerLineColor);
painter.drawLine(0, centerY, labelWidth, centerY);
// Get audio data (skip 44-byte header)
const qint16* samples = reinterpret_cast<const qint16*>(mData.constData() + 44);
int numSamples = (mData.size() - 44) / (mBitsPerSample / 8);
if (mChannels > 1) {
numSamples /= mChannels;
}
if (numSamples < 2) {
mWaveformLabel->setPixmap(mWaveformPixmap);
return;
}
// Draw waveform
painter.setPen(waveColor);
int samplesPerPixel = numSamples / labelWidth;
if (samplesPerPixel < 1) samplesPerPixel = 1;
int amplitude = labelHeight / 4; // Max amplitude (25% above/below center)
for (int x = 0; x < labelWidth; ++x) {
int startSample = x * samplesPerPixel;
int endSample = qMin(startSample + samplesPerPixel, numSamples);
qint16 minVal = 0, maxVal = 0;
for (int i = startSample; i < endSample; ++i) {
int idx = i * mChannels; // Use first channel
if (idx < (mData.size() - 44) / 2) {
qint16 sample = samples[idx];
if (sample < minVal) minVal = sample;
if (sample > maxVal) maxVal = sample;
}
}
// Scale to display
int yMin = centerY - (maxVal * amplitude / 32768);
int yMax = centerY - (minVal * amplitude / 32768);
painter.drawLine(x, yMin, x, yMax);
}
mWaveformLabel->setPixmap(mWaveformPixmap);
}
void AudioExportDialog::onFormatChanged(const QString& format)
{
ExportDialog::onFormatChanged(format);
showOptionsForFormat(format);
}
void AudioExportDialog::showOptionsForFormat(const QString& format)
{
QString fmt = format.toLower();
if (fmt == "wav") {
mOptionsStack->setCurrentWidget(mWavOptionsWidget);
} else if (fmt == "mp3") {
mOptionsStack->setCurrentWidget(mMp3OptionsWidget);
} else if (fmt == "ogg") {
mOptionsStack->setCurrentWidget(mOggOptionsWidget);
} else if (fmt == "flac") {
mOptionsStack->setCurrentWidget(mFlacOptionsWidget);
}
}
int AudioExportDialog::mp3Bitrate() const
{
return mBitrateCombo ? mBitrateCombo->currentData().toInt() : 256;
}
int AudioExportDialog::oggQuality() const
{
return mOggQualitySlider ? mOggQualitySlider->value() : 5;
}
int AudioExportDialog::flacCompression() const
{
return mFlacCompressionSlider ? mFlacCompressionSlider->value() : 5;
}
void AudioExportDialog::onBitrateChanged(int index)
{
Q_UNUSED(index);
// Could update size estimate here
}
void AudioExportDialog::onOggQualityChanged(int value)
{
if (mOggQualitySpinBox) {
mOggQualitySpinBox->blockSignals(true);
mOggQualitySpinBox->setValue(value);
mOggQualitySpinBox->blockSignals(false);
}
}
void AudioExportDialog::onFlacCompressionChanged(int value)
{
if (mFlacCompressionSpinBox) {
mFlacCompressionSpinBox->blockSignals(true);
mFlacCompressionSpinBox->setValue(value);
mFlacCompressionSpinBox->blockSignals(false);
}
}

View File

@ -1,82 +0,0 @@
#ifndef AUDIOEXPORTDIALOG_H
#define AUDIOEXPORTDIALOG_H
#include "exportdialog.h"
class QSlider;
class QSpinBox;
class QComboBox;
class QStackedWidget;
/**
* @brief Export dialog for audio with waveform preview and format-specific options.
*
* Shows a waveform visualization and provides bitrate/quality settings
* for different output formats (WAV, MP3, OGG, FLAC).
*/
class AudioExportDialog : public ExportDialog
{
Q_OBJECT
public:
explicit AudioExportDialog(QWidget *parent = nullptr);
// Audio-specific settings
int mp3Bitrate() const;
int oggQuality() const; // -1 to 10
int flacCompression() const; // 0-8
protected:
void setupPreview() override;
void updatePreview() override;
QStringList supportedFormats() const override;
void onFormatChanged(const QString& format) override;
private slots:
void onBitrateChanged(int index);
void onOggQualityChanged(int value);
void onFlacCompressionChanged(int value);
private:
void parseWavInfo();
void drawWaveform();
void showOptionsForFormat(const QString& format);
// Waveform display
QLabel* mWaveformLabel;
QPixmap mWaveformPixmap;
// Audio info
QLabel* mDurationLabel;
QLabel* mSampleRateLabel;
QLabel* mChannelsLabel;
QLabel* mBitDepthLabel;
// Parsed audio info
int mSampleRate;
int mChannels;
int mBitsPerSample;
double mDuration;
// Format-specific option widgets
QStackedWidget* mOptionsStack;
// WAV options (none)
QWidget* mWavOptionsWidget;
// MP3 options
QWidget* mMp3OptionsWidget;
QComboBox* mBitrateCombo;
// OGG options
QWidget* mOggOptionsWidget;
QSlider* mOggQualitySlider;
QSpinBox* mOggQualitySpinBox;
// FLAC options
QWidget* mFlacOptionsWidget;
QSlider* mFlacCompressionSlider;
QSpinBox* mFlacCompressionSpinBox;
};
#endif // AUDIOEXPORTDIALOG_H

View File

@ -1,566 +0,0 @@
#include "audiopreviewwidget.h"
#include <QHeaderView>
#include <QtEndian>
#include <QPainter>
#include <QPen>
#include <QDir>
#include <QShowEvent>
#include <QResizeEvent>
#include <QTimer>
AudioPreviewWidget::AudioPreviewWidget(QWidget *parent)
: QWidget(parent)
, mPlayer(new QMediaPlayer(this))
, mAudioOutput(new QAudioOutput(this))
, mAudioBuffer(nullptr)
, mPositionTimer(new QTimer(this))
, mDuration(0)
, mCalculatedDuration(0)
, mSampleRate(0)
, mChannels(0)
, mBitsPerSample(0)
, mDataSize(0)
, mAudioFormat(0)
, mBigEndian(false)
{
mPlayer->setAudioOutput(mAudioOutput);
mAudioOutput->setVolume(0.5);
// Timer for updating position slider during playback
mPositionTimer->setInterval(50); // 50ms = 20 updates per second
connect(mPositionTimer, &QTimer::timeout, this, &AudioPreviewWidget::onUpdatePosition);
// Create UI
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
// Splitter for preview and metadata
auto *splitter = new QSplitter(Qt::Horizontal, this);
// Left side - audio controls
auto *controlsWidget = new QWidget(splitter);
controlsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
auto *controlsLayout = new QVBoxLayout(controlsWidget);
controlsLayout->setContentsMargins(4, 4, 4, 4);
mFilenameLabel = new QLabel(this);
mFilenameLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
mFilenameLabel->setStyleSheet("QLabel { background-color: #252526; color: #888; padding: 4px 8px; font-size: 11px; }");
controlsLayout->addWidget(mFilenameLabel);
// Waveform display - expands vertically
mWaveformLabel = new QLabel(this);
mWaveformLabel->setMinimumHeight(80);
mWaveformLabel->setMinimumWidth(400);
mWaveformLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
mWaveformLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
mWaveformLabel->setStyleSheet("QLabel { background-color: #1e1e1e; border: 1px solid #333; }");
controlsLayout->addWidget(mWaveformLabel, 1);
// Time and position
auto *positionLayout = new QHBoxLayout();
mTimeLabel = new QLabel("00:00 / 00:00", this);
mPositionSlider = new QSlider(Qt::Horizontal, this);
mPositionSlider->setRange(0, 0);
mPositionSlider->setSingleStep(100); // 100ms per arrow key
mPositionSlider->setPageStep(1000); // 1 second per page up/down
mPositionSlider->setTracking(true); // Update while dragging
positionLayout->addWidget(mPositionSlider, 1);
positionLayout->addWidget(mTimeLabel);
controlsLayout->addLayout(positionLayout);
// Playback controls
auto *buttonLayout = new QHBoxLayout();
mPlayButton = new QPushButton("Play", this);
mStopButton = new QPushButton("Stop", this);
mVolumeSlider = new QSlider(Qt::Horizontal, this);
mVolumeSlider->setRange(0, 100);
mVolumeSlider->setValue(50);
mVolumeSlider->setMaximumWidth(100);
auto *volumeLabel = new QLabel("Vol:", this);
buttonLayout->addWidget(mPlayButton);
buttonLayout->addWidget(mStopButton);
buttonLayout->addStretch();
buttonLayout->addWidget(volumeLabel);
buttonLayout->addWidget(mVolumeSlider);
controlsLayout->addLayout(buttonLayout);
splitter->addWidget(controlsWidget);
// Right side - metadata tree
mMetadataTree = new QTreeWidget(splitter);
mMetadataTree->setHeaderLabels({"Property", "Value"});
mMetadataTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
mMetadataTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
mMetadataTree->setAlternatingRowColors(true);
splitter->addWidget(mMetadataTree);
splitter->setSizes({400, 200});
mainLayout->addWidget(splitter);
// Connect signals
connect(mPlayButton, &QPushButton::clicked, this, &AudioPreviewWidget::onPlayPause);
connect(mStopButton, &QPushButton::clicked, this, &AudioPreviewWidget::onStop);
connect(mPositionSlider, &QSlider::sliderMoved, this, &AudioPreviewWidget::onSliderMoved);
connect(mVolumeSlider, &QSlider::valueChanged, this, [this](int value) {
mAudioOutput->setVolume(value / 100.0);
});
connect(mPlayer, &QMediaPlayer::positionChanged, this, &AudioPreviewWidget::onPositionChanged);
connect(mPlayer, &QMediaPlayer::durationChanged, this, &AudioPreviewWidget::onDurationChanged);
connect(mPlayer, &QMediaPlayer::mediaStatusChanged, this, &AudioPreviewWidget::onMediaStatusChanged);
// Connect to theme changes
connect(&Settings::instance(), &Settings::themeChanged, this, &AudioPreviewWidget::applyTheme);
// Apply current theme
applyTheme(Settings::instance().theme());
}
AudioPreviewWidget::~AudioPreviewWidget()
{
mPlayer->stop();
if (mAudioBuffer) {
mAudioBuffer->close();
delete mAudioBuffer;
}
}
void AudioPreviewWidget::parseWavHeader(const QByteArray &data)
{
if (data.size() < 44) return;
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
// Check RIFF header - RIFF = little-endian, RIFX = big-endian
mBigEndian = false;
if (data.left(4) == "RIFX") {
mBigEndian = true;
} else if (data.left(4) != "RIFF") {
return;
}
if (data.mid(8, 4) != "WAVE") return;
// Helper lambdas for endian-aware reading
auto readU16 = [d, this](int offset) -> quint16 {
return mBigEndian ? qFromBigEndian<quint16>(d + offset) : qFromLittleEndian<quint16>(d + offset);
};
auto readU32 = [d, this](int offset) -> quint32 {
return mBigEndian ? qFromBigEndian<quint32>(d + offset) : qFromLittleEndian<quint32>(d + offset);
};
// Find fmt chunk
int pos = 12;
while (pos < data.size() - 8) {
QString chunkId = QString::fromLatin1(data.mid(pos, 4));
quint32 chunkSize = readU32(pos + 4);
if (chunkId == "fmt ") {
if (static_cast<qint64>(pos) + 8 + chunkSize <= data.size()) {
mAudioFormat = readU16(pos + 8);
mChannels = readU16(pos + 10);
mSampleRate = readU32(pos + 12);
mBitsPerSample = readU16(pos + 22);
}
} else if (chunkId == "data") {
mDataSize = chunkSize;
break;
}
pos += 8 + chunkSize;
if (chunkSize % 2) pos++; // Padding
}
}
bool AudioPreviewWidget::loadFromData(const QByteArray &data, const QString &filename)
{
mFilename = filename;
mFilenameLabel->setText(filename);
mAudioData = data;
// Parse WAV header for info
parseWavHeader(data);
// Add WAV info to metadata tree
mMetadataTree->clear();
auto *formatItem = new QTreeWidgetItem(mMetadataTree);
formatItem->setText(0, "Format");
QString formatStr = mBigEndian ? "WAV (RIFX/BE)" : "WAV (RIFF/LE)";
formatItem->setText(1, formatStr);
if (mAudioFormat > 0) {
auto *codecItem = new QTreeWidgetItem(mMetadataTree);
codecItem->setText(0, "Audio Codec");
QString codecStr;
switch (mAudioFormat) {
case 1: codecStr = "PCM"; break;
case 2: codecStr = "MS ADPCM"; break;
case 6: codecStr = "A-law"; break;
case 7: codecStr = "u-law"; break;
case 17: codecStr = "IMA ADPCM"; break;
case 85: codecStr = "MP3"; break;
case 0x165: codecStr = "XMA"; break;
case 0x166: codecStr = "XMA2"; break;
default: codecStr = QString("0x%1").arg(mAudioFormat, 0, 16); break;
}
codecItem->setText(1, codecStr);
}
if (mSampleRate > 0) {
auto *srItem = new QTreeWidgetItem(mMetadataTree);
srItem->setText(0, "Sample Rate");
srItem->setText(1, QString("%1 Hz").arg(mSampleRate));
}
if (mChannels > 0) {
auto *chItem = new QTreeWidgetItem(mMetadataTree);
chItem->setText(0, "Channels");
chItem->setText(1, mChannels == 1 ? "Mono" : (mChannels == 2 ? "Stereo" : QString::number(mChannels)));
}
if (mBitsPerSample > 0) {
auto *bpsItem = new QTreeWidgetItem(mMetadataTree);
bpsItem->setText(0, "Bit Depth");
bpsItem->setText(1, QString("%1-bit").arg(mBitsPerSample));
}
if (mDataSize > 0) {
auto *sizeItem = new QTreeWidgetItem(mMetadataTree);
sizeItem->setText(0, "Data Size");
sizeItem->setText(1, QString("%1 bytes").arg(mDataSize));
}
// Calculate duration from WAV header data
mCalculatedDuration = 0;
// Always try to play - QMediaPlayer/Windows Media Foundation may support more formats
bool canPlay = true;
if (mSampleRate > 0 && mChannels > 0 && mDataSize > 0 && mBitsPerSample > 0) {
// Calculate duration assuming PCM - many "XMA2" labeled files contain PCM data
int bytesPerSample = mBitsPerSample / 8;
mCalculatedDuration = static_cast<double>(mDataSize) / (mSampleRate * mChannels * bytesPerSample);
auto *durItem = new QTreeWidgetItem(mMetadataTree);
durItem->setText(0, "Duration");
int mins = static_cast<int>(mCalculatedDuration) / 60;
int secs = static_cast<int>(mCalculatedDuration) % 60;
int ms = static_cast<int>((mCalculatedDuration - static_cast<int>(mCalculatedDuration)) * 1000);
durItem->setText(1, QString("%1:%2.%3")
.arg(mins, 2, 10, QChar('0'))
.arg(secs, 2, 10, QChar('0'))
.arg(ms, 3, 10, QChar('0')));
}
// Disable playback for unsupported formats
if (!canPlay) {
mPlayButton->setEnabled(false);
mPlayButton->setText("N/A");
mPlayButton->setToolTip("XMA/XMA2 playback not supported");
mStopButton->setEnabled(false);
mPositionSlider->setEnabled(false);
} else {
mPlayButton->setEnabled(true);
mPlayButton->setText("Play");
mPlayButton->setToolTip("");
mStopButton->setEnabled(true);
mPositionSlider->setEnabled(true);
}
auto *fileSizeItem = new QTreeWidgetItem(mMetadataTree);
fileSizeItem->setText(0, "File Size");
fileSizeItem->setText(1, QString("%1 bytes").arg(data.size()));
// Debug: show first 32 bytes as hex (includes fmt chunk start)
auto *headerItem = new QTreeWidgetItem(mMetadataTree);
headerItem->setText(0, "Header (hex)");
QString headerHex;
for (int i = 0; i < qMin(32, data.size()); i++) {
headerHex += QString("%1 ").arg(static_cast<uchar>(data[i]), 2, 16, QChar('0'));
}
headerItem->setText(1, headerHex.trimmed());
// Debug: show raw format value
auto *rawFmtItem = new QTreeWidgetItem(mMetadataTree);
rawFmtItem->setText(0, "Raw Format");
rawFmtItem->setText(1, QString("0x%1 (%2)").arg(mAudioFormat, 4, 16, QChar('0')).arg(mAudioFormat));
// Setup audio buffer for playback
if (mAudioBuffer) {
mAudioBuffer->close();
delete mAudioBuffer;
}
mAudioBuffer = new QBuffer(this);
mAudioBuffer->setData(mAudioData);
mAudioBuffer->open(QIODevice::ReadOnly);
mPlayer->setSourceDevice(mAudioBuffer);
// Initialize slider and time display with calculated duration (authoritative)
if (mCalculatedDuration > 0) {
mDuration = static_cast<qint64>(mCalculatedDuration * 1000);
mPositionSlider->setRange(0, static_cast<int>(mDuration));
mPositionSlider->setValue(0);
}
updateTimeLabel();
// Draw waveform visualization
drawWaveform();
return true;
}
void AudioPreviewWidget::drawWaveform()
{
if (mAudioData.isEmpty()) return;
// Use actual label size for dynamic sizing
int width = qMax(400, mWaveformLabel->width());
int height = qMax(80, mWaveformLabel->height());
mWaveformPixmap = QPixmap(width, height);
mWaveformPixmap.fill(mBgColor);
QPainter painter(&mWaveformPixmap);
painter.setRenderHint(QPainter::Antialiasing);
int centerY = height / 2;
int amplitudeHeight = height / 4; // Half of available space (25% above + 25% below center)
// Draw center line
painter.setPen(mBorderColor);
painter.drawLine(0, centerY, width, centerY);
// Find data section
int dataStart = 44; // Standard WAV header
if (mAudioData.size() > dataStart)
{
const qint16 *samples = reinterpret_cast<const qint16*>(mAudioData.constData() + dataStart);
int numSamples = (mAudioData.size() - dataStart) / 2;
int samplesPerPixel = qMax(1, numSamples / width);
painter.setPen(mAccentColor); // Theme accent color
for (int x = 0; x < width && x * samplesPerPixel < numSamples; x++)
{
qint16 minVal = 0, maxVal = 0;
for (int i = 0; i < samplesPerPixel && (x * samplesPerPixel + i) < numSamples; i++)
{
qint16 sample = samples[x * samplesPerPixel + i];
minVal = qMin(minVal, sample);
maxVal = qMax(maxVal, sample);
}
// Scale to full amplitude height
int yMin = centerY - (maxVal * amplitudeHeight / 32768);
int yMax = centerY - (minVal * amplitudeHeight / 32768);
painter.drawLine(x, yMin, x, yMax);
}
}
painter.end();
updateWaveformPosition();
}
void AudioPreviewWidget::applyTheme(const Theme &theme)
{
mAccentColor = QColor(theme.accentColor);
mBgColor = QColor(theme.backgroundColor);
mPanelColor = QColor(theme.panelColor);
mTextColor = QColor(theme.textColor);
mTextColorMuted = QColor(theme.textColorMuted);
mBorderColor = QColor(theme.borderColor);
// Update UI element styles
mFilenameLabel->setStyleSheet(QString(
"QLabel { background-color: %1; color: %2; padding: 4px 8px; font-size: 11px; }"
).arg(theme.panelColor, theme.textColorMuted));
mWaveformLabel->setStyleSheet(QString(
"QLabel { background-color: %1; border: 1px solid %2; }"
).arg(theme.backgroundColor, theme.borderColor));
mMetadataTree->setStyleSheet(QString(
"QTreeWidget { background-color: %1; color: %2; border: none; }"
"QTreeWidget::item:selected { background-color: %3; color: white; }"
"QTreeWidget::item:alternate { background-color: %4; }"
"QHeaderView::section { background-color: %4; color: %5; padding: 4px; border: none; }"
).arg(theme.backgroundColor, theme.textColor, theme.accentColor, theme.panelColor, theme.textColorMuted));
// Redraw waveform with new colors
if (!mAudioData.isEmpty()) {
drawWaveform();
}
}
void AudioPreviewWidget::updateWaveformPosition()
{
if (mWaveformPixmap.isNull()) return;
QPixmap displayPixmap = mWaveformPixmap.copy();
QPainter painter(&displayPixmap);
// Use calculated duration as authoritative source (in ms)
qint64 durationMs = static_cast<qint64>(mCalculatedDuration * 1000);
if (durationMs <= 0) durationMs = mDuration;
// Draw playback position line
if (durationMs > 0)
{
qint64 pos = mPlayer->position();
double progress = static_cast<double>(pos) / static_cast<double>(durationMs);
int xPos = static_cast<int>(progress * displayPixmap.width());
// Use lighter version of accent color for position line
painter.setPen(QPen(mAccentColor.lighter(140), 3));
painter.drawLine(xPos, 0, xPos, displayPixmap.height());
}
painter.end();
// Scale to fill the label
int targetWidth = mWaveformLabel->width();
int targetHeight = mWaveformLabel->height();
if (targetWidth < 100) targetWidth = 400;
if (targetHeight < 50) targetHeight = 120;
mWaveformLabel->setPixmap(displayPixmap.scaled(targetWidth, targetHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
}
void AudioPreviewWidget::setMetadata(const QVariantMap &metadata)
{
// Add custom metadata from parsed fields (caller provides only visible fields)
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
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 AudioPreviewWidget::onPlayPause()
{
if (mPlayer->playbackState() == QMediaPlayer::PlayingState) {
mPlayer->pause();
mPlayButton->setText("Play");
mPositionTimer->stop();
} else {
mPlayer->play();
mPlayButton->setText("Pause");
mPositionTimer->start();
}
}
void AudioPreviewWidget::onStop()
{
mPlayer->stop();
mPositionTimer->stop();
mPlayButton->setText("Play");
updateWaveformPosition();
}
void AudioPreviewWidget::onPositionChanged(qint64 position)
{
if (!mPositionSlider->isSliderDown()) {
mPositionSlider->setValue(static_cast<int>(position));
}
updateTimeLabel();
updateWaveformPosition();
}
void AudioPreviewWidget::onDurationChanged(qint64 duration)
{
// Prefer calculated duration from WAV header as it's more reliable
if (mCalculatedDuration > 0) {
mDuration = static_cast<qint64>(mCalculatedDuration * 1000);
} else if (duration > 0) {
mDuration = duration;
}
mPositionSlider->setRange(0, static_cast<int>(mDuration));
updateTimeLabel();
}
void AudioPreviewWidget::onSliderMoved(int position)
{
mPlayer->setPosition(position);
updateWaveformPosition();
}
void AudioPreviewWidget::onMediaStatusChanged(QMediaPlayer::MediaStatus status)
{
if (status == QMediaPlayer::EndOfMedia) {
mPositionTimer->stop();
mPlayButton->setText("Play");
mPlayer->setPosition(0);
updateWaveformPosition();
} else if (status == QMediaPlayer::LoadedMedia) {
// Set duration from calculated value (authoritative)
if (mCalculatedDuration > 0) {
mDuration = static_cast<qint64>(mCalculatedDuration * 1000);
mPositionSlider->setRange(0, static_cast<int>(mDuration));
updateTimeLabel();
}
}
}
void AudioPreviewWidget::onUpdatePosition()
{
// Always update when timer fires - let the timer start/stop control this
qint64 position = mPlayer->position();
// Update slider if not being dragged
if (!mPositionSlider->isSliderDown()) {
mPositionSlider->blockSignals(true);
mPositionSlider->setValue(static_cast<int>(position));
mPositionSlider->blockSignals(false);
}
updateTimeLabel();
updateWaveformPosition();
}
void AudioPreviewWidget::updateTimeLabel()
{
qint64 pos = mPlayer->position();
// Always use calculated duration as authoritative source
qint64 dur = static_cast<qint64>(mCalculatedDuration * 1000);
if (dur <= 0) dur = mDuration;
// Format: MM:SS.mmm
QString posStr = QString("%1:%2.%3")
.arg(pos / 60000, 2, 10, QChar('0'))
.arg((pos / 1000) % 60, 2, 10, QChar('0'))
.arg(pos % 1000, 3, 10, QChar('0'));
QString durStr = QString("%1:%2.%3")
.arg(dur / 60000, 2, 10, QChar('0'))
.arg((dur / 1000) % 60, 2, 10, QChar('0'))
.arg(dur % 1000, 3, 10, QChar('0'));
mTimeLabel->setText(posStr + " / " + durStr);
}
void AudioPreviewWidget::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
// Redraw waveform now that widget is properly sized
if (!mAudioData.isEmpty()) {
QTimer::singleShot(0, this, &AudioPreviewWidget::drawWaveform);
}
}
void AudioPreviewWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
// Redraw waveform at new size
if (!mAudioData.isEmpty()) {
drawWaveform();
}
}

View File

@ -1,90 +0,0 @@
#ifndef AUDIOPREVIEWWIDGET_H
#define AUDIOPREVIEWWIDGET_H
#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QSlider>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMediaPlayer>
#include <QAudioOutput>
#include <QBuffer>
#include <QTreeWidget>
#include <QSplitter>
#include <QTimer>
#include <QColor>
#include "settings.h"
class AudioPreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit AudioPreviewWidget(QWidget *parent = nullptr);
~AudioPreviewWidget();
bool loadFromData(const QByteArray &data, const QString &filename);
void setMetadata(const QVariantMap &metadata);
private slots:
void onPlayPause();
void onStop();
void onPositionChanged(qint64 position);
void onDurationChanged(qint64 duration);
void onSliderMoved(int position);
void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
void onUpdatePosition();
void applyTheme(const Theme &theme);
private:
void updateTimeLabel();
void parseWavHeader(const QByteArray &data);
void drawWaveform();
void updateWaveformPosition();
protected:
void showEvent(QShowEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
QMediaPlayer *mPlayer;
QAudioOutput *mAudioOutput;
QBuffer *mAudioBuffer;
QByteArray mAudioData;
QTimer *mPositionTimer;
// UI elements
QLabel *mFilenameLabel;
QLabel *mWaveformLabel;
QLabel *mTimeLabel;
QPushButton *mPlayButton;
QPushButton *mStopButton;
QSlider *mPositionSlider;
QSlider *mVolumeSlider;
QTreeWidget *mMetadataTree;
qint64 mDuration;
QString mFilename;
QPixmap mWaveformPixmap; // Store base waveform for position overlay
double mCalculatedDuration; // Duration in seconds from WAV header
// WAV info
int mSampleRate;
int mChannels;
int mBitsPerSample;
int mDataSize;
int mAudioFormat; // 1=PCM, 2=ADPCM, etc.
bool mBigEndian;
// Theme colors
QColor mAccentColor;
QColor mBgColor;
QColor mPanelColor;
QColor mTextColor;
QColor mTextColorMuted;
QColor mBorderColor;
};
#endif // AUDIOPREVIEWWIDGET_H

View File

@ -1,467 +0,0 @@
#include "batchexportdialog.h"
#include "settings.h"
#include "exportdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QHeaderView>
#include <QProgressBar>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QFileDialog>
#include <QStandardPaths>
BatchExportDialog::BatchExportDialog(QWidget *parent)
: QDialog(parent)
, mExporting(false)
, mCancelled(false)
, mItemTree(nullptr)
, mSelectionLabel(nullptr)
, mOutputPath(nullptr)
, mBrowseButton(nullptr)
, mPreserveStructure(nullptr)
, mConflictCombo(nullptr)
, mImageFormatCombo(nullptr)
, mAudioFormatCombo(nullptr)
, mProgressBar(nullptr)
, mProgressLabel(nullptr)
, mButtonBox(nullptr)
, mExportButton(nullptr)
, mCancelButton(nullptr)
{
setWindowTitle("Batch Export");
setMinimumSize(600, 500);
setModal(true);
setupUI();
}
void BatchExportDialog::setupUI()
{
QVBoxLayout* mainLayout = new QVBoxLayout(this);
// Item tree with checkboxes
mItemTree = new QTreeWidget(this);
mItemTree->setHeaderLabels({"Name", "Type", "Size"});
mItemTree->setRootIsDecorated(true);
mItemTree->setAlternatingRowColors(true);
mItemTree->header()->setStretchLastSection(false);
mItemTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
mItemTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
mItemTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
mainLayout->addWidget(mItemTree, 1);
connect(mItemTree, &QTreeWidget::itemChanged, this, &BatchExportDialog::onItemChanged);
// Selection buttons
QHBoxLayout* selectionLayout = new QHBoxLayout();
QPushButton* selectAllBtn = new QPushButton("Select All", this);
QPushButton* selectNoneBtn = new QPushButton("Select None", this);
QPushButton* selectImagesBtn = new QPushButton("Images Only", this);
QPushButton* selectAudioBtn = new QPushButton("Audio Only", this);
mSelectionLabel = new QLabel("Selected: 0 of 0", this);
selectionLayout->addWidget(selectAllBtn);
selectionLayout->addWidget(selectNoneBtn);
selectionLayout->addWidget(selectImagesBtn);
selectionLayout->addWidget(selectAudioBtn);
selectionLayout->addStretch();
selectionLayout->addWidget(mSelectionLabel);
mainLayout->addLayout(selectionLayout);
connect(selectAllBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectAll);
connect(selectNoneBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectNone);
connect(selectImagesBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectImages);
connect(selectAudioBtn, &QPushButton::clicked, this, &BatchExportDialog::onSelectAudio);
// Output options group
QGroupBox* optionsGroup = new QGroupBox("Export Options", this);
QGridLayout* optionsLayout = new QGridLayout(optionsGroup);
// Output directory
optionsLayout->addWidget(new QLabel("Output:", this), 0, 0);
mOutputPath = new QLineEdit(this);
mOutputPath->setText(Settings::instance().batchExportDirectory());
if (mOutputPath->text().isEmpty()) {
mOutputPath->setText(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
}
optionsLayout->addWidget(mOutputPath, 0, 1);
mBrowseButton = new QPushButton("Browse...", this);
optionsLayout->addWidget(mBrowseButton, 0, 2);
connect(mBrowseButton, &QPushButton::clicked, this, &BatchExportDialog::onBrowseClicked);
// Preserve structure
mPreserveStructure = new QCheckBox("Preserve folder structure", this);
mPreserveStructure->setChecked(Settings::instance().batchExportPreserveStructure());
optionsLayout->addWidget(mPreserveStructure, 1, 0, 1, 3);
// Conflict resolution
optionsLayout->addWidget(new QLabel("Conflicts:", this), 2, 0);
mConflictCombo = new QComboBox(this);
mConflictCombo->addItem("Append number", "number");
mConflictCombo->addItem("Overwrite", "overwrite");
mConflictCombo->addItem("Skip", "skip");
QString savedResolution = Settings::instance().batchExportConflictResolution();
for (int i = 0; i < mConflictCombo->count(); ++i) {
if (mConflictCombo->itemData(i).toString() == savedResolution) {
mConflictCombo->setCurrentIndex(i);
break;
}
}
optionsLayout->addWidget(mConflictCombo, 2, 1, 1, 2);
// Format options
optionsLayout->addWidget(new QLabel("Image format:", this), 3, 0);
mImageFormatCombo = new QComboBox(this);
mImageFormatCombo->addItem("PNG", "png");
mImageFormatCombo->addItem("JPG", "jpg");
mImageFormatCombo->addItem("BMP", "bmp");
mImageFormatCombo->addItem("TGA", "tga");
mImageFormatCombo->addItem("TIFF", "tiff");
QString savedImageFormat = Settings::instance().defaultImageExportFormat();
for (int i = 0; i < mImageFormatCombo->count(); ++i) {
if (mImageFormatCombo->itemData(i).toString() == savedImageFormat) {
mImageFormatCombo->setCurrentIndex(i);
break;
}
}
optionsLayout->addWidget(mImageFormatCombo, 3, 1, 1, 2);
optionsLayout->addWidget(new QLabel("Audio format:", this), 4, 0);
mAudioFormatCombo = new QComboBox(this);
mAudioFormatCombo->addItem("WAV", "wav");
mAudioFormatCombo->addItem("MP3", "mp3");
mAudioFormatCombo->addItem("OGG", "ogg");
mAudioFormatCombo->addItem("FLAC", "flac");
QString savedAudioFormat = Settings::instance().defaultAudioExportFormat();
for (int i = 0; i < mAudioFormatCombo->count(); ++i) {
if (mAudioFormatCombo->itemData(i).toString() == savedAudioFormat) {
mAudioFormatCombo->setCurrentIndex(i);
break;
}
}
optionsLayout->addWidget(mAudioFormatCombo, 4, 1, 1, 2);
mainLayout->addWidget(optionsGroup);
// Progress section
mProgressBar = new QProgressBar(this);
mProgressBar->setVisible(false);
mainLayout->addWidget(mProgressBar);
mProgressLabel = new QLabel(this);
mProgressLabel->setVisible(false);
mainLayout->addWidget(mProgressLabel);
// Dialog buttons
mButtonBox = new QDialogButtonBox(this);
mExportButton = mButtonBox->addButton("Export All", QDialogButtonBox::AcceptRole);
mCancelButton = mButtonBox->addButton(QDialogButtonBox::Cancel);
mainLayout->addWidget(mButtonBox);
connect(mExportButton, &QPushButton::clicked, this, &BatchExportDialog::onExportClicked);
connect(mCancelButton, &QPushButton::clicked, this, &QDialog::reject);
}
void BatchExportDialog::setItems(const QList<BatchExportItem>& items)
{
mItems = items;
populateTree();
updateSelectionCount();
// Update title with item count
setWindowTitle(QString("Batch Export (%1 items)").arg(items.size()));
}
void BatchExportDialog::populateTree()
{
mItemTree->clear();
mItemTree->blockSignals(true);
// Group items by path
QMap<QString, QTreeWidgetItem*> folderItems;
for (int i = 0; i < mItems.size(); ++i) {
const BatchExportItem& item = mItems[i];
// Determine parent folder
QString folder;
QString name = item.name;
int lastSlash = item.path.lastIndexOf('/');
if (lastSlash > 0) {
folder = item.path.left(lastSlash);
}
QTreeWidgetItem* parent = nullptr;
if (!folder.isEmpty()) {
if (!folderItems.contains(folder)) {
// Create folder item
QTreeWidgetItem* folderItem = new QTreeWidgetItem(mItemTree);
folderItem->setText(0, folder);
folderItem->setCheckState(0, Qt::Checked);
folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate);
folderItem->setData(0, Qt::UserRole, -1); // -1 indicates folder
folderItems[folder] = folderItem;
}
parent = folderItems[folder];
}
// Create item
QTreeWidgetItem* treeItem = parent ? new QTreeWidgetItem(parent) : new QTreeWidgetItem(mItemTree);
treeItem->setText(0, name);
treeItem->setCheckState(0, item.selected ? Qt::Checked : Qt::Unchecked);
treeItem->setData(0, Qt::UserRole, i); // Store index
// Type column
QString typeStr;
switch (item.contentType) {
case ExportDialog::Image: typeStr = "Image"; break;
case ExportDialog::Audio: typeStr = "Audio"; break;
case ExportDialog::Video: typeStr = "Video"; break;
case ExportDialog::Text: typeStr = "Text"; break;
default: typeStr = "Binary"; break;
}
treeItem->setText(1, typeStr);
// Size column
qint64 size = item.data.size();
QString sizeStr;
if (size >= 1024 * 1024) {
sizeStr = QString("%1 MB").arg(size / (1024.0 * 1024.0), 0, 'f', 1);
} else if (size >= 1024) {
sizeStr = QString("%1 KB").arg(size / 1024.0, 0, 'f', 0);
} else {
sizeStr = QString("%1 B").arg(size);
}
treeItem->setText(2, sizeStr);
}
mItemTree->expandAll();
mItemTree->blockSignals(false);
}
QString BatchExportDialog::outputDirectory() const
{
return mOutputPath->text();
}
bool BatchExportDialog::preserveStructure() const
{
return mPreserveStructure->isChecked();
}
QString BatchExportDialog::conflictResolution() const
{
return mConflictCombo->currentData().toString();
}
QString BatchExportDialog::imageFormat() const
{
return mImageFormatCombo->currentData().toString();
}
QString BatchExportDialog::audioFormat() const
{
return mAudioFormatCombo->currentData().toString();
}
QList<BatchExportItem> BatchExportDialog::selectedItems() const
{
QList<BatchExportItem> selected;
std::function<void(QTreeWidgetItem*)> collectSelected = [&](QTreeWidgetItem* item) {
int idx = item->data(0, Qt::UserRole).toInt();
if (idx >= 0 && idx < mItems.size()) {
if (item->checkState(0) == Qt::Checked) {
selected.append(mItems[idx]);
}
}
for (int i = 0; i < item->childCount(); ++i) {
collectSelected(item->child(i));
}
};
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
collectSelected(mItemTree->topLevelItem(i));
}
return selected;
}
void BatchExportDialog::updateProgress(int current, int total, const QString& currentItem)
{
mProgressBar->setMaximum(total);
mProgressBar->setValue(current);
mProgressLabel->setText(QString("Exporting: %1").arg(currentItem));
}
void BatchExportDialog::onExportCompleted(int succeeded, int failed, int skipped)
{
mExporting = false;
mProgressBar->setVisible(false);
mProgressLabel->setVisible(false);
mExportButton->setEnabled(true);
mExportButton->setText("Export All");
// Show results
mProgressLabel->setText(QString("Completed: %1 succeeded, %2 failed, %3 skipped")
.arg(succeeded).arg(failed).arg(skipped));
mProgressLabel->setVisible(true);
if (failed == 0) {
accept();
}
}
void BatchExportDialog::onSelectAll()
{
mItemTree->blockSignals(true);
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
QTreeWidgetItem* item = mItemTree->topLevelItem(i);
item->setCheckState(0, Qt::Checked);
}
mItemTree->blockSignals(false);
updateSelectionCount();
}
void BatchExportDialog::onSelectNone()
{
mItemTree->blockSignals(true);
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
QTreeWidgetItem* item = mItemTree->topLevelItem(i);
item->setCheckState(0, Qt::Unchecked);
}
mItemTree->blockSignals(false);
updateSelectionCount();
}
void BatchExportDialog::onSelectImages()
{
mItemTree->blockSignals(true);
std::function<void(QTreeWidgetItem*)> setByType = [&](QTreeWidgetItem* item) {
int idx = item->data(0, Qt::UserRole).toInt();
if (idx >= 0 && idx < mItems.size()) {
bool isImage = mItems[idx].contentType == ExportDialog::Image;
item->setCheckState(0, isImage ? Qt::Checked : Qt::Unchecked);
}
for (int i = 0; i < item->childCount(); ++i) {
setByType(item->child(i));
}
};
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
setByType(mItemTree->topLevelItem(i));
}
mItemTree->blockSignals(false);
updateSelectionCount();
}
void BatchExportDialog::onSelectAudio()
{
mItemTree->blockSignals(true);
std::function<void(QTreeWidgetItem*)> setByType = [&](QTreeWidgetItem* item) {
int idx = item->data(0, Qt::UserRole).toInt();
if (idx >= 0 && idx < mItems.size()) {
bool isAudio = mItems[idx].contentType == ExportDialog::Audio;
item->setCheckState(0, isAudio ? Qt::Checked : Qt::Unchecked);
}
for (int i = 0; i < item->childCount(); ++i) {
setByType(item->child(i));
}
};
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
setByType(mItemTree->topLevelItem(i));
}
mItemTree->blockSignals(false);
updateSelectionCount();
}
void BatchExportDialog::onBrowseClicked()
{
QString dir = QFileDialog::getExistingDirectory(this, "Select Output Directory",
mOutputPath->text());
if (!dir.isEmpty()) {
mOutputPath->setText(dir);
}
}
void BatchExportDialog::onExportClicked()
{
if (mExporting) {
mCancelled = true;
return;
}
// Save settings
Settings::instance().setBatchExportDirectory(mOutputPath->text());
Settings::instance().setBatchExportPreserveStructure(mPreserveStructure->isChecked());
Settings::instance().setBatchExportConflictResolution(mConflictCombo->currentData().toString());
// Start export
mExporting = true;
mCancelled = false;
mExportButton->setText("Cancel");
mProgressBar->setVisible(true);
mProgressBar->setValue(0);
mProgressLabel->setVisible(true);
// Signal acceptance - MainWindow will handle actual export
accept();
}
void BatchExportDialog::onItemChanged(QTreeWidgetItem* item, int column)
{
Q_UNUSED(item);
Q_UNUSED(column);
updateSelectionCount();
}
void BatchExportDialog::updateSelectionCount()
{
int selected = 0;
int total = 0;
std::function<void(QTreeWidgetItem*)> count = [&](QTreeWidgetItem* item) {
int idx = item->data(0, Qt::UserRole).toInt();
if (idx >= 0) {
total++;
if (item->checkState(0) == Qt::Checked) {
selected++;
}
}
for (int i = 0; i < item->childCount(); ++i) {
count(item->child(i));
}
};
for (int i = 0; i < mItemTree->topLevelItemCount(); ++i) {
count(mItemTree->topLevelItem(i));
}
mSelectionLabel->setText(QString("Selected: %1 of %2").arg(selected).arg(total));
mExportButton->setEnabled(selected > 0);
}
int BatchExportDialog::countByType(int contentType) const
{
int count = 0;
for (const BatchExportItem& item : mItems) {
if (item.contentType == contentType) {
count++;
}
}
return count;
}

View File

@ -1,99 +0,0 @@
#ifndef BATCHEXPORTDIALOG_H
#define BATCHEXPORTDIALOG_H
#include <QDialog>
#include <QList>
class QTreeWidget;
class QTreeWidgetItem;
class QProgressBar;
class QLabel;
class QLineEdit;
class QPushButton;
class QCheckBox;
class QComboBox;
class QDialogButtonBox;
/**
* @brief Item data for batch export operations.
*/
struct BatchExportItem
{
QString name; // Display name
QString path; // Relative path (for folder structure)
QByteArray data; // Raw data to export
int contentType; // ContentType enum value
bool selected; // Whether to export
};
/**
* @brief Dialog for batch exporting multiple items.
*
* Shows a checkable tree of items with progress tracking,
* folder structure preservation, and conflict resolution options.
*/
class BatchExportDialog : public QDialog
{
Q_OBJECT
public:
explicit BatchExportDialog(QWidget *parent = nullptr);
// Set items to potentially export
void setItems(const QList<BatchExportItem>& items);
// Get export configuration
QString outputDirectory() const;
bool preserveStructure() const;
QString conflictResolution() const; // "number", "overwrite", "skip"
QString imageFormat() const;
QString audioFormat() const;
// Get selected items
QList<BatchExportItem> selectedItems() const;
signals:
void exportProgress(int current, int total, const QString& currentItem);
void exportCompleted(int succeeded, int failed, int skipped);
public slots:
void updateProgress(int current, int total, const QString& currentItem);
void onExportCompleted(int succeeded, int failed, int skipped);
private slots:
void onSelectAll();
void onSelectNone();
void onSelectImages();
void onSelectAudio();
void onBrowseClicked();
void onExportClicked();
void onItemChanged(QTreeWidgetItem* item, int column);
private:
void setupUI();
void updateSelectionCount();
void populateTree();
int countByType(int contentType) const;
// Data
QList<BatchExportItem> mItems;
bool mExporting;
bool mCancelled;
// UI Elements
QTreeWidget* mItemTree;
QLabel* mSelectionLabel;
QLineEdit* mOutputPath;
QPushButton* mBrowseButton;
QCheckBox* mPreserveStructure;
QComboBox* mConflictCombo;
QComboBox* mImageFormatCombo;
QComboBox* mAudioFormatCombo;
QProgressBar* mProgressBar;
QLabel* mProgressLabel;
QDialogButtonBox* mButtonBox;
QPushButton* mExportButton;
QPushButton* mCancelButton;
};
#endif // BATCHEXPORTDIALOG_H

View File

@ -1,158 +0,0 @@
#include "binaryexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPlainTextEdit>
#include <QFont>
#include <QComboBox>
#include <QGroupBox>
BinaryExportDialog::BinaryExportDialog(QWidget *parent)
: ExportDialog(ContentType::Binary, parent)
, mHexPreview(nullptr)
, mSizeLabel(nullptr)
, mBytesPerLine(16)
, mPreviewBytes(512) // Show first 512 bytes in preview
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
setupPreview();
}
void BinaryExportDialog::setupPreview()
{
// Create hex preview inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mHexPreview = new QPlainTextEdit(previewContainer());
mHexPreview->setReadOnly(true);
mHexPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
// Use monospace font for hex display
QFont monoFont("Consolas", 9);
monoFont.setStyleHint(QFont::Monospace);
mHexPreview->setFont(monoFont);
// Dark theme styling
mHexPreview->setStyleSheet(
"QPlainTextEdit {"
" background-color: #1e1e1e;"
" color: #d4d4d4;"
" border: none;"
" selection-background-color: #264f78;"
"}"
);
mHexPreview->setPlaceholderText("No data loaded");
previewLayout->addWidget(mHexPreview);
// Size info label
QHBoxLayout* infoLayout = new QHBoxLayout();
mSizeLabel = new QLabel("Size: --", this);
infoLayout->addWidget(mSizeLabel);
infoLayout->addStretch();
previewLayout->addLayout(infoLayout);
// Add description to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
QLabel* descLabel = new QLabel("Raw binary export preserves data exactly as-is.", this);
descLabel->setStyleSheet("color: #808080;");
descLabel->setWordWrap(true);
optionsLayout->addWidget(descLabel);
optionsLayout->addStretch();
}
}
void BinaryExportDialog::updatePreview()
{
if (mData.isEmpty()) {
mHexPreview->setPlainText("");
mSizeLabel->setText("Size: --");
return;
}
// Update size label
qint64 size = mData.size();
QString sizeStr;
if (size >= 1024 * 1024) {
sizeStr = QString("Size: %1 MB (%2 bytes)")
.arg(size / (1024.0 * 1024.0), 0, 'f', 2)
.arg(size);
} else if (size >= 1024) {
sizeStr = QString("Size: %1 KB (%2 bytes)")
.arg(size / 1024.0, 0, 'f', 1)
.arg(size);
} else {
sizeStr = QString("Size: %1 bytes").arg(size);
}
mSizeLabel->setText(sizeStr);
updateHexPreview();
}
QStringList BinaryExportDialog::supportedFormats() const
{
return {"bin", "dat", "raw"};
}
void BinaryExportDialog::updateHexPreview()
{
if (mData.isEmpty()) {
mHexPreview->setPlainText("");
return;
}
QString hexText;
int bytesToShow = qMin(mPreviewBytes, static_cast<int>(mData.size()));
const unsigned char* data = reinterpret_cast<const unsigned char*>(mData.constData());
for (int i = 0; i < bytesToShow; i += mBytesPerLine) {
// Offset
QString line = QString("%1 ").arg(i, 8, 16, QChar('0')).toUpper();
// Hex bytes
QString hexPart;
QString asciiPart;
for (int j = 0; j < mBytesPerLine; ++j) {
int idx = i + j;
if (idx < bytesToShow) {
unsigned char byte = data[idx];
hexPart += QString("%1 ").arg(byte, 2, 16, QChar('0')).toUpper();
// ASCII representation
if (byte >= 32 && byte < 127) {
asciiPart += QChar(byte);
} else {
asciiPart += '.';
}
} else {
hexPart += " ";
asciiPart += ' ';
}
// Add extra space in middle for readability
if (j == 7) {
hexPart += ' ';
}
}
line += hexPart + " |" + asciiPart + "|";
hexText += line + "\n";
}
// Add truncation notice if needed
if (mData.size() > mPreviewBytes) {
hexText += QString("\n... (%1 more bytes not shown)")
.arg(mData.size() - mPreviewBytes);
}
mHexPreview->setPlainText(hexText);
}

View File

@ -1,38 +0,0 @@
#ifndef BINARYEXPORTDIALOG_H
#define BINARYEXPORTDIALOG_H
#include "exportdialog.h"
class QPlainTextEdit;
class QSpinBox;
/**
* @brief Export dialog for binary/raw data with hex preview.
*
* Shows a hex dump preview of the data and provides options
* for raw binary export.
*/
class BinaryExportDialog : public ExportDialog
{
Q_OBJECT
public:
explicit BinaryExportDialog(QWidget *parent = nullptr);
protected:
void setupPreview() override;
void updatePreview() override;
QStringList supportedFormats() const override;
private:
void updateHexPreview();
QPlainTextEdit* mHexPreview;
QLabel* mSizeLabel;
// Preview settings
int mBytesPerLine;
int mPreviewBytes; // How many bytes to show in preview
};
#endif // BINARYEXPORTDIALOG_H

41
app/d3dbsp_structs.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef D3DBSP_STRUCTS_H
#define D3DBSP_STRUCTS_H
#include <QByteArray>
// Define Lump Structure
struct Lump {
QByteArray content;
quint32 size = 0;
bool isEmpty = true;
};
// Lump Index Entry Structure
struct LumpIndexEntry {
quint32 type;
quint32 length;
};
// Bink structure definitions
struct BINKRECT {
int Left;
int Top;
int Width;
int Height;
};
struct BINK {
int Width;
int Height;
uint32_t Frames;
uint32_t FrameNum;
uint32_t FrameRate;
uint32_t FrameRateDiv;
uint32_t ReadError;
uint32_t OpenFlags;
BINKRECT FrameRects;
uint32_t NumRects;
uint32_t FrameChangePercent;
};
#endif // D3DBSP_STRUCTS_H

182
app/ddsviewer.cpp Normal file
View File

@ -0,0 +1,182 @@
#include "ddsviewer.h"
#include "enums.h"
#include "ui_ddsviewer.h"
DDSViewer::DDSViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::DDSViewer)
{
ui->setupUi(this);
mDDSFile = nullptr;
}
DDSViewer::~DDSViewer() {
delete ui;
}
void DDSViewer::SetDDSFile(std::shared_ptr<DDSFile> aDDSFile) {
mDDSFile.swap(aDDSFile);
ui->label_Title->setText(mDDSFile->fileStem + ".dds");
char magicData[5];
magicData[0] = static_cast<char>(mDDSFile->header.magic & 0xFF);
magicData[1] = static_cast<char>((mDDSFile->header.magic >> 8) & 0xFF);
magicData[2] = static_cast<char>((mDDSFile->header.magic >> 16) & 0xFF);
magicData[3] = static_cast<char>((mDDSFile->header.magic >> 24) & 0xFF);
magicData[4] = '\0';
// If youre using Qt and want a QString:
QString magicStr = QString::fromLatin1(magicData);
ui->lineEdit_Magic->setText(magicStr);
ui->spinBox_Size->setValue(mDDSFile->header.size);
ui->checkBox_CapsValid->setChecked((mDDSFile->header.flags & DDSD_CAPS) != 0);
ui->checkBox_HeightValid->setChecked((mDDSFile->header.flags & DDSD_HEIGHT) != 0);
ui->checkBox_WidthValid->setChecked((mDDSFile->header.flags & DDSD_WIDTH) != 0);
ui->checkBox_PitchValid->setChecked((mDDSFile->header.flags & DDSD_PITCH) != 0);
ui->checkBox_PFValid->setChecked((mDDSFile->header.flags & DDSD_PIXELFORMAT) != 0);
ui->checkBox_MipmapCountValid->setChecked((mDDSFile->header.flags & DDSD_MIPMAPCOUNT) != 0);
ui->checkBox_LinearSizeValid->setChecked((mDDSFile->header.flags & DDSD_LINEARSIZE) != 0);
ui->checkBox_DepthValid->setChecked((mDDSFile->header.flags & DDSD_DEPTH) != 0);
ui->spinBox_PLSize->setValue(mDDSFile->header.pitchOrLinearSize);
ui->spinBox_Depth->setValue(mDDSFile->header.depth);
ui->spinBox_Width->setValue(mDDSFile->header.width);
ui->spinBox_Height->setValue(mDDSFile->header.height);
ui->spinBox_MipmapCount->setValue(mDDSFile->header.mipMapCount);
ui->spinBox_Res1->setValue(mDDSFile->header.reserved1[0]);
ui->spinBox_Res2->setValue(mDDSFile->header.reserved1[1]);
ui->spinBox_Res3->setValue(mDDSFile->header.reserved1[2]);
ui->spinBox_Res4->setValue(mDDSFile->header.reserved1[3]);
ui->spinBox_Res5->setValue(mDDSFile->header.reserved1[4]);
ui->spinBox_Res6->setValue(mDDSFile->header.reserved1[5]);
ui->spinBox_Res7->setValue(mDDSFile->header.reserved1[6]);
ui->spinBox_Res8->setValue(mDDSFile->header.reserved1[7]);
ui->spinBox_Res9->setValue(mDDSFile->header.reserved1[8]);
ui->spinBox_Res10->setValue(mDDSFile->header.reserved1[9]);
ui->spinBox_Res11->setValue(mDDSFile->header.reserved1[10]);
ui->spinBox_Res12->setValue(mDDSFile->header.reserved2);
ui->spinBox_PF_Size->setValue(mDDSFile->header.pixelFormat.size);
ui->checkBox_PF_AlphaPxValid->setChecked((mDDSFile->header.pixelFormat.flags & DDPF_ALPHAPIXELS) != 0);
ui->checkBox_PF_AlphaOnlyValid->setChecked((mDDSFile->header.pixelFormat.flags & DDPF_ALPHA) != 0);
ui->checkBox_PF_FormatValid->setChecked((mDDSFile->header.pixelFormat.flags & DDPF_FOURCC) != 0);
ui->checkBox_PF_RGBValid->setChecked((mDDSFile->header.pixelFormat.flags & DDPF_RGB) != 0);
ui->checkBox_PF_YUVValid->setChecked((mDDSFile->header.pixelFormat.flags & DDPF_YUV) != 0);
ui->checkBox_PF_LuminanceValid->setChecked((mDDSFile->header.pixelFormat.flags & DDPF_LUMINANCE) != 0);
QString formatStr = QString::number(mDDSFile->header.pixelFormat.format);
switch (mDDSFile->header.pixelFormat.format) {
case IWI_FORMAT_ARGB32:
formatStr = "ARGB32";
break;
case IWI_FORMAT_RGB24:
formatStr = "RGB24";
break;
case IWI_FORMAT_GA16:
formatStr = "GA16";
break;
case IWI_FORMAT_A8:
formatStr = "A8";
break;
case IWI_FORMAT_DXT1:
formatStr = "DXT1";
break;
case IWI_FORMAT_DXT3:
formatStr = "DXT3";
break;
case IWI_FORMAT_DXT5:
formatStr = "DXT5";
break;
}
ui->lineEdit_PF_Format->setText(formatStr);
ui->spinBox_PF_RGBBitCount->setValue(mDDSFile->header.pixelFormat.rgbBitCount);
ui->spinBox_RedBitCount->setValue(mDDSFile->header.pixelFormat.rBitMask);
ui->spinBox_GreenBitCount->setValue(mDDSFile->header.pixelFormat.gBitMask);
ui->spinBox_BlueBitCount->setValue(mDDSFile->header.pixelFormat.bBitMask);
ui->spinBox_AlphaBitMask->setValue(mDDSFile->header.pixelFormat.aBitMask);
ui->checkBox_Caps1_TextureValid->setChecked((mDDSFile->header.caps.caps1 & DDSCAPS_TEXTURE) != 0);
ui->checkBox_Caps1_ComplexValid->setChecked((mDDSFile->header.caps.caps1 & DDSCAPS_COMPLEX) != 0);
ui->checkBox_Caps1_MipmapValid->setChecked((mDDSFile->header.caps.caps1 & DDSCAPS_MIPMAP) != 0);
ui->checkBox_Caps2_CubemapValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP) != 0);
ui->checkBox_Caps2_CMPXValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP_POSITIVEX) != 0);
ui->checkBox_Caps2_CMNXValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX) != 0);
ui->checkBox_Caps2_CMPYValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP_POSITIVEY) != 0);
ui->checkBox_Caps2_CMNYValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY) != 0);
ui->checkBox_Caps2_CMPZValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ) != 0);
ui->checkBox_Caps2_CMNZValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ) != 0);
ui->checkBox_Caps2_VolumeValid->setChecked((mDDSFile->header.caps.caps2 & DDSCAPS2_VOLUME) != 0);
ui->spinBox_Caps_DDSX->setValue(mDDSFile->header.caps.dDSX);
ui->spinBox_Caps_Res->setValue(mDDSFile->header.caps.reserved);
ui->comboBox_Mipmap->clear();
for (auto mipmap : mDDSFile->mipmaps) {
ui->comboBox_Mipmap->addItem(QString("%1x%2").arg(mipmap.width).arg(mipmap.height));
}
connect(ui->comboBox_Mipmap, &QComboBox::currentIndexChanged, this, &DDSViewer::MipmapIndexChanged);
if (!mDDSFile->mipmaps.empty()) {
MipmapIndexChanged(0);
}
}
void DDSViewer::MipmapIndexChanged(int aMipmapIndex) {
if (aMipmapIndex == -1) { return; }
auto mipmaps = mDDSFile->mipmaps;
auto mipmap = mipmaps[aMipmapIndex];
ui->spinBox_MipmapSize->setValue(mipmap.size);
ui->spinBox_MipmapWidth->setValue(mipmap.width);
ui->spinBox_MipmapHeight->setValue(mipmap.height);
// Validate Data
if (mipmap.size <= 0) {
qDebug() << "Error: Mipmap data is empty!";
return;
}
if (mipmap.width <= 0 || mipmap.height <= 0) {
qDebug() << "Error: Invalid mipmap dimensions!";
return;
}
// Ensure data size matches expected size
int bytesPerPixel = 4; // RGBA8888
quint32 expectedSize = mipmap.width * mipmap.height * bytesPerPixel;
if (mipmap.size < expectedSize) {
qDebug() << "Error: Mipmap data size mismatch! Expected:" << expectedSize << ", Got:" << mipmap.size;
return;
}
// Create QImage
const unsigned char* imageData = reinterpret_cast<const unsigned char*>(mipmap.data.constData());
QImage image(reinterpret_cast<const uchar*>(imageData),
mipmap.width, mipmap.height,
mipmap.width * bytesPerPixel, // Stride
QImage::Format_RGBA8888);
if (image.isNull()) {
qDebug() << "Error: QImage creation failed!";
return;
}
// Convert to QPixmap
QPixmap pixmap = QPixmap::fromImage(image);
if (pixmap.isNull()) {
qDebug() << "Error: QPixmap conversion failed!";
return;
}
// Scale and display
pixmap = pixmap.scaled(ui->label_Image->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
ui->label_Image->setPixmap(pixmap);
}

29
app/ddsviewer.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef DDSVIEWER_H
#define DDSVIEWER_H
#include "ddsfile.h"
#include <QWidget>
namespace Ui {
class DDSViewer;
}
class DDSViewer : public QWidget
{
Q_OBJECT
public:
explicit DDSViewer(QWidget *parent = nullptr);
~DDSViewer();
void SetDDSFile(std::shared_ptr<DDSFile> aDDSFile);
private slots:
void MipmapIndexChanged(int aMipmapIndex);
private:
Ui::DDSViewer *ui;
std::shared_ptr<DDSFile> mDDSFile;
};
#endif // DDSVIEWER_H

1712
app/ddsviewer.ui Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +0,0 @@
#include "definitionviewer.h"
#include <QFileInfo>
#include <QDir>
#include <QCoreApplication>
DefinitionViewer::DefinitionViewer(const QVector<DefinitionLoadResult>& results, QWidget *parent)
: QDialog(parent)
, mResults(results)
{
setWindowTitle("XPlor - Definition Viewer");
setMinimumSize(600, 400);
resize(700, 500);
auto* layout = new QVBoxLayout(this);
mSummaryLabel = new QLabel(this);
layout->addWidget(mSummaryLabel);
mTreeWidget = new QTreeWidget(this);
mTreeWidget->setHeaderLabels({"Definition", "Status", "Error"});
mTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch);
mTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
mTreeWidget->header()->setSectionResizeMode(2, QHeaderView::Stretch);
mTreeWidget->setAlternatingRowColors(true);
layout->addWidget(mTreeWidget);
auto* closeButton = new QPushButton("Close", this);
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
layout->addWidget(closeButton);
populateTree();
}
void DefinitionViewer::setResults(const QVector<DefinitionLoadResult>& results)
{
mResults = results;
populateTree();
}
void DefinitionViewer::populateTree()
{
mTreeWidget->clear();
// Group by subdirectory
QMap<QString, QVector<const DefinitionLoadResult*>> byFolder;
QString basePath = QCoreApplication::applicationDirPath() + "/definitions/";
for (const auto& result : mResults) {
QString relativePath = result.filePath;
if (relativePath.startsWith(basePath)) {
relativePath = relativePath.mid(basePath.length());
}
QFileInfo fi(relativePath);
QString folder = fi.path();
if (folder == ".") folder = "(root)";
byFolder[folder].append(&result);
}
int successCount = 0;
int failCount = 0;
for (auto it = byFolder.begin(); it != byFolder.end(); ++it) {
const QString& folder = it.key();
const auto& items = it.value();
auto* folderItem = new QTreeWidgetItem(mTreeWidget);
folderItem->setText(0, folder);
folderItem->setExpanded(true);
int folderSuccess = 0;
int folderFail = 0;
for (const auto* result : items) {
auto* item = new QTreeWidgetItem(folderItem);
item->setText(0, result->fileName);
if (result->success) {
item->setText(1, "OK");
item->setForeground(0, QColor(0, 128, 0));
item->setForeground(1, QColor(0, 128, 0));
folderSuccess++;
successCount++;
} else {
item->setText(1, "FAILED");
item->setText(2, result->errorMessage);
item->setForeground(0, QColor(192, 0, 0));
item->setForeground(1, QColor(192, 0, 0));
item->setForeground(2, QColor(128, 128, 128));
folderFail++;
failCount++;
}
}
// Set folder color based on contents
if (folderFail == 0) {
folderItem->setForeground(0, QColor(0, 128, 0));
} else if (folderSuccess == 0) {
folderItem->setForeground(0, QColor(192, 0, 0));
} else {
folderItem->setForeground(0, QColor(200, 140, 0));
}
folderItem->setText(1, QString("%1/%2").arg(folderSuccess).arg(folderSuccess + folderFail));
}
mSummaryLabel->setText(QString("Loaded: %1 successful, %2 failed (%3 total)")
.arg(successCount)
.arg(failCount)
.arg(successCount + failCount));
if (failCount > 0) {
mSummaryLabel->setStyleSheet("color: #c00000; font-weight: bold;");
} else {
mSummaryLabel->setStyleSheet("color: #008000; font-weight: bold;");
}
}

View File

@ -1,30 +0,0 @@
#ifndef DEFINITIONVIEWER_H
#define DEFINITIONVIEWER_H
#include <QDialog>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <QHeaderView>
#include <QPushButton>
#include <QLabel>
#include "mainwindow.h"
class DefinitionViewer : public QDialog
{
Q_OBJECT
public:
explicit DefinitionViewer(const QVector<DefinitionLoadResult>& results, QWidget *parent = nullptr);
void setResults(const QVector<DefinitionLoadResult>& results);
private:
void populateTree();
QVector<DefinitionLoadResult> mResults;
QTreeWidget* mTreeWidget;
QLabel* mSummaryLabel;
};
#endif // DEFINITIONVIEWER_H

View File

@ -1,54 +0,0 @@
#include "dirtystatemanager.h"
#include <QWidget>
DirtyStateManager& DirtyStateManager::instance() {
static DirtyStateManager instance;
return instance;
}
void DirtyStateManager::markDirty(QWidget* tab) {
if (!tab) return;
if (!m_dirtyTabs.contains(tab)) {
m_dirtyTabs.insert(tab);
emit dirtyStateChanged(tab, true);
}
}
void DirtyStateManager::markClean(QWidget* tab) {
if (!tab) return;
if (m_dirtyTabs.contains(tab)) {
m_dirtyTabs.remove(tab);
emit dirtyStateChanged(tab, false);
}
}
bool DirtyStateManager::isDirty(QWidget* tab) const {
return m_dirtyTabs.contains(tab);
}
QList<QWidget*> DirtyStateManager::dirtyTabs() const {
return m_dirtyTabs.values();
}
bool DirtyStateManager::hasDirtyTabs() const {
return !m_dirtyTabs.isEmpty();
}
void DirtyStateManager::setFilePath(QWidget* tab, const QString& path) {
if (tab) {
m_filePaths[tab] = path;
}
}
QString DirtyStateManager::filePath(QWidget* tab) const {
return m_filePaths.value(tab);
}
void DirtyStateManager::removeTab(QWidget* tab) {
if (!tab) return;
m_dirtyTabs.remove(tab);
m_filePaths.remove(tab);
}

View File

@ -1,92 +0,0 @@
#ifndef DIRTYSTATEMANAGER_H
#define DIRTYSTATEMANAGER_H
#include <QObject>
#include <QSet>
#include <QMap>
class QWidget;
/**
* @brief Tracks dirty (unsaved changes) state for editor tabs.
*
* The DirtyStateManager is a singleton that tracks which editor widgets
* have unsaved changes. It emits signals when dirty state changes, allowing
* the UI to update tab titles (add/remove asterisk) and prompt on close.
*/
class DirtyStateManager : public QObject {
Q_OBJECT
public:
static DirtyStateManager& instance();
/**
* @brief Mark a tab as having unsaved changes.
* @param tab The widget to mark as dirty
*/
void markDirty(QWidget* tab);
/**
* @brief Mark a tab as having no unsaved changes.
* @param tab The widget to mark as clean
*/
void markClean(QWidget* tab);
/**
* @brief Check if a tab has unsaved changes.
* @param tab The widget to check
* @return true if the tab has unsaved changes
*/
bool isDirty(QWidget* tab) const;
/**
* @brief Get list of all tabs with unsaved changes.
* @return List of dirty widgets
*/
QList<QWidget*> dirtyTabs() const;
/**
* @brief Check if any tabs have unsaved changes.
* @return true if at least one tab is dirty
*/
bool hasDirtyTabs() const;
/**
* @brief Store the original file path for a tab (for Save functionality).
* @param tab The widget
* @param path The file path
*/
void setFilePath(QWidget* tab, const QString& path);
/**
* @brief Get the original file path for a tab.
* @param tab The widget
* @return The file path, or empty string if not set
*/
QString filePath(QWidget* tab) const;
/**
* @brief Remove tracking for a tab (call when tab is closed).
* @param tab The widget being closed
*/
void removeTab(QWidget* tab);
signals:
/**
* @brief Emitted when a tab's dirty state changes.
* @param tab The affected widget
* @param isDirty The new dirty state
*/
void dirtyStateChanged(QWidget* tab, bool isDirty);
private:
DirtyStateManager() = default;
~DirtyStateManager() = default;
DirtyStateManager(const DirtyStateManager&) = delete;
DirtyStateManager& operator=(const DirtyStateManager&) = delete;
QSet<QWidget*> m_dirtyTabs;
QMap<QWidget*, QString> m_filePaths;
};
#endif // DIRTYSTATEMANAGER_H

View File

@ -1,221 +0,0 @@
#include "exportdialog.h"
#include "exportmanager.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QStandardPaths>
ExportDialog::ExportDialog(ContentType type, QWidget *parent)
: QDialog(parent)
, mContentType(type)
, mPreviewContainer(nullptr)
, mOptionsContainer(nullptr)
, mFormatCombo(nullptr)
, mOutputPath(nullptr)
, mBrowseButton(nullptr)
, mButtonBox(nullptr)
, mRememberSettings(nullptr)
, mInfoLabel(nullptr)
{
// Set dialog title based on content type
QString title;
switch (type) {
case Image: title = "Export Image"; break;
case Audio: title = "Export Audio"; break;
case Video: title = "Export Video"; break;
case Text: title = "Export Text"; break;
case Binary: title = "Export Data"; break;
}
setWindowTitle(title);
setMinimumWidth(500);
setModal(true);
setupCommonUI();
}
void ExportDialog::setupCommonUI()
{
mMainLayout = new QVBoxLayout(this);
// Content area: preview on left, options on right
mContentLayout = new QHBoxLayout();
// Left side: preview container
mLeftLayout = new QVBoxLayout();
mPreviewContainer = new QWidget(this);
mPreviewContainer->setMinimumSize(256, 256);
mPreviewContainer->setMaximumSize(300, 300);
mPreviewContainer->setStyleSheet("background-color: #1e1e1e; border: 1px solid #3e3e3e;");
mLeftLayout->addWidget(mPreviewContainer);
// Info label below preview
mInfoLabel = new QLabel(this);
mInfoLabel->setAlignment(Qt::AlignCenter);
mLeftLayout->addWidget(mInfoLabel);
mLeftLayout->addStretch();
mContentLayout->addLayout(mLeftLayout);
// Right side: format and options
mRightLayout = new QVBoxLayout();
// Format selection
QHBoxLayout* formatLayout = new QHBoxLayout();
QLabel* formatLabel = new QLabel("Format:", this);
mFormatCombo = new QComboBox(this);
formatLayout->addWidget(formatLabel);
formatLayout->addWidget(mFormatCombo, 1);
mRightLayout->addLayout(formatLayout);
// Options container (subclasses add format-specific options here)
mOptionsContainer = new QGroupBox("Options", this);
QVBoxLayout* optionsLayout = new QVBoxLayout(mOptionsContainer);
optionsLayout->setContentsMargins(8, 8, 8, 8);
mRightLayout->addWidget(mOptionsContainer);
mRightLayout->addStretch();
mContentLayout->addLayout(mRightLayout, 1);
mMainLayout->addLayout(mContentLayout);
// Output path
QHBoxLayout* pathLayout = new QHBoxLayout();
QLabel* outputLabel = new QLabel("Output:", this);
mOutputPath = new QLineEdit(this);
mBrowseButton = new QPushButton("Browse...", this);
pathLayout->addWidget(outputLabel);
pathLayout->addWidget(mOutputPath, 1);
pathLayout->addWidget(mBrowseButton);
mMainLayout->addLayout(pathLayout);
// Remember settings checkbox
mRememberSettings = new QCheckBox("Remember these settings", this);
mRememberSettings->setChecked(Settings::instance().exportRememberSettings());
mMainLayout->addWidget(mRememberSettings);
// Dialog buttons
mButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, this);
mButtonBox->button(QDialogButtonBox::Ok)->setText("Export");
mMainLayout->addWidget(mButtonBox);
// Connect signals
connect(mBrowseButton, &QPushButton::clicked, this, &ExportDialog::onBrowseClicked);
connect(mFormatCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ExportDialog::onFormatComboChanged);
connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
void ExportDialog::setData(const QByteArray& data, const QString& suggestedName)
{
mData = data;
mSuggestedName = suggestedName;
// Set default output path
QString dir;
switch (mContentType) {
case Image:
dir = ExportManager::instance().lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
break;
case Audio:
dir = ExportManager::instance().lastExportDir("audio");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
break;
default:
dir = ExportManager::instance().lastExportDir("raw");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
break;
}
// Get base name without extension
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "export";
// Get default format
QString format = mFormatCombo->currentText().toLower();
if (format.isEmpty() && mFormatCombo->count() > 0) {
format = mFormatCombo->itemText(0).toLower();
}
mOutputPath->setText(dir + "/" + baseName + "." + format);
// Update preview (subclass implementation)
updatePreview();
}
void ExportDialog::setMetadata(const QVariantMap& metadata)
{
mMetadata = metadata;
}
QString ExportDialog::selectedFormat() const
{
return mFormatCombo->currentText().toLower();
}
QString ExportDialog::outputPath() const
{
return mOutputPath->text();
}
bool ExportDialog::rememberSettings() const
{
return mRememberSettings->isChecked();
}
void ExportDialog::onFormatChanged(const QString& format)
{
// Update output path extension
QString path = mOutputPath->text();
QFileInfo fi(path);
QString newPath = fi.path() + "/" + fi.baseName() + "." + format.toLower();
mOutputPath->setText(newPath);
}
void ExportDialog::onBrowseClicked()
{
QString filter;
QString format = selectedFormat();
// Build filter based on content type
switch (mContentType) {
case Image:
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
break;
case Audio:
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
break;
case Text:
filter = "Text Files (*.txt);;All Files (*)";
break;
case Binary:
default:
filter = "Binary Files (*.bin *.dat);;All Files (*)";
break;
}
QString path = QFileDialog::getSaveFileName(this, "Export", mOutputPath->text(), filter);
if (!path.isEmpty()) {
mOutputPath->setText(path);
}
}
void ExportDialog::onFormatComboChanged(int index)
{
Q_UNUSED(index);
QString format = mFormatCombo->currentText();
onFormatChanged(format);
}

View File

@ -1,100 +0,0 @@
#ifndef EXPORTDIALOG_H
#define EXPORTDIALOG_H
#include <QDialog>
#include <QVariantMap>
#include <QByteArray>
class QComboBox;
class QLabel;
class QDialogButtonBox;
class QPushButton;
class QCheckBox;
class QLineEdit;
class QVBoxLayout;
class QHBoxLayout;
class QGroupBox;
/**
* @brief Base class for media-specific export dialogs.
*
* Provides common UI elements and functionality for exporting different
* content types (images, audio, binary, etc.)
*/
class ExportDialog : public QDialog
{
Q_OBJECT
public:
enum ContentType {
Image,
Audio,
Video,
Text,
Binary
};
explicit ExportDialog(ContentType type, QWidget *parent = nullptr);
virtual ~ExportDialog() = default;
// Set data and metadata to export
void setData(const QByteArray& data, const QString& suggestedName);
void setMetadata(const QVariantMap& metadata);
// Get export settings
QString selectedFormat() const;
QString outputPath() const;
bool rememberSettings() const;
// Content type
ContentType contentType() const { return mContentType; }
protected:
// Subclasses implement these
virtual void setupPreview() = 0;
virtual void updatePreview() = 0;
virtual QStringList supportedFormats() const = 0;
virtual void onFormatChanged(const QString& format);
// Setup common UI elements
void setupCommonUI();
// Get the preview area (subclasses add their preview widget here)
QWidget* previewContainer() const { return mPreviewContainer; }
// Get the options area (subclasses add format-specific options here)
QGroupBox* optionsContainer() const { return mOptionsContainer; }
// Access common widgets
QComboBox* formatCombo() const { return mFormatCombo; }
QLineEdit* outputPathEdit() const { return mOutputPath; }
// Member data
ContentType mContentType;
QByteArray mData;
QString mSuggestedName;
QVariantMap mMetadata;
private slots:
void onBrowseClicked();
void onFormatComboChanged(int index);
private:
// Common UI elements
QWidget* mPreviewContainer;
QGroupBox* mOptionsContainer;
QComboBox* mFormatCombo;
QLineEdit* mOutputPath;
QPushButton* mBrowseButton;
QDialogButtonBox* mButtonBox;
QCheckBox* mRememberSettings;
QLabel* mInfoLabel;
// Layouts
QVBoxLayout* mMainLayout;
QHBoxLayout* mContentLayout;
QVBoxLayout* mLeftLayout;
QVBoxLayout* mRightLayout;
};
#endif // EXPORTDIALOG_H

View File

@ -1,764 +0,0 @@
#include "exportmanager.h"
#include "settings.h"
#include "imageexportdialog.h"
#include "audioexportdialog.h"
#include "binaryexportdialog.h"
#include "batchexportdialog.h"
#include "../libs/dsl/dslkeys.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QClipboard>
#include <QApplication>
#include <QMimeData>
#include <QProcess>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <QImageReader>
ExportManager& ExportManager::instance() {
static ExportManager instance;
return instance;
}
ExportManager::ExportManager(QObject* parent)
: QObject(parent)
{
}
ExportManager::ContentType ExportManager::detectContentType(const QVariantMap& vars) const {
// Check for viewer type in vars
if (DslKeys::contains(vars, DslKey::Viewer)) {
QString viewer = DslKeys::get(vars, DslKey::Viewer).toString();
if (viewer == "image") return Image;
if (viewer == "audio") return Audio;
if (viewer == "text") return Text;
if (viewer == "hex") return Binary;
}
return Unknown;
}
ExportManager::ContentType ExportManager::detectContentType(const QByteArray& data, const QString& filename) const {
QString ext = QFileInfo(filename).suffix().toLower();
// Check by extension
QStringList imageExts = {"png", "jpg", "jpeg", "bmp", "tga", "dds", "tiff", "tif", "webp", "gif"};
QStringList audioExts = {"wav", "mp3", "ogg", "flac", "aiff", "wma", "m4a"};
QStringList textExts = {"txt", "xml", "json", "csv", "ini", "cfg", "log", "md", "htm", "html"};
if (imageExts.contains(ext)) return Image;
if (audioExts.contains(ext)) return Audio;
if (textExts.contains(ext)) return Text;
// Check by magic bytes
if (data.size() >= 4) {
// PNG
if (data.startsWith("\x89PNG")) return Image;
// JPEG
if (data.startsWith("\xFF\xD8\xFF")) return Image;
// BMP
if (data.startsWith("BM")) return Image;
// GIF
if (data.startsWith("GIF8")) return Image;
// RIFF (WAV)
if (data.startsWith("RIFF") && data.size() >= 12 && data.mid(8, 4) == "WAVE") return Audio;
// OGG
if (data.startsWith("OggS")) return Audio;
// FLAC
if (data.startsWith("fLaC")) return Audio;
// ID3 (MP3)
if (data.startsWith("ID3") || (data.size() >= 2 && (uchar)data[0] == 0xFF && ((uchar)data[1] & 0xE0) == 0xE0)) return Audio;
}
// Check if it looks like text
bool looksLikeText = true;
int checkLen = qMin(data.size(), 1024);
for (int i = 0; i < checkLen && looksLikeText; ++i) {
char c = data[i];
if (c < 0x09 || (c > 0x0D && c < 0x20 && c != 0x1B)) {
looksLikeText = false;
}
}
if (looksLikeText && !data.isEmpty()) return Text;
return Binary;
}
bool ExportManager::exportRawData(const QByteArray& data, const QString& suggestedName, QWidget* parent) {
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "export";
QString dir = lastExportDir("raw");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
QString savePath = QFileDialog::getSaveFileName(parent, "Export Raw Data",
dir + "/" + baseName + ".bin",
"Binary Files (*.bin *.dat);;All Files (*)");
if (savePath.isEmpty()) return false;
setLastExportDir("raw", QFileInfo(savePath).path());
QFile file(savePath);
if (!file.open(QIODevice::WriteOnly)) {
emit exportError(QString("Failed to open file for writing: %1").arg(file.errorString()));
return false;
}
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
bool ExportManager::exportImage(const QImage& image, const QString& format, const QString& suggestedName, QWidget* parent) {
if (image.isNull()) {
emit exportError("Cannot export null image");
return false;
}
QString ext = format.toLower();
QString filter = getFilterForFormat(ext);
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "image";
QString dir = lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QString savePath = QFileDialog::getSaveFileName(parent, "Export Image",
dir + "/" + baseName + "." + ext, filter);
if (savePath.isEmpty()) return false;
setLastExportDir("image", QFileInfo(savePath).path());
bool success = false;
if (ext == "tga") {
success = saveTGA(image, savePath);
} else {
// Qt handles png, jpg, bmp, tiff natively
success = image.save(savePath, ext.toUpper().toUtf8().constData());
}
if (!success) {
emit exportError(QString("Failed to save image as %1").arg(ext.toUpper()));
return false;
}
emit exportCompleted(savePath, true);
return true;
}
bool ExportManager::exportAudio(const QByteArray& wavData, const QString& format, const QString& suggestedName, QWidget* parent) {
if (wavData.isEmpty()) {
emit exportError("No audio data to export");
return false;
}
QString ext = format.toLower();
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "audio";
QString dir = lastExportDir("audio");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QString filter;
if (ext == "wav") filter = "WAV Files (*.wav)";
else if (ext == "mp3") filter = "MP3 Files (*.mp3)";
else if (ext == "ogg") filter = "OGG Files (*.ogg)";
else if (ext == "flac") filter = "FLAC Files (*.flac)";
else filter = "All Files (*)";
QString savePath = QFileDialog::getSaveFileName(parent, "Export Audio",
dir + "/" + baseName + "." + ext, filter);
if (savePath.isEmpty()) return false;
setLastExportDir("audio", QFileInfo(savePath).path());
if (ext == "wav") {
// Direct write for WAV
QFile file(savePath);
if (!file.open(QIODevice::WriteOnly)) {
emit exportError(QString("Failed to open file: %1").arg(file.errorString()));
return false;
}
file.write(wavData);
file.close();
emit exportCompleted(savePath, true);
return true;
}
// Need FFmpeg for MP3/OGG/FLAC conversion
if (!hasFFmpeg()) {
QMessageBox::warning(parent, "FFmpeg Required",
QString("FFmpeg is required to export audio as %1.\n\n"
"Install FFmpeg and ensure it's in your system PATH,\n"
"or configure the path in Edit > Preferences > Tools.").arg(ext.toUpper()));
return false;
}
// Write temp WAV file
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (!tempWav.open()) {
emit exportError("Failed to create temporary file");
return false;
}
tempWav.write(wavData);
QString tempPath = tempWav.fileName();
tempWav.close();
bool success = convertWithFFmpeg(tempPath, savePath, ext);
// Cleanup temp file
QFile::remove(tempPath);
if (success) {
emit exportCompleted(savePath, true);
}
return success;
}
bool ExportManager::exportText(const QByteArray& data, const QString& suggestedName, QWidget* parent) {
QString baseName = QFileInfo(suggestedName).baseName();
if (baseName.isEmpty()) baseName = "text";
QString dir = lastExportDir("text");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
QString savePath = QFileDialog::getSaveFileName(parent, "Export Text",
dir + "/" + baseName + ".txt",
"Text Files (*.txt);;All Files (*)");
if (savePath.isEmpty()) return false;
setLastExportDir("text", QFileInfo(savePath).path());
QFile file(savePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
emit exportError(QString("Failed to open file: %1").arg(file.errorString()));
return false;
}
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
bool ExportManager::exportWithDialog(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent) {
switch (type) {
case Image: {
QImage image;
if (image.loadFromData(data)) {
return exportImageWithDialog(image, name, parent);
}
// Fall back to binary export
break;
}
case Audio: {
AudioExportDialog dialog(parent);
dialog.setData(data, name);
if (dialog.exec() == QDialog::Accepted) {
QString format = dialog.selectedFormat();
QString path = dialog.outputPath();
if (dialog.rememberSettings()) {
Settings::instance().setDefaultAudioExportFormat(format);
Settings::instance().setAudioMp3Bitrate(dialog.mp3Bitrate());
Settings::instance().setAudioOggQuality(dialog.oggQuality());
Settings::instance().setAudioFlacCompression(dialog.flacCompression());
}
// Export based on format
if (format == "wav") {
QFile file(path);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(path, true);
return true;
}
} else {
// Need FFmpeg conversion
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (tempWav.open()) {
tempWav.write(data);
QString tempPath = tempWav.fileName();
tempWav.close();
bool success = convertWithFFmpeg(tempPath, path, format);
QFile::remove(tempPath);
if (success) {
emit exportCompleted(path, true);
return true;
}
}
}
}
return false;
}
case Text:
return exportText(data, name, parent);
case Binary:
case Unknown:
default:
break;
}
// Binary export dialog
BinaryExportDialog dialog(parent);
dialog.setData(data, name);
if (dialog.exec() == QDialog::Accepted) {
QString path = dialog.outputPath();
QFile file(path);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(path, true);
return true;
}
}
return false;
}
bool ExportManager::exportImageWithDialog(const QImage& image, const QString& name, QWidget* parent) {
if (image.isNull()) {
emit exportError("Cannot export null image");
return false;
}
ImageExportDialog dialog(parent);
dialog.setImage(image, name);
if (dialog.exec() == QDialog::Accepted) {
QString format = dialog.selectedFormat();
QString path = dialog.outputPath();
if (dialog.rememberSettings()) {
Settings::instance().setDefaultImageExportFormat(format);
Settings::instance().setImageJpegQuality(dialog.jpegQuality());
Settings::instance().setImagePngCompression(dialog.pngCompression());
}
setLastExportDir("image", QFileInfo(path).path());
bool success = false;
if (format == "tga") {
success = saveTGA(image, path);
} else if (format == "jpg" || format == "jpeg") {
success = image.save(path, "JPEG", dialog.jpegQuality());
} else if (format == "png") {
// Qt doesn't support PNG compression level directly in save()
// We'd need to use QImageWriter for that
success = image.save(path, "PNG");
} else {
success = image.save(path, format.toUpper().toUtf8().constData());
}
if (success) {
emit exportCompleted(path, true);
return true;
} else {
emit exportError(QString("Failed to save image as %1").arg(format.toUpper()));
}
}
return false;
}
bool ExportManager::quickExport(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent) {
QString baseName = QFileInfo(name).baseName();
if (baseName.isEmpty()) baseName = "export";
QString dir, format, filter;
switch (type) {
case Image:
dir = lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
format = Settings::instance().defaultImageExportFormat();
filter = getFilterForFormat(format);
break;
case Audio:
dir = lastExportDir("audio");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
format = Settings::instance().defaultAudioExportFormat();
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
break;
case Text:
dir = lastExportDir("text");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
format = "txt";
filter = "Text Files (*.txt)";
break;
default:
dir = lastExportDir("raw");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
format = "bin";
filter = "Binary Files (*.bin)";
break;
}
QString savePath = QFileDialog::getSaveFileName(parent, "Quick Export",
dir + "/" + baseName + "." + format, filter);
if (savePath.isEmpty()) return false;
// Handle type-specific export
switch (type) {
case Image: {
QImage image;
if (image.loadFromData(data)) {
setLastExportDir("image", QFileInfo(savePath).path());
QString ext = QFileInfo(savePath).suffix().toLower();
if (ext == "tga") {
return saveTGA(image, savePath);
}
int quality = (ext == "jpg" || ext == "jpeg") ?
Settings::instance().imageJpegQuality() : -1;
if (image.save(savePath, ext.toUpper().toUtf8().constData(), quality)) {
emit exportCompleted(savePath, true);
return true;
}
}
break;
}
case Audio: {
setLastExportDir("audio", QFileInfo(savePath).path());
QString ext = QFileInfo(savePath).suffix().toLower();
if (ext == "wav") {
QFile file(savePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
} else {
// FFmpeg conversion
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (tempWav.open()) {
tempWav.write(data);
QString tempPath = tempWav.fileName();
tempWav.close();
bool success = convertWithFFmpeg(tempPath, savePath, ext);
QFile::remove(tempPath);
if (success) {
emit exportCompleted(savePath, true);
return true;
}
}
}
break;
}
default: {
setLastExportDir("raw", QFileInfo(savePath).path());
QFile file(savePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
emit exportCompleted(savePath, true);
return true;
}
break;
}
}
return false;
}
bool ExportManager::quickExportImage(const QImage& image, const QString& name, QWidget* parent) {
if (image.isNull()) return false;
QString baseName = QFileInfo(name).baseName();
if (baseName.isEmpty()) baseName = "image";
QString dir = lastExportDir("image");
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QString format = Settings::instance().defaultImageExportFormat();
QString filter = getFilterForFormat(format);
QString savePath = QFileDialog::getSaveFileName(parent, "Quick Export Image",
dir + "/" + baseName + "." + format, filter);
if (savePath.isEmpty()) return false;
setLastExportDir("image", QFileInfo(savePath).path());
QString ext = QFileInfo(savePath).suffix().toLower();
if (ext == "tga") {
if (saveTGA(image, savePath)) {
emit exportCompleted(savePath, true);
return true;
}
return false;
}
int quality = (ext == "jpg" || ext == "jpeg") ?
Settings::instance().imageJpegQuality() : -1;
if (image.save(savePath, ext.toUpper().toUtf8().constData(), quality)) {
emit exportCompleted(savePath, true);
return true;
}
return false;
}
bool ExportManager::batchExport(const QList<BatchExportItem>& items, QWidget* parent) {
if (items.isEmpty()) return false;
BatchExportDialog dialog(parent);
dialog.setItems(items);
if (dialog.exec() != QDialog::Accepted) return false;
QList<BatchExportItem> selected = dialog.selectedItems();
if (selected.isEmpty()) return false;
QString outputDir = dialog.outputDirectory();
bool preserveStructure = dialog.preserveStructure();
QString conflictResolution = dialog.conflictResolution();
QString imageFormat = dialog.imageFormat();
QString audioFormat = dialog.audioFormat();
int succeeded = 0, failed = 0, skipped = 0;
for (int i = 0; i < selected.size(); ++i) {
const BatchExportItem& item = selected[i];
emit batchExportProgress(i + 1, selected.size(), item.name);
// Determine output path
QString relativePath = preserveStructure ? item.path : item.name;
QString ext;
switch (item.contentType) {
case Image: ext = imageFormat; break;
case Audio: ext = audioFormat; break;
case Text: ext = "txt"; break;
default: ext = "bin"; break;
}
QString baseName = QFileInfo(relativePath).baseName();
QString subDir = preserveStructure ? QFileInfo(relativePath).path() : "";
QString targetDir = subDir.isEmpty() ? outputDir : outputDir + "/" + subDir;
QString targetPath = targetDir + "/" + baseName + "." + ext;
// Ensure directory exists
QDir().mkpath(targetDir);
// Handle conflicts
if (QFile::exists(targetPath)) {
if (conflictResolution == "skip") {
skipped++;
continue;
} else if (conflictResolution == "number") {
int num = 1;
QString newPath;
do {
newPath = targetDir + "/" + baseName + QString("_%1.").arg(num++) + ext;
} while (QFile::exists(newPath) && num < 1000);
targetPath = newPath;
}
// "overwrite" - just proceed
}
// Export based on type
bool success = false;
switch (item.contentType) {
case Image: {
QImage image;
if (image.loadFromData(item.data)) {
if (ext == "tga") {
success = saveTGA(image, targetPath);
} else {
int quality = (ext == "jpg") ? Settings::instance().imageJpegQuality() : -1;
success = image.save(targetPath, ext.toUpper().toUtf8().constData(), quality);
}
}
break;
}
case Audio:
if (ext == "wav") {
QFile file(targetPath);
if (file.open(QIODevice::WriteOnly)) {
file.write(item.data);
file.close();
success = true;
}
} else {
QTemporaryFile tempWav;
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
if (tempWav.open()) {
tempWav.write(item.data);
QString tempPath = tempWav.fileName();
tempWav.close();
success = convertWithFFmpeg(tempPath, targetPath, ext);
QFile::remove(tempPath);
}
}
break;
default: {
QFile file(targetPath);
if (file.open(QIODevice::WriteOnly)) {
file.write(item.data);
file.close();
success = true;
}
break;
}
}
if (success) {
succeeded++;
} else {
failed++;
}
}
emit batchExportCompleted(succeeded, failed, skipped);
return failed == 0;
}
QStringList ExportManager::supportedImageFormats() const {
return {"png", "jpg", "bmp", "tiff", "tga"};
}
QStringList ExportManager::supportedAudioFormats() const {
return {"wav", "mp3", "ogg", "flac"};
}
bool ExportManager::hasFFmpeg() const {
if (!m_ffmpegChecked) {
m_cachedFFmpegPath = findFFmpegPath();
m_ffmpegChecked = true;
}
return !m_cachedFFmpegPath.isEmpty();
}
void ExportManager::copyImageToClipboard(const QImage& image) {
if (image.isNull()) return;
QApplication::clipboard()->setImage(image);
}
void ExportManager::copyTextToClipboard(const QString& text) {
QApplication::clipboard()->setText(text);
}
void ExportManager::copyBinaryAsHex(const QByteArray& data) {
QString hex = data.toHex(' ').toUpper();
QApplication::clipboard()->setText(hex);
}
QString ExportManager::lastExportDir(const QString& category) const {
return m_lastDirs.value(category);
}
void ExportManager::setLastExportDir(const QString& category, const QString& dir) {
m_lastDirs[category] = dir;
}
bool ExportManager::saveTGA(const QImage& image, const QString& path) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) return false;
QImage img = image.convertToFormat(QImage::Format_ARGB32);
// TGA Header (18 bytes)
QByteArray header(18, 0);
header[2] = 2; // Uncompressed true-color
header[12] = img.width() & 0xFF;
header[13] = (img.width() >> 8) & 0xFF;
header[14] = img.height() & 0xFF;
header[15] = (img.height() >> 8) & 0xFF;
header[16] = 32; // 32 bits per pixel
header[17] = 0x20; // Top-left origin
file.write(header);
// Write BGRA pixels (TGA uses BGRA order)
for (int y = 0; y < img.height(); ++y) {
for (int x = 0; x < img.width(); ++x) {
QRgb pixel = img.pixel(x, y);
char bgra[4] = {
static_cast<char>(qBlue(pixel)),
static_cast<char>(qGreen(pixel)),
static_cast<char>(qRed(pixel)),
static_cast<char>(qAlpha(pixel))
};
file.write(bgra, 4);
}
}
return true;
}
QString ExportManager::getFilterForFormat(const QString& format) const {
QString ext = format.toLower();
if (ext == "png") return "PNG Images (*.png)";
if (ext == "jpg" || ext == "jpeg") return "JPEG Images (*.jpg *.jpeg)";
if (ext == "bmp") return "BMP Images (*.bmp)";
if (ext == "tiff" || ext == "tif") return "TIFF Images (*.tiff *.tif)";
if (ext == "tga") return "TGA Images (*.tga)";
if (ext == "webp") return "WebP Images (*.webp)";
return "All Files (*)";
}
bool ExportManager::convertWithFFmpeg(const QString& inputPath, const QString& outputPath, const QString& format) {
QString ffmpegPath = m_cachedFFmpegPath;
if (ffmpegPath.isEmpty()) {
ffmpegPath = findFFmpegPath();
}
if (ffmpegPath.isEmpty()) {
emit exportError("FFmpeg not found");
return false;
}
QProcess ffmpeg;
QStringList args;
args << "-y"; // Overwrite output
args << "-i" << inputPath;
// Format-specific encoding settings
if (format == "mp3") {
args << "-codec:a" << "libmp3lame" << "-qscale:a" << "2";
} else if (format == "ogg") {
args << "-codec:a" << "libvorbis" << "-qscale:a" << "5";
} else if (format == "flac") {
args << "-codec:a" << "flac";
}
args << outputPath;
ffmpeg.start(ffmpegPath, args);
if (!ffmpeg.waitForFinished(30000)) {
emit exportError("FFmpeg conversion timed out");
return false;
}
if (ffmpeg.exitCode() != 0) {
QString errorOutput = QString::fromUtf8(ffmpeg.readAllStandardError());
emit exportError(QString("FFmpeg conversion failed: %1").arg(errorOutput.left(200)));
return false;
}
return true;
}
QString ExportManager::findFFmpegPath() const {
// Use Settings' ffmpegPath which handles auto-detection
return Settings::instance().ffmpegPath();
}

View File

@ -1,101 +0,0 @@
#ifndef EXPORTMANAGER_H
#define EXPORTMANAGER_H
#include <QObject>
#include <QImage>
#include <QVariantMap>
#include <QStringList>
class QWidget;
class QTreeWidgetItem;
struct BatchExportItem;
/**
* @brief The ExportManager class handles all export operations for tree items.
*
* This singleton provides centralized export functionality with format-specific
* options for images, audio, text, and raw binary data.
*/
class ExportManager : public QObject {
Q_OBJECT
public:
enum ContentType {
Unknown,
Image,
Audio,
Video,
Text,
Binary
};
static ExportManager& instance();
// Content type detection
ContentType detectContentType(const QVariantMap& vars) const;
ContentType detectContentType(const QByteArray& data, const QString& filename) const;
// Dialog-based export (shows full options dialog)
bool exportWithDialog(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent);
bool exportImageWithDialog(const QImage& image, const QString& name, QWidget* parent);
// Quick export (uses saved defaults, minimal UI - just file picker)
bool quickExport(const QByteArray& data, const QString& name,
ContentType type, QWidget* parent);
bool quickExportImage(const QImage& image, const QString& name, QWidget* parent);
// Batch export (shows batch dialog for multiple items)
bool batchExport(const QList<BatchExportItem>& items, QWidget* parent);
// Legacy export methods (direct export with file dialog)
bool exportRawData(const QByteArray& data, const QString& suggestedName, QWidget* parent);
bool exportImage(const QImage& image, const QString& format, const QString& suggestedName, QWidget* parent);
bool exportAudio(const QByteArray& wavData, const QString& format, const QString& suggestedName, QWidget* parent);
bool exportText(const QByteArray& data, const QString& suggestedName, QWidget* parent);
// Format support
QStringList supportedImageFormats() const;
QStringList supportedAudioFormats() const;
bool hasFFmpeg() const;
// Clipboard operations
void copyImageToClipboard(const QImage& image);
void copyTextToClipboard(const QString& text);
void copyBinaryAsHex(const QByteArray& data);
// Last directory tracking
QString lastExportDir(const QString& category) const;
void setLastExportDir(const QString& category, const QString& dir);
signals:
void exportCompleted(const QString& path, bool success);
void exportError(const QString& error);
// Batch export signals
void batchExportProgress(int current, int total, const QString& currentItem);
void batchExportCompleted(int succeeded, int failed, int skipped);
private:
explicit ExportManager(QObject* parent = nullptr);
~ExportManager() = default;
// Disable copy
ExportManager(const ExportManager&) = delete;
ExportManager& operator=(const ExportManager&) = delete;
// Image format helpers
bool saveTGA(const QImage& image, const QString& path);
QString getFilterForFormat(const QString& format) const;
// Audio conversion
bool convertWithFFmpeg(const QString& inputPath, const QString& outputPath, const QString& format);
QString findFFmpegPath() const;
// State
QMap<QString, QString> m_lastDirs;
mutable QString m_cachedFFmpegPath;
mutable bool m_ffmpegChecked = false;
};
#endif // EXPORTMANAGER_H

27
app/fastfileviewer.cpp Normal file
View File

@ -0,0 +1,27 @@
#include "fastfileviewer.h"
#include "asset_structs.h"
#include "ui_fastfileviewer.h"
FastFileViewer::FastFileViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::FFViewer)
, mFastFile(nullptr)
{
ui->setupUi(this);
}
FastFileViewer::~FastFileViewer()
{
delete ui;
}
void FastFileViewer::SetFastFile(std::shared_ptr<FastFile> aFastFile) {
mFastFile.swap(aFastFile);
ui->label_Title->setText(mFastFile->GetStem());
ui->comboBox_Company->setCurrentIndex(mFastFile->GetCompany());
ui->comboBox_FileType->setCurrentIndex(mFastFile->GetType());
ui->checkBox_Signed->setChecked(mFastFile->GetSignage() == SIGNAGE_SIGNED);
ui->lineEdit_Magic->setText(mFastFile->GetMagic());
ui->spinBox_Version->setValue(mFastFile->GetVersion());
}

26
app/fastfileviewer.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef FASTFILEVIEWER_H
#define FASTFILEVIEWER_H
#include "asset_structs.h"
#include "fastfile.h"
#include <QWidget>
namespace Ui {
class FFViewer;
}
class FastFileViewer : public QWidget
{
Q_OBJECT
public:
explicit FastFileViewer(QWidget *parent = nullptr);
~FastFileViewer();
void SetFastFile(std::shared_ptr<FastFile> aFastFile);
private:
Ui::FFViewer *ui;
std::shared_ptr<FastFile> mFastFile;
};
#endif // FASTFILEVIEWER_H

197
app/fastfileviewer.ui Normal file
View File

@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FFViewer</class>
<widget class="QWidget" name="FFViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>428</width>
<height>459</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>FastFile 0</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Header</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Company:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_Company">
<property name="enabled">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Infinity Ward</string>
</property>
</item>
<item>
<property name="text">
<string>Treyarch</string>
</property>
</item>
<item>
<property name="text">
<string>Sledgehammer</string>
</property>
</item>
<item>
<property name="text">
<string>Neversoft</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>File Type:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_FileType">
<property name="enabled">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>FastFile</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Signed:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkBox_Signed">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Is signed</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Magic:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_Magic"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="spinBox_Version">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,67 +0,0 @@
#include "fieldeditcommand.h"
// Command ID for merging consecutive edits to the same field
static const int FIELD_EDIT_COMMAND_ID = 1001;
FieldEditCommand::FieldEditCommand(int journalId,
const QString& fieldName,
const QVariant& oldValue,
const QVariant& newValue,
ApplyCallback applyCallback,
QUndoCommand* parent)
: QUndoCommand(parent)
, m_journalId(journalId)
, m_fieldName(fieldName)
, m_oldValue(oldValue)
, m_newValue(newValue)
, m_applyCallback(applyCallback)
{
setText(QString("Edit %1").arg(fieldName));
}
void FieldEditCommand::undo()
{
if (m_applyCallback) {
m_applyCallback(m_fieldName, m_oldValue);
}
}
void FieldEditCommand::redo()
{
// Skip the first redo because the value was already applied
// when the user made the edit (before command was pushed)
if (m_firstRedo) {
m_firstRedo = false;
return;
}
if (m_applyCallback) {
m_applyCallback(m_fieldName, m_newValue);
}
}
int FieldEditCommand::id() const
{
return FIELD_EDIT_COMMAND_ID;
}
bool FieldEditCommand::mergeWith(const QUndoCommand* other)
{
// Merge consecutive edits to the same field in the same journal
// This prevents creating a new undo entry for every keystroke
if (other->id() != id()) {
return false;
}
const FieldEditCommand* otherEdit = static_cast<const FieldEditCommand*>(other);
// Only merge if same journal and field
if (otherEdit->m_journalId != m_journalId ||
otherEdit->m_fieldName != m_fieldName) {
return false;
}
// Keep original old value, take new value from other command
m_newValue = otherEdit->m_newValue;
return true;
}

View File

@ -1,45 +0,0 @@
#ifndef FIELDEDITCOMMAND_H
#define FIELDEDITCOMMAND_H
#include <QUndoCommand>
#include <QVariant>
#include <QString>
#include <functional>
/**
* @brief QUndoCommand for field value edits in ScriptTypeEditorWidget
*
* Stores old and new values for a field, allowing undo/redo of edits.
* Uses callbacks to apply values since the actual application logic
* is in the editor widget.
*/
class FieldEditCommand : public QUndoCommand
{
public:
using ApplyCallback = std::function<void(const QString& fieldName, const QVariant& value)>;
FieldEditCommand(int journalId,
const QString& fieldName,
const QVariant& oldValue,
const QVariant& newValue,
ApplyCallback applyCallback,
QUndoCommand* parent = nullptr);
void undo() override;
void redo() override;
int id() const override;
bool mergeWith(const QUndoCommand* other) override;
QString fieldName() const { return m_fieldName; }
int journalId() const { return m_journalId; }
private:
int m_journalId;
QString m_fieldName;
QVariant m_oldValue;
QVariant m_newValue;
ApplyCallback m_applyCallback;
bool m_firstRedo = true; // Skip first redo since value is already applied
};
#endif // FIELDEDITCOMMAND_H

View File

@ -1,605 +0,0 @@
#include "hexviewerwidget.h"
#include <QHeaderView>
#include <QFontDatabase>
#include <QScrollBar>
#include <QPainter>
#include <QResizeEvent>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QClipboard>
#include <QApplication>
// ============================================================================
// HexView - Virtualized hex viewer with direct painting
// ============================================================================
HexView::HexView(QWidget *parent)
: QAbstractScrollArea(parent)
{
// Set up monospace font
mMonoFont = QFont("Consolas", 10);
if (!QFontDatabase::hasFamily("Consolas")) {
mMonoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
mMonoFont.setPointSize(10);
}
QFontMetrics fm(mMonoFont);
mCharWidth = fm.horizontalAdvance('0');
mLineHeight = fm.height();
// Default dark theme colors
mBgColor = QColor("#1e1e1e");
mTextColor = QColor("#888888");
mOffsetColor = QColor("#ad0c0c");
mNullColor = QColor("#505050");
mHighColor = QColor("#ad0c0c");
mPrintableColor = QColor("#c0c0c0");
mControlColor = QColor("#707070");
mNonPrintableColor = QColor("#909090");
mBorderColor = QColor("#333333");
mSelectionColor = QColor("#264f78"); // Blue selection highlight
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
viewport()->setAutoFillBackground(false);
viewport()->setMouseTracking(true);
setFocusPolicy(Qt::StrongFocus);
}
void HexView::setData(const QByteArray &data)
{
mData = data;
recalculateBytesPerLine();
updateScrollBars();
viewport()->update();
}
void HexView::setTheme(const Theme &theme)
{
mBgColor = QColor(theme.backgroundColor);
mTextColor = QColor(theme.textColorMuted);
mOffsetColor = QColor(theme.accentColor);
mHighColor = QColor(theme.accentColor);
mBorderColor = QColor(theme.borderColor);
// Derived colors based on theme brightness
int brightness = mBgColor.lightness();
if (brightness < 128) {
// Dark theme
mNullColor = QColor("#505050");
mPrintableColor = QColor("#c0c0c0");
mControlColor = QColor("#707070");
mNonPrintableColor = QColor("#909090");
} else {
// Light theme
mNullColor = QColor("#a0a0a0");
mPrintableColor = QColor("#303030");
mControlColor = QColor("#808080");
mNonPrintableColor = QColor("#606060");
}
viewport()->update();
}
void HexView::setBytesPerLine(int bytes)
{
if (bytes != mBytesPerLine && bytes >= 8) {
mBytesPerLine = bytes;
updateScrollBars();
viewport()->update();
}
}
void HexView::updateScrollBars()
{
if (mData.isEmpty()) {
verticalScrollBar()->setRange(0, 0);
horizontalScrollBar()->setRange(0, 0);
return;
}
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
// Account for header (1 line) + gap (4px ~= 0.25 lines)
int headerHeight = mLineHeight + 4;
int availableHeight = viewport()->height() - headerHeight;
int visibleLines = qMax(1, availableHeight / mLineHeight);
verticalScrollBar()->setRange(0, qMax(0, totalLines - visibleLines));
verticalScrollBar()->setPageStep(visibleLines);
verticalScrollBar()->setSingleStep(1);
// Horizontal: offset(10) + hex(3*n) + separator(2) + ascii(n)
int contentWidth = 10 * mCharWidth + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth + mBytesPerLine * mCharWidth;
int viewportWidth = viewport()->width();
horizontalScrollBar()->setRange(0, qMax(0, contentWidth - viewportWidth));
horizontalScrollBar()->setPageStep(viewportWidth);
}
void HexView::recalculateBytesPerLine()
{
if (mData.isEmpty()) return;
int viewportWidth = viewport()->width();
if (viewportWidth < 100) return;
// Calculate available space for hex bytes
// Format: XXXXXXXX HH HH HH HH HH HH ... | AAAA...
// Offset = 10 chars (8 hex + 2 spaces)
// Each byte = 3 chars hex + 1 char ascii
// Separator = 2 chars
int fixedWidth = 10 * mCharWidth + 2 * mCharWidth; // offset + separator
int remaining = viewportWidth - fixedWidth;
// Each byte needs: 3 chars hex + 1 char ascii = 4 chars
int bytesPerLine = remaining / (4 * mCharWidth);
// Round down to multiple of 8 for clean display
bytesPerLine = qMax(8, (bytesPerLine / 8) * 8);
bytesPerLine = qMin(32, bytesPerLine);
if (bytesPerLine != mBytesPerLine) {
mBytesPerLine = bytesPerLine;
updateScrollBars();
}
}
void HexView::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(viewport());
painter.setFont(mMonoFont);
painter.setRenderHint(QPainter::TextAntialiasing);
// Fill background
painter.fillRect(viewport()->rect(), mBgColor);
if (mData.isEmpty()) {
painter.setPen(mTextColor);
painter.drawText(viewport()->rect(), Qt::AlignCenter, "No data");
return;
}
int xOffset = -horizontalScrollBar()->value();
int firstLine = verticalScrollBar()->value();
int visibleLines = viewport()->height() / mLineHeight + 2;
int totalLines = (mData.size() + mBytesPerLine - 1) / mBytesPerLine;
// Calculate column positions (no extra gaps - uniform spacing)
int offsetX = xOffset;
int hexX = offsetX + 10 * mCharWidth;
int asciiX = hexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
// Draw header line
int y = mLineHeight;
painter.setPen(mOffsetColor);
painter.drawText(offsetX, y - 4, "Offset");
// Draw hex column headers (uniform spacing)
painter.setPen(mTextColor);
for (int i = 0; i < mBytesPerLine; i++) {
int x = hexX + i * 3 * mCharWidth;
painter.drawText(x, y - 4, QString("%1").arg(i, 2, 16, QChar('0')).toUpper());
}
// Draw "Decoded" header
painter.setPen(mOffsetColor);
painter.drawText(asciiX, y - 4, "Decoded");
// Draw separator line
painter.setPen(mBorderColor);
painter.drawLine(0, y, viewport()->width(), y);
y += 4; // Small gap after header
// Get selection range (normalized so start <= end)
int selStart = qMin(mSelectionStart, mSelectionEnd);
int selEnd = qMax(mSelectionStart, mSelectionEnd);
bool hasSelection = mSelectionStart >= 0 && mSelectionEnd >= 0;
// Draw data rows (only visible ones)
for (int line = 0; line < visibleLines && (firstLine + line) < totalLines; line++) {
int dataLine = firstLine + line;
int offset = dataLine * mBytesPerLine;
int bytesInLine = qMin(mBytesPerLine, mData.size() - offset);
y += mLineHeight;
// Draw offset
painter.setPen(mOffsetColor);
QString offsetStr = QString("%1").arg(offset, 8, 16, QChar('0')).toUpper();
painter.drawText(offsetX, y, offsetStr);
// Draw hex bytes (uniform spacing)
for (int i = 0; i < mBytesPerLine; i++) {
int x = hexX + i * 3 * mCharWidth;
if (i < bytesInLine) {
int byteIndex = offset + i;
quint8 byte = static_cast<quint8>(mData[byteIndex]);
// Draw selection background for hex
if (hasSelection && byteIndex >= selStart && byteIndex <= selEnd) {
QRect selRect(x - 1, y - mLineHeight + 4, 2 * mCharWidth + 2, mLineHeight);
painter.fillRect(selRect, mSelectionColor);
}
painter.setPen(getByteColor(byte));
QString byteStr = QString("%1").arg(byte, 2, 16, QChar('0')).toUpper();
painter.drawText(x, y, byteStr);
}
}
// Draw vertical separator
int sepX = asciiX - mCharWidth;
painter.setPen(mBorderColor);
painter.drawLine(sepX, y - mLineHeight + 4, sepX, y + 2);
// Draw ASCII/Decoded - show extended ASCII like HxD
for (int i = 0; i < mBytesPerLine; i++) {
int x = asciiX + i * mCharWidth;
if (i < bytesInLine) {
int byteIndex = offset + i;
quint8 byte = static_cast<quint8>(mData[byteIndex]);
// Draw selection background for decoded
if (hasSelection && byteIndex >= selStart && byteIndex <= selEnd) {
QRect selRect(x, y - mLineHeight + 4, mCharWidth, mLineHeight);
painter.fillRect(selRect, mSelectionColor);
}
painter.setPen(getAsciiColor(byte));
// Show actual character for printable ASCII and extended ASCII (Latin-1)
// Control chars (0x00-0x1F) and DEL (0x7F) show as '.'
QChar c;
if (byte < 0x20 || byte == 0x7F) {
c = '.';
} else {
c = QChar::fromLatin1(static_cast<char>(byte));
}
painter.drawText(x, y, QString(c));
}
}
}
}
void HexView::resizeEvent(QResizeEvent *event)
{
QAbstractScrollArea::resizeEvent(event);
recalculateBytesPerLine();
updateScrollBars();
}
void HexView::scrollContentsBy(int dx, int dy)
{
Q_UNUSED(dx);
Q_UNUSED(dy);
viewport()->update();
}
QColor HexView::getByteColor(quint8 byte) const
{
if (byte == 0x00) {
return mNullColor;
} else if (byte == 0xFF) {
return mHighColor;
} else if (byte >= 0x20 && byte < 0x7F) {
return mPrintableColor;
} else if (byte < 0x20) {
return mControlColor;
} else {
return mNonPrintableColor;
}
}
QColor HexView::getAsciiColor(quint8 byte) const
{
if (byte < 0x20 || byte == 0x7F) {
return mNullColor; // Control characters shown as '.'
} else if (byte >= 0x20 && byte < 0x7F) {
return mPrintableColor; // Standard ASCII
} else {
return mNonPrintableColor; // Extended ASCII (0x80-0xFF)
}
}
void HexView::updateColumnPositions()
{
mOffsetX = -horizontalScrollBar()->value();
mHexX = mOffsetX + 10 * mCharWidth;
mAsciiX = mHexX + mBytesPerLine * 3 * mCharWidth + 2 * mCharWidth;
mHeaderHeight = mLineHeight + 4;
}
int HexView::byteIndexAtPos(const QPoint &pos, SelectionSource *source) const
{
// Check if in header area
if (pos.y() < mHeaderHeight) {
return -1;
}
// Calculate which line
int lineY = pos.y() - mHeaderHeight;
int line = lineY / mLineHeight + verticalScrollBar()->value();
int col = -1;
// Check if in hex area
int hexEndX = mHexX + mBytesPerLine * 3 * mCharWidth;
if (pos.x() >= mHexX && pos.x() < hexEndX) {
int relX = pos.x() - mHexX;
col = relX / (3 * mCharWidth);
if (source) *source = SelectionSource::Hex;
}
// Check if in decoded/ASCII area
else if (pos.x() >= mAsciiX) {
int relX = pos.x() - mAsciiX;
col = relX / mCharWidth;
if (source) *source = SelectionSource::Decoded;
}
if (col < 0 || col >= mBytesPerLine) {
return -1;
}
int byteIndex = line * mBytesPerLine + col;
if (byteIndex < 0 || byteIndex >= mData.size()) {
return -1;
}
return byteIndex;
}
void HexView::clearSelection()
{
mSelectionStart = -1;
mSelectionEnd = -1;
mSelectionSource = SelectionSource::None;
viewport()->update();
}
bool HexView::hasSelection() const
{
return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
}
QString HexView::getSelectedHex() const
{
if (!hasSelection()) return QString();
int start = qMin(mSelectionStart, mSelectionEnd);
int end = qMax(mSelectionStart, mSelectionEnd);
QString result;
for (int i = start; i <= end && i < mData.size(); i++) {
result += QString("%1").arg(static_cast<quint8>(mData[i]), 2, 16, QChar('0')).toUpper();
}
return result;
}
QString HexView::getSelectedDecoded() const
{
if (!hasSelection()) return QString();
int start = qMin(mSelectionStart, mSelectionEnd);
int end = qMax(mSelectionStart, mSelectionEnd);
QString result;
for (int i = start; i <= end && i < mData.size(); i++) {
quint8 byte = static_cast<quint8>(mData[i]);
if (byte < 0x20 || byte == 0x7F) {
result += '.';
} else {
result += QChar::fromLatin1(static_cast<char>(byte));
}
}
return result;
}
void HexView::copyToClipboard()
{
if (!hasSelection()) return;
QString text;
if (mSelectionSource == SelectionSource::Hex) {
text = getSelectedHex();
} else {
text = getSelectedDecoded();
}
QApplication::clipboard()->setText(text);
}
void HexView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
updateColumnPositions();
SelectionSource source = SelectionSource::None;
int idx = byteIndexAtPos(event->pos(), &source);
if (idx >= 0) {
mSelectionStart = idx;
mSelectionEnd = idx;
mSelectionSource = source;
mSelecting = true;
setFocus();
viewport()->update();
} else {
clearSelection();
}
}
QAbstractScrollArea::mousePressEvent(event);
}
void HexView::mouseMoveEvent(QMouseEvent *event)
{
if (mSelecting && (event->buttons() & Qt::LeftButton)) {
updateColumnPositions();
int idx = byteIndexAtPos(event->pos());
if (idx >= 0) {
mSelectionEnd = idx;
viewport()->update();
}
}
QAbstractScrollArea::mouseMoveEvent(event);
}
void HexView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
mSelecting = false;
}
QAbstractScrollArea::mouseReleaseEvent(event);
}
void HexView::keyPressEvent(QKeyEvent *event)
{
if (event->matches(QKeySequence::Copy)) {
copyToClipboard();
event->accept();
return;
}
// Ctrl+A to select all
if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_A) {
if (!mData.isEmpty()) {
mSelectionStart = 0;
mSelectionEnd = mData.size() - 1;
mSelectionSource = SelectionSource::Hex;
viewport()->update();
}
event->accept();
return;
}
QAbstractScrollArea::keyPressEvent(event);
}
// ============================================================================
// HexViewerWidget
// ============================================================================
HexViewerWidget::HexViewerWidget(QWidget *parent)
: QWidget(parent)
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Splitter for hex view and metadata
mSplitter = new QSplitter(Qt::Horizontal, this);
// Left side - hex view container
auto *hexContainer = new QWidget(mSplitter);
auto *hexLayout = new QVBoxLayout(hexContainer);
hexLayout->setContentsMargins(0, 0, 0, 0);
hexLayout->setSpacing(0);
// Info label
mInfoLabel = new QLabel(hexContainer);
hexLayout->addWidget(mInfoLabel);
// Hex view
mHexView = new HexView(hexContainer);
hexLayout->addWidget(mHexView, 1);
mSplitter->addWidget(hexContainer);
// Metadata tree
mMetadataTree = new QTreeWidget(mSplitter);
mMetadataTree->setHeaderLabels({"Property", "Value"});
mMetadataTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
mMetadataTree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
mMetadataTree->setAlternatingRowColors(true);
mSplitter->addWidget(mMetadataTree);
mSplitter->setSizes({700, 250});
mainLayout->addWidget(mSplitter);
// Connect to theme changes
connect(&Settings::instance(), &Settings::themeChanged, this, &HexViewerWidget::applyTheme);
// Apply current theme
applyTheme(Settings::instance().theme());
}
void HexViewerWidget::applyTheme(const Theme &theme)
{
mCurrentTheme = theme;
// Update info label
mInfoLabel->setStyleSheet(QString(
"QLabel { background-color: %1; color: %2; padding: 4px 8px; font-size: 11px; }"
).arg(theme.panelColor, theme.textColorMuted));
// Update hex view
mHexView->setTheme(theme);
// Update metadata tree
mMetadataTree->setStyleSheet(QString(
"QTreeWidget { background-color: %1; color: %2; border: none; }"
"QTreeWidget::item:selected { background-color: %3; color: white; }"
"QTreeWidget::item:alternate { background-color: %4; }"
"QHeaderView::section { background-color: %4; color: %5; padding: 4px; border: none; }"
).arg(theme.backgroundColor, theme.textColor, theme.accentColor, theme.panelColor, theme.textColorMuted));
}
void HexViewerWidget::setData(const QByteArray &data, const QString &filename)
{
mData = data;
mFilename = filename;
// Update info label
QString info = QString("%1 | %2 bytes").arg(filename).arg(data.size());
if (data.size() >= 4) {
QString magic;
for (int i = 0; i < qMin(4, data.size()); i++) {
char c = data[i];
magic += (c >= 32 && c < 127) ? c : '.';
}
info += QString(" | Magic: %1").arg(magic);
}
mInfoLabel->setText(info);
// Update hex view
mHexView->setData(data);
// Update metadata tree
mMetadataTree->clear();
auto *sizeItem = new QTreeWidgetItem(mMetadataTree);
sizeItem->setText(0, "File Size");
sizeItem->setText(1, QString("%1 bytes").arg(data.size()));
auto *filenameItem = new QTreeWidgetItem(mMetadataTree);
filenameItem->setText(0, "Filename");
filenameItem->setText(1, filename);
if (data.size() >= 4) {
auto *magicItem = new QTreeWidgetItem(mMetadataTree);
magicItem->setText(0, "Magic Bytes");
QString hexMagic;
for (int i = 0; i < qMin(16, data.size()); i++) {
hexMagic += QString("%1 ").arg(static_cast<quint8>(data[i]), 2, 16, QChar('0')).toUpper();
}
magicItem->setText(1, hexMagic.trimmed());
}
}
void HexViewerWidget::setMetadata(const QVariantMap &metadata)
{
// Add metadata from parsed fields (caller provides only visible fields)
for (auto it = metadata.begin(); it != metadata.end(); ++it) {
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());
}
}
}

View File

@ -1,112 +0,0 @@
#ifndef HEXVIEWERWIDGET_H
#define HEXVIEWERWIDGET_H
#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTreeWidget>
#include <QSplitter>
#include <QAbstractScrollArea>
#include <QFont>
#include <QScrollBar>
#include "settings.h"
// Custom hex view widget with virtualized rendering and selection
class HexView : public QAbstractScrollArea
{
Q_OBJECT
public:
enum class SelectionSource { None, Hex, Decoded };
explicit HexView(QWidget *parent = nullptr);
void setData(const QByteArray &data);
void setTheme(const Theme &theme);
void setBytesPerLine(int bytes);
int bytesPerLine() const { return mBytesPerLine; }
// Selection
void clearSelection();
bool hasSelection() const;
QString getSelectedHex() const; // Returns hex without spaces
QString getSelectedDecoded() const; // Returns decoded ASCII/Latin-1
void copyToClipboard();
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void scrollContentsBy(int dx, int dy) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
void updateScrollBars();
void recalculateBytesPerLine();
QColor getByteColor(quint8 byte) const;
QColor getAsciiColor(quint8 byte) const;
int byteIndexAtPos(const QPoint &pos, SelectionSource *source = nullptr) const;
void updateColumnPositions();
QByteArray mData;
QFont mMonoFont;
int mBytesPerLine = 16;
int mCharWidth = 0;
int mLineHeight = 0;
// Column positions (calculated once, updated on resize)
int mOffsetX = 0;
int mHexX = 0;
int mAsciiX = 0;
int mHeaderHeight = 0;
// Selection state
int mSelectionStart = -1;
int mSelectionEnd = -1;
SelectionSource mSelectionSource = SelectionSource::None;
bool mSelecting = false;
// Theme colors
QColor mBgColor;
QColor mTextColor;
QColor mOffsetColor;
QColor mNullColor;
QColor mHighColor;
QColor mPrintableColor;
QColor mControlColor;
QColor mNonPrintableColor;
QColor mBorderColor;
QColor mSelectionColor;
};
class HexViewerWidget : public QWidget
{
Q_OBJECT
public:
explicit HexViewerWidget(QWidget *parent = nullptr);
~HexViewerWidget() = default;
void setData(const QByteArray &data, const QString &filename);
void setMetadata(const QVariantMap &metadata);
private slots:
void applyTheme(const Theme &theme);
private:
QByteArray mData;
QString mFilename;
QSplitter *mSplitter;
QLabel *mInfoLabel;
HexView *mHexView;
QTreeWidget *mMetadataTree;
Theme mCurrentTheme;
};
#endif // HEXVIEWERWIDGET_H

View File

@ -1,282 +0,0 @@
#include "imageexportdialog.h"
#include "settings.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>
#include <QComboBox>
#include <QStackedWidget>
#include <QGroupBox>
#include <QFileInfo>
ImageExportDialog::ImageExportDialog(QWidget *parent)
: ExportDialog(ContentType::Image, parent)
, mPreviewLabel(nullptr)
, mOptionsStack(nullptr)
, mJpegOptionsWidget(nullptr)
, mJpegQualitySlider(nullptr)
, mJpegQualitySpinBox(nullptr)
, mPngOptionsWidget(nullptr)
, mPngCompressionSlider(nullptr)
, mPngCompressionSpinBox(nullptr)
, mNoOptionsWidget(nullptr)
{
// Populate format combo
for (const QString& fmt : supportedFormats()) {
formatCombo()->addItem(fmt.toUpper());
}
// Set default format from settings
QString defaultFormat = Settings::instance().defaultImageExportFormat().toUpper();
int index = formatCombo()->findText(defaultFormat);
if (index >= 0) {
formatCombo()->setCurrentIndex(index);
}
setupPreview();
}
void ImageExportDialog::setupPreview()
{
// Create preview label inside preview container
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
previewLayout->setContentsMargins(0, 0, 0, 0);
mPreviewLabel = new QLabel(previewContainer());
mPreviewLabel->setAlignment(Qt::AlignCenter);
mPreviewLabel->setMinimumSize(256, 256);
mPreviewLabel->setText("No image loaded");
mPreviewLabel->setStyleSheet("color: #808080;");
previewLayout->addWidget(mPreviewLabel);
// Create stacked widget for format-specific options
mOptionsStack = new QStackedWidget(this);
// JPEG options
mJpegOptionsWidget = new QWidget(this);
QVBoxLayout* jpegLayout = new QVBoxLayout(mJpegOptionsWidget);
jpegLayout->setContentsMargins(0, 0, 0, 0);
QLabel* jpegQualityLabel = new QLabel("Quality:", mJpegOptionsWidget);
QHBoxLayout* jpegSliderLayout = new QHBoxLayout();
mJpegQualitySlider = new QSlider(Qt::Horizontal, mJpegOptionsWidget);
mJpegQualitySlider->setRange(1, 100);
mJpegQualitySlider->setValue(Settings::instance().imageJpegQuality());
mJpegQualitySpinBox = new QSpinBox(mJpegOptionsWidget);
mJpegQualitySpinBox->setRange(1, 100);
mJpegQualitySpinBox->setValue(Settings::instance().imageJpegQuality());
jpegSliderLayout->addWidget(mJpegQualitySlider);
jpegSliderLayout->addWidget(mJpegQualitySpinBox);
jpegLayout->addWidget(jpegQualityLabel);
jpegLayout->addLayout(jpegSliderLayout);
jpegLayout->addStretch();
connect(mJpegQualitySlider, &QSlider::valueChanged, this, &ImageExportDialog::onJpegQualityChanged);
connect(mJpegQualitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mJpegQualitySlider, &QSlider::setValue);
// PNG options
mPngOptionsWidget = new QWidget(this);
QVBoxLayout* pngLayout = new QVBoxLayout(mPngOptionsWidget);
pngLayout->setContentsMargins(0, 0, 0, 0);
QLabel* pngCompressionLabel = new QLabel("Compression (0=fast, 9=best):", mPngOptionsWidget);
QHBoxLayout* pngSliderLayout = new QHBoxLayout();
mPngCompressionSlider = new QSlider(Qt::Horizontal, mPngOptionsWidget);
mPngCompressionSlider->setRange(0, 9);
mPngCompressionSlider->setValue(Settings::instance().imagePngCompression());
mPngCompressionSpinBox = new QSpinBox(mPngOptionsWidget);
mPngCompressionSpinBox->setRange(0, 9);
mPngCompressionSpinBox->setValue(Settings::instance().imagePngCompression());
pngSliderLayout->addWidget(mPngCompressionSlider);
pngSliderLayout->addWidget(mPngCompressionSpinBox);
pngLayout->addWidget(pngCompressionLabel);
pngLayout->addLayout(pngSliderLayout);
pngLayout->addStretch();
connect(mPngCompressionSlider, &QSlider::valueChanged, this, &ImageExportDialog::onPngCompressionChanged);
connect(mPngCompressionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
mPngCompressionSlider, &QSlider::setValue);
// No options widget (for formats without settings)
mNoOptionsWidget = new QWidget(this);
QVBoxLayout* noOptionsLayout = new QVBoxLayout(mNoOptionsWidget);
noOptionsLayout->setContentsMargins(0, 0, 0, 0);
QLabel* noOptionsLabel = new QLabel("No additional options for this format.", mNoOptionsWidget);
noOptionsLabel->setStyleSheet("color: #808080;");
noOptionsLayout->addWidget(noOptionsLabel);
noOptionsLayout->addStretch();
// Add to stacked widget
mOptionsStack->addWidget(mJpegOptionsWidget); // Index 0
mOptionsStack->addWidget(mPngOptionsWidget); // Index 1
mOptionsStack->addWidget(mNoOptionsWidget); // Index 2
// Add stacked widget to options container
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
if (optionsLayout) {
optionsLayout->addWidget(mOptionsStack);
}
// Show options for current format
showOptionsForFormat(formatCombo()->currentText());
}
void ImageExportDialog::updatePreview()
{
if (mImage.isNull()) {
mPreviewLabel->setText("No image loaded");
return;
}
// Scale image to fit preview area while maintaining aspect ratio
QSize previewSize = mPreviewLabel->size();
QPixmap pixmap = QPixmap::fromImage(mImage.scaled(
previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
mPreviewLabel->setPixmap(pixmap);
updateFileSizeEstimate();
}
QStringList ImageExportDialog::supportedFormats() const
{
return {"png", "jpg", "bmp", "tiff", "tga"};
}
void ImageExportDialog::setImage(const QImage& image, const QString& suggestedName)
{
mImage = image;
mSuggestedName = suggestedName;
// Update info label with image details
QString info = QString("%1 x %2, %3-bit")
.arg(image.width())
.arg(image.height())
.arg(image.depth());
// Find the info label (it's in the base class)
QList<QLabel*> labels = findChildren<QLabel*>();
for (QLabel* label : labels) {
if (label->objectName().isEmpty() && label != mPreviewLabel) {
// This is likely the info label
label->setText(info);
break;
}
}
// Call base class to set output path
setData(QByteArray(), suggestedName);
updatePreview();
}
void ImageExportDialog::onFormatChanged(const QString& format)
{
ExportDialog::onFormatChanged(format);
showOptionsForFormat(format);
updateFileSizeEstimate();
}
void ImageExportDialog::showOptionsForFormat(const QString& format)
{
QString fmt = format.toLower();
if (fmt == "jpg" || fmt == "jpeg") {
mOptionsStack->setCurrentWidget(mJpegOptionsWidget);
} else if (fmt == "png") {
mOptionsStack->setCurrentWidget(mPngOptionsWidget);
} else {
mOptionsStack->setCurrentWidget(mNoOptionsWidget);
}
}
int ImageExportDialog::jpegQuality() const
{
return mJpegQualitySlider ? mJpegQualitySlider->value() : 90;
}
int ImageExportDialog::pngCompression() const
{
return mPngCompressionSlider ? mPngCompressionSlider->value() : 6;
}
void ImageExportDialog::onJpegQualityChanged(int value)
{
if (mJpegQualitySpinBox) {
mJpegQualitySpinBox->blockSignals(true);
mJpegQualitySpinBox->setValue(value);
mJpegQualitySpinBox->blockSignals(false);
}
updateFileSizeEstimate();
}
void ImageExportDialog::onPngCompressionChanged(int value)
{
if (mPngCompressionSpinBox) {
mPngCompressionSpinBox->blockSignals(true);
mPngCompressionSpinBox->setValue(value);
mPngCompressionSpinBox->blockSignals(false);
}
updateFileSizeEstimate();
}
void ImageExportDialog::updateFileSizeEstimate()
{
if (mImage.isNull()) return;
QString format = selectedFormat();
qint64 estimatedSize = 0;
// Rough size estimates
int pixelCount = mImage.width() * mImage.height();
if (format == "jpg" || format == "jpeg") {
// JPEG: roughly quality/100 * pixelCount * 0.3 bytes
double qualityFactor = jpegQuality() / 100.0;
estimatedSize = static_cast<qint64>(pixelCount * 0.3 * qualityFactor);
} else if (format == "png") {
// PNG: depends on compression and image content, rough estimate
double compressionFactor = 1.0 - (pngCompression() * 0.08);
estimatedSize = static_cast<qint64>(pixelCount * 3 * compressionFactor * 0.5);
} else if (format == "bmp") {
// BMP: uncompressed, 3-4 bytes per pixel + header
estimatedSize = pixelCount * 4 + 54;
} else if (format == "tga") {
// TGA: uncompressed, 4 bytes per pixel + header
estimatedSize = pixelCount * 4 + 18;
} else if (format == "tiff") {
// TIFF: similar to PNG
estimatedSize = static_cast<qint64>(pixelCount * 3 * 0.5);
}
// Format size string
QString sizeStr;
if (estimatedSize > 1024 * 1024) {
sizeStr = QString("Est: ~%1 MB").arg(estimatedSize / (1024.0 * 1024.0), 0, 'f', 1);
} else if (estimatedSize > 1024) {
sizeStr = QString("Est: ~%1 KB").arg(estimatedSize / 1024.0, 0, 'f', 0);
} else {
sizeStr = QString("Est: ~%1 bytes").arg(estimatedSize);
}
// Update info label
QString info = QString("%1 x %2, %3-bit\n%4")
.arg(mImage.width())
.arg(mImage.height())
.arg(mImage.depth())
.arg(sizeStr);
QList<QLabel*> labels = findChildren<QLabel*>();
for (QLabel* label : labels) {
if (label->objectName().isEmpty() && label != mPreviewLabel &&
!label->text().contains("Quality") && !label->text().contains("Compression") &&
!label->text().contains("Format") && !label->text().contains("Output") &&
!label->text().contains("No additional")) {
label->setText(info);
break;
}
}
}

View File

@ -1,68 +0,0 @@
#ifndef IMAGEEXPORTDIALOG_H
#define IMAGEEXPORTDIALOG_H
#include "exportdialog.h"
#include <QImage>
class QSlider;
class QSpinBox;
class QStackedWidget;
/**
* @brief Export dialog for images with preview and format-specific options.
*
* Shows a thumbnail preview of the image and provides quality/compression
* settings for different output formats (PNG, JPEG, TGA, BMP, TIFF).
*/
class ImageExportDialog : public ExportDialog
{
Q_OBJECT
public:
explicit ImageExportDialog(QWidget *parent = nullptr);
// Set image directly (already decoded)
void setImage(const QImage& image, const QString& suggestedName);
// Get configured image for export
QImage exportImage() const { return mImage; }
// Format-specific options
int jpegQuality() const;
int pngCompression() const;
protected:
void setupPreview() override;
void updatePreview() override;
QStringList supportedFormats() const override;
void onFormatChanged(const QString& format) override;
private slots:
void onJpegQualityChanged(int value);
void onPngCompressionChanged(int value);
private:
void updateFileSizeEstimate();
void showOptionsForFormat(const QString& format);
QImage mImage;
QLabel* mPreviewLabel;
// Format-specific option widgets
QStackedWidget* mOptionsStack;
// JPEG options
QWidget* mJpegOptionsWidget;
QSlider* mJpegQualitySlider;
QSpinBox* mJpegQualitySpinBox;
// PNG options
QWidget* mPngOptionsWidget;
QSlider* mPngCompressionSlider;
QSpinBox* mPngCompressionSpinBox;
// No options widget (for TGA, BMP, TIFF)
QWidget* mNoOptionsWidget;
};
#endif // IMAGEEXPORTDIALOG_H

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +0,0 @@
#ifndef IMAGEPREVIEWWIDGET_H
#define IMAGEPREVIEWWIDGET_H
#include <QWidget>
#include <QLabel>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QPixmap>
#include <QImage>
#include <QMouseEvent>
#include <QTreeWidget>
#include <QSplitter>
class ImagePreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit ImagePreviewWidget(QWidget *parent = nullptr);
~ImagePreviewWidget() = default;
// Load image from raw bytes (TGA, PNG, etc.)
bool loadFromData(const QByteArray &data, const QString &format = QString());
// Load image from file path
bool loadFromFile(const QString &path);
// 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;
// Export current displayed image as PNG for debugging
bool exportDebugImage(const QString &suffix = QString());
protected:
// Mouse events for drag-to-pan
bool eventFilter(QObject *obj, QEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
QLabel *mImageLabel;
QLabel *mInfoLabel;
QScrollArea *mScrollArea;
QTreeWidget *mMetadataTree;
QString mFilename;
QSize mImageSize;
// Drag-to-pan state
bool mDragging;
QPoint mLastDragPos;
// Zoom state
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);
// Load RCB pixel block (Avatar g4rc texture format)
QImage loadRCBPixel(const QByteArray &data);
// Load raw DXT1/DXT5 data with auto-detected dimensions
QImage loadRawDXT(const QByteArray &data, bool tryDXT5First = false);
// Load DDS (DirectDraw Surface) texture
QImage loadDDS(const QByteArray &data);
// Detected image info
QString mDetectedFormat;
int mBitsPerPixel;
QString mCompression;
};
#endif // IMAGEPREVIEWWIDGET_H

28
app/imagewidget.cpp Normal file
View File

@ -0,0 +1,28 @@
#include "imagewidget.h"
#include "ui_imagewidget.h"
ImageWidget::ImageWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::ImageWidget)
{
ui->setupUi(this);
}
ImageWidget::~ImageWidget()
{
delete ui;
}
void ImageWidget::SetImage(std::shared_ptr<Image> aImage)
{
mImage = aImage;
ui->lineEdit_Name->setText(aImage->name);
ui->lineEdit_Role->setText(aImage->materialName);
ui->comboBox_Compression->setCurrentIndex(aImage->compression);
}
std::shared_ptr<Image> ImageWidget::GetImage()
{
return mImage;
}

32
app/imagewidget.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef IMAGEWIDGET_H
#define IMAGEWIDGET_H
#include "enums.h"
#include "dds_structs.h"
#include "d3dbsp_structs.h"
#include "asset_structs.h"
#include "ipak_structs.h"
#include <QWidget>
namespace Ui {
class ImageWidget;
}
class ImageWidget : public QWidget
{
Q_OBJECT
public:
explicit ImageWidget(QWidget *parent = nullptr);
~ImageWidget();
void SetImage(std::shared_ptr<Image> aImage);
std::shared_ptr<Image> GetImage();
private:
std::shared_ptr<Image> mImage;
Ui::ImageWidget *ui;
};
#endif // IMAGEWIDGET_H

144
app/imagewidget.ui Normal file
View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ImageWidget</class>
<widget class="QWidget" name="ImageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>589</width>
<height>422</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Image Role:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_Role"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_Name"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Compression:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_Compression">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>DXT1</string>
</property>
</item>
<item>
<property name="text">
<string>DXT3</string>
</property>
</item>
<item>
<property name="text">
<string>DXT5</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_Preview">
<property name="minimumSize">
<size>
<width>250</width>
<height>250</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>250</width>
<height>250</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

98
app/iwiviewer.cpp Normal file
View File

@ -0,0 +1,98 @@
#include "iwiviewer.h"
#include "enums.h"
#include "ui_iwiviewer.h"
IWIViewer::IWIViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::IWIViewer)
{
ui->setupUi(this);
}
IWIViewer::~IWIViewer()
{
delete ui;
}
void IWIViewer::SetIWIFile(std::shared_ptr<IWIFile> aIWIFile) {
mIWIFile.swap(aIWIFile);
ui->label_Title->setText(mIWIFile->fileStem + ".iwi");
// If youre using Qt and want a QString:
QString magicStr = QString::fromLatin1(mIWIFile->header.Magic, 3);
ui->lineEdit_Magic->setText(magicStr);
ui->spinBox_Version->setValue(mIWIFile->header.Version);
ui->spinBox_Depth->setValue(mIWIFile->info.Depth);
QString formatStr = "";
switch (mIWIFile->info.Format) {
case IWI_FORMAT_ARGB32:
formatStr = "ARGB32";
break;
case IWI_FORMAT_RGB24:
formatStr = "RGB24";
break;
case IWI_FORMAT_GA16:
formatStr = "GA16";
break;
case IWI_FORMAT_A8:
formatStr = "A8";
break;
case IWI_FORMAT_DXT1:
formatStr = "DXT1";
break;
case IWI_FORMAT_DXT3:
formatStr = "DXT3";
break;
case IWI_FORMAT_DXT5:
formatStr = "DXT5";
break;
}
ui->lineEdit_Format->setText(formatStr);
ui->spinBox_Height->setValue(mIWIFile->info.Height);
ui->spinBox_Width->setValue(mIWIFile->info.Width);
ui->spinBox_Usage->setValue(mIWIFile->info.Usage);
ui->comboBox_Mipmap->clear();
for (auto mipmap : mIWIFile->mipmaps) {
ui->comboBox_Mipmap->addItem(QString::number(mipmap.offset));
}
connect(ui->comboBox_Mipmap, &QComboBox::currentIndexChanged, this, &IWIViewer::MipmapIndexChanged);
if (!mIWIFile->mipmaps.empty()) {
MipmapIndexChanged(0);
}
}
void IWIViewer::MipmapIndexChanged(int aMipmapIndex) {
auto mipmaps = mIWIFile->mipmaps;
if (aMipmapIndex == -1) { return; }
auto mipmap = mipmaps[aMipmapIndex];
ui->spinBox_MipmapSize->setValue(mipmap.size);
ui->spinBox_MipmapOffset->setValue(mipmap.offset);
const unsigned char* imageData = reinterpret_cast<const unsigned char*>(mipmap.data.constData());
QImage image(reinterpret_cast<const uchar*>(imageData),
mIWIFile->info.Width, mIWIFile->info.Height,
QImage::Format_RGBA8888);
if (image.isNull()) {
qDebug() << "Error: QImage creation failed!";
return;
}
// Convert to QPixmap
QPixmap pixmap = QPixmap::fromImage(image);
if (pixmap.isNull()) {
qDebug() << "Error: QPixmap conversion failed!";
return;
}
// Scale and display
pixmap = pixmap.scaled(ui->label_Image->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
ui->label_Image->setPixmap(pixmap);
}

27
app/iwiviewer.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef IWIVIEWER_H
#define IWIVIEWER_H
#include "iwifile.h"
#include <QWidget>
namespace Ui {
class IWIViewer;
}
class IWIViewer : public QWidget
{
Q_OBJECT
public:
explicit IWIViewer(QWidget *parent = nullptr);
~IWIViewer();
void MipmapIndexChanged(int aMipmapIndex);
void SetIWIFile(std::shared_ptr<IWIFile> aIWIFile);
private:
Ui::IWIViewer *ui;
std::shared_ptr<IWIFile> mIWIFile;
};
#endif // IWIVIEWER_H

548
app/iwiviewer.ui Normal file
View File

@ -0,0 +1,548 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>IWIViewer</class>
<widget class="QWidget" name="IWIViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1246</width>
<height>909</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>IWI File 0</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox_17">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Header</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Magic:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_Magic">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_26">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_Version">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="suffix">
<string> B</string>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_14">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Info</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_40">
<item>
<widget class="QLabel" name="label_38">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Depth:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_Depth">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_41">
<item>
<widget class="QLabel" name="label_35">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Format</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_Format">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_43">
<item>
<widget class="QLabel" name="label_39">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Height:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_Height">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_44">
<item>
<widget class="QLabel" name="label_40">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Width:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_Width">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_45">
<item>
<widget class="QLabel" name="label_41">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Usage</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_Usage">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_21">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Mipmaps</string>
</property>
<layout class="QFormLayout" name="formLayout_5">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_25">
<item>
<spacer name="horizontalSpacer_4">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_32">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Select Mipmap:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_Mipmap">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_55">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Size: </string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_MipmapSize">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_56">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Offset:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_MipmapOffset">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupBox_26">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Preview</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_22">
<item>
<spacer name="verticalSpacer_11">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_Image">
<property name="minimumSize">
<size>
<width>600</width>
<height>600</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>600</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">border: 2px solid white;</string>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_12">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_13">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>116</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,271 +0,0 @@
#include "listpreviewwidget.h"
#include <QFileInfo>
ListPreviewWidget::ListPreviewWidget(QWidget *parent)
: QWidget(parent)
{
setupUi();
// Connect to theme changes
connect(&Settings::instance(), &Settings::themeChanged,
this, &ListPreviewWidget::applyTheme);
applyTheme(Settings::instance().theme());
}
void ListPreviewWidget::setupUi()
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Info label at top
mInfoLabel = new QLabel(this);
mInfoLabel->setContentsMargins(8, 4, 8, 4);
mainLayout->addWidget(mInfoLabel);
// Splitter for table and metadata
mSplitter = new QSplitter(Qt::Horizontal, this);
mainLayout->addWidget(mSplitter, 1);
// Table widget for list items
mTableWidget = new QTableWidget(this);
mTableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
mTableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
mTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
mTableWidget->horizontalHeader()->setStretchLastSection(true);
mTableWidget->verticalHeader()->setVisible(false);
mTableWidget->setAlternatingRowColors(true);
mSplitter->addWidget(mTableWidget);
// Metadata tree on the right
mMetadataTree = new QTreeWidget(this);
mMetadataTree->setHeaderLabels({"Property", "Value"});
mMetadataTree->setColumnCount(2);
mMetadataTree->header()->setStretchLastSection(true);
mMetadataTree->setMinimumWidth(200);
mSplitter->addWidget(mMetadataTree);
// Set initial splitter sizes (70% table, 30% metadata)
mSplitter->setSizes({700, 300});
// Connect selection change
connect(mTableWidget, &QTableWidget::itemSelectionChanged,
this, &ListPreviewWidget::onItemSelectionChanged);
}
void ListPreviewWidget::setListData(const QVariantList &items, const QString &listName)
{
mItems = items;
mListName = listName;
// Update info label
mInfoLabel->setText(QString("%1 | %2 items")
.arg(listName.isEmpty() ? "List" : listName)
.arg(items.size()));
populateTable(items);
}
void ListPreviewWidget::populateTable(const QVariantList &items)
{
mTableWidget->clear();
mTableWidget->setRowCount(0);
if (items.isEmpty()) {
mTableWidget->setColumnCount(1);
mTableWidget->setHorizontalHeaderLabels({"(empty)"});
return;
}
// Collect all unique keys from all items to determine columns
QStringList columns;
for (const QVariant &item : items) {
if (item.typeId() == QMetaType::QVariantMap) {
const QVariantMap map = item.toMap();
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
const QString &key = it.key();
// Skip internal fields and binary data
if (key.startsWith('_') && key != "_name")
continue;
if (it.value().typeId() == QMetaType::QByteArray)
continue;
if (!columns.contains(key))
columns.append(key);
}
}
}
// If items are simple values (not maps), use a single column
if (columns.isEmpty()) {
columns.append("Value");
}
// Setup columns
mTableWidget->setColumnCount(columns.size());
mTableWidget->setHorizontalHeaderLabels(columns);
// Add rows
mTableWidget->setRowCount(items.size());
for (int row = 0; row < items.size(); ++row) {
const QVariant &item = items[row];
if (item.typeId() == QMetaType::QVariantMap) {
const QVariantMap map = item.toMap();
for (int col = 0; col < columns.size(); ++col) {
const QString &key = columns[col];
QVariant val = map.value(key);
QString displayText;
if (val.typeId() == QMetaType::QVariantMap) {
displayText = QString("{%1 fields}").arg(val.toMap().size());
} else if (val.typeId() == QMetaType::QVariantList) {
displayText = QString("[%1 items]").arg(val.toList().size());
} else if (val.typeId() == QMetaType::QByteArray) {
displayText = QString("<%1 bytes>").arg(val.toByteArray().size());
} else {
displayText = val.toString();
}
auto *tableItem = new QTableWidgetItem(displayText);
mTableWidget->setItem(row, col, tableItem);
}
} else {
// Simple value
auto *tableItem = new QTableWidgetItem(item.toString());
mTableWidget->setItem(row, 0, tableItem);
}
}
// Resize columns to content
mTableWidget->resizeColumnsToContents();
// Clear metadata tree initially
mMetadataTree->clear();
}
void ListPreviewWidget::setMetadata(const QVariantMap &metadata)
{
// This can be used to set overall list metadata
updateMetadataTree(metadata);
}
void ListPreviewWidget::clear()
{
mItems.clear();
mListName.clear();
mTableWidget->clear();
mTableWidget->setRowCount(0);
mTableWidget->setColumnCount(0);
mMetadataTree->clear();
mInfoLabel->setText("");
}
void ListPreviewWidget::onItemSelectionChanged()
{
int row = mTableWidget->currentRow();
if (row >= 0 && row < mItems.size()) {
const QVariant &item = mItems[row];
if (item.typeId() == QMetaType::QVariantMap) {
updateMetadataTree(item.toMap());
} else {
QVariantMap simple;
simple["value"] = item;
simple["index"] = row;
updateMetadataTree(simple);
}
}
}
void ListPreviewWidget::updateMetadataTree(const QVariantMap &item)
{
mMetadataTree->clear();
for (auto it = item.constBegin(); it != item.constEnd(); ++it) {
const QString &key = it.key();
const QVariant &val = it.value();
auto *treeItem = new QTreeWidgetItem(mMetadataTree);
treeItem->setText(0, key);
if (val.typeId() == QMetaType::QVariantMap) {
treeItem->setText(1, QString("{%1 fields}").arg(val.toMap().size()));
// Add nested items
const QVariantMap nested = val.toMap();
for (auto nit = nested.constBegin(); nit != nested.constEnd(); ++nit) {
auto *child = new QTreeWidgetItem(treeItem);
child->setText(0, nit.key());
if (nit.value().typeId() == QMetaType::QByteArray) {
child->setText(1, QString("<%1 bytes>").arg(nit.value().toByteArray().size()));
} else {
child->setText(1, nit.value().toString());
}
}
} else if (val.typeId() == QMetaType::QVariantList) {
treeItem->setText(1, QString("[%1 items]").arg(val.toList().size()));
} else if (val.typeId() == QMetaType::QByteArray) {
treeItem->setText(1, QString("<%1 bytes>").arg(val.toByteArray().size()));
} else {
treeItem->setText(1, val.toString());
}
}
mMetadataTree->expandAll();
mMetadataTree->resizeColumnToContents(0);
}
void ListPreviewWidget::applyTheme(const Theme &theme)
{
// Style info label
mInfoLabel->setStyleSheet(QString(
"QLabel {"
" background-color: %1;"
" color: %2;"
" border-bottom: 1px solid %3;"
"}"
).arg(theme.panelColor, theme.textColorMuted, theme.borderColor));
// Style table widget
mTableWidget->setStyleSheet(QString(
"QTableWidget {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" gridline-color: %3;"
"}"
"QTableWidget::item {"
" padding: 4px;"
"}"
"QTableWidget::item:selected {"
" background-color: %4;"
"}"
"QTableWidget::item:alternate {"
" background-color: %5;"
"}"
"QHeaderView::section {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" padding: 4px;"
" font-weight: bold;"
"}"
).arg(theme.panelColor, theme.textColor, theme.borderColor,
theme.accentColor, theme.backgroundColor));
// Style metadata tree
mMetadataTree->setStyleSheet(QString(
"QTreeWidget {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
"}"
"QTreeWidget::item:selected {"
" background-color: %4;"
"}"
"QHeaderView::section {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" padding: 4px;"
"}"
).arg(theme.panelColor, theme.textColor, theme.borderColor, theme.accentColor));
}

View File

@ -1,48 +0,0 @@
#ifndef LISTPREVIEWWIDGET_H
#define LISTPREVIEWWIDGET_H
#include <QWidget>
#include <QTableWidget>
#include <QTreeWidget>
#include <QLabel>
#include <QSplitter>
#include <QVBoxLayout>
#include <QHeaderView>
#include "settings.h"
class ListPreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit ListPreviewWidget(QWidget *parent = nullptr);
~ListPreviewWidget() = default;
// Set list data from QVariantList (each item is a QVariantMap with fields)
void setListData(const QVariantList &items, const QString &listName = QString());
// Set metadata for the sidebar
void setMetadata(const QVariantMap &metadata);
// Clear the widget
void clear();
private slots:
void applyTheme(const Theme &theme);
void onItemSelectionChanged();
private:
void setupUi();
void populateTable(const QVariantList &items);
void updateMetadataTree(const QVariantMap &item);
QLabel *mInfoLabel;
QTableWidget *mTableWidget;
QTreeWidget *mMetadataTree;
QSplitter *mSplitter;
QVariantList mItems;
QString mListName;
};
#endif // LISTPREVIEWWIDGET_H

62
app/localstringviewer.cpp Normal file
View File

@ -0,0 +1,62 @@
#include "localstringviewer.h"
#include "ui_localstringviewer.h"
LocalStringViewer::LocalStringViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::LocalStringViewer),
mVersion(),
mConfigPath(),
mFileNotes() {
ui->setupUi(this);
ui->tableWidget_Strings->setColumnCount(2);
ui->tableWidget_Strings->setRowCount(0);
ui->tableWidget_Strings->setColumnWidth(0, 200);
ui->tableWidget_Strings->horizontalHeader()->setStretchLastSection(true);
}
LocalStringViewer::~LocalStringViewer() {
delete ui;
}
void LocalStringViewer::SetVersion(quint32 aVersion) {
mVersion = aVersion;
ui->spinBox_Version->setValue(mVersion);
}
void LocalStringViewer::SetConfigPath(const QString aConfigPath) {
mConfigPath = aConfigPath;
ui->lineEdit_Config->setText(mConfigPath);
}
void LocalStringViewer::SetFileNotes(const QString aFileNotes) {
mFileNotes = aFileNotes;
ui->plainTextEdit_FileNotes->setPlainText(mFileNotes);
}
void LocalStringViewer::AddLocalString(LocalString aLocalString) {
mLocalStrings.append(aLocalString);
ui->tableWidget_Strings->setRowCount(mLocalStrings.size());
ui->groupBox_LocalStrViewer->setTitle(QString("Entries (%1)").arg(mLocalStrings.size()));
QTableWidgetItem *aliasItem = new QTableWidgetItem(aLocalString.alias);
QTableWidgetItem *stringItem = new QTableWidgetItem(aLocalString.string);
ui->tableWidget_Strings->setItem(mLocalStrings.size() - 1, 0, aliasItem);
ui->tableWidget_Strings->setItem(mLocalStrings.size() - 1, 1, stringItem);
}
void LocalStringViewer::SetZoneFile(std::shared_ptr<ZoneFile> aZoneFile) {
mLocalStrings.clear();
ui->tableWidget_Strings->clear();
ui->label_Title->setText(aZoneFile->GetStem().section('.', 0, 0) + ".str");
for (const LocalString &localStr : aZoneFile->GetAssetMap().localStrings) {
AddLocalString(localStr);
}
}

34
app/localstringviewer.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef LOCALSTRINGVIEWER_H
#define LOCALSTRINGVIEWER_H
#include "asset_structs.h"
#include "zonefile.h"
#include <QWidget>
namespace Ui {
class LocalStringViewer;
}
class LocalStringViewer : public QWidget
{
Q_OBJECT
public:
explicit LocalStringViewer(QWidget *parent = nullptr);
~LocalStringViewer();
void SetVersion(quint32 aVersion);
void SetConfigPath(const QString aConfigPath);
void SetFileNotes(const QString aFileNotes);
void AddLocalString(LocalString aLocalString);
void SetZoneFile(std::shared_ptr<ZoneFile> aZoneFile);
private:
Ui::LocalStringViewer *ui;
quint32 mVersion;
QString mConfigPath;
QString mFileNotes;
QVector<LocalString> mLocalStrings;
};
#endif // LOCALSTRINGVIEWER_H

194
app/localstringviewer.ui Normal file
View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LocalStringViewer</class>
<widget class="QWidget" name="LocalStringViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>841</width>
<height>457</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>841</width>
<height>457</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>LocalString File 0</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>325</width>
<height>398</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>325</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Header</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>File Notes:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_Config">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>C:\cod5\cod\cod5\bin\StringEd.cfg</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Config:</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="toolButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_Version">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QPlainTextEdit" name="plainTextEdit_FileNotes">
<property name="placeholderText">
<string>Files notes...</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_LocalStrViewer">
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Entries</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTableWidget" name="tableWidget_Strings">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,472 +1,11 @@
#include "mainwindow.h"
#include "splashscreen.h"
#include "typeregistry.h"
#include "settings.h"
#include "compression.h"
#include "logmanager.h"
// Application metadata
#define APP_NAME "XPlor"
#define APP_VERSION "1.8"
#define APP_ORG_NAME "RedLine Solutions LLC."
#define APP_ORG_DOMAIN "redline.llc"
#include <QApplication>
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QFile>
#include <QFileInfo>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QTextStream>
#include <QThread>
#include <QTimer>
#include <QImage>
#include <QIcon>
#include <iostream>
#ifdef Q_OS_WIN
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include <stdio.h>
static bool g_consoleAllocated = false; // True if we created our own console (need to wait at end)
// Attach to parent console on Windows for CLI mode
static void attachWindowsConsole() {
// Try to attach to parent console (works when launched from cmd.exe)
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
// Redirect stdout
FILE* fp;
freopen_s(&fp, "CONOUT$", "w", stdout);
setvbuf(stdout, NULL, _IONBF, 0);
// Redirect stderr
freopen_s(&fp, "CONOUT$", "w", stderr);
setvbuf(stderr, NULL, _IONBF, 0);
// Redirect stdin
freopen_s(&fp, "CONIN$", "r", stdin);
// Fix C++ streams
std::cout.clear();
std::cerr.clear();
std::cin.clear();
} else {
// No parent console - allocate our own console window
if (AllocConsole()) {
g_consoleAllocated = true;
FILE* fp;
freopen_s(&fp, "CONOUT$", "w", stdout);
setvbuf(stdout, NULL, _IONBF, 0);
freopen_s(&fp, "CONOUT$", "w", stderr);
setvbuf(stderr, NULL, _IONBF, 0);
freopen_s(&fp, "CONIN$", "r", stdin);
std::cout.clear();
std::cerr.clear();
std::cin.clear();
// Set console title
SetConsoleTitleA("XPlor CLI");
}
}
}
#endif
// Convert QVariantMap to JSON recursively
static QJsonValue variantToJson(const QVariant& v) {
if (v.typeId() == QMetaType::QVariantMap) {
QJsonObject obj;
const QVariantMap map = v.toMap();
for (auto it = map.begin(); it != map.end(); ++it) {
obj[it.key()] = variantToJson(it.value());
}
return obj;
} else if (v.typeId() == QMetaType::QVariantList) {
QJsonArray arr;
for (const QVariant& item : v.toList()) {
arr.append(variantToJson(item));
}
return arr;
} else if (v.typeId() == QMetaType::QString) {
return v.toString();
} else if (v.typeId() == QMetaType::Int || v.typeId() == QMetaType::LongLong) {
return v.toLongLong();
} else if (v.typeId() == QMetaType::UInt || v.typeId() == QMetaType::ULongLong) {
return v.toLongLong();
} else if (v.typeId() == QMetaType::Double || v.typeId() == QMetaType::Float) {
return v.toDouble();
} else if (v.typeId() == QMetaType::Bool) {
return v.toBool();
} else if (v.typeId() == QMetaType::QByteArray) {
return QString("0x") + v.toByteArray().toHex();
} else if (v.isNull()) {
return QJsonValue::Null;
}
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;
static void initCliLog() {
QString logPath = QCoreApplication::applicationDirPath() + "/xplor_cli.log";
g_logFile = new QFile(logPath);
g_logFile->open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text);
}
static void cliOut(const QString& msg) {
std::cout << msg.toStdString() << std::flush;
if (g_logFile && g_logFile->isOpen()) {
g_logFile->write(msg.toUtf8());
g_logFile->flush();
}
}
static void cliErr(const QString& msg) {
std::cerr << msg.toStdString() << std::flush;
if (g_logFile && g_logFile->isOpen()) {
g_logFile->write(msg.toUtf8());
g_logFile->flush();
}
}
static int runCli(const QString& filePath, const QString& game, const QString& platform, bool jsonOutput) {
TypeRegistry registry;
const QString definitionsDir = QCoreApplication::applicationDirPath() + "/definitions/";
QDirIterator it(definitionsDir, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
int defCount = 0;
while (it.hasNext()) {
const QString path = it.next();
const QString fileName = QFileInfo(path).fileName();
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
cliErr("[ERROR] Cannot open definition: " + path + "\n");
continue;
}
try {
registry.ingestScript(QString::fromUtf8(f.readAll()), path);
defCount++;
} catch (const std::exception& e) {
cliErr("[ERROR] Loading " + fileName + ": " + e.what() + "\n");
return 1;
}
}
if (defCount == 0) {
cliErr("[ERROR] No definitions found in: " + definitionsDir + "\n");
return 1;
}
if (!jsonOutput) {
cliErr("[INFO] Loaded " + QString::number(defCount) + " definitions\n");
}
QFile inputFile(filePath);
if (!inputFile.open(QIODevice::ReadOnly)) {
cliErr("[ERROR] Cannot open file: " + filePath + "\n");
return 1;
}
const QString rootType = registry.chooseType(&inputFile, filePath);
if (rootType.isEmpty()) {
cliErr("[ERROR] No matching definition for file: " + filePath + "\n");
return 1;
}
if (!jsonOutput) {
cliErr("[INFO] Matched type: " + rootType + "\n");
cliErr("[INFO] Parsing...\n");
}
QVariantMap result;
try {
result = registry.parse(rootType, &inputFile, filePath, nullptr);
} catch (const std::exception& e) {
cliErr("[ERROR] Parse failed: " + QString(e.what()) + "\n");
if (jsonOutput) {
QJsonObject errorObj;
errorObj["success"] = false;
errorObj["error"] = QString(e.what());
errorObj["file"] = filePath;
errorObj["type"] = rootType;
cliOut(QJsonDocument(errorObj).toJson(QJsonDocument::Compact) + "\n");
}
return 1;
}
if (jsonOutput) {
QJsonObject output;
output["success"] = true;
output["file"] = filePath;
output["type"] = rootType;
output["data"] = variantToJson(result).toObject();
cliOut(QJsonDocument(output).toJson(QJsonDocument::Indented) + "\n");
} else {
cliErr("[SUCCESS] Parsed " + filePath + " as " + rootType + "\n");
if (result.contains("asset_count")) {
cliErr("[INFO] Asset count: " + result["asset_count"].toString() + "\n");
}
if (result.contains("parsed_assets")) {
QVariantList assets = result["parsed_assets"].toList();
QHash<QString, int> typeCounts;
for (const QVariant& a : assets) {
QVariantMap am = a.toMap();
QString t = am.value("_type", "unknown").toString();
typeCounts[t]++;
}
cliErr("[INFO] Parsed assets:\n");
for (auto it = typeCounts.begin(); it != typeCounts.end(); ++it) {
cliErr(" " + it.key() + ": " + QString::number(it.value()) + "\n");
}
}
cliOut("OK\n");
}
return 0;
}
int main(int argc, char *argv[])
{
bool cliMode = false;
for (int i = 1; i < argc; i++) {
QString arg(argv[i]);
if (arg == "--cli" || arg == "--parse" || arg == "-p" || arg == "--json") {
cliMode = true;
break;
}
}
if (cliMode) {
#ifdef Q_OS_WIN
attachWindowsConsole();
#endif
QCoreApplication app(argc, argv);
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 - 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 parsed data as JSON");
parser.addOption(jsonOption);
QCommandLineOption gameOption(QStringList() << "game" << "g", "Game identifier (e.g., COD4, COD5, MW2)", "game", "COD4");
parser.addOption(gameOption);
QCommandLineOption platformOption(QStringList() << "platform" << "t", "Target platform (PC, Xbox360, PS3)", "platform", "PC");
parser.addOption(platformOption);
parser.addPositionalArgument("file", "Binary file to parse (e.g., FastFile, archive)");
parser.process(app);
const QStringList args = parser.positionalArguments();
if (args.isEmpty()) {
cliErr("Error: No input file specified\n\n");
cliErr(parser.helpText() + "\n");
return 1;
}
int result = runCli(args.first(), parser.value(gameOption), parser.value(platformOption),
parser.isSet(jsonOption));
#ifdef Q_OS_WIN
// If we allocated our own console window, wait for user input before closing
if (g_consoleAllocated) {
cliErr("\nPress Enter to exit...");
std::cin.get();
}
#endif
return result;
}
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);
}
// Initialize QuickBMS path from settings
QString quickBmsPath = Settings::instance().quickBmsPath();
if (!quickBmsPath.isEmpty()) {
Compression::setQuickBmsPath(quickBmsPath);
}
// 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();
LogManager::instance().addEntry(QString("[INIT] Loading %1 definition files...").arg(total));
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)) {
LogManager::instance().addError(QString("[DEF] Failed to open: %1").arg(fileName));
defResults.append({path, fileName, false, "Failed to open file"});
} else {
try {
registry.ingestScript(QString::fromUtf8(f.readAll()), path);
LogManager::instance().addEntry(QString("[DEF] Loaded: %1").arg(fileName));
defResults.append({path, fileName, true, QString()});
} catch (const std::exception& e) {
LogManager::instance().addError(QString("[DEF] Error in %1: %2").arg(fileName).arg(e.what()));
defResults.append({path, fileName, false, QString::fromUtf8(e.what())});
}
}
loaded++;
}
int successCount = std::count_if(defResults.begin(), defResults.end(),
[](const DefinitionLoadResult& r) { return r.success; });
LogManager::instance().addEntry(QString("[INIT] Loaded %1/%2 definitions successfully").arg(successCount).arg(total));
LogManager::instance().addLine();
splash.setStatus("Creating main window...");
splash.setProgress(85, 100);
a.processEvents();
MainWindow w;
// 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);
// Check for file argument to open (GUI mode)
QStringList args = QCoreApplication::arguments();
for (int i = 1; i < args.size(); ++i) {
const QString& arg = args[i];
// Skip options (start with -)
if (arg.startsWith("-")) continue;
// Try to open as file
if (QFileInfo::exists(arg)) {
QTimer::singleShot(100, &w, [&w, arg]() {
w.openFile(arg);
});
break; // Only open first file
}
}
w.show();
return a.exec();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,22 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "d3dbsp_structs.h"
#include "asset_structs.h"
#include "xtreewidget.h"
#include <QMainWindow>
#include <QFrame>
#include "typeregistry.h"
#include "treebuilder.h"
#include "settings.h"
struct DefinitionLoadResult {
QString filePath;
QString fileName;
bool success;
QString errorMessage;
};
class XTreeWidget;
class XTreeWidgetItem;
class QPlainTextEdit;
class QProgressBar;
class QUndoStack;
#include <QFileDialog>
#include <QStandardPaths>
#include <QMessageBox>
#include <QDebug>
#include <QTableWidgetItem>
#include <QTreeWidgetItem>
#include <QDockWidget>
#include <QPlainTextEdit>
#include <QMimeData>
#include <QProgressBar>
#include <windows.h>
QT_BEGIN_NAMESPACE
namespace Ui {
@ -34,32 +31,25 @@ class MainWindow : public QMainWindow
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void LoadDefinitions(); // For reparse
void setTypeRegistry(TypeRegistry&& registry, const QVector<DefinitionLoadResult>& results);
void LoadTreeCategories();
void Reset();
bool openFile(const QString& filePath); // Open and parse a file
const QVector<DefinitionLoadResult>& definitionResults() const { return mDefinitionResults; }
const TypeRegistry& typeRegistry() const { return mTypeRegistry; }
TreeBuilder& treeBuilder() { return mTreeBuilder; }
// Undo/Redo support
QUndoStack* undoStack() const { return mUndoStack; }
void pushFieldEdit(int journalId, const QString& fieldName,
const QVariant& oldValue, const QVariant& newValue);
private slots:
bool OpenFastFile(const QString aFastFilePath);
bool OpenFastFile();
bool OpenZoneFile(const QString aZoneFilePath, bool fromFF = false);
bool OpenZoneFile();
int LoadFile_D3DBSP(const QString aFilePath);
int LoadFile_IPAK(const QString aFilePath);
int LoadFile_XSUB(const QString aFilePath);
int LoadFile_IWI(const QString aFilePath);
int LoadFile_DDS(const QString aFilePath);
int LoadFile_DDSFiles(const QStringList aFilePaths);
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);
public:
// Save functionality for write-back
bool saveTab(QWidget* tab);
bool saveTabAs(QWidget* tab);
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
@ -69,49 +59,21 @@ protected:
private:
Ui::MainWindow *ui;
QMap<QString, int> mTypeMap;
QStringList mTypeOrder;
quint32 mTagCount;
quint32 mRecordCount;
QMap<QString, QString> mRawFileMap;
QMap<QString, Image> mImageMap;
QMap<QString, QTreeWidgetItem*> mTreeMap;
QMap<QString, QVector<QPair<QString, QString>>> mStrTableMap;
XTreeWidget *mTreeWidget;
QPlainTextEdit *mLogWidget;
QProgressBar *mProgressBar;
QFrame *mRibbon;
TypeRegistry mTypeRegistry;
TreeBuilder mTreeBuilder;
QStringList mOpenedFilePaths; // Track opened files for reparsing
QVector<DefinitionLoadResult> mDefinitionResults;
// Undo/Redo support
QUndoStack *mUndoStack;
// Actions - File menu
QAction *actionNew;
QAction *actionOpen;
QAction *actionOpenFolder;
QAction *actionSave;
QAction *actionSaveAs;
// Actions - Edit menu
QAction *actionUndo;
QAction *actionRedo;
QAction *actionCut;
QAction *actionCopy;
QAction *actionPaste;
QAction *actionRename;
QAction *actionDelete;
QAction *actionFind;
QAction *actionClearUndoHistory;
QAction *actionPreferences;
// Actions - Tools menu
QAction *actionRunTests;
QAction *actionViewDefinitions;
// Actions - Help menu
QAction *actionAbout;
QAction *actionCheckForUpdates;
QAction *actionOpenMaintenanceTool;
QAction *actionReportIssue;
// Actions - Toolbar
QAction *actionReparse;
quint32 mBSPVersion;
quint32 mDiskLumpCount;
QVector<quint32> mDiskLumpOrder;
QMap<quint32, Lump> mLumps;
};
#endif // MAINWINDOW_H

View File

@ -1,93 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1579</width>
<height>857</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>550</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>XPlor</string>
</property>
<property name="styleSheet">
<string notr="true">QMainWindow {
}</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>-1</number>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1579</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="MenuDef">
<property name="title">
<string>File</string>
</property>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
<string>Tools</string>
</property>
</widget>
<addaction name="MenuDef"/>
<addaction name="menuEdit"/>
<addaction name="menuTools"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<resources/>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1579</width>
<height>857</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>550</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>XPlor</string>
</property>
<property name="styleSheet">
<string notr="true">QMainWindow {
}</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>-1</number>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1579</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<widget class="QMenu" name="menuRecent">
<property name="title">
<string>Recent...</string>
</property>
</widget>
<widget class="QMenu" name="menuImport">
<property name="title">
<string>Import...</string>
</property>
</widget>
<addaction name="actionNew_File_2"/>
<addaction name="actionNew_Fast_File"/>
<addaction name="actionNew_Zone_File"/>
<addaction name="separator"/>
<addaction name="actionOpen_Fast_File"/>
<addaction name="actionOpen_Zone_File"/>
<addaction name="actionOpen_Folder"/>
<addaction name="menuImport"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionSave_As"/>
<addaction name="separator"/>
<addaction name="menuRecent"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
<widget class="QMenu" name="menuUndo_History">
<property name="title">
<string>Undo History...</string>
</property>
</widget>
<widget class="QMenu" name="menuRedo_History">
<property name="title">
<string>Redo History...</string>
</property>
</widget>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
<addaction name="separator"/>
<addaction name="actionCut"/>
<addaction name="actionCopy"/>
<addaction name="actionPaste"/>
<addaction name="actionRename"/>
<addaction name="actionDelete"/>
<addaction name="separator"/>
<addaction name="actionFind_2"/>
<addaction name="actionEdit_Value"/>
<addaction name="actionEdit_as_Hex"/>
<addaction name="separator"/>
<addaction name="menuUndo_History"/>
<addaction name="menuRedo_History"/>
<addaction name="actionClear_Undo_History"/>
<addaction name="separator"/>
<addaction name="actionPreferences"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionCheck_for_Updates"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuEdit"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="actionNew_File_2">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_NewFile.png</normaloff>:/icons/icons/Icon_NewFile.png</iconset>
</property>
<property name="text">
<string>New</string>
</property>
</action>
<action name="actionNew_Fast_File">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_NewFile.png</normaloff>:/icons/icons/Icon_NewFile.png</iconset>
</property>
<property name="text">
<string>New Fast File</string>
</property>
</action>
<action name="actionNew_Zone_File">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_NewFile.png</normaloff>:/icons/icons/Icon_NewFile.png</iconset>
</property>
<property name="text">
<string>New Zone File</string>
</property>
</action>
<action name="actionOpen_Fast_File">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_OpenFile.png</normaloff>:/icons/icons/Icon_OpenFile.png</iconset>
</property>
<property name="text">
<string>Open Fast File</string>
</property>
</action>
<action name="actionOpen_Zone_File">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_OpenFile.png</normaloff>:/icons/icons/Icon_OpenFile.png</iconset>
</property>
<property name="text">
<string>Open Zone File</string>
</property>
</action>
<action name="actionOpen_Folder">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_OpenFile.png</normaloff>:/icons/icons/Icon_OpenFile.png</iconset>
</property>
<property name="text">
<string>Open Folder</string>
</property>
</action>
<action name="actionSave">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_Save.png</normaloff>:/icons/icons/Icon_Save.png</iconset>
</property>
<property name="text">
<string>Save</string>
</property>
</action>
<action name="actionSave_As">
<property name="text">
<string>Save As</string>
</property>
</action>
<action name="actione">
<property name="text">
<string>e</string>
</property>
</action>
<action name="actionFile">
<property name="text">
<string>File</string>
</property>
</action>
<action name="actionFolder">
<property name="text">
<string>Folder</string>
</property>
</action>
<action name="actionNew_File">
<property name="text">
<string>New File</string>
</property>
</action>
<action name="actionNew_Fast_File_2">
<property name="text">
<string>New Fast File</string>
</property>
</action>
<action name="actionNew_Zone_File_2">
<property name="text">
<string>New Zone File</string>
</property>
</action>
<action name="actionFrom_Clipboard">
<property name="text">
<string>From Clipboard</string>
</property>
</action>
<action name="actionMaterial">
<property name="text">
<string>Material</string>
</property>
</action>
<action name="actionSound">
<property name="text">
<string>Sound</string>
</property>
</action>
<action name="actionUndo">
<property name="text">
<string>Undo</string>
</property>
</action>
<action name="actionRedo">
<property name="text">
<string>Redo</string>
</property>
</action>
<action name="actionCut">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_Cut.png</normaloff>:/icons/icons/Icon_Cut.png</iconset>
</property>
<property name="text">
<string>Cut</string>
</property>
</action>
<action name="actionCopy">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_Copy.png</normaloff>:/icons/icons/Icon_Copy.png</iconset>
</property>
<property name="text">
<string>Copy</string>
</property>
</action>
<action name="actionPaste">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_Paste.png</normaloff>:/icons/icons/Icon_Paste.png</iconset>
</property>
<property name="text">
<string>Paste</string>
</property>
</action>
<action name="actionRename">
<property name="text">
<string>Rename</string>
</property>
</action>
<action name="actionEdit_Value">
<property name="text">
<string>Edit Value</string>
</property>
</action>
<action name="actionEdit_as_Hex">
<property name="text">
<string>Edit as Hex</string>
</property>
</action>
<action name="actionDelete">
<property name="text">
<string>Delete</string>
</property>
</action>
<action name="actiond">
<property name="text">
<string>d</string>
</property>
</action>
<action name="actiond_2">
<property name="text">
<string>d</string>
</property>
</action>
<action name="actionClear_Undo_History">
<property name="text">
<string>Clear Undo History</string>
</property>
</action>
<action name="actionFind">
<property name="text">
<string>Find</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About</string>
</property>
</action>
<action name="actionChange_Icons">
<property name="text">
<string>Change Icons</string>
</property>
</action>
<action name="actionCheck_for_Updates">
<property name="text">
<string>Check for Updates</string>
</property>
</action>
<action name="actionFind_2">
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/icons/icons/Icon_Find.png</normaloff>:/icons/icons/Icon_Find.png</iconset>
</property>
<property name="text">
<string>Find</string>
</property>
</action>
<action name="actionPreferences">
<property name="text">
<string>Preferences...</string>
</property>
</action>
</widget>
<resources>
<include location="../data/data.qrc"/>
</resources>
<connections/>
</ui>

35
app/materialviewer.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "materialviewer.h"
#include "ui_materialviewer.h"
MaterialViewer::MaterialViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MaterialViewer) {
ui->setupUi(this);
}
MaterialViewer::~MaterialViewer() {
delete ui;
}
QString ToHexStr(quint32 in) {
return QString("%1").arg(in, 8, 16, QChar('0')).toUpper();
}
void MaterialViewer::SetMaterial(std::shared_ptr<Material> aMaterial) {
ui->lineEdit_NamePtr->setText(ToHexStr(aMaterial->namePtr));
ui->lineEdit_Name->setText(aMaterial->name);
ui->lineEdit_RefPtr->setText(ToHexStr(aMaterial->refNamePtr));
ui->lineEdit_RefName->setText(aMaterial->refName);
QString unknownStr = "";
foreach (quint32 unknownPtr, aMaterial->pointers) {
unknownStr += ToHexStr(unknownPtr) + "\n";
}
ui->lineEdit_Unknowns->setText(unknownStr);
ui->lineEdit_StateA->setText(ToHexStr(aMaterial->stateBits[0]));
ui->lineEdit_StateA->setText(ToHexStr(aMaterial->stateBits[1]));
ui->spinBox_TextureCount->setValue(aMaterial->textureCount);
ui->spinBox_ConstCount->setValue(aMaterial->constCount);
ui->lineEdit_TechSetPtr->setText(ToHexStr(aMaterial->techSetPtr));
ui->lineEdit_TexturePtr->setText(ToHexStr(aMaterial->texturePtr));
ui->lineEdit_ConstantPtr->setText(ToHexStr(aMaterial->constPtr));
}

27
app/materialviewer.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef MATERIALVIEWER_H
#define MATERIALVIEWER_H
#include "asset_structs.h"
#include <QWidget>
#include <QScrollArea>
namespace Ui {
class MaterialViewer;
}
class MaterialViewer : public QWidget
{
Q_OBJECT
public:
explicit MaterialViewer(QWidget *parent = nullptr);
~MaterialViewer();
void SetMaterial(std::shared_ptr<Material> aMaterial);
private:
Ui::MaterialViewer *ui;
};
#endif // MATERIALVIEWER_H

236
app/materialviewer.ui Normal file
View File

@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MaterialViewer</class>
<widget class="QWidget" name="MaterialViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1001</width>
<height>650</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Material 0</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_3">
<property name="minimumSize">
<size>
<width>325</width>
<height>398</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>325</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Header</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name Ptr:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_NamePtr"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Ref Ptr:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Ref Name:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Unknowns:</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>State A:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>State B:</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Texture Count:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_Name"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="lineEdit_RefPtr"/>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_RefName"/>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_Unknowns"/>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_StateA"/>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="lineEdit_StateB"/>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="spinBox_TextureCount"/>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="spinBox_ConstCount"/>
</item>
<item row="9" column="1">
<widget class="QLineEdit" name="lineEdit_TechSetPtr"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Constant Count:</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Tech Set Ptr:</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Texture Ptr:</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Constant Ptr:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="lineEdit_TexturePtr"/>
</item>
<item row="11" column="1">
<widget class="QLineEdit" name="lineEdit_ConstantPtr"/>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Data</string>
</property>
<layout class="QGridLayout" name="gridLayout_6"/>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>143</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

14
app/modelviewer.cpp Normal file
View File

@ -0,0 +1,14 @@
#include "modelviewer.h"
#include "ui_modelviewer.h"
ModelViewer::ModelViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::ModelViewer)
{
ui->setupUi(this);
}
ModelViewer::~ModelViewer()
{
delete ui;
}

22
app/modelviewer.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef MODELVIEWER_H
#define MODELVIEWER_H
#include <QWidget>
namespace Ui {
class ModelViewer;
}
class ModelViewer : public QWidget
{
Q_OBJECT
public:
explicit ModelViewer(QWidget *parent = nullptr);
~ModelViewer();
private:
Ui::ModelViewer *ui;
};
#endif // MODELVIEWER_H

624
app/modelviewer.ui Normal file
View File

@ -0,0 +1,624 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModelViewer</class>
<widget class="QWidget" name="ModelViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1001</width>
<height>897</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Properties</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name Pointer:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_NamePtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_42">
<property name="text">
<string>Model Name:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Tag Count:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_TagCount">
<property name="suffix">
<string> tags</string>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Root Tag Count: </string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="spinBox_RootTagCount">
<property name="suffix">
<string> root tags</string>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Surface Count: </string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="spinBox_SurfaceCount">
<property name="suffix">
<string> surfaces</string>
</property>
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Unknown A:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QSpinBox" name="spinBox_UnknownA">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Bone Name Pointer:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Parent List Pointer:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="spinBox_ParentListPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Quats Pointer:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="spinBox_QuatsPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Transformation Pointer:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="spinBox_TransPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Classification Pointer:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Base Material Pointer:</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QSpinBox" name="spinBox_BaseMatPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Surfaces Pointer;</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QSpinBox" name="spinBox_SurfacesPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Material Handlers Pointer:</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QSpinBox" name="spinBox_MatHandlesPtr">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Coll Surf Pointer:</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_2">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Coll Surface Count:</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_3">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Contents:</string>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_4">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Bone Info Pointer:</string>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_5">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="18" column="0">
<widget class="QLabel" name="label_28">
<property name="text">
<string>Radius:</string>
</property>
</widget>
</item>
<item row="18" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_2"/>
</item>
<item row="19" column="0">
<widget class="QLabel" name="label_29">
<property name="text">
<string>Min X: </string>
</property>
</widget>
</item>
<item row="19" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_3"/>
</item>
<item row="20" column="0">
<widget class="QLabel" name="label_30">
<property name="text">
<string>Min Y: </string>
</property>
</widget>
</item>
<item row="20" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_4"/>
</item>
<item row="21" column="0">
<widget class="QLabel" name="label_31">
<property name="text">
<string>Min Z: </string>
</property>
</widget>
</item>
<item row="21" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_5"/>
</item>
<item row="22" column="0">
<widget class="QLabel" name="label_32">
<property name="text">
<string>Max X: </string>
</property>
</widget>
</item>
<item row="22" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_6"/>
</item>
<item row="23" column="0">
<widget class="QLabel" name="label_33">
<property name="text">
<string>Max Y: </string>
</property>
</widget>
</item>
<item row="23" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_7"/>
</item>
<item row="24" column="0">
<widget class="QLabel" name="label_34">
<property name="text">
<string>Max Z: </string>
</property>
</widget>
</item>
<item row="24" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_8"/>
</item>
<item row="25" column="0">
<widget class="QLabel" name="label_35">
<property name="text">
<string>Lod Count:</string>
</property>
</widget>
</item>
<item row="25" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_7">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="26" column="0">
<widget class="QLabel" name="label_36">
<property name="text">
<string>Coll Lod:</string>
</property>
</widget>
</item>
<item row="26" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_8">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="27" column="0">
<widget class="QLabel" name="label_37">
<property name="text">
<string>Stream Info Pointer:</string>
</property>
</widget>
</item>
<item row="27" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_9">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="28" column="0">
<widget class="QLabel" name="label_38">
<property name="text">
<string>Memory Usage:</string>
</property>
</widget>
</item>
<item row="28" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_10">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="29" column="0">
<widget class="QLabel" name="label_39">
<property name="text">
<string>Flags:</string>
</property>
</widget>
</item>
<item row="29" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_11">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="30" column="0">
<widget class="QLabel" name="label_40">
<property name="text">
<string>Phys Preset Pointer:</string>
</property>
</widget>
</item>
<item row="30" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_12">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="31" column="0">
<widget class="QLabel" name="label_41">
<property name="text">
<string>Phys Geometry Pointer:</string>
</property>
</widget>
</item>
<item row="31" column="1">
<widget class="QSpinBox" name="spinBox_ClassPtr_13">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Lod Info</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Lod Info Index:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_LodIndex"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Distance:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Surface Count:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_2">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Surface Index:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_3">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Part Bit 1: </string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_4">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Part Bit 2: </string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_5">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Part Bit 3: </string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_6">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Part Bit 4: </string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_7">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Part Bit 5: </string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="spinBox_BoneNamePtr_8">
<property name="maximum">
<number>1000000000</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3DWindow">
<property name="title">
<string>3D Window</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

Binary file not shown.

View File

@ -1,695 +1,35 @@
#include "preferenceeditor.h"
#include "settings.h"
#include "compression.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QDialogButtonBox>
#include <QFontDatabase>
#include <QFileDialog>
#include <QFileInfo>
#include <QFormLayout>
#include <QFrame>
#include <QMessageBox>
PreferenceEditor::PreferenceEditor(QWidget *parent)
: QDialog(parent)
{
setupUi();
loadSettings();
applyStylesheet();
}
void PreferenceEditor::setupUi()
{
setWindowTitle("XPlor Preferences");
setMinimumSize(700, 500);
resize(800, 550);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(10);
mainLayout->setContentsMargins(15, 15, 15, 15);
// Main content area
auto *contentLayout = new QHBoxLayout();
contentLayout->setSpacing(15);
// Category list
m_categoryList = new QListWidget(this);
m_categoryList->setFixedWidth(160);
m_categoryList->setSpacing(2);
m_categoryList->addItem("Appearance");
m_categoryList->addItem("General");
m_categoryList->addItem("Tools");
m_categoryList->addItem("Debug & Logging");
m_categoryList->addItem("Tree Browser");
m_categoryList->addItem("Previews");
m_categoryList->setCurrentRow(0);
connect(m_categoryList, &QListWidget::currentRowChanged, this, &PreferenceEditor::onCategoryChanged);
contentLayout->addWidget(m_categoryList);
// Page stack
m_pageStack = new QStackedWidget(this);
createAppearancePage();
createGeneralPage();
createToolsPage();
createDebugPage();
createTreePage();
createPreviewPage();
contentLayout->addWidget(m_pageStack, 1);
mainLayout->addLayout(contentLayout, 1);
// Separator
auto *separator = new QFrame(this);
separator->setFrameShape(QFrame::HLine);
separator->setStyleSheet("background-color: #3c3c3c;");
mainLayout->addWidget(separator);
// Button box
auto *buttonBox = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply,
this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &PreferenceEditor::onAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &PreferenceEditor::onApply);
mainLayout->addWidget(buttonBox);
}
void PreferenceEditor::createAppearancePage()
{
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setSpacing(15);
// Title
auto *titleLabel = new QLabel("Appearance", page);
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
layout->addWidget(titleLabel);
// Theme group
auto *themeGroup = new QGroupBox("Color Theme", page);
auto *themeLayout = new QVBoxLayout(themeGroup);
themeLayout->setSpacing(10);
auto *themeRow = new QHBoxLayout();
auto *themeLabel = new QLabel("Theme:", themeGroup);
themeRow->addWidget(themeLabel);
m_themeCombo = new QComboBox(themeGroup);
for (const QString &themeName : Settings::instance().availableThemes()) {
m_themeCombo->addItem(themeName);
}
connect(m_themeCombo, &QComboBox::currentTextChanged, this, &PreferenceEditor::updateThemePreview);
themeRow->addWidget(m_themeCombo, 1);
themeLayout->addLayout(themeRow);
// Theme preview
m_themePreview = new QLabel(themeGroup);
m_themePreview->setFixedHeight(80);
m_themePreview->setAlignment(Qt::AlignCenter);
themeLayout->addWidget(m_themePreview);
// Theme description
auto *themeDesc = new QLabel(
"Choose a color theme for the application. The theme affects the accent color "
"and overall appearance. Changes will take effect after restarting the application.",
themeGroup);
themeDesc->setWordWrap(true);
themeDesc->setStyleSheet("color: #888; font-size: 11px;");
themeLayout->addWidget(themeDesc);
layout->addWidget(themeGroup);
layout->addStretch();
m_pageStack->addWidget(page);
}
void PreferenceEditor::updateThemePreview()
{
Theme t = Settings::getTheme(m_themeCombo->currentText());
QString previewStyle = QString(
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
"stop:0 %1, stop:0.15 %1, stop:0.15 %2, stop:0.85 %2, stop:0.85 %3, stop:1 %3);"
"border: 2px solid %4; border-radius: 6px; color: %5; font-weight: bold;"
).arg(t.accentColor, t.backgroundColor, t.panelColor, t.borderColor, t.textColor);
m_themePreview->setStyleSheet(previewStyle);
m_themePreview->setText(t.name);
}
void PreferenceEditor::createGeneralPage()
{
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setSpacing(15);
// Title
auto *titleLabel = new QLabel("General Settings", page);
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
layout->addWidget(titleLabel);
// Export settings group
auto *exportGroup = new QGroupBox("Export Settings", page);
auto *exportLayout = new QFormLayout(exportGroup);
exportLayout->setSpacing(10);
auto *dirLayout = new QHBoxLayout();
m_exportDirEdit = new QLineEdit(exportGroup);
m_exportDirEdit->setPlaceholderText("Export directory path...");
dirLayout->addWidget(m_exportDirEdit);
auto *browseBtn = new QPushButton("Browse...", exportGroup);
browseBtn->setFixedWidth(80);
connect(browseBtn, &QPushButton::clicked, this, &PreferenceEditor::onBrowseExportDir);
dirLayout->addWidget(browseBtn);
exportLayout->addRow("Export Directory:", dirLayout);
m_autoExportCheck = new QCheckBox("Automatically export resources when parsing", exportGroup);
exportLayout->addRow("", m_autoExportCheck);
layout->addWidget(exportGroup);
// View settings group
auto *viewGroup = new QGroupBox("View Settings", page);
auto *viewLayout = new QFormLayout(viewGroup);
viewLayout->setSpacing(10);
m_fontFamilyCombo = new QComboBox(viewGroup);
QStringList families = QFontDatabase::families();
for (const QString &family : families) {
if (QFontDatabase::isFixedPitch(family) || family.contains("Mono", Qt::CaseInsensitive) ||
family == "Segoe UI" || family == "Roboto" || family == "Arial") {
m_fontFamilyCombo->addItem(family);
}
}
viewLayout->addRow("Font Family:", m_fontFamilyCombo);
m_fontSizeSpin = new QSpinBox(viewGroup);
m_fontSizeSpin->setRange(8, 24);
m_fontSizeSpin->setSuffix(" pt");
viewLayout->addRow("Font Size:", m_fontSizeSpin);
m_zoomSpin = new QSpinBox(viewGroup);
m_zoomSpin->setRange(50, 200);
m_zoomSpin->setSuffix(" %");
m_zoomSpin->setSingleStep(10);
viewLayout->addRow("View Zoom:", m_zoomSpin);
layout->addWidget(viewGroup);
// Reset group
auto *resetGroup = new QGroupBox("Reset", page);
auto *resetLayout = new QVBoxLayout(resetGroup);
resetLayout->setSpacing(10);
auto *resetDesc = new QLabel(
"Reset all settings to their default values. This will restore the default theme (XPlor Dark) "
"and clear all customizations.",
resetGroup);
resetDesc->setWordWrap(true);
resetDesc->setStyleSheet("color: #888; font-size: 11px;");
resetLayout->addWidget(resetDesc);
auto *resetBtn = new QPushButton("Reset All Settings", resetGroup);
resetBtn->setStyleSheet("background-color: #8a0a0a;");
connect(resetBtn, &QPushButton::clicked, this, &PreferenceEditor::onResetSettings);
resetLayout->addWidget(resetBtn);
layout->addWidget(resetGroup);
layout->addStretch();
m_pageStack->addWidget(page);
}
void PreferenceEditor::createToolsPage()
{
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setSpacing(15);
// Title
auto *titleLabel = new QLabel("External Tools", page);
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
layout->addWidget(titleLabel);
// QuickBMS group
auto *quickbmsGroup = new QGroupBox("QuickBMS", page);
auto *quickbmsLayout = new QVBoxLayout(quickbmsGroup);
quickbmsLayout->setSpacing(10);
auto *quickbmsDesc = new QLabel(
"QuickBMS is used for decompressing certain Xbox 360 formats (LZXTDECODE). "
"If not configured, XPlor will attempt to find it automatically.",
quickbmsGroup);
quickbmsDesc->setWordWrap(true);
quickbmsDesc->setStyleSheet("color: #888; font-size: 11px;");
quickbmsLayout->addWidget(quickbmsDesc);
auto *pathLayout = new QHBoxLayout();
m_quickBmsEdit = new QLineEdit(quickbmsGroup);
m_quickBmsEdit->setPlaceholderText("Path to quickbms.exe...");
pathLayout->addWidget(m_quickBmsEdit);
auto *browseBtn = new QPushButton("Browse...", quickbmsGroup);
browseBtn->setFixedWidth(80);
connect(browseBtn, &QPushButton::clicked, this, &PreferenceEditor::onBrowseQuickBms);
pathLayout->addWidget(browseBtn);
quickbmsLayout->addLayout(pathLayout);
m_quickBmsStatus = new QLabel(quickbmsGroup);
m_quickBmsStatus->setStyleSheet("font-size: 11px;");
quickbmsLayout->addWidget(m_quickBmsStatus);
// Update status when path changes
connect(m_quickBmsEdit, &QLineEdit::textChanged, this, [this](const QString &path) {
if (path.isEmpty()) {
m_quickBmsStatus->setText("No path configured - will auto-detect");
m_quickBmsStatus->setStyleSheet("color: #888; font-size: 11px;");
} else if (QFileInfo::exists(path)) {
m_quickBmsStatus->setText("Found: " + path);
m_quickBmsStatus->setStyleSheet("color: #4caf50; font-size: 11px;");
} else {
m_quickBmsStatus->setText("Not found at specified path");
m_quickBmsStatus->setStyleSheet("color: #f44336; font-size: 11px;");
}
});
layout->addWidget(quickbmsGroup);
layout->addStretch();
m_pageStack->addWidget(page);
}
void PreferenceEditor::createDebugPage()
{
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setSpacing(15);
// Title
auto *titleLabel = new QLabel("Debug & Logging", page);
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
layout->addWidget(titleLabel);
// Logging group
auto *logGroup = new QGroupBox("Logging Options", page);
auto *logLayout = new QVBoxLayout(logGroup);
logLayout->setSpacing(10);
m_debugLoggingCheck = new QCheckBox("Enable debug logging", logGroup);
m_debugLoggingCheck->setToolTip("Log detailed parsing information to the Logs panel.\n"
"Shows parse positions, chunk processing, and tree routing.");
logLayout->addWidget(m_debugLoggingCheck);
m_verboseParsingCheck = new QCheckBox("Verbose parsing output", logGroup);
m_verboseParsingCheck->setToolTip("Show additional details during file parsing.\n"
"Includes field assignments and type information.");
logLayout->addWidget(m_verboseParsingCheck);
m_logToFileCheck = new QCheckBox("Save logs to file", logGroup);
m_logToFileCheck->setToolTip("Write all log entries to xplor.log in the application directory.");
logLayout->addWidget(m_logToFileCheck);
layout->addWidget(logGroup);
// Info label
auto *infoLabel = new QLabel(
"Debug logging provides detailed information about file parsing and "
"data processing. Enable this when troubleshooting issues with file "
"parsing or to understand how data is being processed.\n\n"
"Note: Extensive logging may affect performance with large files.",
page);
infoLabel->setWordWrap(true);
infoLabel->setStyleSheet("color: #888; padding: 10px; background-color: #252526; border-radius: 4px;");
layout->addWidget(infoLabel);
layout->addStretch();
m_pageStack->addWidget(page);
}
void PreferenceEditor::createTreePage()
{
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setSpacing(15);
// Title
auto *titleLabel = new QLabel("Tree Browser", page);
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
layout->addWidget(titleLabel);
// Display group
auto *displayGroup = new QGroupBox("Display Options", page);
auto *displayLayout = new QVBoxLayout(displayGroup);
displayLayout->setSpacing(10);
m_showCountsCheck = new QCheckBox("Show item counts on categories", displayGroup);
m_showCountsCheck->setToolTip("Display the number of children next to category names.");
displayLayout->addWidget(m_showCountsCheck);
m_collapseDefaultCheck = new QCheckBox("Collapse items by default", displayGroup);
m_collapseDefaultCheck->setToolTip("New items are collapsed when added to the tree.");
displayLayout->addWidget(m_collapseDefaultCheck);
layout->addWidget(displayGroup);
// Organization group
auto *orgGroup = new QGroupBox("Organization", page);
auto *orgLayout = new QVBoxLayout(orgGroup);
orgLayout->setSpacing(10);
m_groupByExtCheck = new QCheckBox("Group resources by file extension", orgGroup);
m_groupByExtCheck->setToolTip("Organize resources into subfolders based on file extension (.wav, .tga, etc.)");
orgLayout->addWidget(m_groupByExtCheck);
m_naturalSortCheck = new QCheckBox("Natural sorting (1, 2, 10 instead of 1, 10, 2)", orgGroup);
m_naturalSortCheck->setToolTip("Sort items numerically when they contain numbers.");
orgLayout->addWidget(m_naturalSortCheck);
layout->addWidget(orgGroup);
layout->addStretch();
m_pageStack->addWidget(page);
}
void PreferenceEditor::createPreviewPage()
{
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setSpacing(15);
// Title
auto *titleLabel = new QLabel("Preview Settings", page);
titleLabel->setStyleSheet("font-size: 14px; font-weight: bold; color: #ad0c0c;");
layout->addWidget(titleLabel);
// File Type Associations group
auto *fileTypeGroup = new QGroupBox("File Type Associations", page);
auto *fileTypeLayout = new QFormLayout(fileTypeGroup);
fileTypeLayout->setSpacing(10);
m_textExtensionsEdit = new QLineEdit(fileTypeGroup);
m_textExtensionsEdit->setPlaceholderText("txt, xml, json, cfg, lua...");
m_textExtensionsEdit->setToolTip("Comma-separated list of extensions to open in text viewer");
fileTypeLayout->addRow("Text Viewer:", m_textExtensionsEdit);
m_imageExtensionsEdit = new QLineEdit(fileTypeGroup);
m_imageExtensionsEdit->setPlaceholderText("tga, dds, png, jpg...");
m_imageExtensionsEdit->setToolTip("Comma-separated list of extensions to open in image viewer");
fileTypeLayout->addRow("Image Viewer:", m_imageExtensionsEdit);
m_audioExtensionsEdit = new QLineEdit(fileTypeGroup);
m_audioExtensionsEdit->setPlaceholderText("wav, mp3, ogg...");
m_audioExtensionsEdit->setToolTip("Comma-separated list of extensions to open in audio player");
fileTypeLayout->addRow("Audio Player:", m_audioExtensionsEdit);
auto *fileTypeDesc = new QLabel(
"File types not listed above will open in the hex viewer. "
"Enter extensions without dots, separated by commas.",
fileTypeGroup);
fileTypeDesc->setWordWrap(true);
fileTypeDesc->setStyleSheet("color: #888; font-size: 11px;");
fileTypeLayout->addRow("", fileTypeDesc);
layout->addWidget(fileTypeGroup);
// Audio group
auto *audioGroup = new QGroupBox("Audio Preview", page);
auto *audioLayout = new QVBoxLayout(audioGroup);
audioLayout->setSpacing(10);
m_audioAutoPlayCheck = new QCheckBox("Auto-play audio when selected", audioGroup);
audioLayout->addWidget(m_audioAutoPlayCheck);
layout->addWidget(audioGroup);
// Image group
auto *imageGroup = new QGroupBox("Image Preview", page);
auto *imageLayout = new QVBoxLayout(imageGroup);
imageLayout->setSpacing(10);
m_imageShowGridCheck = new QCheckBox("Show transparency grid", imageGroup);
m_imageShowGridCheck->setToolTip("Display a checkered grid behind transparent images.");
imageLayout->addWidget(m_imageShowGridCheck);
m_imageAutoZoomCheck = new QCheckBox("Auto-zoom to fit", imageGroup);
m_imageAutoZoomCheck->setToolTip("Automatically scale images to fit the preview area.");
imageLayout->addWidget(m_imageAutoZoomCheck);
layout->addWidget(imageGroup);
layout->addStretch();
m_pageStack->addWidget(page);
}
void PreferenceEditor::loadSettings()
{
Settings &s = Settings::instance();
// Appearance
int themeIdx = m_themeCombo->findText(s.currentTheme());
if (themeIdx >= 0) m_themeCombo->setCurrentIndex(themeIdx);
updateThemePreview();
// General
m_exportDirEdit->setText(s.exportDirectory());
m_autoExportCheck->setChecked(s.autoExportOnParse());
// Tools
QString qbmsPath = s.quickBmsPath();
m_quickBmsEdit->setText(qbmsPath);
// Trigger status update
emit m_quickBmsEdit->textChanged(qbmsPath);
int fontIdx = m_fontFamilyCombo->findText(s.fontFamily());
if (fontIdx >= 0) m_fontFamilyCombo->setCurrentIndex(fontIdx);
m_fontSizeSpin->setValue(s.fontSize());
m_zoomSpin->setValue(s.viewZoom());
// Debug
m_debugLoggingCheck->setChecked(s.debugLoggingEnabled());
m_verboseParsingCheck->setChecked(s.verboseParsingEnabled());
m_logToFileCheck->setChecked(s.logToFileEnabled());
// Tree
m_showCountsCheck->setChecked(s.showItemCounts());
m_collapseDefaultCheck->setChecked(s.collapseByDefault());
m_groupByExtCheck->setChecked(s.groupByExtension());
m_naturalSortCheck->setChecked(s.naturalSorting());
// Previews
m_audioAutoPlayCheck->setChecked(s.audioAutoPlay());
m_imageShowGridCheck->setChecked(s.imageShowGrid());
m_imageAutoZoomCheck->setChecked(s.imageAutoZoom());
// File Type Associations
m_textExtensionsEdit->setText(s.textFileExtensions().join(", "));
m_imageExtensionsEdit->setText(s.imageFileExtensions().join(", "));
m_audioExtensionsEdit->setText(s.audioFileExtensions().join(", "));
}
void PreferenceEditor::saveSettings()
{
Settings &s = Settings::instance();
// Appearance
s.setCurrentTheme(m_themeCombo->currentText());
// General
s.setExportDirectory(m_exportDirEdit->text());
s.setAutoExportOnParse(m_autoExportCheck->isChecked());
// Tools
QString quickBmsPath = m_quickBmsEdit->text();
s.setQuickBmsPath(quickBmsPath);
Compression::setQuickBmsPath(quickBmsPath);
s.setFontFamily(m_fontFamilyCombo->currentText());
s.setFontSize(m_fontSizeSpin->value());
s.setViewZoom(m_zoomSpin->value());
// Debug
s.setDebugLoggingEnabled(m_debugLoggingCheck->isChecked());
s.setVerboseParsingEnabled(m_verboseParsingCheck->isChecked());
s.setLogToFileEnabled(m_logToFileCheck->isChecked());
// Tree
s.setShowItemCounts(m_showCountsCheck->isChecked());
s.setCollapseByDefault(m_collapseDefaultCheck->isChecked());
s.setGroupByExtension(m_groupByExtCheck->isChecked());
s.setNaturalSorting(m_naturalSortCheck->isChecked());
// Previews
s.setAudioAutoPlay(m_audioAutoPlayCheck->isChecked());
s.setImageShowGrid(m_imageShowGridCheck->isChecked());
s.setImageAutoZoom(m_imageAutoZoomCheck->isChecked());
// File Type Associations - parse comma-separated lists
auto parseExtList = [](const QString& text) -> QStringList {
QStringList result;
for (QString ext : text.split(',', Qt::SkipEmptyParts)) {
ext = ext.trimmed().toLower();
if (ext.startsWith('.')) ext = ext.mid(1);
if (!ext.isEmpty()) result.append(ext);
}
return result;
};
s.setTextFileExtensions(parseExtList(m_textExtensionsEdit->text()));
s.setImageFileExtensions(parseExtList(m_imageExtensionsEdit->text()));
s.setAudioFileExtensions(parseExtList(m_audioExtensionsEdit->text()));
s.sync();
}
void PreferenceEditor::applyStylesheet()
{
setStyleSheet(R"(
QDialog {
background-color: #1e1e1e;
color: #d4d4d4;
}
QListWidget {
background-color: #252526;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 5px;
}
QListWidget::item {
padding: 8px;
border-radius: 3px;
}
QListWidget::item:selected {
background-color: #ad0c0c;
color: white;
}
QListWidget::item:hover:!selected {
background-color: #2d2d30;
}
QGroupBox {
font-weight: bold;
border: 1px solid #3c3c3c;
border-radius: 4px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
}
QCheckBox {
spacing: 8px;
}
QCheckBox::indicator {
width: 16px;
height: 16px;
}
QCheckBox::indicator:unchecked {
border: 1px solid #5c5c5c;
background-color: #2d2d30;
border-radius: 3px;
}
QCheckBox::indicator:checked {
background-color: #ad0c0c;
border: 1px solid #ad0c0c;
border-radius: 3px;
}
QLineEdit, QComboBox, QSpinBox {
background-color: #2d2d30;
border: 1px solid #3c3c3c;
border-radius: 3px;
padding: 5px;
min-height: 20px;
}
QLineEdit:focus, QComboBox:focus, QSpinBox:focus {
border-color: #ad0c0c;
}
QComboBox::drop-down {
border: none;
padding-right: 5px;
}
QPushButton {
background-color: #3c3c3c;
border: 1px solid #5c5c5c;
border-radius: 3px;
padding: 6px 15px;
min-width: 70px;
}
QPushButton:hover {
background-color: #4c4c4c;
}
QPushButton:pressed {
background-color: #ad0c0c;
}
QDialogButtonBox QPushButton {
min-width: 80px;
}
)");
}
void PreferenceEditor::onCategoryChanged()
{
m_pageStack->setCurrentIndex(m_categoryList->currentRow());
}
void PreferenceEditor::onApply()
{
saveSettings();
}
void PreferenceEditor::onAccept()
{
saveSettings();
accept();
}
void PreferenceEditor::onBrowseExportDir()
{
QString dir = QFileDialog::getExistingDirectory(this, "Select Export Directory",
m_exportDirEdit->text());
if (!dir.isEmpty()) {
m_exportDirEdit->setText(dir);
}
}
void PreferenceEditor::onBrowseQuickBms()
{
QString startDir = m_quickBmsEdit->text();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
} else {
startDir = QFileInfo(startDir).absolutePath();
}
QString file = QFileDialog::getOpenFileName(
this,
"Locate QuickBMS Executable",
startDir,
"QuickBMS (quickbms.exe);;All Files (*.*)"
);
if (!file.isEmpty()) {
m_quickBmsEdit->setText(file);
}
}
void PreferenceEditor::onResetSettings()
{
QMessageBox::StandardButton reply = QMessageBox::question(
this,
"Reset Settings",
"Are you sure you want to reset all settings to defaults?\n\n"
"This will restore the default theme and clear all customizations.",
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No
);
if (reply == QMessageBox::Yes) {
Settings::instance().resetToDefaults();
loadSettings(); // Reload UI with defaults
QMessageBox::information(this, "Settings Reset", "All settings have been reset to defaults.");
}
}
#include "preferenceeditor.h"
#include "ui_preferenceeditor.h"
PreferenceEditor::PreferenceEditor(QWidget *parent)
: QDialog(parent)
, ui(new Ui::PreferenceEditor)
{
ui->setupUi(this);
ui->frame_View->show();
ui->frame_TreeWidget->hide();
ui->frame_FileEditors->hide();
connect(ui->listWidget_Categories, &QListWidget::itemSelectionChanged, this, [this]() {
const QString itemText = ui->listWidget_Categories->selectedItems().first()->text();
if (itemText == "View") {
ui->frame_View->show();
ui->frame_TreeWidget->hide();
ui->frame_FileEditors->hide();
} else if (itemText == "Tree Widget") {
ui->frame_View->hide();
ui->frame_TreeWidget->show();
ui->frame_FileEditors->hide();
} else if (itemText == "File Editors") {
ui->frame_View->hide();
ui->frame_TreeWidget->hide();
ui->frame_FileEditors->show();
}
});
}
PreferenceEditor::~PreferenceEditor()
{
delete ui;
}

View File

@ -1,86 +1,22 @@
#ifndef PREFERENCEEDITOR_H
#define PREFERENCEEDITOR_H
#include <QDialog>
#include <QListWidget>
#include <QStackedWidget>
#include <QCheckBox>
#include <QSpinBox>
#include <QComboBox>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
class PreferenceEditor : public QDialog
{
Q_OBJECT
public:
explicit PreferenceEditor(QWidget *parent = nullptr);
~PreferenceEditor() = default;
private slots:
void onCategoryChanged();
void onApply();
void onAccept();
void onBrowseExportDir();
void onBrowseQuickBms();
void onResetSettings();
private:
void setupUi();
void createAppearancePage();
void createGeneralPage();
void createToolsPage();
void createDebugPage();
void createTreePage();
void createPreviewPage();
void loadSettings();
void saveSettings();
void applyStylesheet();
void updateThemePreview();
// UI Components
QListWidget *m_categoryList;
QStackedWidget *m_pageStack;
// Appearance Page
QComboBox *m_themeCombo;
QLabel *m_themePreview;
// General Page
QLineEdit *m_exportDirEdit;
QCheckBox *m_autoExportCheck;
// Tools Page
QLineEdit *m_quickBmsEdit;
QLabel *m_quickBmsStatus;
// Debug Page
QCheckBox *m_debugLoggingCheck;
QCheckBox *m_verboseParsingCheck;
QCheckBox *m_logToFileCheck;
// Tree Page
QCheckBox *m_showCountsCheck;
QCheckBox *m_collapseDefaultCheck;
QCheckBox *m_groupByExtCheck;
QCheckBox *m_naturalSortCheck;
// Preview Page
QCheckBox *m_audioAutoPlayCheck;
QCheckBox *m_imageShowGridCheck;
QCheckBox *m_imageAutoZoomCheck;
// File Type Associations
QLineEdit *m_textExtensionsEdit;
QLineEdit *m_imageExtensionsEdit;
QLineEdit *m_audioExtensionsEdit;
// View Page
QComboBox *m_fontFamilyCombo;
QSpinBox *m_fontSizeSpin;
QSpinBox *m_zoomSpin;
};
#endif // PREFERENCEEDITOR_H
#ifndef PREFERENCEEDITOR_H
#define PREFERENCEEDITOR_H
#include <QDialog>
namespace Ui {
class PreferenceEditor;
}
class PreferenceEditor : public QDialog
{
Q_OBJECT
public:
explicit PreferenceEditor(QWidget *parent = nullptr);
~PreferenceEditor();
private:
Ui::PreferenceEditor *ui;
};
#endif // PREFERENCEEDITOR_H

File diff suppressed because it is too large Load Diff

View File

@ -1,150 +0,0 @@
#include "reportissuedialog.h"
#include "ui_reportissuedialog.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QNetworkRequest>
#include <QRegularExpression>
namespace {
const QString GITEA_BASE_URL = "https://code.redline.llc";
const QString REPO_OWNER = "njohnson";
const QString REPO_NAME = "XPlor";
}
ReportIssueDialog::ReportIssueDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::ReportIssueDialog)
, mNetworkManager(new QNetworkAccessManager(this))
{
ui->setupUi(this);
#ifdef GITEA_ACCESS_TOKEN
mAccessToken = GITEA_ACCESS_TOKEN;
#endif
connect(ui->buttonSend, &QPushButton::clicked, this, &ReportIssueDialog::onSendClicked);
connect(ui->buttonCancel, &QPushButton::clicked, this, &ReportIssueDialog::onCancelClicked);
connect(mNetworkManager, &QNetworkAccessManager::finished, this, &ReportIssueDialog::onNetworkReplyFinished);
}
ReportIssueDialog::~ReportIssueDialog()
{
delete ui;
}
bool ReportIssueDialog::isValidEmail(const QString &email)
{
static QRegularExpression emailRegex(
R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"
);
return emailRegex.match(email).hasMatch();
}
bool ReportIssueDialog::validateFields()
{
QString summary = ui->lineEditSummary->text().trimmed();
QString details = ui->textEditDetails->toPlainText().trimmed();
QString email = ui->lineEditEmail->text().trimmed();
if (summary.isEmpty()) {
QMessageBox::warning(this, tr("Required Field"), tr("Please enter a summary for the issue."));
ui->lineEditSummary->setFocus();
return false;
}
if (details.isEmpty()) {
QMessageBox::warning(this, tr("Required Field"), tr("Please describe the issue in detail."));
ui->textEditDetails->setFocus();
return false;
}
if (email.isEmpty()) {
QMessageBox::warning(this, tr("Required Field"), tr("Please enter your email address."));
ui->lineEditEmail->setFocus();
return false;
}
if (!isValidEmail(email)) {
QMessageBox::warning(this, tr("Invalid Email"), tr("Please enter a valid email address."));
ui->lineEditEmail->setFocus();
return false;
}
return true;
}
void ReportIssueDialog::onSendClicked()
{
if (!validateFields()) {
return;
}
QString title = ui->lineEditSummary->text().trimmed();
QString details = ui->textEditDetails->toPlainText().trimmed();
QString email = ui->lineEditEmail->text().trimmed();
QString body = details + QString("\n\n---\nContact: %1").arg(email);
ui->buttonSend->setEnabled(false);
ui->buttonSend->setText(tr("Sending..."));
sendIssueReport(title, body, email);
}
void ReportIssueDialog::onCancelClicked()
{
reject();
}
void ReportIssueDialog::sendIssueReport(const QString &title, const QString &body, const QString &/*email*/)
{
QUrl url(QString("%1/api/v1/repos/%2/%3/issues")
.arg(GITEA_BASE_URL, REPO_OWNER, REPO_NAME));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!mAccessToken.isEmpty()) {
request.setRawHeader("Authorization", "token " + mAccessToken.toUtf8());
}
QJsonObject json;
json["title"] = title;
json["body"] = body;
json["labels"] = QJsonArray{12};
mNetworkManager->post(request, QJsonDocument(json).toJson());
}
void ReportIssueDialog::onNetworkReplyFinished(QNetworkReply *reply)
{
ui->buttonSend->setEnabled(true);
ui->buttonSend->setText(tr("Send Report"));
QByteArray responseData = reply->readAll();
QString responseStr = QString::fromUtf8(responseData);
if (reply->error() != QNetworkReply::NoError) {
QString errorStr = reply->errorString();
if (errorStr.isEmpty()) {
errorStr = "Unknown network error";
}
QMessageBox::critical(this, tr("Error"),
tr("Failed to send issue report:\n%1").arg(errorStr));
} else {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status == 201) {
QMessageBox::information(this, tr("Success"),
tr("Issue reported successfully. Thank you for your feedback!"));
accept();
} else {
QMessageBox::warning(this, tr("Failed"),
tr("Unexpected response from server (status %1).").arg(status));
}
}
reply->deleteLater();
}

View File

@ -1,35 +0,0 @@
#ifndef REPORTISSUEDIALOG_H
#define REPORTISSUEDIALOG_H
#include <QDialog>
#include <QNetworkAccessManager>
#include <QNetworkReply>
namespace Ui {
class ReportIssueDialog;
}
class ReportIssueDialog : public QDialog
{
Q_OBJECT
public:
explicit ReportIssueDialog(QWidget *parent = nullptr);
~ReportIssueDialog();
private slots:
void onSendClicked();
void onCancelClicked();
void onNetworkReplyFinished(QNetworkReply *reply);
private:
Ui::ReportIssueDialog *ui;
QNetworkAccessManager *mNetworkManager;
QString mAccessToken;
bool validateFields();
bool isValidEmail(const QString &email);
void sendIssueReport(const QString &title, const QString &body, const QString &email);
};
#endif // REPORTISSUEDIALOG_H

View File

@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ReportIssueDialog</class>
<widget class="QDialog" name="ReportIssueDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>450</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>350</height>
</size>
</property>
<property name="windowTitle">
<string>Report an Issue</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>16</number>
</property>
<property name="topMargin">
<number>16</number>
</property>
<property name="rightMargin">
<number>16</number>
</property>
<property name="bottomMargin">
<number>16</number>
</property>
<item>
<widget class="QLabel" name="labelInstructions">
<property name="text">
<string>Please describe the issue you encountered. All fields are required.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelSummary">
<property name="text">
<string>Summary *</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEditSummary">
<property name="placeholderText">
<string>Brief description of the issue</string>
</property>
<property name="maxLength">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelDetails">
<property name="text">
<string>Details *</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="textEditDetails">
<property name="placeholderText">
<string>What happened? What were you doing when the issue occurred?</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelEmail">
<property name="text">
<string>Email *</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEditEmail">
<property name="placeholderText">
<string>your@email.com</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buttonCancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonSend">
<property name="text">
<string>Send Report</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

58
app/rumblefileviewer.cpp Normal file
View File

@ -0,0 +1,58 @@
#include "rumblefileviewer.h"
#include "ui_rumblefileviewer.h"
RumbleFileViewer::RumbleFileViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::RumbleFileViewer)
, mPropertyCount()
, mRumbleFile(nullptr) {
ui->setupUi(this);
ui->tableWidget_Properties->setColumnCount(2);
ui->tableWidget_Properties->setRowCount(0);
ui->tableWidget_Properties->setColumnWidth(0, 200);
ui->tableWidget_Properties->horizontalHeader()->setStretchLastSection(true);
}
RumbleFileViewer::~RumbleFileViewer() {
delete ui;
}
void RumbleFileViewer::SetRumbleFile(std::shared_ptr<RawFile> aRumbleFile) {
mRumbleFile = aRumbleFile;
ui->tableWidget_Properties->clear();
const QString magic = aRumbleFile->contents.left(6);
if (magic != "RUMBLE") {
qDebug() << "Rumble file has invalid magic: " << magic;
return;
}
int firstIndex = 0;
int secondIndex = 0;
int thirdIndex = 0;
int startIndex = 0;
for (int i = 0; i < aRumbleFile->contents.count("\\") / 2; i++) {
ui->tableWidget_Properties->setRowCount(i + 1);
ui->spinBox_Entries->setValue(i + 1);
firstIndex = aRumbleFile->contents.indexOf("\\", startIndex);
secondIndex = aRumbleFile->contents.indexOf("\\", firstIndex + 1);
thirdIndex = aRumbleFile->contents.indexOf("\\", secondIndex + 1);
if (thirdIndex == -1) {
thirdIndex = aRumbleFile->contents.size();
}
const QString keyStr = aRumbleFile->contents.mid(firstIndex + 1, secondIndex - firstIndex - 1);
QTableWidgetItem *keyItem = new QTableWidgetItem(keyStr);
ui->tableWidget_Properties->setItem(i, 0, keyItem);
const QString valStr = aRumbleFile->contents.mid(secondIndex + 1, thirdIndex - secondIndex - 1);
QTableWidgetItem *valueItem = new QTableWidgetItem(valStr);
ui->tableWidget_Properties->setItem(i, 1, valueItem);
startIndex = thirdIndex;
}
}

28
app/rumblefileviewer.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef RUMBLEFILEVIEWER_H
#define RUMBLEFILEVIEWER_H
#include "asset_structs.h"
#include "zonefile.h"
#include <QWidget>
namespace Ui {
class RumbleFileViewer;
}
class RumbleFileViewer : public QWidget
{
Q_OBJECT
public:
explicit RumbleFileViewer(QWidget *parent = nullptr);
~RumbleFileViewer();
void SetRumbleFile(std::shared_ptr<RawFile> aRumbleFile);
private:
Ui::RumbleFileViewer *ui;
quint32 mPropertyCount;
std::shared_ptr<RawFile> mRumbleFile;
};
#endif // RUMBLEFILEVIEWER_H

153
app/rumblefileviewer.ui Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RumbleFileViewer</class>
<widget class="QWidget" name="RumbleFileViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>841</width>
<height>457</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>841</width>
<height>457</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Rumble File Viewer</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>325</width>
<height>398</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>325</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Header</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_Entries">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Entries:</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_LocalStrViewer">
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Properties</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTableWidget" name="tableWidget_Properties">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

54
app/rumblegraphviewer.cpp Normal file
View File

@ -0,0 +1,54 @@
#include "rumblegraphviewer.h"
#include "ui_rumblegraphviewer.h"
RumbleGraphViewer::RumbleGraphViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::RumbleGraphViewer),
mEntryCount(),
mRumbleGraphFile(nullptr) {
ui->setupUi(this);
ui->tableWidget_Entries->setColumnCount(2);
ui->tableWidget_Entries->setHorizontalHeaderLabels({ "X", "Y" });
ui->tableWidget_Entries->setRowCount(0);
ui->tableWidget_Entries->setColumnWidth(0, 200);
ui->tableWidget_Entries->horizontalHeader()->setStretchLastSection(true);
}
RumbleGraphViewer::~RumbleGraphViewer() {
delete ui;
}
void RumbleGraphViewer::SetRumbleGraphFile(const std::shared_ptr<RawFile> aRawFile) {
mRumbleGraphFile = aRawFile;
QDataStream rawFileStream(mRumbleGraphFile->contents.toLatin1());
QByteArray magic(15, Qt::Uninitialized);
rawFileStream.readRawData(magic.data(), 15);
rawFileStream.skipRawData(4);
char sectionChar;
rawFileStream >> sectionChar;
int sectionCount = sectionChar - '0';
ui->tableWidget_Entries->setRowCount(sectionCount);
ui->spinBox_Entries->setValue(sectionCount);
ui->groupBox_LocalStrViewer->setTitle(QString("Entries (%1)").arg(sectionCount));
rawFileStream.skipRawData(2);
for (int i = 0; i < sectionCount; i++) {
QByteArray xVal(6, Qt::Uninitialized), yVal(6, Qt::Uninitialized);
rawFileStream.readRawData(xVal.data(), 6);
rawFileStream.skipRawData(1);
rawFileStream.readRawData(yVal.data(), 6);
rawFileStream.skipRawData(2);
QTableWidgetItem *xItem = new QTableWidgetItem(xVal);
QTableWidgetItem *yItem = new QTableWidgetItem(yVal);
ui->tableWidget_Entries->setItem(i, 0, xItem);
ui->tableWidget_Entries->setItem(i, 1, yItem);
}
}

30
app/rumblegraphviewer.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef RUMBLEGRAPHVIEWER_H
#define RUMBLEGRAPHVIEWER_H
#include "asset_structs.h"
#include "zonefile.h"
#include <QWidget>
namespace Ui {
class RumbleGraphViewer;
}
class RumbleGraphViewer : public QWidget
{
Q_OBJECT
public:
explicit RumbleGraphViewer(QWidget *parent = nullptr);
~RumbleGraphViewer();
void SetEntryCount(quint32 aCount);
void SetRumbleGraphFile(const std::shared_ptr<RawFile> aRawFile);
void SetZoneFile(std::shared_ptr<ZoneFile> aZoneFile);
private:
Ui::RumbleGraphViewer *ui;
quint32 mEntryCount;
std::shared_ptr<RawFile> mRumbleGraphFile;
};
#endif // RUMBLEGRAPHVIEWER_H

153
app/rumblegraphviewer.ui Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RumbleGraphViewer</class>
<widget class="QWidget" name="RumbleGraphViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>841</width>
<height>457</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>841</width>
<height>457</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Rumble Graph File </string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>325</width>
<height>398</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>325</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Header</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Entry Count: </string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_Entries">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_LocalStrViewer">
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="font">
<font>
<family>Roboto</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="title">
<string>Entries</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTableWidget" name="tableWidget_Entries">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,831 +0,0 @@
#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();
}

View File

@ -1,190 +0,0 @@
#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);
// Tools
QString quickBmsPath() const;
void setQuickBmsPath(const QString& path);
static QString findQuickBms(); // Auto-detect QuickBMS
QString pythonPath() const;
void setPythonPath(const QString& path);
static QString findPython(); // Auto-detect Python
QString ffmpegPath() const;
void setFFmpegPath(const QString& path);
static QString findFFmpeg(); // Auto-detect FFmpeg
QString scriptsDirectory() const;
void setScriptsDirectory(const QString& path);
// 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);
// File Type Associations
// Returns the viewer type for a given extension: "hex", "text", "image", "audio", "list"
QString viewerForExtension(const QString& extension) const;
void setViewerForExtension(const QString& extension, const QString& viewer);
QStringList textFileExtensions() const;
void setTextFileExtensions(const QStringList& extensions);
QStringList imageFileExtensions() const;
void setImageFileExtensions(const QStringList& extensions);
QStringList audioFileExtensions() const;
void setAudioFileExtensions(const QStringList& extensions);
QStringList listFileExtensions() const;
void setListFileExtensions(const QStringList& extensions);
// Export Settings
QString defaultImageExportFormat() const;
void setDefaultImageExportFormat(const QString& format);
QString defaultAudioExportFormat() const;
void setDefaultAudioExportFormat(const QString& format);
int imageJpegQuality() const; // 1-100, default 90
void setImageJpegQuality(int quality);
int imagePngCompression() const; // 0-9, default 6
void setImagePngCompression(int level);
int audioMp3Bitrate() const; // 128, 192, 256, 320 kbps
void setAudioMp3Bitrate(int bitrate);
int audioOggQuality() const; // -1 to 10, default 5
void setAudioOggQuality(int quality);
int audioFlacCompression() const; // 0-8, default 5
void setAudioFlacCompression(int level);
bool exportRememberSettings() const;
void setExportRememberSettings(bool remember);
QString batchExportDirectory() const;
void setBatchExportDirectory(const QString& path);
bool batchExportPreserveStructure() const;
void setBatchExportPreserveStructure(bool preserve);
QString batchExportConflictResolution() const; // "number", "overwrite", "skip"
void setBatchExportConflictResolution(const QString& resolution);
// 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();
// Reset all settings to defaults
void resetToDefaults();
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

80
app/soundviewer.cpp Normal file
View File

@ -0,0 +1,80 @@
#include "soundviewer.h"
#include "ui_soundviewer.h"
SoundViewer::SoundViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::SoundViewer)
, player(new QMediaPlayer())
, buffer(new QBuffer())
{
ui->setupUi(this);
connect(ui->pushButton_Play, &QPushButton::clicked, player, &QMediaPlayer::play);
connect(ui->pushButton_Pause, &QPushButton::clicked, player, &QMediaPlayer::pause);
connect(ui->pushButton_Stop, &QPushButton::clicked, this, [this]() {
if (player->isPlaying()) {
player->stop();
}
});
connect(ui->pushButton_SkipForward, &QPushButton::clicked, this, [this]() {
player->setPosition(player->position() + 30);
});
connect(ui->pushButton_SkipBack, &QPushButton::clicked, this, [this]() {
player->setPosition(player->position() - 30);
});
connect(player, &QMediaPlayer::positionChanged, player, [this](qint64 position) {
ui->horizontalSlider->setSliderPosition(position);
ui->label_Time->setText(QString("%1:%2:%3")
.arg(position / 60000)
.arg((position % 60000) / 1000)
.arg(position % 1000));
});
connect(player, &QMediaPlayer::durationChanged, player, [this](qint64 duration) {
ui->horizontalSlider->setMaximum(duration);
ui->label_TimeMax->setText(QString("%1:%2:%3")
.arg(duration / 60000)
.arg((duration % 60000) / 1000)
.arg(duration % 1000));
});
connect(ui->horizontalSlider, &QSlider::sliderMoved, this, [this](int position) {
player->setPosition(position);
});
for (auto outputDevice : QMediaDevices::audioOutputs()) {
ui->comboBox_Output->addItem(outputDevice.description());
}
connect(ui->comboBox_Output, &QComboBox::currentIndexChanged, this, [this](int index) {
auto outputDevice = QMediaDevices::audioOutputs()[index];
QAudioOutput *audioOutput = new QAudioOutput(outputDevice);
player->setAudioOutput(audioOutput);
});
auto outputDevice = QMediaDevices::defaultAudioOutput();
QAudioOutput *audioOutput = new QAudioOutput(outputDevice);
player->setAudioOutput(audioOutput);
}
SoundViewer::~SoundViewer()
{
delete buffer;
delete player;
delete ui;
}
void SoundViewer::SetSound(std::shared_ptr<Sound> aSound)
{
buffer->setData(aSound->data);
if (!buffer->open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open QBuffer.";
return;
}
ui->groupBox->setTitle(aSound->path);
player->setSourceDevice(buffer);
}
void SoundViewer::SetOutput(QAudioOutput *aOutput) {
if (!aOutput) { return; }
player->setAudioOutput(aOutput);
}

34
app/soundviewer.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef SOUNDVIEWER_H
#define SOUNDVIEWER_H
#include "asset_structs.h"
#include <QWidget>
#include <QMediaPlayer>
#include <QBuffer>
#include <QAudioDevice>
#include <QMediaDevices>
#include <QAudioOutput>
namespace Ui {
class SoundViewer;
}
class SoundViewer : public QWidget
{
Q_OBJECT
public:
explicit SoundViewer(QWidget *parent = nullptr);
~SoundViewer();
void SetSound(std::shared_ptr<Sound> aSound);
void SetOutput(QAudioOutput *aOutput);
private:
Ui::SoundViewer *ui;
QMediaPlayer *player;
QBuffer *buffer;
};
#endif // SOUNDVIEWER_H

2573
app/soundviewer.ui Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,219 +0,0 @@
#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);
}

View File

@ -1,50 +0,0 @@
#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

36
app/stringtableviewer.cpp Normal file
View File

@ -0,0 +1,36 @@
#include "stringtableviewer.h"
#include "ui_stringtableviewer.h"
StringTableViewer::StringTableViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::StringTableViewer)
{
ui->setupUi(this);
}
StringTableViewer::~StringTableViewer()
{
delete ui;
}
void StringTableViewer::SetStringTable(std::shared_ptr<StringTable> aStringTable) {
ui->tableWidget_Strings->clear();
ui->tableWidget_Strings->setRowCount(aStringTable->rowCount);
ui->tableWidget_Strings->setColumnCount(aStringTable->columnCount);
int currentIndex = 0;
for (const QString &key : aStringTable->content.keys()) {
const QString value = aStringTable->content[key];
QTableWidgetItem *tableKeyItem = new QTableWidgetItem();
tableKeyItem->setText(key);
ui->tableWidget_Strings->setItem(currentIndex, 0, tableKeyItem);
QTableWidgetItem *tableValItem = new QTableWidgetItem();
tableValItem->setText(value);
ui->tableWidget_Strings->setItem(currentIndex, 1, tableValItem);
currentIndex++;
}
}

25
app/stringtableviewer.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef STRINGTABLEVIEWER_H
#define STRINGTABLEVIEWER_H
#include "asset_structs.h"
#include <QWidget>
namespace Ui {
class StringTableViewer;
}
class StringTableViewer : public QWidget
{
Q_OBJECT
public:
explicit StringTableViewer(QWidget *parent = nullptr);
~StringTableViewer();
void SetStringTable(std::shared_ptr<StringTable> aStringTable);
private:
Ui::StringTableViewer *ui;
};
#endif // STRINGTABLEVIEWER_H

24
app/stringtableviewer.ui Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>StringTableViewer</class>
<widget class="QWidget" name="StringTableViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>525</width>
<height>752</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableWidget" name="tableWidget_Strings"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

25
app/techsetviewer.cpp Normal file
View File

@ -0,0 +1,25 @@
#include "techsetviewer.h"
#include "ui_techsetviewer.h"
TechSetViewer::TechSetViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::TechSetViewer)
{
ui->setupUi(this);
}
TechSetViewer::~TechSetViewer()
{
delete ui;
}
void TechSetViewer::SetTechSet(std::shared_ptr<TechSet> aTechSet) {
ui->listWidget_Ptrs->clear();
ui->label_Title->setText(aTechSet->name);
int ptrIndex = 1;
for (auto ptr : aTechSet->pointers) {
ui->listWidget_Ptrs->addItem(QString("Pointer %1: %2").arg(ptrIndex).arg(ptr));
ptrIndex++;
}
}

25
app/techsetviewer.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef TECHSETVIEWER_H
#define TECHSETVIEWER_H
#include "asset_structs.h"
#include <QWidget>
namespace Ui {
class TechSetViewer;
}
class TechSetViewer : public QWidget
{
Q_OBJECT
public:
explicit TechSetViewer(QWidget *parent = nullptr);
~TechSetViewer();
void SetTechSet(std::shared_ptr<TechSet> aTechSet);
private:
Ui::TechSetViewer *ui;
};
#endif // TECHSETVIEWER_H

77
app/techsetviewer.ui Normal file
View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TechSetViewer</class>
<widget class="QWidget" name="TechSetViewer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>961</width>
<height>756</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_Title">
<property name="font">
<font>
<family>Roboto</family>
<pointsize>16</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Technique Set 0</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Unknown Pointers:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="listWidget_Ptrs"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>363</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,149 +0,0 @@
#include "textviewerwidget.h"
#include <QFileInfo>
#include <QHeaderView>
TextViewerWidget::TextViewerWidget(QWidget *parent)
: QWidget(parent)
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Info label at top
mInfoLabel = new QLabel(this);
mInfoLabel->setContentsMargins(8, 4, 8, 4);
mainLayout->addWidget(mInfoLabel);
// Splitter for text view and metadata
mSplitter = new QSplitter(Qt::Horizontal, this);
mainLayout->addWidget(mSplitter, 1);
// Text editor (read-only)
mTextEdit = new QPlainTextEdit(this);
mTextEdit->setReadOnly(true);
mTextEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
// Use monospace font
QFont monoFont("Consolas", 10);
monoFont.setStyleHint(QFont::Monospace);
mTextEdit->setFont(monoFont);
mSplitter->addWidget(mTextEdit);
// Metadata tree on the right
mMetadataTree = new QTreeWidget(this);
mMetadataTree->setHeaderLabels({"Property", "Value"});
mMetadataTree->setColumnCount(2);
mMetadataTree->header()->setStretchLastSection(true);
mMetadataTree->setMinimumWidth(200);
mSplitter->addWidget(mMetadataTree);
// Set initial splitter sizes (80% text, 20% metadata)
mSplitter->setSizes({800, 200});
// Connect to theme changes
connect(&Settings::instance(), &Settings::themeChanged,
this, &TextViewerWidget::applyTheme);
applyTheme(Settings::instance().theme());
}
void TextViewerWidget::setData(const QByteArray &data, const QString &filename)
{
mData = data;
mFilename = filename;
// Set text content
QString text = QString::fromUtf8(data);
mTextEdit->setPlainText(text);
// Update info label
QFileInfo fi(filename);
int lineCount = text.count('\n') + 1;
mInfoLabel->setText(QString("%1 | %2 bytes | %3 lines")
.arg(filename)
.arg(data.size())
.arg(lineCount));
// Setup syntax highlighting based on extension
setupSyntaxHighlighting(fi.suffix().toLower());
}
void TextViewerWidget::setMetadata(const QVariantMap &metadata)
{
mMetadataTree->clear();
for (auto it = metadata.constBegin(); it != metadata.constEnd(); ++it) {
const QString &key = it.key();
const QVariant &val = it.value();
// Skip internal fields and large binary data
if (key.startsWith('_') && key != "_name" && key != "_type")
continue;
if (val.typeId() == QMetaType::QByteArray)
continue;
auto *item = new QTreeWidgetItem(mMetadataTree);
item->setText(0, key);
if (val.typeId() == QMetaType::QVariantMap) {
item->setText(1, QString("{%1 fields}").arg(val.toMap().size()));
} else if (val.typeId() == QMetaType::QVariantList) {
item->setText(1, QString("[%1 items]").arg(val.toList().size()));
} else {
item->setText(1, val.toString());
}
}
mMetadataTree->resizeColumnToContents(0);
}
void TextViewerWidget::applyTheme(const Theme &theme)
{
mCurrentTheme = theme;
// Style the text editor
QString textStyle = QString(
"QPlainTextEdit {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" selection-background-color: %4;"
"}"
).arg(theme.panelColor, theme.textColor, theme.borderColor, theme.accentColor);
mTextEdit->setStyleSheet(textStyle);
// Style info label
mInfoLabel->setStyleSheet(QString(
"QLabel {"
" background-color: %1;"
" color: %2;"
" border-bottom: 1px solid %3;"
"}"
).arg(theme.panelColor, theme.textColorMuted, theme.borderColor));
// Style metadata tree
mMetadataTree->setStyleSheet(QString(
"QTreeWidget {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
"}"
"QTreeWidget::item:selected {"
" background-color: %4;"
"}"
"QHeaderView::section {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" padding: 4px;"
"}"
).arg(theme.panelColor, theme.textColor, theme.borderColor, theme.accentColor));
}
void TextViewerWidget::setupSyntaxHighlighting(const QString &extension)
{
// Basic syntax highlighting could be added here in the future
// For now, just adjust display based on file type
Q_UNUSED(extension)
}

View File

@ -1,42 +0,0 @@
#ifndef TEXTVIEWERWIDGET_H
#define TEXTVIEWERWIDGET_H
#include <QWidget>
#include <QPlainTextEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSplitter>
#include <QTreeWidget>
#include "settings.h"
class TextViewerWidget : public QWidget
{
Q_OBJECT
public:
explicit TextViewerWidget(QWidget *parent = nullptr);
~TextViewerWidget() = default;
void setData(const QByteArray &data, const QString &filename);
void setMetadata(const QVariantMap &metadata);
private slots:
void applyTheme(const Theme &theme);
private:
void setupSyntaxHighlighting(const QString &extension);
QByteArray mData;
QString mFilename;
QSplitter *mSplitter;
QLabel *mInfoLabel;
QPlainTextEdit *mTextEdit;
QTreeWidget *mMetadataTree;
Theme mCurrentTheme;
};
#endif // TEXTVIEWERWIDGET_H

View File

@ -1,340 +0,0 @@
#include "treebuilder.h"
#include "xtreewidget.h"
#include "xtreewidgetitem.h"
#include "typeregistry.h"
#include "logmanager.h"
#include <QCollator>
#include <algorithm>
// Natural sort comparator for strings with numbers (e.g., "chunk1" < "chunk2" < "chunk10")
static bool naturalLessThan(const QString& a, const QString& b) {
static QCollator collator;
collator.setNumericMode(true);
collator.setCaseSensitivity(Qt::CaseInsensitive);
return collator.compare(a, b) < 0;
}
TreeBuilder::TreeBuilder(XTreeWidget* tree, const TypeRegistry& registry)
: m_tree(tree)
, m_registry(&registry)
{
}
void TreeBuilder::reset()
{
m_categoryRoots.clear();
}
QString TreeBuilder::pluralizeType(const QString& typeName) const
{
const auto& mod = m_registry->module();
const auto it = mod.types.find(typeName);
QString groupLabel = (it != mod.types.end() && !it->display.isEmpty())
? it->display
: typeName;
// Don't double the 's' if already ends with 's'
if (groupLabel.endsWith('s') || groupLabel.endsWith('S'))
return groupLabel;
return groupLabel + "s";
}
XTreeWidgetItem* TreeBuilder::ensureTypeCategoryRoot(const QString& typeName, const QString& displayOverride)
{
const QString categoryKey = displayOverride.isEmpty() ? typeName : displayOverride;
if (m_categoryRoots.contains(categoryKey))
return m_categoryRoots[categoryKey];
auto* root = new XTreeWidgetItem(m_tree);
QString categoryLabel;
if (displayOverride.isEmpty()) {
categoryLabel = pluralizeType(typeName);
} else if (displayOverride.endsWith('s') || displayOverride.endsWith('S')) {
categoryLabel = displayOverride; // Don't double the 's'
} else {
categoryLabel = displayOverride + "s";
}
root->setText(0, categoryLabel);
root->setData(0, Qt::UserRole + 1, "CATEGORY");
root->setData(0, Qt::UserRole + 2, typeName);
m_tree->addTopLevelItem(root);
m_categoryRoots.insert(categoryKey, root);
return root;
}
XTreeWidgetItem* TreeBuilder::ensureSubcategory(XTreeWidgetItem* parent, const QString& childTypeName)
{
// Look for existing subcategory
for (int i = 0; i < parent->childCount(); i++) {
auto* c = static_cast<XTreeWidgetItem*>(parent->child(i));
if (c->data(0, Qt::UserRole + 1).toString() == "SUBCATEGORY" &&
c->data(0, Qt::UserRole + 2).toString() == childTypeName)
{
return c;
}
}
auto* sub = new XTreeWidgetItem(parent);
sub->setText(0, pluralizeType(childTypeName));
sub->setData(0, Qt::UserRole + 1, "SUBCATEGORY");
sub->setData(0, Qt::UserRole + 2, childTypeName);
parent->addChild(sub);
sub->setExpanded(false);
return sub;
}
XTreeWidgetItem* TreeBuilder::addInstanceNode(XTreeWidgetItem* parent, const QString& displayName,
const QString& typeName, const QVariantMap& vars)
{
auto* inst = new XTreeWidgetItem(parent);
inst->setText(0, displayName);
inst->setData(0, Qt::UserRole + 1, "INSTANCE");
inst->setData(0, Qt::UserRole + 2, typeName);
inst->setData(0, Qt::UserRole + 3, vars);
parent->addChild(inst);
inst->setExpanded(false);
return inst;
}
QString TreeBuilder::instanceDisplayFor(const QVariantMap& obj, const QString& fallbackType,
const QString& fallbackKey, std::optional<int> index)
{
// _name takes priority (explicit name set by script via set_name())
if (DslKeys::contains(obj, DslKey::Name)) {
const QString s = DslKeys::getString(obj, DslKey::Name);
if (!s.isEmpty()) return s;
}
// _display is secondary (set via set_display())
if (DslKeys::contains(obj, DslKey::Display)) {
const QString s = DslKeys::getString(obj, DslKey::Display);
if (!s.isEmpty()) return s;
}
// Index-based fallback for array items - ensures unique names
// This comes BEFORE type fallback to avoid all items showing same type name
if (!fallbackKey.isEmpty() && index.has_value()) {
return QString("%1[%2]").arg(fallbackKey).arg(*index);
}
// Type fallback (for non-array items)
if (!fallbackType.isEmpty()) return fallbackType;
if (!fallbackKey.isEmpty()) {
return fallbackKey;
}
return QStringLiteral("<unnamed>");
}
void TreeBuilder::addParsedFile(const QString& typeName, const QVariantMap& vars, const QString& fileName)
{
XTreeWidgetItem* cat = ensureTypeCategoryRoot(typeName, DslKeys::getString(vars, DslKey::Display));
const QString displayName = DslKeys::contains(vars, DslKey::Name)
? DslKeys::getString(vars, DslKey::Name)
: fileName;
XTreeWidgetItem* inst = addInstanceNode(cat, displayName, typeName, vars);
routeNestedObjects(inst, vars);
}
void TreeBuilder::routeNestedObjects(XTreeWidgetItem* parent, const QVariantMap& vars)
{
for (auto it = vars.begin(); it != vars.end(); ++it) {
const QString& key = it.key();
const QVariant& v = it.value();
// Child object (QVariantMap with _type)
if (v.typeId() == QMetaType::QVariantMap) {
const QVariantMap child = v.toMap();
// Skip hidden objects
if (DslKeys::get(child, DslKey::Hidden).toBool()) {
continue;
}
const QString childType = DslKeys::getString(child, DslKey::Type);
if (!childType.isEmpty()) {
auto* subcat = ensureSubcategory(parent, childType);
const QString childDisplay = instanceDisplayFor(child, childType, key);
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
routeNestedObjects(childInst, child);
}
continue;
}
// Array of child objects
if (v.typeId() == QMetaType::QVariantList) {
// Check for skip marker
if (DslKeys::hasSkipTree(vars, key)) {
LogManager::instance().debug(QString("[TREE] Skipping array '%1' (has skip_tree marker)").arg(key));
continue;
}
const QVariantList list = v.toList();
LogManager::instance().debug(QString("[TREE] Processing array '%1' with %2 items")
.arg(key).arg(list.size()));
for (int i = 0; i < list.size(); i++) {
if (list[i].typeId() != QMetaType::QVariantMap) {
continue;
}
const QVariantMap child = list[i].toMap();
// Skip hidden objects
if (DslKeys::get(child, DslKey::Hidden).toBool()) {
continue;
}
const QString childType = DslKeys::getString(child, DslKey::Type);
if (childType.isEmpty()) {
continue;
}
auto* subcat = ensureSubcategory(parent, childType);
const QString childDisplay = instanceDisplayFor(child, childType, key, i);
auto* childInst = addInstanceNode(subcat, childDisplay, childType, child);
routeNestedObjects(childInst, child);
}
}
}
}
void TreeBuilder::organizeChildrenByExtension(XTreeWidgetItem* parent)
{
if (!parent) return;
// Recursively process children first (depth-first)
for (int i = 0; i < parent->childCount(); i++) {
organizeChildrenByExtension(static_cast<XTreeWidgetItem*>(parent->child(i)));
}
// Only organize SUBCATEGORY nodes
QString nodeType = parent->data(0, Qt::UserRole + 1).toString();
if (nodeType != "SUBCATEGORY") return;
// Group children by extension
QMap<QString, QList<XTreeWidgetItem*>> byExtension;
QList<XTreeWidgetItem*> noExtension;
QList<XTreeWidgetItem*> nonInstanceChildren;
// Take all children
QList<XTreeWidgetItem*> children;
while (parent->childCount() > 0) {
children.append(static_cast<XTreeWidgetItem*>(parent->takeChild(0)));
}
for (auto* child : children) {
QString childNodeType = child->data(0, Qt::UserRole + 1).toString();
// Keep non-instance children as-is
if (childNodeType == "SUBCATEGORY" || childNodeType == "EXTENSION_GROUP") {
nonInstanceChildren.append(child);
continue;
}
QVariantMap vars = child->data(0, Qt::UserRole + 3).toMap();
QString name = DslKeys::getString(vars, DslKey::Name);
// Skip names starting with dot (indexed chunk names)
if (name.startsWith('.')) {
noExtension.append(child);
continue;
}
int dotPos = name.lastIndexOf('.');
if (dotPos > 0) {
QString ext = name.mid(dotPos + 1).toLower();
byExtension[ext].append(child);
} else {
noExtension.append(child);
}
}
// Re-add non-instance children first
for (auto* child : nonInstanceChildren) {
parent->addChild(child);
}
// Only create groups if multiple extensions exist
int uniqueExtensions = byExtension.size() + (noExtension.isEmpty() ? 0 : 1);
if (uniqueExtensions <= 1) {
// Put all items back directly (sorted)
QList<XTreeWidgetItem*> allItems;
for (auto& list : byExtension) {
allItems.append(list);
}
allItems.append(noExtension);
std::sort(allItems.begin(), allItems.end(), [](XTreeWidgetItem* a, XTreeWidgetItem* b) {
return naturalLessThan(a->text(0), b->text(0));
});
for (auto* child : allItems) {
parent->addChild(child);
}
return;
}
// Create extension groups
QStringList sortedExts = byExtension.keys();
std::sort(sortedExts.begin(), sortedExts.end());
for (const QString& ext : sortedExts) {
auto* extGroup = new XTreeWidgetItem(parent);
extGroup->setText(0, QString(".%1").arg(ext));
extGroup->setData(0, Qt::UserRole + 1, "EXTENSION_GROUP");
extGroup->setData(0, Qt::UserRole + 2, ext);
QList<XTreeWidgetItem*>& items = byExtension[ext];
std::sort(items.begin(), items.end(), [](XTreeWidgetItem* a, XTreeWidgetItem* b) {
return naturalLessThan(a->text(0), b->text(0));
});
for (auto* item : items) {
extGroup->addChild(item);
}
extGroup->setExpanded(false);
}
// Add items with no extension at the end
if (!noExtension.isEmpty()) {
auto* otherGroup = new XTreeWidgetItem(parent);
otherGroup->setText(0, "(other)");
otherGroup->setData(0, Qt::UserRole + 1, "EXTENSION_GROUP");
otherGroup->setData(0, Qt::UserRole + 2, "");
std::sort(noExtension.begin(), noExtension.end(), [](XTreeWidgetItem* a, XTreeWidgetItem* b) {
return naturalLessThan(a->text(0), b->text(0));
});
for (auto* item : noExtension) {
otherGroup->addChild(item);
}
otherGroup->setExpanded(false);
}
}
void TreeBuilder::updateNodeCounts(XTreeWidgetItem* node)
{
if (!node) return;
// Recursively update children first
for (int i = 0; i < node->childCount(); i++) {
updateNodeCounts(static_cast<XTreeWidgetItem*>(node->child(i)));
}
// Update count for grouping nodes
QString nodeType = node->data(0, Qt::UserRole + 1).toString();
if (nodeType == "SUBCATEGORY" || nodeType == "CATEGORY" || nodeType == "EXTENSION_GROUP") {
int count = node->childCount();
if (count > 0) {
QString currentText = node->text(0);
int parenPos = currentText.lastIndexOf(" (");
if (parenPos > 0) {
currentText = currentText.left(parenPos);
}
node->setText(0, QString("%1 (%2)").arg(currentText).arg(count));
}
}
}

View File

@ -1,53 +0,0 @@
#ifndef TREEBUILDER_H
#define TREEBUILDER_H
#include "dslkeys.h"
#include <QVariantMap>
#include <QString>
#include <QHash>
#include <optional>
class XTreeWidget;
class XTreeWidgetItem;
class TypeRegistry;
class TreeBuilder
{
public:
TreeBuilder() : m_tree(nullptr), m_registry(nullptr) {}
TreeBuilder(XTreeWidget* tree, const TypeRegistry& registry);
// Build tree from parsed data
void addParsedFile(const QString& typeName, const QVariantMap& vars, const QString& fileName);
// Category management
XTreeWidgetItem* ensureTypeCategoryRoot(const QString& typeName, const QString& displayOverride = {});
XTreeWidgetItem* ensureSubcategory(XTreeWidgetItem* parent, const QString& childTypeName);
// Instance management
XTreeWidgetItem* addInstanceNode(XTreeWidgetItem* parent, const QString& displayName,
const QString& typeName, const QVariantMap& vars);
// Recursively route nested objects into tree
void routeNestedObjects(XTreeWidgetItem* parent, const QVariantMap& vars);
// Post-processing
void organizeChildrenByExtension(XTreeWidgetItem* parent);
void updateNodeCounts(XTreeWidgetItem* node);
// Utilities
QString pluralizeType(const QString& typeName) const;
static QString instanceDisplayFor(const QVariantMap& obj, const QString& fallbackType,
const QString& fallbackKey = {}, std::optional<int> index = std::nullopt);
// Clear all category roots
void reset();
private:
XTreeWidget* m_tree;
const TypeRegistry* m_registry;
QHash<QString, XTreeWidgetItem*> m_categoryRoots;
};
#endif // TREEBUILDER_H

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,64 @@
#ifndef XTREEWIDGET_H
#define XTREEWIDGET_H
#include <QTreeWidget>
class QTreeWidgetItem;
class XTreeWidget : public QTreeWidget
{
Q_OBJECT
public:
explicit XTreeWidget(QWidget *parent = nullptr);
// Helper methods for batch export
int countExportableChildren(QTreeWidgetItem* parent) const;
int countExportableChildrenByType(QTreeWidgetItem* parent, int contentType) const;
signals:
void ItemSelected(const QString itemText, QTreeWidgetItem* item);
void ItemClosed(const QString itemText);
void Cleared();
// Export signals
void exportRequested(const QString& format, QTreeWidgetItem* item);
void quickExportRequested(QTreeWidgetItem* item);
void exportDialogRequested(QTreeWidgetItem* item);
void batchExportRequested(QTreeWidgetItem* parentItem);
void batchExportByTypeRequested(QTreeWidgetItem* parentItem, int contentType);
protected:
void ItemSelectionChanged();
void PrepareContextMenu(const QPoint &pos);
private:
void prepareInstanceContextMenu(QMenu* menu, QTreeWidgetItem* item);
void prepareContainerContextMenu(QMenu* menu, QTreeWidgetItem* item);
};
#endif // XTREEWIDGET_H
#ifndef XTREEWIDGET_H
#define XTREEWIDGET_H
#include "d3dbsp_structs.h"
#include "asset_structs.h"
#include "ddsfile.h"
#include "iwifile.h"
#include "fastfile.h"
#include "xtreewidgetitem.h"
#include "zonefile.h"
#include "utils.h"
#include <QTreeWidget>
#include <QFileDialog>
class XTreeWidget : public QTreeWidget
{
Q_OBJECT
public:
explicit XTreeWidget(QWidget *parent = nullptr);
~XTreeWidget();
void AddFastFile(std::shared_ptr<FastFile> aFastFile);
void AddZoneFile(std::shared_ptr<ZoneFile> aZoneFile, XTreeWidgetItem *aParentItem = nullptr);
void AddIWIFile(std::shared_ptr<IWIFile> aIWIFile);
void AddDDSFile(std::shared_ptr<DDSFile> aDDSFile);
std::shared_ptr<ZoneFile> FindZoneFile(const QString aStem);
std::shared_ptr<FastFile> FindFastFile(const QString aStem);
bool HasZoneFile(const QString aStem);
bool HasFastFile(const QString aStem);
void CloseFastFile(const QString aFFName);
signals:
void DDSFileSelected(std::shared_ptr<DDSFile> aDDSFile, const QString aParentName);
void IWIFileSelected(std::shared_ptr<IWIFile> aIWIFile, const QString aParentName);
void FastFileSelected(std::shared_ptr<FastFile> aFastFile, const QString aParentName);
void ZoneFileSelected(std::shared_ptr<ZoneFile> aZoneFile, const QString aParentName);
void LocalStringSelected(std::shared_ptr<ZoneFile> aZoneFile, const QString aParentName);
void RawFileSelected(std::shared_ptr<RawFile> aRawFile, const QString aParentName);
void ImageSelected(std::shared_ptr<Image> aImage, const QString aParentName);
void TechSetSelected(std::shared_ptr<TechSet> aZoneFile, const QString aParentName);
void StrTableSelected(std::shared_ptr<StringTable> aStrTable, const QString aParentName);
void MenuSelected(std::shared_ptr<Menu> aMenu, const QString aParentName);
void SoundSelected(std::shared_ptr<Sound> aSound, const QString aParentName);
void MaterialSelected(std::shared_ptr<Material> aMaterial, const QString aParentName);
void ItemSelected(const QString itemText);
void ItemClosed(const QString itemText);
void Cleared();
protected:
void ItemSelectionChanged();
void PrepareContextMenu(const QPoint &pos);
private:
QMap<QString, std::shared_ptr<FastFile>> mFastFiles;
QMap<QString, std::shared_ptr<ZoneFile>> mZoneFiles;
QMap<QString, std::shared_ptr<DDSFile>> mDDSFiles;
QMap<QString, std::shared_ptr<IWIFile>> mIWIFiles;
};
#endif // XTREEWIDGET_H

View File

@ -1,83 +1,56 @@
#include "xtreewidgetitem.h"
XTreeWidgetItem::XTreeWidgetItem(QTreeWidget *parent, bool group)
: QTreeWidgetItem(parent)
, isGroup(group) {
}
XTreeWidgetItem::XTreeWidgetItem(QTreeWidgetItem *parent, bool group)
: QTreeWidgetItem(parent)
, isGroup(group) {
}
bool XTreeWidgetItem::operator<(const QTreeWidgetItem &other) const {
// Attempt to cast the other item to our custom type.
const XTreeWidgetItem* otherItem = dynamic_cast<const XTreeWidgetItem*>(&other);
if (otherItem) {
bool thisIsGroup = this->childCount() > 0;
bool otherIsGroup = otherItem->childCount() > 0;
if (thisIsGroup != otherIsGroup) {
return otherIsGroup; // Groups should come before non-groups
}
}
// Fallback to the default string comparison on the current sort column.
return QTreeWidgetItem::operator<(other);
}
XTreeWidgetItem& XTreeWidgetItem::operator=(const XTreeWidgetItem &other)
{
if (this != &other) {
// Copy text and icon for each column.
const int colCount = other.columnCount();
for (int i = 0; i < colCount; ++i) {
setText(i, other.text(i));
setIcon(i, other.icon(i));
}
// Copy custom members.
this->isGroup = other.isGroup;
}
return *this;
}
bool XTreeWidgetItem::GetIsGroup() const
{
return isGroup;
}
void XTreeWidgetItem::SetIsGroup(bool aIsGroup)
{
isGroup = aIsGroup;
}
void XTreeWidgetItem::setModified(bool modified)
{
if (m_modified == modified) return;
if (modified && m_originalText.isEmpty()) {
// Store original text before adding indicator
m_originalText = text(0);
}
m_modified = modified;
if (modified) {
// Add asterisk indicator
if (!text(0).endsWith(" *")) {
setText(0, m_originalText + " *");
}
} else {
// Remove asterisk indicator
if (!m_originalText.isEmpty()) {
setText(0, m_originalText);
}
}
}
bool XTreeWidgetItem::isModified() const
{
return m_modified;
}
#include "xtreewidgetitem.h"
XTreeWidgetItem::XTreeWidgetItem(QTreeWidget *parent, bool group)
: QTreeWidgetItem(parent)
, isGroup(group)
, mCategory(CATEGORY_NONE) {
}
XTreeWidgetItem::XTreeWidgetItem(QTreeWidgetItem *parent, bool group)
: QTreeWidgetItem(parent)
, isGroup(group)
, mCategory(CATEGORY_NONE) {
}
void XTreeWidgetItem::SetCategory(TREE_CATEGORY category)
{
mCategory = category;
}
TREE_CATEGORY XTreeWidgetItem::GetCategory()
{
return mCategory;
}
bool XTreeWidgetItem::operator<(const QTreeWidgetItem &other) const {
// Attempt to cast the other item to our custom type.
const XTreeWidgetItem* otherItem = dynamic_cast<const XTreeWidgetItem*>(&other);
if (otherItem) {
bool thisIsGroup = this->childCount() > 0;
bool otherIsGroup = otherItem->childCount() > 0;
if (thisIsGroup != otherIsGroup) {
return otherIsGroup; // Groups should come before non-groups
}
}
// Fallback to the default string comparison on the current sort column.
return QTreeWidgetItem::operator<(other);
}
XTreeWidgetItem& XTreeWidgetItem::operator=(const XTreeWidgetItem &other)
{
if (this != &other) {
// Copy text and icon for each column.
const int colCount = other.columnCount();
for (int i = 0; i < colCount; ++i) {
setText(i, other.text(i));
setIcon(i, other.icon(i));
}
// Copy custom members.
this->isGroup = other.isGroup;
}
return *this;
}

View File

@ -1,34 +1,36 @@
#ifndef XTREEWIDGETITEM_H
#define XTREEWIDGETITEM_H
#include <QTreeWidgetItem>
class QTreeWidget;
// Custom item class
class XTreeWidgetItem : public QTreeWidgetItem
{
public:
XTreeWidgetItem(QTreeWidget *parent, bool group = false);
XTreeWidgetItem(QTreeWidgetItem *parent, bool group = false);
~XTreeWidgetItem() = default;
// Override the less-than operator to customize sorting.
bool operator<(const QTreeWidgetItem &other) const override;
XTreeWidgetItem &operator =(const XTreeWidgetItem &other);
bool GetIsGroup() const;
void SetIsGroup(bool aIsGroup);
// Modified state for edit indicator
void setModified(bool modified);
bool isModified() const;
private:
bool isGroup;
bool m_modified = false;
QString m_originalText;
};
#endif // XTREEWIDGETITEM_H
#ifndef XTREEWIDGETITEM_H
#define XTREEWIDGETITEM_H
#include <QTreeWidget>
#include <QTreeWidgetItem>
enum TREE_CATEGORY {
CATEGORY_NONE = 0x00,
CATEGORY_FILE = 0x01,
CATEGORY_TYPE = 0x02
};
// Custom item class
class XTreeWidgetItem : public QTreeWidgetItem
{
public:
// Flag to indicate if the item is a collapsible group/header.
bool isGroup;
// Constructors: default to non-group unless specified.
XTreeWidgetItem(QTreeWidget *parent, bool group = false);
XTreeWidgetItem(QTreeWidgetItem *parent, bool group = false);
void SetCategory(TREE_CATEGORY category);
TREE_CATEGORY GetCategory();
// Override the less-than operator to customize sorting.
bool operator<(const QTreeWidgetItem &other) const override;
XTreeWidgetItem &operator =(const XTreeWidgetItem &other);
private:
TREE_CATEGORY mCategory;
};
#endif // XTREEWIDGETITEM_H

158
app/zonefileviewer.cpp Normal file
View File

@ -0,0 +1,158 @@
#include "zonefileviewer.h"
#include "ui_zonefileviewer.h"
#include "statusbarmanager.h"
ZoneFileViewer::ZoneFileViewer(QWidget *parent)
: QWidget(parent)
, ui(new Ui::ZoneFileViewer) {
ui->setupUi(this);
mZoneFile = nullptr;
ui->tableWidget_RecordCounts->setColumnCount(3);
ui->tableWidget_RecordCounts->setHorizontalHeaderLabels({ "Identifier", "Asset", "Count" });
ui->tableWidget_RecordCounts->horizontalHeader()->setStretchLastSection(true);
ui->tableWidget_RecordOrder->setColumnCount(3);
ui->tableWidget_RecordOrder->setHorizontalHeaderLabels({ "Identifier", "Asset", "Count" });
ui->tableWidget_RecordOrder->horizontalHeader()->setStretchLastSection(true);
connect(ui->lineEdit_TagSearch, &QLineEdit::textChanged, this, &ZoneFileViewer::SortTags);
connect(ui->tableWidget_RecordCounts, &QTableWidget::itemSelectionChanged, this, &ZoneFileViewer::HighlightRecordInOrder);
connect(ui->listWidget_Tags, &QListWidget::currentTextChanged, this, [this](const QString &aCurrentText) {
StatusBarManager::instance().updateStatus(QString("Selected tag '%1' with index %2").arg(aCurrentText).arg(mZoneFile->GetTags().indexOf(aCurrentText)));
});
}
ZoneFileViewer::~ZoneFileViewer() {
delete ui;
}
void ZoneFileViewer::HighlightRecordInOrder() {
ui->tableWidget_RecordOrder->clearSelection();
foreach (auto selectedItem, ui->tableWidget_RecordCounts->selectedItems()) {
int selectedRow = selectedItem->row();
const QString assetId = ui->tableWidget_RecordCounts->item(selectedRow, 0)->text();
for (int i = 0; i < ui->tableWidget_RecordOrder->rowCount(); i++) {
const QString testAssetId = ui->tableWidget_RecordOrder->item(i, 0)->text();
if (testAssetId != assetId) { continue; }
ui->tableWidget_RecordOrder->selectRow(i);
ui->tableWidget_RecordOrder->item(i, 0)->setSelected(true);
ui->tableWidget_RecordOrder->item(i, 1)->setSelected(true);
ui->tableWidget_RecordOrder->item(i, 2)->setSelected(true);
ui->tableWidget_RecordOrder->item(i, 3)->setSelected(true);
}
}
}
void ZoneFileViewer::SortTags(const QString &aSearchText) {
ui->listWidget_Tags->clear();
const QStringList tags = mZoneFile->GetTags();
if (aSearchText.isEmpty()) {
ui->listWidget_Tags->addItems(tags);
return;
}
QStringList sortedTags;
foreach (const QString tag, tags) {
if (tag.contains(aSearchText)) {
sortedTags << tag;
}
}
StatusBarManager::instance().updateStatus(QString("Found %1 tags.").arg(sortedTags.size()));
ui->listWidget_Tags->addItems(sortedTags);
}
void ZoneFileViewer::SetZoneFile(std::shared_ptr<ZoneFile> aZoneFile) {
mZoneFile = aZoneFile;
ui->tableWidget_RecordCounts->clearContents();
ui->tableWidget_RecordOrder->clearContents();
ui->listWidget_Tags->clear();
const QStringList tags = mZoneFile->GetTags();
ui->listWidget_Tags->addItems(tags);
ui->label_Title->setText(mZoneFile->GetBaseStem() + ".zone");
ui->groupBox_Tags->setTitle(QString("Tags (%1)").arg(tags.size()));
if (tags.isEmpty()) {
ui->groupBox_Tags->hide();
} else {
ui->groupBox_Tags->show();
}
QMap<QString, int> recordCounts = QMap<QString, int>();
QVector<QPair<QString, int>> assetOccurances = QVector<QPair<QString, int>>();
for (const QString &record : mZoneFile->GetRecords()) {
if (!recordCounts.contains(record)) {
recordCounts[record] = 0;
}
recordCounts[record]++;
if (!assetOccurances.isEmpty() && assetOccurances.last().first == record) {
assetOccurances.last().second++;
continue;
}
QPair<QString, int> assetOccurance(record, 1);
assetOccurances << assetOccurance;
}
ui->tableWidget_RecordOrder->setRowCount(assetOccurances.size());
int assetIndex = 0;
foreach (auto assetOccurance, assetOccurances) {
const QString record = assetOccurance.first;
AssetType assetType = mZoneFile->AssetStrToEnum(record);
int assetCount = assetOccurance.second;
QIcon assetIcon = mZoneFile->AssetTypeToIcon(assetType);
if (assetIcon.isNull()) {
qDebug() << "Icon is null for record: " << record;
}
QTableWidgetItem *recordItem = new QTableWidgetItem(record.toUpper());
QTableWidgetItem *recordStrItem = new QTableWidgetItem(mZoneFile->AssetEnumToStr(assetType));
QTableWidgetItem *recordCountItem = new QTableWidgetItem(QString::number(assetCount));
recordItem->setIcon(assetIcon);
ui->tableWidget_RecordOrder->setItem(assetIndex, 0, recordItem);
ui->tableWidget_RecordOrder->setItem(assetIndex, 1, recordStrItem);
ui->tableWidget_RecordOrder->setItem(assetIndex, 2, recordCountItem);
assetIndex++;
}
int recordIndex = 0;
for (const QString &record : recordCounts.keys()) {
int recordCount = recordCounts[record];
AssetType assetType = mZoneFile->AssetStrToEnum(record);
QIcon assetIcon = mZoneFile->AssetTypeToIcon(assetType);
if (assetIcon.isNull()) {
qDebug() << "Icon is null for record: " << record;
}
ui->tableWidget_RecordCounts->setRowCount(recordIndex + 1);
QTableWidgetItem *recordItem = new QTableWidgetItem(record.toUpper());
QTableWidgetItem *recordCountStrItem = new QTableWidgetItem(mZoneFile->AssetEnumToStr(assetType));
QTableWidgetItem *recordCountItem = new QTableWidgetItem(QString::number(recordCount));
recordItem->setIcon(assetIcon);
ui->tableWidget_RecordCounts->setItem(recordIndex, 0, recordItem);
ui->tableWidget_RecordCounts->setItem(recordIndex, 1, recordCountStrItem);
ui->tableWidget_RecordCounts->setItem(recordIndex, 2, recordCountItem);
recordIndex++;
}
ui->tableWidget_RecordOrder->resizeColumnsToContents();
ui->tableWidget_RecordCounts->resizeColumnsToContents();
}

Some files were not shown because too many files have changed in this diff Show More