Compare commits
No commits in common. "main" and "feature/icon_factory" have entirely different histories.
main
...
feature/ic
@ -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 ==="
|
||||
@ -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
91
.gitignore
vendored
@ -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
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "tools/hexes"]
|
||||
path = tools/hexes
|
||||
url = https://code.redline.llc/njohnson/Hexes.git
|
||||
147
README.md
147
README.md
@ -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
|
||||
@ -3,8 +3,8 @@ TEMPLATE = subdirs
|
||||
SUBDIRS += libs \
|
||||
app \
|
||||
tools \
|
||||
#tests
|
||||
tests
|
||||
|
||||
#tests.depends = libs
|
||||
tests.depends = libs
|
||||
app.depends = libs
|
||||
tools.depends = libs
|
||||
|
||||
75
XScript.xml
75
XScript.xml
@ -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">= == != < <= > >= && || + - * / % << >> & ^ |</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>
|
||||
402
app/LICENSE
402
app/LICENSE
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
200
app/app.pro
200
app/app.pro
@ -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)
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
@ -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
41
app/d3dbsp_structs.h
Normal 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
182
app/ddsviewer.cpp
Normal 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 you’re 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
29
app/ddsviewer.h
Normal 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
1712
app/ddsviewer.ui
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
@ -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();
|
||||
}
|
||||
@ -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
27
app/fastfileviewer.cpp
Normal 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
26
app/fastfileviewer.h
Normal 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
197
app/fastfileviewer.ui
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
@ -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
28
app/imagewidget.cpp
Normal 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
32
app/imagewidget.h
Normal 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
144
app/imagewidget.ui
Normal 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
98
app/iwiviewer.cpp
Normal 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 you’re 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
27
app/iwiviewer.h
Normal 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
548
app/iwiviewer.ui
Normal 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>
|
||||
@ -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));
|
||||
}
|
||||
@ -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
62
app/localstringviewer.cpp
Normal 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
34
app/localstringviewer.h
Normal 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
194
app/localstringviewer.ui
Normal 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>
|
||||
463
app/main.cpp
463
app/main.cpp
@ -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();
|
||||
}
|
||||
|
||||
3089
app/mainwindow.cpp
3089
app/mainwindow.cpp
File diff suppressed because it is too large
Load Diff
118
app/mainwindow.h
118
app/mainwindow.h
@ -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
|
||||
|
||||
@ -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
35
app/materialviewer.cpp
Normal 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
27
app/materialviewer.h
Normal 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
236
app/materialviewer.ui
Normal 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
14
app/modelviewer.cpp
Normal 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
22
app/modelviewer.h
Normal 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
624
app/modelviewer.ui
Normal 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.
@ -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;
|
||||
}
|
||||
|
||||
@ -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
@ -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();
|
||||
}
|
||||
@ -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
|
||||
@ -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
58
app/rumblefileviewer.cpp
Normal 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
28
app/rumblefileviewer.h
Normal 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
153
app/rumblefileviewer.ui
Normal 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
54
app/rumblegraphviewer.cpp
Normal 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
30
app/rumblegraphviewer.h
Normal 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
153
app/rumblegraphviewer.ui
Normal 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>
|
||||
831
app/settings.cpp
831
app/settings.cpp
@ -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();
|
||||
}
|
||||
190
app/settings.h
190
app/settings.h
@ -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
80
app/soundviewer.cpp
Normal 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
34
app/soundviewer.h
Normal 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
2573
app/soundviewer.ui
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
@ -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
36
app/stringtableviewer.cpp
Normal 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
25
app/stringtableviewer.h
Normal 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
24
app/stringtableviewer.ui
Normal 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
25
app/techsetviewer.cpp
Normal 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
25
app/techsetviewer.h
Normal 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
77
app/techsetviewer.ui
Normal 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>
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
@ -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(®istry)
|
||||
{
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
1055
app/xtreewidget.cpp
1055
app/xtreewidget.cpp
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
158
app/zonefileviewer.cpp
Normal 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
Loading…
x
Reference in New Issue
Block a user